diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -1,9 +1,12 @@
-# Haskell Dockerfile Linter
+# Hadolint - Haskell Dockerfile Linter
 
-[![Build Status][github-actions-img]][github-actions]
 [![GPL-3 licensed][license-img]][license]
 [![GitHub release][release-img]][release]
-![GitHub downloads][downloads-img]
+[![Hackage version][hackage-img]][hackage]
+[![GitHub downloads][downloads-img]][release]
+[![Docker pulls][docker-img]][docker]
+[![Build Status][github-actions-img]][github-actions]
+
 <img align="right" alt="pipecat" width="150"
 src="https://hadolint.github.io/hadolint/img/cat_container.png" />
 
@@ -165,9 +168,10 @@
   -c,--config FILENAME     Path to the configuration file
   --file-path-in-report FILEPATHINREPORT
                            The file path referenced in the generated report.
-                           This only applies for the 'checkstyle' format and is
-                           useful when running Hadolint with Docker to set the
-                           correct file path.
+                           This only applies for the 'checkstyle', 'codeclimate',
+                           'sonarqube', 'junit' and 'gitlab_codeclimate' formats
+                           and is useful when running Hadolint with Docker to set
+                           the correct file path.
   --no-fail                Don't exit with a failure status code when any rule
                            is violated
   --no-color               Don't colorize output
@@ -175,7 +179,7 @@
                            stderr
   -f,--format ARG          The output format for the results [tty | json |
                            checkstyle | codeclimate | gitlab_codeclimate | gnu |
-                           codacy | sonarqube | sarif] (default: tty)
+                           codacy | sonarqube | sarif | junit] (default: tty)
   --error RULECODE         Make the rule `RULECODE` have the level `error`
   --warning RULECODE       Make the rule `RULECODE` have the level `warning`
   --info RULECODE          Make the rule `RULECODE` have the level `info`
@@ -219,7 +223,7 @@
 
 ```yaml
 failure-threshold: string               # name of threshold level (error | warning | info | style | ignore | none)
-format: string                          # Output format (tty | json | checkstyle | codeclimate | gitlab_codeclimate | gnu | codacy)
+format: string                          # Output format (tty | json | checkstyle | codeclimate | gitlab_codeclimate | gnu | codacy | sonarqube | sarif | junit)
 ignored: [string]                       # list of rules
 label-schema:                           # See Linting Labels below for specific label-schema details
   author: string                        # Your name
@@ -318,7 +322,7 @@
 NO_COLOR=1                               # Set or unset. See https://no-color.org
 HADOLINT_NOFAIL=1                        # Truthy value e.g. 1, true or yes
 HADOLINT_VERBOSE=1                       # Truthy value e.g. 1, true or yes
-HADOLINT_FORMAT=json                     # Output format (tty | json | checkstyle | codeclimate | gitlab_codeclimate | gnu | codacy | sarif )
+HADOLINT_FORMAT=json                     # Output format (tty | json | checkstyle | codeclimate | gitlab_codeclimate | gnu | codacy | sarif | junit )
 HADOLINT_FAILURE_THRESHOLD=info          # threshold level (error | warning | info | style | ignore | none)
 HADOLINT_OVERRIDE_ERROR=DL3010,DL3020    # comma separated list of rule codes
 HADOLINT_OVERRIDE_WARNING=DL3010,DL3020  # comma separated list of rule codes
@@ -356,12 +360,14 @@
 # hadolint ignore=DL3006
 FROM ubuntu
 
-# hadolint ignore=DL3003,SC1035
+# hadolint ignore=DL3003,SC1035 # We accept these issues, because ...
 RUN cd /tmp && echo "hello!"
 ```
 
 The comment "inline ignores" applies only to the statement following it.
 
+Comments are allowed after the `ignore=...`, you need an extra `#`
+
 ### Global ignores
 
 Rules can also be ignored on a per-file basis using the global ignore pragma.
@@ -369,7 +375,7 @@
 instead of just the next line.
 
 ```dockerfile
-# hadolint global ignore=DL3003,DL3006,SC1035
+# hadolint global ignore=DL3003,DL3006,SC1035 # We accept these issues, because ...
 FROM ubuntu
 
 RUN cd /tmp && echo "foo"
@@ -532,7 +538,11 @@
 | [DL3059](https://github.com/hadolint/hadolint/wiki/DL3059)   | Info             | Multiple consecutive `RUN` instructions. Consider consolidation.                                                                                    |
 | [DL3060](https://github.com/hadolint/hadolint/wiki/DL3060)   | Info             | `yarn cache clean` missing after `yarn install` was run.                                                                                            |
 | [DL3061](https://github.com/hadolint/hadolint/wiki/DL3061)   | Error            | Invalid instruction order. Dockerfile must begin with `FROM`, `ARG` or comment.                                                                     |
-| [DL3062](https://github.com/hadolint/hadolint/wiki/DL3061)   | Warning          | Pin versions in go install. Instead of `go install <package>` use `go install <package>@<version>`                                                  |
+| [DL3062](https://github.com/hadolint/hadolint/wiki/DL3062)   | Warning          | Pin versions in go install. Instead of `go install <package>` use `go install <package>@<version>`                                                  |
+| [DL3063](https://github.com/hadolint/hadolint/wiki/DL3063)   | Warning          | Stage name should not be a reserved word                                                                                                            |
+| [DL3064](https://github.com/hadolint/hadolint/wiki/DL3064)   | Warning          |  Potentially sensitive data should not be used in the `ARG` or `ENV` commands                                                                       |
+| [DL3066](https://github.com/hadolint/hadolint/wiki/DL3066)   | Info             | Non-numeric user-id may not be resolvable by host system                                                                                            |
+| [DL3067](https://github.com/hadolint/hadolint/wiki/DL3067)   | Warning          | Do not copy an entire filesystem from another stage                                                                                                 |
 | [DL4000](https://github.com/hadolint/hadolint/wiki/DL4000)   | Error            | `MAINTAINER` is deprecated.                                                                                                                         |
 | [DL4001](https://github.com/hadolint/hadolint/wiki/DL4001)   | Warning          | Either use Wget or Curl but not both.                                                                                                               |
 | [DL4003](https://github.com/hadolint/hadolint/wiki/DL4003)   | Warning          | Multiple `CMD` instructions found.                                                                                                                  |
@@ -556,7 +566,7 @@
 | [SC1083](https://github.com/koalaman/shellcheck/wiki/SC1083) |                  | This `{/}` is literal. Check expression (missing `;/\n`?) or quote it.                                                                              |
 | [SC1086](https://github.com/koalaman/shellcheck/wiki/SC1086) |                  | Don't use `$` on the iterator name in for loops.                                                                                                    |
 | [SC1087](https://github.com/koalaman/shellcheck/wiki/SC1087) |                  | Braces are required when expanding arrays, as in `${array[idx]}`.                                                                                   |
-| [SC1091](https://github.com/koalaman/shellcheck/wiki/SC1091) |                  | Not following: Reasons include: file not found, no permissions, not included on the command line, not allowing shellcheck to follow files with -x, etc.                                                                                    |
+| [SC1091](https://github.com/koalaman/shellcheck/wiki/SC1091) |                  | Not following: Reasons include: file not found, no permissions, not included on the command line, not allowing shellcheck to follow files with -x, etc. |
 | [SC1095](https://github.com/koalaman/shellcheck/wiki/SC1095) |                  | You need a space or linefeed between the function name and body.                                                                                    |
 | [SC1097](https://github.com/koalaman/shellcheck/wiki/SC1097) |                  | Unexpected `==`. For assignment, use `=`. For comparison, use `[ .. ]` or `[[ .. ]]`.                                                               |
 | [SC1098](https://github.com/koalaman/shellcheck/wiki/SC1098) |                  | Quote/escape special characters when using `eval`, e.g. `eval "a=(b)"`.                                                                             |
@@ -565,7 +575,7 @@
 | [SC2015](https://github.com/koalaman/shellcheck/wiki/SC2015) |                  | Note that <code>A && B &#124;&#124; C</code> is not if-then-else. C may run when A is true.                                                         |
 | [SC2026](https://github.com/koalaman/shellcheck/wiki/SC2026) |                  | This word is outside of quotes. Did you intend to 'nest '"'single quotes'"' instead'?                                                               |
 | [SC2028](https://github.com/koalaman/shellcheck/wiki/SC2028) |                  | `echo` won't expand escape sequences. Consider `printf`.                                                                                            |
-| [SC2035](https://github.com/koalaman/shellcheck/wiki/SC2035) |                  | Use `./*glob*` or `-- *glob*` so names with dashes won't become options.                                                                             |
+| [SC2035](https://github.com/koalaman/shellcheck/wiki/SC2035) |                  | Use `./*glob*` or `-- *glob*` so names with dashes won't become options.                                                                            |
 | [SC2039](https://github.com/koalaman/shellcheck/wiki/SC2039) |                  | In POSIX sh, something is undefined.                                                                                                                |
 | [SC2046](https://github.com/koalaman/shellcheck/wiki/SC2046) |                  | Quote this to prevent word splitting                                                                                                                |
 | [SC2086](https://github.com/koalaman/shellcheck/wiki/SC2086) |                  | Double quote to prevent globbing and word splitting.                                                                                                |
@@ -595,6 +605,7 @@
 
     ```bash
     cabal configure
+    cabal update
     cabal build
     ```
 
@@ -695,9 +706,14 @@
 [github-actions]: https://github.com/hadolint/hadolint/actions/workflows/haskell.yml
 [license-img]: https://img.shields.io/badge/license-GPL--3-blue.svg
 [license]: https://tldrlegal.com/l/gpl-3.0
-[release-img]: https://img.shields.io/github/release/hadolint/hadolint.svg
+[release-img]: https://img.shields.io/github/v/release/hadolint/hadolint?logo=github
+[release-date]: https://img.shields.io/github/release-date/hadolint/hadolint?logo=github
 [release]: https://github.com/hadolint/hadolint/releases/latest
-[downloads-img]: https://img.shields.io/github/downloads/hadolint/hadolint/total.svg
+[docker]: https://hub.docker.com/r/hadolint/hadolint
+[hackage]: https://hackage.haskell.org/package/hadolint
+[downloads-img]: https://img.shields.io/github/downloads/hadolint/hadolint/total?logo=github
+[hackage-img]: https://img.shields.io/hackage/v/hadolint?logo=haskell
+[docker-img]: https://img.shields.io/docker/pulls/hadolint/hadolint?logo=docker
 [best practice]: https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices
 [shellcheck]: https://github.com/koalaman/shellcheck
 [release page]: https://github.com/hadolint/hadolint/releases/latest
diff --git a/app/Main.hs b/app/Main.hs
--- a/app/Main.hs
+++ b/app/Main.hs
@@ -2,7 +2,7 @@
 
 import Control.Monad (when)
 import Data.Default
-import Hadolint (OutputFormat (..), printResults, DLSeverity (..))
+import Hadolint (OutputFormat (..), write, DLSeverity (..))
 import Hadolint.Config
 import Prettyprinter
 import qualified Data.List.NonEmpty as NonEmpty
@@ -35,8 +35,8 @@
   IO ()
 exitProgram conf res
   | noFail conf = exitSuccess
-  | CodeclimateJson == format conf = exitSuccess
-  | Codacy == format conf = exitSuccess
+  | CodeclimateJson `elem` formats conf = exitSuccess
+  | Codacy `elem` formats conf = exitSuccess
   | all (`noFailure` failureThreshold conf) res = exitSuccess
   | otherwise = exitFailure
 
@@ -47,8 +47,9 @@
 runLint cmd conf = do
   let files = NonEmpty.fromList $ dockerfiles cmd
       filePathInReport = filePathInReportOption cmd
+      destinations = output cmd
   res <- Hadolint.lintIO conf files
-  printResults (format conf) (noColor conf) filePathInReport res
+  write destinations (formats conf) (noColor conf) filePathInReport res
   exitProgram conf res
 
 execute :: CommandlineConfig -> Configuration -> IO ()
diff --git a/app/embedded.hs b/app/embedded.hs
new file mode 100644
--- /dev/null
+++ b/app/embedded.hs
@@ -0,0 +1,79 @@
+-- | Hadolint Embedded
+--
+-- This is a special version of Hadolint designed to be compiled to a WASM artifact
+-- and embedded in a website.
+-- It will read the content of the Dockerfile from a textarea element with id
+-- `dockerfile__code` and output the linting result as JSON string to stdout in
+-- the WASM runtime environment.
+
+import Data.Default
+import qualified Data.Sequence as Seq
+import qualified Data.Text as Text
+import GHC.Wasm.Prim
+import qualified Hadolint
+import qualified Hadolint.Formatter.Format as Format
+import qualified Language.Docker as Docker
+import Language.Docker.Parser (DockerfileError, Error)
+import Language.Docker.Syntax (Dockerfile)
+
+
+main :: IO ()
+main = error "ignore this"
+
+
+-- JavaScript imports --
+foreign import javascript unsafe "document.getElementById($1)"
+  js_document_getElementById :: JSString -> IO JSVal
+
+foreign import javascript unsafe "document.getElementById($1).value"
+  js_document_getElementValue :: JSString -> IO JSString
+
+foreign import javascript unsafe "$1.addEventListener($2, $3)"
+  js_addEventListener :: JSVal -> JSString -> JSVal -> IO ()
+
+foreign import javascript "wrapper"
+  asEventListener :: (JSVal -> IO ()) -> IO JSVal
+
+foreign import javascript unsafe "$1.textContent = $2;"
+  js_setText :: JSVal -> JSString -> IO ()
+
+
+-- JavaScript exports --
+foreign export javascript "setup" setup :: IO ()
+setup :: IO ()
+setup = do
+  lintInput <- js_document_getElementById (toJSString "lint")
+  lintInputCallback <- asEventListener onLint
+  js_addEventListener lintInput (toJSString "click") lintInputCallback
+
+  versionOutput <- js_document_getElementById (toJSString "version")
+  js_setText versionOutput $ toJSString Hadolint.getVersion
+
+foreign export javascript "lint" lint :: IO ()
+lint :: IO ()
+lint = do
+  dftext <- js_document_getElementValue (toJSString "dockerfile__code")
+  showResult $ doLint . Docker.parseText $ jsStringToText dftext
+
+
+-- Internal functions --
+
+-- event handler - just ignores the event object
+onLint :: JSVal -> IO ()
+onLint _ = lint
+
+doLint :: Either Error Dockerfile -> Format.Result Text.Text DockerfileError
+doLint res =
+  case res of
+    Left err -> Format.Result file ( Seq.singleton err ) mempty
+    Right ast -> Format.Result file mempty ( analyze ast )
+  where
+    file = Text.pack "Dockerfile"
+    analyze ast = Seq.unstableSort ( Hadolint.analyze def ast )
+
+showResult :: Format.Result Text.Text DockerfileError -> IO ()
+showResult result = do
+  Hadolint.write ["-"] [Hadolint.Json] True Nothing [ result ]
+
+jsStringToText :: JSString -> Text.Text
+jsStringToText = Text.pack . fromJSString
diff --git a/hadolint.cabal b/hadolint.cabal
--- a/hadolint.cabal
+++ b/hadolint.cabal
@@ -1,6 +1,6 @@
 cabal-version:      2.0
 name:               hadolint
-version:            2.14.0
+version:            2.15.0
 synopsis:           Dockerfile Linter JavaScript API
 description:
   A smarter Dockerfile linter that helps you build best practice Docker images.
@@ -33,6 +33,7 @@
     Hadolint.Formatter.Codeclimate
     Hadolint.Formatter.Format
     Hadolint.Formatter.Gnu
+    Hadolint.Formatter.JUnit
     Hadolint.Formatter.Json
     Hadolint.Formatter.Sarif
     Hadolint.Formatter.SonarQube
@@ -102,6 +103,11 @@
     Hadolint.Rule.DL3060
     Hadolint.Rule.DL3061
     Hadolint.Rule.DL3062
+    Hadolint.Rule.DL3063
+    Hadolint.Rule.DL3064
+    Hadolint.Rule.DL3065
+    Hadolint.Rule.DL3066
+    Hadolint.Rule.DL3067
     Hadolint.Rule.DL4000
     Hadolint.Rule.DL4001
     Hadolint.Rule.DL4003
@@ -149,7 +155,7 @@
     , foldl                 >=1.4.18   && <1.5
     , gitrev                >=1.3.1    && <1.4
     , HsYAML                >=0.2.1    && <0.3
-    , language-docker       >=15.0.0   && <16
+    , language-docker       >=16.0.0   && <17
     , megaparsec            >=9.0.0    && <9.8
     , mtl                   >=2.3.1    && <2.4
     , network-uri           >=2.6.4    && <2.7
@@ -162,7 +168,7 @@
     , spdx                  >=1.1      && <1.2
     , split                 >=0.2.5    && <0.3
     , text                  >=2.1.2    && <2.2
-    , time                  >=1.12.2   && <1.13
+    , time                  >=1.14     && <1.15
     , timerep               >=2.1.0    && <2.2
     , void                  >=0.7.3    && <0.8
     , xml-conduit           >=1.10.0   && <1.11
@@ -170,6 +176,10 @@
   default-language:   GHC2021
 
 executable hadolint
+  if !arch(wasm32)
+    buildable: True
+  else
+    buildable: False
   main-is:            Main.hs
   other-modules:      Paths_hadolint
   autogen-modules:    Paths_hadolint
@@ -205,6 +215,30 @@
 
   default-language:   GHC2021
 
+executable hadolint-embedded
+  if arch(wasm32)
+    buildable: True
+  else
+    buildable: False
+  main-is:        embedded.hs
+  hs-source-dirs: app
+
+  ghc-options:
+    -Wall -Wcompat -Wincomplete-record-updates
+    -Wincomplete-uni-patterns -Wredundant-constraints
+    -no-hs-main -optl-mexec-model=reactor -optl-Wl,--export=setup
+
+  build-depends:
+      base                  >=4.20.1    && <5
+    , ghc-experimental      >=9.1003.0  && <9.1201
+    , containers            >=0.7       && <0.8
+    , data-default          >=0.8.0     && <0.9
+    , hadolint
+    , language-docker
+    , text                  >=2.1.2     && <2.2
+
+  default-language:   GHC2021
+
 test-suite hadolint-unit-tests
   type:               exitcode-stdio-1.0
   main-is:            Spec.hs
@@ -216,7 +250,9 @@
     Hadolint.Config.SpecHook
     Hadolint.Formatter.GnuSpec
     Hadolint.Formatter.ParseErrorSpec
+    Hadolint.Formatter.JUnitSpec
     Hadolint.Formatter.SarifSpec
+    Hadolint.Formatter.SonarQubeSpec
     Hadolint.Formatter.TTYSpec
     Hadolint.PragmaSpec
     Hadolint.Rule.DL1001Spec
@@ -279,6 +315,11 @@
     Hadolint.Rule.DL3060Spec
     Hadolint.Rule.DL3061Spec
     Hadolint.Rule.DL3062Spec
+    Hadolint.Rule.DL3063Spec
+    Hadolint.Rule.DL3064Spec
+    Hadolint.Rule.DL3065Spec
+    Hadolint.Rule.DL3066Spec
+    Hadolint.Rule.DL3067Spec
     Hadolint.Rule.DL4000Spec
     Hadolint.Rule.DL4001Spec
     Hadolint.Rule.DL4003Spec
@@ -331,5 +372,7 @@
     , silently
     , split
     , text
+    , time
+    , xml-conduit
 
   default-language:   GHC2021
diff --git a/src/Hadolint/Config/Commandline.hs b/src/Hadolint/Config/Commandline.hs
--- a/src/Hadolint/Config/Commandline.hs
+++ b/src/Hadolint/Config/Commandline.hs
@@ -44,6 +44,7 @@
       configFile :: Maybe FilePath,
       dockerfiles :: [String],
       filePathInReportOption :: Maybe FilePath,
+      output :: [FilePath],
       configuration :: PartialConfiguration
     }
   deriving (Eq, Show)
@@ -55,6 +56,7 @@
     <*> parseConfigFile
     <*> parseFiles
     <*> parseFilePathInReportOption
+    <*> parseOutput
     <*> parseConfiguration
   where
     parseVersion = switch (long "version" <> short 'v' <> help "Show version")
@@ -75,12 +77,23 @@
             ( long "file-path-in-report"
                 <> metavar "FILEPATHINREPORT"
                 <> help "The file path referenced in the generated report.\
-                        \ This only applies for the 'checkstyle' format and is\
-                        \ useful when running Hadolint with Docker to set the\
-                        \ correct file path."
+                        \ This only applies for the 'checkstyle', 'codeclimate',\
+                        \ 'sonarqube', 'junit' and 'gitlab_codeclimate' formats \
+                        \ and is useful when running Hadolint with Docker to set \
+                        \ the correct file path."
             )
         )
 
+    parseOutput =
+      many
+        ( strOption
+            ( short 'o'
+                <> long "output"
+                <> metavar "OUTPUT"
+                <> help "Output destination file"
+            )
+        )
+
     parseConfiguration =
       PartialConfiguration
         <$> parseNoFail
@@ -130,7 +143,7 @@
         )
 
     parseOutputFormat =
-      optional $
+      many $
         option
           ( maybeReader (readMaybeOutputFormat . pack) )
           ( long "format"
@@ -138,7 +151,7 @@
               <> help
                 "The output format for the results [tty | json | checkstyle |\
                 \ codeclimate | gitlab_codeclimate | gnu | codacy | sonarqube |\
-                \ sarif] (default: tty)"
+                \ sarif | junit] (default: tty)"
               <> completeWith
                   [ "tty",
                     "json",
@@ -147,7 +160,8 @@
                     "gitlab_codeclimate",
                     "codacy",
                     "sonarqube",
-                    "sarif"
+                    "sarif",
+                    "junit"
                   ]
           )
 
diff --git a/src/Hadolint/Config/Configuration.hs b/src/Hadolint/Config/Configuration.hs
--- a/src/Hadolint/Config/Configuration.hs
+++ b/src/Hadolint/Config/Configuration.hs
@@ -27,7 +27,7 @@
     { noFail :: Bool,
       noColor :: Bool,
       verbose :: Bool,
-      format :: OutputFormat,
+      formats :: [OutputFormat],
       errorRules :: [RuleCode],
       warningRules :: [RuleCode],
       infoRules :: [RuleCode],
@@ -47,7 +47,6 @@
       False
       False
       False
-      def
       mempty
       mempty
       mempty
@@ -55,6 +54,7 @@
       mempty
       mempty
       mempty
+      mempty
       False
       False
       def
@@ -66,7 +66,7 @@
     (fromMaybe (noFail config) (partialNoFail partial))
     (fromMaybe (noColor config) (partialNoColor partial))
     (fromMaybe (verbose config) (partialVerbose partial))
-    (fromMaybe (format config) (partialFormat partial))
+    (formats config <> partialFormats partial)
     (errorRules config <> partialErrorRules partial)
     (warningRules config <> partialWarningRules partial)
     (infoRules config <> partialInfoRules partial)
@@ -85,7 +85,7 @@
           [ "Configuration:",
             "no fail:" <+> pretty (noFail c),
             "no color:" <+> pretty (noColor c),
-            "output format:" <+> pretty (format c),
+            nest 2 ( "output format:\n" <> prettyPrintList pretty (formats c) ),
             "failure threshold:" <+> pretty (failureThreshold c),
             prettyPrintRulelist "error" (errorRules c),
             prettyPrintRulelist "warning" (warningRules c),
@@ -132,7 +132,7 @@
     { partialNoFail :: Maybe Bool,
       partialNoColor :: Maybe Bool,
       partialVerbose :: Maybe Bool,
-      partialFormat :: Maybe OutputFormat,
+      partialFormats :: [OutputFormat],
       partialErrorRules :: [RuleCode],
       partialWarningRules :: [RuleCode],
       partialInfoRules :: [RuleCode],
@@ -192,7 +192,9 @@
     partialNoFail <- m .:? "no-fail" .!= Nothing
     partialNoColor <- m .:? "no-color" .!= Nothing
     partialVerbose <- m .:? "verbose" .!= Nothing
-    partialFormat <- m .:? "format"
+    partialFmt <- m .:? "format" .!= Nothing
+    partialFmts <- m .:? "formats" .!= mempty
+    let partialFormats = partialFmts <> listFromMaybe partialFmt
     override <- m .:? "override" .!= mempty
     ignored <- m .:? "ignored" .!= mempty
     trusted <- m .:? "trustedRegistries" .!= mempty
@@ -207,6 +209,10 @@
         partialAllowedRegistries = Set.fromList (coerce (trusted :: [Text]))
     partialFailureThreshold <- m .:? "failure-threshold"
     return PartialConfiguration {..}
+
+listFromMaybe :: Maybe a -> [a]
+listFromMaybe Nothing = []
+listFromMaybe (Just a) = [a]
 
 
 data OverrideConfig = OverrideConfig
diff --git a/src/Hadolint/Config/Environment.hs b/src/Hadolint/Config/Environment.hs
--- a/src/Hadolint/Config/Environment.hs
+++ b/src/Hadolint/Config/Environment.hs
@@ -54,10 +54,18 @@
 truthy :: String -> Bool
 truthy s = map toLower s `elem` ["1", "y", "on", "true", "yes"]
 
-getFormat :: IO (Maybe OutputFormat)
+getFormat :: IO [OutputFormat]
 getFormat = do
-  fmt <- lookupEnv "HADOLINT_FORMAT"
-  return $ (readMaybeOutputFormat . pack) =<< fmt
+  maybeString <- lookupEnv "HADOLINT_FORMAT"
+  case maybeString of
+    Nothing -> return []
+    Just s -> return $ flt $ map readMaybeOutputFormat $ splitOn "," (pack s)
+
+  where
+    flt :: [Maybe a] -> [a]
+    flt [] = []
+    flt (Nothing:xs) = flt xs
+    flt ((Just x):xs) = x:flt xs
 
 getOverrideList :: String -> IO [RuleCode]
 getOverrideList env = do
diff --git a/src/Hadolint/Formatter.hs b/src/Hadolint/Formatter.hs
--- a/src/Hadolint/Formatter.hs
+++ b/src/Hadolint/Formatter.hs
@@ -1,7 +1,7 @@
 module Hadolint.Formatter
   ( OutputFormat (..),
     Result (..),
-    printResults,
+    write,
     readMaybeOutputFormat,
   )
 where
@@ -13,27 +13,56 @@
 import qualified Hadolint.Formatter.Codacy as FormatCodacy
 import qualified Hadolint.Formatter.Codeclimate as FormatCodeclimate
 import qualified Hadolint.Formatter.Gnu as FormatGnu
+import qualified Hadolint.Formatter.JUnit as FormatJUnit
 import qualified Hadolint.Formatter.Json as FormatJson
 import qualified Hadolint.Formatter.Sarif as FormatSarif
 import qualified Hadolint.Formatter.SonarQube as FormatSonarQube
 import qualified Hadolint.Formatter.TTY as FormatTTY
+import System.IO
 
 
-printResults ::
-  Foldable f =>
+hWrite :: Foldable f =>
+  Handle ->
   OutputFormat ->
   Bool ->
   Maybe FilePath ->
   f (Result Text DockerfileError) ->
   IO ()
-printResults format nocolor filePathInReport allResults =
+hWrite handle format nocolor filePathInReport allResults =
   case format of
-    Checkstyle -> FormatCheckstyle.printResults allResults filePathInReport
-    Codacy -> FormatCodacy.printResults allResults
-    CodeclimateJson -> FormatCodeclimate.printResults allResults
-    GitLabCodeclimateJson -> FormatCodeclimate.printGitLabResults allResults
-    Gnu -> FormatGnu.printResults allResults
-    Json -> FormatJson.printResults allResults
-    Sarif -> FormatSarif.printResults allResults
-    SonarQube -> FormatSonarQube.printResults allResults
-    TTY -> FormatTTY.printResults allResults nocolor
+    Checkstyle -> FormatCheckstyle.hWrite handle allResults filePathInReport
+    Codacy -> FormatCodacy.hWrite handle allResults
+    CodeclimateJson -> FormatCodeclimate.hWrite handle allResults filePathInReport
+    GitLabCodeclimateJson -> FormatCodeclimate.hWriteGitLab handle allResults filePathInReport
+    Gnu -> FormatGnu.hWrite handle allResults
+    JUnit -> FormatJUnit.hWrite handle allResults filePathInReport
+    Json -> FormatJson.hWrite handle allResults
+    Sarif -> FormatSarif.hWrite handle allResults
+    SonarQube -> FormatSonarQube.hWrite handle allResults filePathInReport
+    TTY -> FormatTTY.hWrite handle allResults nocolor
+
+
+write :: Foldable f =>
+  [FilePath] ->
+  [OutputFormat] ->
+  Bool ->
+  Maybe FilePath ->
+  f (Result Text DockerfileError) ->
+  IO ()
+write paths formats nocolor filePathInReport allResults =
+  mapM_ writePath ziplist
+  where
+    ziplist = zipWithDef "-" TTY paths formats
+    writePath ( p, f ) =
+      if p == "-" then hWrite stdout f nocolor filePathInReport allResults
+      else withFile p WriteMode (\h -> hWrite h f nocolor filePathInReport allResults)
+
+
+zipWithDef :: FilePath -> OutputFormat -> [FilePath] -> [OutputFormat] -> [(FilePath, OutputFormat)]
+zipWithDef defP defF []     []     = [(defP, defF)]
+zipWithDef defP _    []     [f]    = [(defP, f)]
+zipWithDef _    defF [p]    []     = [(p, defF)]
+zipWithDef _    _    [p]    [f]    = [(p, f)]
+zipWithDef defP defF []     (f:fs) = (defP, f):zipWithDef defP defF [] fs
+zipWithDef defP defF (p:ps) []     = (p, defF):zipWithDef defP defF ps []
+zipWithDef defP defF (p:ps) (f:fs) = (p, f):zipWithDef defP defF ps fs
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,10 +1,10 @@
-module Hadolint.Formatter.Checkstyle ( printResults )
+module Hadolint.Formatter.Checkstyle
+  ( hWrite )
 where
 
 import qualified Data.ByteString.Lazy.Char8 as B
 import Data.Foldable
 import qualified Data.Map as Map
-import qualified Data.Maybe as Maybe
 import qualified Data.Text as Text
 import Hadolint.Formatter.Format
   ( Result (..),
@@ -13,6 +13,7 @@
     severityText,
   )
 import Hadolint.Rule (CheckFailure (..), DLSeverity (..), RuleCode (..))
+import System.IO
 import Text.Megaparsec (TraversableStream)
 import Text.Megaparsec.Error
   ( ParseErrorBundle,
@@ -66,14 +67,14 @@
 toFile ::
   (VisualStream s, TraversableStream s, ShowErrorComponent e) =>
   Result s e -> Maybe FilePath -> XML.Node
-toFile results filePathInReport =
+toFile result filePathInReport =
   XML.NodeElement XML.Element
     { elementName = "file",
       elementAttributes = Map.fromList [("name", filepath)],
-      elementNodes = renderNodes results
+      elementNodes = renderNodes result
     }
   where
-    filepath = if null filePathInReport then filename results else getFilePath filePathInReport
+    filepath = if null filePathInReport then filename result else getFilePath filePathInReport
     filename Result {fileName=fn} = fn
 
 renderResults ::
@@ -82,17 +83,14 @@
 renderResults results filePathInReport = XML.Element
   { elementName = "checkstyle",
     elementAttributes = Map.fromList [("version", "4.3")],
-    elementNodes = Maybe.mapMaybe maybeFile ( toList results )
+    elementNodes = fmap (`toFile` filePathInReport) ( toList results )
   }
-  where
-    maybeFile r = if isEmpty r then Nothing else Just $ toFile r filePathInReport
-    isEmpty Result {errors=e, checks=c} = null e && null c
 
-printResults ::
+hWrite ::
   (Foldable f, VisualStream s, TraversableStream s, ShowErrorComponent e) =>
-  f (Result s e) -> Maybe FilePath -> IO ()
-printResults results filePathInReport =
-  B.putStr $ XML.renderLBS settings document
+  Handle -> f (Result s e) -> Maybe FilePath -> IO ()
+hWrite handle results filePathInReport =
+  B.hPutStr handle $ XML.renderLBS settings document
   where
     settings = XML.def -- use default render settings
     document =
@@ -102,9 +100,7 @@
           documentEpilogue = []
         }
 
+
 getFilePath :: Maybe FilePath -> Text.Text
 getFilePath Nothing = ""
-getFilePath (Just filePath) = toText [filePath]
-
-toText :: [FilePath] -> Text.Text
-toText = foldMap Text.pack
+getFilePath (Just filePath) = Text.pack filePath
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,7 +1,5 @@
 module Hadolint.Formatter.Codacy
-  ( printResults,
-    formatResult,
-  )
+  ( hWrite )
 where
 
 import qualified Control.Foldl as Foldl
@@ -11,6 +9,7 @@
 import qualified Data.Text as Text
 import Hadolint.Formatter.Format (Result (..), errorPosition)
 import Hadolint.Rule (CheckFailure (..), RuleCode (..))
+import System.IO
 import Text.Megaparsec (TraversableStream)
 import Text.Megaparsec.Error
 import Text.Megaparsec.Pos (sourceLine, sourceName, unPos)
@@ -55,11 +54,10 @@
     errorMessages = fmap errorToIssue errors
     checkMessages = fmap (checkToIssue filename) checks
 
-printResults ::
+hWrite ::
   (Foldable f, VisualStream s, TraversableStream s, ShowErrorComponent e) =>
-  f (Result s e) ->
-  IO ()
-printResults results = mapM_ output flattened
+  Handle -> f (Result s e) -> IO ()
+hWrite handle results = mapM_ output flattened
   where
     flattened = Foldl.fold (Foldl.premap formatResult Foldl.mconcat) results
-    output value = B.putStrLn (encode value)
+    output value = B.hPutStr handle ( 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,8 +1,6 @@
 module Hadolint.Formatter.Codeclimate
-  ( printResults,
-    printGitLabResults,
-    formatResult,
-    formatGitLabResult,
+  ( hWrite,
+    hWriteGitLab,
   )
 where
 
@@ -11,12 +9,13 @@
 import Data.Aeson hiding (Result)
 import qualified Data.ByteString.Lazy as B
 import qualified Data.ByteString.Base16 as B16
-import Data.ByteString.Char8 as Char8
+import qualified Data.ByteString.Char8 as Char8
 import Data.Sequence (Seq)
 import qualified Data.Text as Text
 import GHC.Generics
 import Hadolint.Formatter.Format (Result (..), errorPosition)
 import Hadolint.Rule (CheckFailure (..), DLSeverity (..), RuleCode (..))
+import System.IO
 import Text.Megaparsec (TraversableStream)
 import Text.Megaparsec.Error
 import Text.Megaparsec.Pos (sourceColumn, sourceLine, sourceName, unPos)
@@ -31,7 +30,7 @@
 
 data FingerprintIssue = FingerprintIssue
   { issue :: Issue,
-    fingerprint :: ByteString
+    fingerprint :: Char8.ByteString
   }
 
 data Location
@@ -91,14 +90,16 @@
     line = unPos (sourceLine pos)
     column = unPos (sourceColumn pos)
 
-checkToIssue :: Text.Text -> CheckFailure -> Issue
-checkToIssue fileName CheckFailure {..} =
+checkToIssue :: Text.Text -> Maybe FilePath -> CheckFailure -> Issue
+checkToIssue fileName filePathInReport CheckFailure {..} =
   Issue
     { checkName = unRuleCode code,
       description = message,
-      location = LocLine fileName line,
+      location = LocLine reportFileName line,
       impact = severityText severity
     }
+  where
+    reportFileName = if null filePathInReport then fileName else getFilePath filePathInReport
 
 severityText :: DLSeverity -> Text.Text
 severityText severity =
@@ -109,7 +110,7 @@
     DLStyleC -> "minor"
     _ -> ""
 
-generateFingerprint :: Issue -> ByteString
+generateFingerprint :: Issue -> Char8.ByteString
 generateFingerprint = B16.encode . SHA1.hashlazy . encode
 
 issueToFingerprintIssue :: Issue -> FingerprintIssue
@@ -119,23 +120,39 @@
       fingerprint = generateFingerprint i
     }
 
-formatResult :: (VisualStream s, TraversableStream s, ShowErrorComponent e) => Result s e -> Seq Issue
-formatResult (Result filename errors checks) = (errorToIssue <$> errors) <> (checkToIssue filename <$> checks)
+formatResult :: (VisualStream s, TraversableStream s, ShowErrorComponent e) => Result s e -> Maybe FilePath -> Seq Issue
+formatResult (Result filename errors checks) filePathInReport = (errorToIssue <$> errors) <> (checkToIssue filename filePathInReport <$> checks)
 
-formatGitLabResult :: (VisualStream s, TraversableStream s, ShowErrorComponent e) => Result s e -> Seq FingerprintIssue
-formatGitLabResult result = issueToFingerprintIssue <$> formatResult result
+formatGitLabResult ::
+  (VisualStream s, TraversableStream s, ShowErrorComponent e) =>
+  Result s e -> Maybe FilePath ->
+  Seq FingerprintIssue
+formatGitLabResult result filePathInReport = issueToFingerprintIssue <$> formatResult result filePathInReport
 
-printResult :: (VisualStream s, TraversableStream s, ShowErrorComponent e) => Result s e -> IO ()
-printResult result = mapM_ output (formatResult result)
+printResult :: (VisualStream s, TraversableStream s, ShowErrorComponent e) =>
+  Handle -> Result s e -> Maybe FilePath -> IO ()
+printResult handle result filePathInReport = mapM_ output (formatResult result filePathInReport)
   where
     output value = do
-      B.putStr (encode value)
-      B.putStr (B.singleton 0x00)
+      B.hPutStr handle (encode value)
+      B.hPutStr handle (B.singleton 0x00)
 
-printResults :: (VisualStream s, TraversableStream s, ShowErrorComponent e, Foldable f) => f (Result s e) -> IO ()
-printResults = mapM_ printResult
+hWrite :: (VisualStream s, TraversableStream s, ShowErrorComponent e, Foldable f) =>
+  Handle -> f (Result s e) -> Maybe FilePath -> IO ()
+hWrite handle results filePathInReport = flattened
+  where
+    flattened = Foldl.fold (Foldl.premap ( printResult handle ) Foldl.mconcat) results filePathInReport
 
-printGitLabResults :: (Foldable f, VisualStream s, TraversableStream s, ShowErrorComponent e) => f (Result s e) -> IO ()
-printGitLabResults results = B.putStr . encode $ flattened
+hWriteGitLab ::
+  (Foldable f, VisualStream s, TraversableStream s, ShowErrorComponent e) =>
+  Handle -> f (Result s e) -> Maybe FilePath -> IO ()
+hWriteGitLab handle results filePathInReport = B.hPutStr handle . encode $ flattened
   where
-    flattened = Foldl.fold (Foldl.premap formatGitLabResult Foldl.mconcat) results
+    flattened = Foldl.fold (Foldl.premap formatGitLabResult Foldl.mconcat) results filePathInReport
+
+getFilePath :: Maybe FilePath -> Text.Text
+getFilePath Nothing = ""
+getFilePath (Just filePath) = toText [filePath]
+
+toText :: [FilePath] -> Text.Text
+toText = foldMap Text.pack
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
@@ -28,27 +28,29 @@
 import qualified Hadolint.Rule
 
 data OutputFormat
-  = Json
-  | SonarQube
-  | TTY
+  = Checkstyle
+  | Codacy
   | CodeclimateJson
   | GitLabCodeclimateJson
   | Gnu
-  | Checkstyle
-  | Codacy
+  | JUnit
+  | Json
   | Sarif
+  | SonarQube
+  | TTY
   deriving (Eq, Show)
 
 instance Pretty OutputFormat where
-  pretty Json = "json"
-  pretty SonarQube = "sonarqube"
-  pretty TTY = "tty"
+  pretty Checkstyle = "checkstyle"
+  pretty Codacy = "codacy"
   pretty CodeclimateJson = "codeclimate"
   pretty GitLabCodeclimateJson = "gitlab_codeclimate"
   pretty Gnu = "gnu"
-  pretty Checkstyle = "checkstyle"
-  pretty Codacy = "codacy"
+  pretty JUnit = "junit"
+  pretty Json = "json"
   pretty Sarif = "sarif"
+  pretty SonarQube = "sonarqube"
+  pretty TTY = "tty"
 
 instance Semigroup OutputFormat where
   _ <> f = f
@@ -73,15 +75,16 @@
   def = TTY
 
 readMaybeOutputFormat :: Text -> Maybe OutputFormat
-readMaybeOutputFormat "json" = Just Json
-readMaybeOutputFormat "sonarqube" = Just SonarQube
-readMaybeOutputFormat "tty" = Just TTY
+readMaybeOutputFormat "checkstyle" = Just Checkstyle
+readMaybeOutputFormat "codacy" = Just Codacy
 readMaybeOutputFormat "codeclimate" = Just CodeclimateJson
 readMaybeOutputFormat "gitlab_codeclimate" = Just GitLabCodeclimateJson
 readMaybeOutputFormat "gnu" = Just Gnu
-readMaybeOutputFormat "checkstyle" = Just Checkstyle
-readMaybeOutputFormat "codacy" = Just Codacy
+readMaybeOutputFormat "junit" = Just JUnit
+readMaybeOutputFormat "json" = Just Json
 readMaybeOutputFormat "sarif" = Just Sarif
+readMaybeOutputFormat "sonarqube" = Just SonarQube
+readMaybeOutputFormat "tty" = Just TTY
 readMaybeOutputFormat _ = Nothing
 
 
diff --git a/src/Hadolint/Formatter/Gnu.hs b/src/Hadolint/Formatter/Gnu.hs
--- a/src/Hadolint/Formatter/Gnu.hs
+++ b/src/Hadolint/Formatter/Gnu.hs
@@ -3,51 +3,42 @@
 -- See https://www.gnu.org/prep/standards/html_node/Errors.html for reference.
 
 module Hadolint.Formatter.Gnu
-  ( printResults,
-  )
+  ( hWrite )
 where
 
 
-import Data.Text (Text, pack)
+import Data.Text (Text, pack, unpack)
 import Hadolint.Formatter.Format
 import Hadolint.Rule (CheckFailure (..), RuleCode (..))
+import System.IO
 import Text.Megaparsec (TraversableStream)
 import Text.Megaparsec.Error
 import Text.Megaparsec.Stream (VisualStream)
 import qualified Data.List.NonEmpty as NonEmpty
-import qualified Data.Text.IO as TextIO
 
-
-printResults ::
+hWrite ::
   (VisualStream s, TraversableStream s, ShowErrorComponent e, Foldable f) =>
-  f (Result s e) ->
-  IO ()
-printResults = mapM_ printResult
-
+  Handle -> f (Result s e) -> IO ()
+hWrite handle = mapM_ ( printResult handle )
 
 printResult ::
   (VisualStream s, TraversableStream s, ShowErrorComponent e) =>
-  Result s e ->
-  IO ()
-printResult (Result filename errors checks) =
-  printErrors errors >> printChecks filename checks
-
+  Handle -> Result s e -> IO ()
+printResult handle (Result filename errors checks) =
+  printErrors handle errors >> printChecks handle filename checks
 
 printErrors ::
   (VisualStream s, TraversableStream s, ShowErrorComponent e, Foldable f) =>
-  f (ParseErrorBundle s e) ->
-  IO ()
-printErrors = mapM_ (TextIO.putStrLn . formatError)
-
+  Handle -> f (ParseErrorBundle s e) -> IO ()
+printErrors handle = mapM_ ( hPutStrLn handle . unpack . formatError )
 
-printChecks :: (Foldable f) => Text -> f CheckFailure -> IO ()
-printChecks filename = mapM_ (TextIO.putStrLn . formatCheck filename)
+printChecks :: (Foldable f) => Handle -> Text -> f CheckFailure -> IO ()
+printChecks handle filename = mapM_ ( hPutStrLn handle . unpack . formatCheck filename )
 
 
 formatError ::
   (VisualStream s, TraversableStream s, ShowErrorComponent e) =>
-  ParseErrorBundle s e ->
-  Text
+  ParseErrorBundle s e -> Text
 formatError err@(ParseErrorBundle e _) =
   pack $
     "hadolint:"
diff --git a/src/Hadolint/Formatter/JUnit.hs b/src/Hadolint/Formatter/JUnit.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Formatter/JUnit.hs
@@ -0,0 +1,165 @@
+module Hadolint.Formatter.JUnit ( hWrite ) where
+
+
+import qualified Data.ByteString.Lazy.Char8 as B
+import Data.Foldable
+import Data.Maybe
+import qualified Data.Map as Map
+import qualified Data.Text as Text
+import qualified Data.Time as Time
+import Hadolint.Formatter.Format
+  ( Result (..),
+    errorBundlePretty,
+    errorPosition,
+    severityText,
+  )
+import Hadolint.Rule (CheckFailure (..), RuleCode (..))
+import Hadolint.Meta (getShortVersion)
+import System.IO (Handle)
+import Text.Megaparsec (TraversableStream)
+import Text.Megaparsec.Error
+  ( ParseErrorBundle,
+    ShowErrorComponent,
+  )
+import Text.Megaparsec.Pos (sourceColumn, sourceLine, unPos)
+import Text.Megaparsec.Stream (VisualStream)
+import qualified Text.XML as XML
+
+
+providerName :: Text.Text
+providerName = "Hadolint " <> Text.pack getShortVersion
+
+providerID :: Text.Text
+providerID = "hadolint"
+
+
+hWrite ::
+  (Foldable f, VisualStream s, TraversableStream s, ShowErrorComponent e) =>
+  Handle -> f (Result s e) -> Maybe FilePath -> IO ()
+hWrite handle results maybeFilepath = do
+  time <- Time.getCurrentTime
+  B.hPutStr handle $ XML.renderLBS settings $ document time
+
+  where
+    settings = XML.def
+
+    document :: Time.UTCTime -> XML.Document
+    document t =
+      XML.Document
+        { documentPrologue = XML.Prologue [] Nothing [],
+          documentRoot = root t,
+          documentEpilogue = []
+
+        }
+
+    root :: Time.UTCTime -> XML.Element
+    root t =
+      XML.Element
+        { elementName = "testsuites",
+          elementAttributes =
+            Map.fromList
+              [ ("id", runID t),
+                ("name", runName t),
+                ("time", "0.001")
+              ],
+          elementNodes = fmap ( `renderResult` maybeFilepath ) ( toList results )
+        }
+
+    runID :: Time.UTCTime -> Text.Text
+    runID t =
+      Text.pack $ Time.formatTime Time.defaultTimeLocale "%Y%m%d_%H%M%S" t
+
+    runName :: Time.UTCTime -> Text.Text
+    runName t =
+      Text.pack $ Time.formatTime Time.defaultTimeLocale "Hadolint run at %Y-%m-%d %H:%M:%S" t
+
+renderResult ::
+  (VisualStream s, TraversableStream s, ShowErrorComponent e) =>
+  Result s e -> Maybe FilePath -> XML.Node
+renderResult (Result filename errors checks) maybeFilepath =
+  XML.NodeElement XML.Element
+    { elementName = "testsuite",
+      elementAttributes =
+        Map.fromList
+          [ ("id", providerID),
+            ("name", providerName),
+            ("time", "0.001"),
+            ("failures", Text.pack $ show $ length findings),
+            ("errors", Text.pack $ show parseErrors)
+          ],
+      elementNodes = findings
+    }
+  where
+    findings = toList ( errorNodes <> checkNodes )
+    parseErrors = length errorNodes
+    errorNodes = fmap ( `errorToNode` file ) errors
+    checkNodes = fmap ( `checkToNode` file ) checks
+    file = if null maybeFilepath then filename else Text.pack $ fromMaybe "" maybeFilepath
+
+errorToNode ::
+  (VisualStream s, TraversableStream s, ShowErrorComponent e) =>
+  ParseErrorBundle s e -> Text.Text -> XML.Node
+errorToNode err filename =
+  XML.NodeElement XML.Element
+    { elementName = "testcase",
+      elementAttributes =
+        Map.fromList
+          [ ("id", providerID <> ".error"),
+            ("name", "DL1000"),
+            ("time", "0.001")
+          ],
+      elementNodes =
+        [ XML.NodeElement XML.Element
+            { elementName = "failure",
+              elementAttributes =
+                Map.fromList
+                  [ ("type", "error"),
+                    ("message", "parse error at line " <> line <> ", column " <> column)
+                  ],
+              elementNodes =
+                [ XML.NodeContent $
+                    Text.unlines
+                      [ "Parse Error in " <> filename,
+                        "Line: " <> line,
+                        "Column: " <> column,
+                        "Parse Error: " <> Text.pack ( errorBundlePretty err )
+                      ]
+                ]
+            }
+        ]
+    }
+  where
+    line = Text.pack ( show $ unPos $ sourceLine $ errorPosition err )
+    column = Text.pack ( show $ unPos $ sourceColumn $ errorPosition err )
+
+checkToNode :: CheckFailure -> Text.Text -> XML.Node
+checkToNode CheckFailure {..} filename =
+  XML.NodeElement XML.Element
+    { elementName = "testcase",
+      elementAttributes =
+        Map.fromList
+          [ ("id", providerID <> ".rule." <> unRuleCode code),
+            ("time", "0.001")
+          ],
+      elementNodes =
+        [ XML.NodeElement XML.Element
+            { elementName = "failure",
+              elementAttributes =
+                Map.fromList
+                  [ ("type", severityText severity),
+                    ("message", message),
+                    ("id", unRuleCode code)
+                  ],
+              elementNodes =
+                [ XML.NodeContent $
+                    Text.unlines
+                      [ "File: " <> filename,
+                        "Line: " <> Text.pack ( show line ),
+                        "Category: Hadolint - Dockerfile Static Analysis",
+                        severityText severity <> ": " <> message
+                      ]
+                ]
+            }
+        ]
+    }
+
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,7 +1,5 @@
 module Hadolint.Formatter.Json
-  ( printResults,
-    formatResult,
-  )
+  ( hWrite )
 where
 
 import qualified Control.Foldl as Foldl
@@ -16,6 +14,7 @@
     errorMessage
   )
 import Hadolint.Rule (CheckFailure (..), DLSeverity (..), unRuleCode)
+import System.IO
 import Text.Megaparsec (TraversableStream)
 import Text.Megaparsec.Error
 import Text.Megaparsec.Pos (sourceColumn, sourceLine, sourceName, unPos)
@@ -47,11 +46,10 @@
     where
       pos = errorPosition err
 
-printResults ::
+hWrite ::
   (VisualStream s, TraversableStream s, ShowErrorComponent e, Foldable f) =>
-  f (Result s e) ->
-  IO ()
-printResults results = B.putStr . encode $ flattened
+  Handle -> f (Result s e) -> IO ()
+hWrite handle results = B.hPutStr handle . encode $ flattened
   where
     flattened = Foldl.fold (Foldl.premap formatResult Foldl.mconcat) results
 
diff --git a/src/Hadolint/Formatter/Sarif.hs b/src/Hadolint/Formatter/Sarif.hs
--- a/src/Hadolint/Formatter/Sarif.hs
+++ b/src/Hadolint/Formatter/Sarif.hs
@@ -1,7 +1,5 @@
 module Hadolint.Formatter.Sarif
-  ( printResults,
-    formatResult,
-  )
+  ( hWrite )
 where
 
 import qualified Control.Foldl as Foldl
@@ -22,6 +20,7 @@
     DLSeverity (..),
     unRuleCode,
   )
+import System.IO
 import Text.Megaparsec (TraversableStream)
 import Text.Megaparsec.Error
 import Text.Megaparsec.Pos
@@ -115,16 +114,15 @@
     checkMessages = fmap (SarifCheck filename) checks
     errorMessages = fmap SarifError errors
 
-printResults ::
+hWrite ::
   ( VisualStream s,
     TraversableStream s,
     ShowErrorComponent e,
     Foldable f
   ) =>
-  f (Result s e) ->
-  IO ()
-printResults results =
-  B.putStr . encode $
+  Handle -> f (Result s e) -> IO ()
+hWrite handle results =
+  B.hPutStr handle . encode $
     object
       [ ("version", "2.1.0"),
         "$schema"
diff --git a/src/Hadolint/Formatter/SonarQube.hs b/src/Hadolint/Formatter/SonarQube.hs
--- a/src/Hadolint/Formatter/SonarQube.hs
+++ b/src/Hadolint/Formatter/SonarQube.hs
@@ -1,8 +1,6 @@
 module Hadolint.Formatter.SonarQube
-  ( formatResult,
-    printResults
-  )
-  where
+  ( hWrite )
+where
 
 import qualified Control.Foldl as Foldl
 import Data.Aeson hiding (Result)
@@ -19,12 +17,12 @@
     DLSeverity (..),
     unRuleCode
   )
+import System.IO
 import Text.Megaparsec (TraversableStream)
 import Text.Megaparsec.Error
 import Text.Megaparsec.Pos
   ( sourceColumn,
     sourceLine,
-    sourceName,
     unPos
   )
 import Text.Megaparsec.Stream (VisualStream)
@@ -32,7 +30,7 @@
 
 data SonarQubeFormat s e
   = SonarQubeCheck Text.Text CheckFailure
-  | SonarQubeError (ParseErrorBundle s e)
+  | SonarQubeError Text.Text (ParseErrorBundle s e)
 
 instance (VisualStream s,
   TraversableStream s,
@@ -54,7 +52,7 @@
               ]
           ]
       ]
-  toJSON (SonarQubeError err) =
+  toJSON (SonarQubeError filename err) =
     object
       [ "engineId" .= Text.pack "Hadolint",
         "ruleId" .= Text.pack "DL1000",
@@ -62,7 +60,7 @@
         "type" .= Text.pack "BUG",
         "primaryLocation" .= object
           [ "message" .= errorMessage err,
-            "filePath" .= Text.pack (sourceName pos),
+            "filePath" .= filename,
             "textRange" .= object
               [ "startLine" .= linenumber,
                 "endLine" .= linenumber,
@@ -77,20 +75,25 @@
       column = unPos $ sourceColumn pos
 
 
-formatResult :: Result s e -> Seq (SonarQubeFormat s e)
-formatResult (Result filename errors checks) = allMessages
+formatResult :: Maybe FilePath -> Result s e -> Seq (SonarQubeFormat s e)
+formatResult filePathInReport (Result filename errors checks) = allMessages
   where
     allMessages = errorMessages <> checkMessages
-    errorMessages = fmap SonarQubeError errors
-    checkMessages = fmap (SonarQubeCheck filename) checks
+    errorMessages = fmap (SonarQubeError filepath) errors
+    checkMessages = fmap (SonarQubeCheck filepath) checks
+    filepath = if null filePathInReport then filename else getFilePath filePathInReport
 
-printResults :: (VisualStream s,
-  TraversableStream s,
-  ShowErrorComponent e,
-  Foldable f) => f (Result s e) -> IO ()
-printResults results = B.putStr . encode $ object [ "issues" .= flattened ]
+hWrite ::
+  ( VisualStream s,
+    TraversableStream s,
+    ShowErrorComponent e,
+    Foldable f
+  ) =>
+  Handle -> f (Result s e) -> Maybe FilePath -> IO ()
+hWrite handle results filePathInReport =
+  B.hPutStr handle . encode $ object [ "issues" .= flattened ]
   where
-    flattened = Foldl.fold (Foldl.premap formatResult Foldl.mconcat) results
+    flattened = Foldl.fold (Foldl.premap (formatResult filePathInReport) Foldl.mconcat) results
 
 toType :: DLSeverity -> Text.Text
 toType DLErrorC = "BUG"
@@ -101,3 +104,10 @@
 toSeverity DLWarningC = "MAJOR"
 toSeverity DLInfoC = "MINOR"
 toSeverity _ = "INFO"
+
+getFilePath :: Maybe FilePath -> Text.Text
+getFilePath Nothing = ""
+getFilePath (Just filePath) = toText [ filePath ]
+
+toText :: [FilePath] -> Text.Text
+toText = foldMap Text.pack
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
@@ -1,10 +1,7 @@
-{-# LANGUAGE NamedFieldPuns #-}
-{-# LANGUAGE OverloadedStrings #-}
-
 module Hadolint.Formatter.TTY
-  ( printResults,
+  ( hWrite,
     formatCheck,
-    formatError,
+    formatError
   )
 where
 
@@ -14,6 +11,7 @@
 import Hadolint.Formatter.Format
 import Hadolint.Rule (CheckFailure (..), DLSeverity (..), RuleCode (..))
 import Language.Docker.Syntax
+import System.IO (Handle, hPutStrLn)
 import Text.Megaparsec (TraversableStream)
 import Text.Megaparsec.Error
 import Text.Megaparsec.Stream (VisualStream)
@@ -36,16 +34,17 @@
 formatPos :: Filename -> Linenumber -> Text.Text
 formatPos source line = source <> ":" <> Text.pack (show line) <> " "
 
-printResults ::
+hWrite ::
   (VisualStream s, TraversableStream s, ShowErrorComponent e, Foldl.Foldable f) =>
+  Handle ->
   f (Result s e) ->
   Bool ->
   IO ()
-printResults results color = mapM_ printResult results
+hWrite handle results color = mapM_ writeResult results
   where
-    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
+    writeResult Result {fileName, errors, checks} = writeErrors errors >> writeChecks fileName checks
+    writeErrors = mapM_ (hPutStrLn handle . formatError)
+    writeChecks fileName = mapM_ (hPutStrLn handle . Text.unpack . formatCheck color fileName)
 
 colorizedSeverity :: DLSeverity -> Text.Text
 colorizedSeverity s =
diff --git a/src/Hadolint/Lint.hs b/src/Hadolint/Lint.hs
--- a/src/Hadolint/Lint.hs
+++ b/src/Hadolint/Lint.hs
@@ -6,10 +6,9 @@
   )
 where
 
-import Data.Sequence (Seq)
 import Data.Text (Text)
 import Hadolint.Config.Configuration (Configuration (..))
-import Hadolint.Rule (RuleCode, DLSeverity (..), CheckFailure (..))
+import Hadolint.Rule (RuleCode, DLSeverity (..), CheckFailure (..), Failures)
 import Language.Docker.Parser (DockerfileError, Error)
 import Language.Docker.Syntax (Dockerfile)
 import qualified Control.Parallel.Strategies as Parallel
@@ -57,7 +56,7 @@
       ]
     parallelRun = Parallel.parList Parallel.rseq
 
-analyze :: Configuration -> Dockerfile -> Seq Hadolint.Rule.CheckFailure
+analyze :: Configuration -> Dockerfile -> Failures
 analyze config dockerfile = fixer process
   where
     fixer = fixSeverity config
@@ -65,8 +64,8 @@
 
 fixSeverity ::
   Configuration ->
-  Seq CheckFailure ->
-  Seq CheckFailure
+  Failures ->
+  Failures
 fixSeverity Configuration {..} =
   Seq.filter ignoredRules . Seq.mapWithIndex (const correctSeverity)
   where
diff --git a/src/Hadolint/Pragma.hs b/src/Hadolint/Pragma.hs
--- a/src/Hadolint/Pragma.hs
+++ b/src/Hadolint/Pragma.hs
@@ -1,5 +1,6 @@
 module Hadolint.Pragma
   ( ignored,
+    stageIgnored,
     globalIgnored,
     parseIgnorePragma,
     parseShell
@@ -27,6 +28,19 @@
         _ -> acc
     parse acc _ = acc
 
+stageIgnored :: Foldl.Fold (InstructionPos Text) (Map.IntMap (Set.Set RuleCode))
+stageIgnored = Foldl.Fold parse mempty id
+  where
+    parse acc InstructionPos { instruction = Comment comment, lineNumber = line } =
+      case parseStageIgnorePragma comment of
+        Just ignores@(_ : _) -> Map.insert (line + 1) ( Set.fromList . fmap RuleCode $ ignores ) acc
+        _ -> acc
+    parse acc InstructionPos { instruction = From _ , lineNumber = line } =
+      case Map.lookup line acc of
+        Just _ -> acc
+        _ -> Map.insert line mempty acc
+    parse acc _ = acc
+
 globalIgnored :: Foldl.Fold (InstructionPos Text) (Set.Set RuleCode)
 globalIgnored = Foldl.Fold parse mempty id
   where
@@ -39,17 +53,26 @@
 parseIgnorePragma :: Text -> Maybe [Text]
 parseIgnorePragma = Megaparsec.parseMaybe ignoreParser
 
+parseStageIgnorePragma :: Text -> Maybe [Text]
+parseStageIgnorePragma = Megaparsec.parseMaybe stageIgnoreParser
+
 parseGlobalIgnorePragma :: Text -> Maybe [Text]
 parseGlobalIgnorePragma = Megaparsec.parseMaybe globalIgnoreParser
 
 ignoreParser :: Megaparsec.Parsec Void Text [Text]
 ignoreParser = hadolintPragma >> ignore
 
+stageIgnoreParser :: Megaparsec.Parsec Void Text [Text]
+stageIgnoreParser = hadolintPragma >> stage >> ignore
+
 globalIgnoreParser :: Megaparsec.Parsec Void Text [Text]
 globalIgnoreParser = hadolintPragma >> global >> ignore
 
 hadolintPragma :: Megaparsec.Parsec Void Text Text
 hadolintPragma = spaces >> string "hadolint" >> spaces1
+
+stage :: Megaparsec.Parsec Void Text Text
+stage = string "stage" >> spaces1
 
 global :: Megaparsec.Parsec Void Text Text
 global = string "global" >> spaces1
diff --git a/src/Hadolint/Process.hs b/src/Hadolint/Process.hs
--- a/src/Hadolint/Process.hs
+++ b/src/Hadolint/Process.hs
@@ -69,6 +69,11 @@
 import qualified Hadolint.Rule.DL3060
 import qualified Hadolint.Rule.DL3061
 import qualified Hadolint.Rule.DL3062
+import qualified Hadolint.Rule.DL3063
+import qualified Hadolint.Rule.DL3064
+import qualified Hadolint.Rule.DL3065
+import qualified Hadolint.Rule.DL3066
+import qualified Hadolint.Rule.DL3067
 import qualified Hadolint.Rule.DL4000
 import qualified Hadolint.Rule.DL4001
 import qualified Hadolint.Rule.DL4003
@@ -82,6 +87,8 @@
 data AnalisisResult = AnalisisResult
   { -- | The set of ignored rules per line
     ignored :: SMap.IntMap (Set.Set RuleCode),
+    -- | The set of ignored rules per build stage
+    stageIgnored :: SMap.IntMap (Set.Set RuleCode),
     -- | The set of globally ignored rules
     globalIgnored :: Set.Set RuleCode,
     -- | A set of failures collected for reach rule
@@ -95,17 +102,32 @@
 
     shouldKeep CheckFailure {line, code}
       | disableIgnorePragma config = True
-      | code `Set.member` globalIgnored = False
-      | otherwise = Just True /= do
-          ignoreList <- SMap.lookup line ignored
-          return $ code `Set.member` ignoreList
+      | otherwise = not $
+          code `Set.member` Set.unions [ ignores line, stageIgnores line, globalIgnores ]
 
+    ignores :: Int -> Set.Set RuleCode
+    ignores line =
+      case SMap.lookup line ignored of
+        Just set -> set
+        _ -> Set.empty
+
+    stageIgnores :: Int -> Set.Set RuleCode
+    stageIgnores line = do
+      let ls = 0:[x | x <- SMap.keys stageIgnored, x < line]
+      case SMap.lookup (last ls) stageIgnored of
+        Just set -> set
+        _ -> Set.empty
+
+    globalIgnores :: Set.Set RuleCode
+    globalIgnores = globalIgnored
+
 analyze ::
   Configuration ->
   Foldl.Fold (InstructionPos Text.Text) AnalisisResult
 analyze config =
   AnalisisResult
     <$> Hadolint.Pragma.ignored
+    <*> Hadolint.Pragma.stageIgnored
     <*> Hadolint.Pragma.globalIgnored
     <*> Foldl.premap parseShell (failures config)
 
@@ -174,6 +196,11 @@
     <> Hadolint.Rule.DL3060.rule
     <> Hadolint.Rule.DL3061.rule
     <> Hadolint.Rule.DL3062.rule
+    <> Hadolint.Rule.DL3063.rule
+    <> Hadolint.Rule.DL3064.rule
+    <> Hadolint.Rule.DL3065.rule
+    <> Hadolint.Rule.DL3066.rule
+    <> Hadolint.Rule.DL3067.rule
     <> Hadolint.Rule.DL4000.rule
     <> Hadolint.Rule.DL4001.rule
     <> Hadolint.Rule.DL4003.rule
diff --git a/src/Hadolint/Rule/DL3010.hs b/src/Hadolint/Rule/DL3010.hs
--- a/src/Hadolint/Rule/DL3010.hs
+++ b/src/Hadolint/Rule/DL3010.hs
@@ -25,7 +25,7 @@
     message = "Use `ADD` for extracting archives into an image"
 
     check _ _ (From _) = emptyState Empty
-    check line st (Copy (CopyArgs srcs tgt) (CopyFlags _ _ _ NoSource _)) =
+    check line st (Copy (CopyArgs srcs tgt) (CopyFlags _ _ _ _ NoSource _)) =
       st |> modify (rememberArchives line srcs tgt)
     check _ st (Run (RunArgs args _))
       | Acc archives _ <- state st,
diff --git a/src/Hadolint/Rule/DL3013.hs b/src/Hadolint/Rule/DL3013.hs
--- a/src/Hadolint/Rule/DL3013.hs
+++ b/src/Hadolint/Rule/DL3013.hs
@@ -80,8 +80,10 @@
           "prefix",
           "progress-bar",
           "proxy",
+          "python",
           "python-version",
           "root",
+          "root-user-action",
           "src",
           "t",
           "target",
diff --git a/src/Hadolint/Rule/DL3016.hs b/src/Hadolint/Rule/DL3016.hs
--- a/src/Hadolint/Rule/DL3016.hs
+++ b/src/Hadolint/Rule/DL3016.hs
@@ -81,7 +81,7 @@
 
 
 ignoreFlags :: [Text.Text]
-ignoreFlags = ["loglevel"]
+ignoreFlags = ["loglevel", "registry"]
 
 gitPrefixes :: [Text.Text]
 gitPrefixes = ["git://", "git+ssh://", "git+http://", "git+https://"]
diff --git a/src/Hadolint/Rule/DL3018.hs b/src/Hadolint/Rule/DL3018.hs
--- a/src/Hadolint/Rule/DL3018.hs
+++ b/src/Hadolint/Rule/DL3018.hs
@@ -1,12 +1,11 @@
 module Hadolint.Rule.DL3018 (rule) where
 
-import qualified Data.Text as Text
+import Data.Text qualified as Text
 import Hadolint.Rule
 import Hadolint.Shell (ParsedShell)
-import qualified Hadolint.Shell as Shell
+import Hadolint.Shell qualified as Shell
 import Language.Docker.Syntax
 
-
 rule :: Rule ParsedShell
 rule = dl3018 <> onbuild dl3018
 {-# INLINEABLE rule #-}
@@ -24,22 +23,22 @@
         ( \as ->
             and
               [ versionFixed p || packageFile p
-                | p <- apkAddPackages as
+              | p <- apkAddPackages as
               ]
         )
         args
     check _ = True
-    versionFixed package = "=" `Text.isInfixOf` package
+    versionFixed package = any (`Text.isInfixOf` package) ["=", "~", ">", "<"]
     packageFile package = ".apk" `Text.isSuffixOf` package
 {-# INLINEABLE dl3018 #-}
 
 apkAddPackages :: ParsedShell -> [Text.Text]
 apkAddPackages args =
   [ arg
-    | cmd <- Shell.presentCommands args,
-      Shell.cmdHasArgs "apk" ["add"] cmd,
-      arg <- Shell.getArgsNoFlags (dropTarget cmd),
-      arg /= "add"
+  | 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/DL3022.hs b/src/Hadolint/Rule/DL3022.hs
--- a/src/Hadolint/Rule/DL3022.hs
+++ b/src/Hadolint/Rule/DL3022.hs
@@ -20,7 +20,7 @@
 
     check _ st (From BaseImage {alias = Just (ImageAlias als)}) = st |> modify (incAndAddName als)
     check _ st (From BaseImage {}) = st |> modify incCount
-    check line st (Copy (CopyArgs _ _) (CopyFlags _ _ _ (CopySource s) _))
+    check line st (Copy (CopyArgs _ _) (CopyFlags _ _ _ _ (CopySource s) _))
       | ":" `Text.isInfixOf` dropQuotes s = st
       | isMember s (state st) = st
       | otherwise = case Read.decimal s of
diff --git a/src/Hadolint/Rule/DL3023.hs b/src/Hadolint/Rule/DL3023.hs
--- a/src/Hadolint/Rule/DL3023.hs
+++ b/src/Hadolint/Rule/DL3023.hs
@@ -11,7 +11,7 @@
     message = "`COPY --from` cannot reference its own `FROM` alias"
 
     check _ st f@(From _) = st |> replaceWith (Just f) -- Remember the last FROM instruction found
-    check line st@(State _ (Just fromInstr)) (Copy (CopyArgs _ _) (CopyFlags _ _ _ (CopySource stageName) _))
+    check line st@(State _ (Just fromInstr)) (Copy (CopyArgs _ _) (CopyFlags _ _ _ _ (CopySource stageName) _))
       | aliasMustBe (/= stageName) fromInstr = st
       | otherwise = st |> addFail CheckFailure {..}
     -- cannot copy from the same stage!
diff --git a/src/Hadolint/Rule/DL3024.hs b/src/Hadolint/Rule/DL3024.hs
--- a/src/Hadolint/Rule/DL3024.hs
+++ b/src/Hadolint/Rule/DL3024.hs
@@ -11,10 +11,8 @@
     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 line st (From BaseImage {alias = Just (ImageAlias als)})
+      | Set.member als (state st) = st |> addFail CheckFailure {..}
+      | otherwise = st |> modify (Set.insert als)
     check _ st _ = st
 {-# INLINEABLE rule #-}
diff --git a/src/Hadolint/Rule/DL3025.hs b/src/Hadolint/Rule/DL3025.hs
--- a/src/Hadolint/Rule/DL3025.hs
+++ b/src/Hadolint/Rule/DL3025.hs
@@ -12,5 +12,6 @@
 
     check (Cmd (ArgumentsText _)) = False
     check (Entrypoint (ArgumentsText _)) = False
+    check (Healthcheck (Check (CheckArgs (ArgumentsText _) _ _ _ _ _))) = False
     check _ = True
 {-# INLINEABLE rule #-}
diff --git a/src/Hadolint/Rule/DL3026.hs b/src/Hadolint/Rule/DL3026.hs
--- a/src/Hadolint/Rule/DL3026.hs
+++ b/src/Hadolint/Rule/DL3026.hs
@@ -1,7 +1,9 @@
 module Hadolint.Rule.DL3026 (rule) where
 
+import qualified Data.Maybe as Maybe
 import qualified Data.Set as Set
-import Data.Text (Text, pack, drop, dropEnd, isSuffixOf, isPrefixOf)
+import Data.String ( IsString(..) )
+import Data.Text (Text, pack, unpack, drop, dropEnd, isSuffixOf, isPrefixOf)
 import Hadolint.Rule
 import Language.Docker.Syntax
 
@@ -17,8 +19,25 @@
        in if doCheck (state st) image
             then newState
             else newState |> addFail CheckFailure {..}
+    check line st (Copy _ (CopyFlags _ _ _ _ (CopySource src) _)) =
+      let img = fromString ( Data.Text.unpack src )
+       in if doCheck (state st) img
+            then st
+            else st |> addFail CheckFailure {..}
+    check line st (Run (RunArgs _ (RunFlags m _ _))) =
+       if null $ Set.filter (not . checkMount st) m
+         then st
+         else st |> addFail CheckFailure {..}
     check _ st _ = st
 
+    checkMount st (CacheMount (CacheOpts _ _ _ _ fi _ _ _ _)) =
+      let img = fromString $ unpack $ Maybe.fromMaybe "scratch" fi
+       in doCheck (state st) img
+    checkMount st (BindMount (BindOpts _ _ fi _ _)) =
+      let img = fromString $ unpack $ Maybe.fromMaybe "scratch" fi
+       in doCheck (state st) img
+    checkMount _ _ = True
+
     doCheck st img = Set.member (toImageAlias img) st || Set.null allowed || isAllowed img
 
     toImageAlias = Just . ImageAlias . imageName
@@ -29,13 +48,13 @@
         || isRegistryAllowed "hub.docker.com"
 
     isRegistryAllowed registry = any (\p -> matchRegistry (unRegistry p) registry) allowed
-
-    matchRegistry :: Text -> Text -> Bool
-    matchRegistry allow registry | allow == star = True
-                                 | star `isPrefixOf` allow = Data.Text.drop 1 allow `isSuffixOf` registry
-                                 | star `isSuffixOf` allow = Data.Text.dropEnd 1 allow `isPrefixOf` registry
-                                 | otherwise = registry == allow
-                                  where
-                                      star = pack "*"
-
 {-# INLINEABLE rule #-}
+
+matchRegistry :: Text -> Text -> Bool
+matchRegistry allow registry
+  | allow == star = True
+  | star `isPrefixOf` allow = Data.Text.drop 1 allow `isSuffixOf` registry
+  | star `isSuffixOf` allow = Data.Text.dropEnd 1 allow `isPrefixOf` registry
+  | otherwise = registry == allow
+  where
+      star = pack "*"
diff --git a/src/Hadolint/Rule/DL3032.hs b/src/Hadolint/Rule/DL3032.hs
--- a/src/Hadolint/Rule/DL3032.hs
+++ b/src/Hadolint/Rule/DL3032.hs
@@ -18,9 +18,9 @@
 
     check (Run (RunArgs args _)) =
       foldArguments (Shell.noCommands yumInstall) args
-        || ( foldArguments (Shell.anyCommands yumInstall) args
-               && foldArguments (Shell.anyCommands yumClean) args
-           )
+        || Just True == (
+             (<) <$> foldArguments (Shell.findCommandIndex yumInstall) args
+                 <*> foldArguments (Shell.findCommandIndex yumClean) args)
     check _ = True
 
     yumInstall = Shell.cmdHasArgs "yum" ["install"]
diff --git a/src/Hadolint/Rule/DL3033.hs b/src/Hadolint/Rule/DL3033.hs
--- a/src/Hadolint/Rule/DL3033.hs
+++ b/src/Hadolint/Rule/DL3033.hs
@@ -1,5 +1,8 @@
 module Hadolint.Rule.DL3033 (rule) where
 
+import qualified Data.Map as Map
+import qualified Data.Maybe as Maybe
+import qualified Data.Set as Set
 import qualified Data.Text as Text
 import Hadolint.Rule
 import qualified Hadolint.Shell as Shell
@@ -7,23 +10,38 @@
 import Data.Char (isDigit, isAsciiUpper, isAsciiLower)
 
 
+data Acc
+  = Acc { stageIdx :: Int,
+          stageAliases :: Map.Map Int Text.Text,
+          envs :: Map.Map Int (Set.Set Text.Text),
+          args :: Set.Set Text.Text
+        }
+  | Empty
+  deriving (Show)
+
+
 rule :: Rule Shell.ParsedShell
 rule = dl3033 <> onbuild dl3033
 {-# INLINEABLE rule #-}
 
 dl3033 :: Rule Shell.ParsedShell
-dl3033 = simpleRule code severity message check
+dl3033 = customRule check (emptyState Empty)
   where
     code = "DL3033"
     severity = DLWarningC
     message = "Specify version with `yum install -y <package>-<version>`."
 
-    check (Run (RunArgs args _)) =
-      foldArguments (all packageVersionFixed . yumPackages) args
-        && foldArguments (all moduleVersionFixed . yumModules) args
-    check _ = True
+    check line st (Run (RunArgs a _))
+      | foldArguments (all ( packageVersionFixed ( state st ) ) . yumPackages) a
+          && foldArguments (all moduleVersionFixed . yumModules) a = st
+      | otherwise = st |> addFail CheckFailure {..}
+    check _ st (Env pairs) = st |> modify (registerEnvs pairs)
+    check _ st (Arg arg _) = st |> modify (registerArg arg)
+    check _ st (From bi) = st |> modify (newStage bi)
+    check _ st _ = st
 {-# INLINEABLE dl3033 #-}
 
+
 yumPackages :: Shell.ParsedShell -> [Text.Text]
 yumPackages args =
   [ arg
@@ -32,21 +50,34 @@
       arg <- installFilter cmd
   ]
 
-packageVersionFixed :: Text.Text -> Bool
-packageVersionFixed package
+packageVersionFixed :: Acc -> Text.Text -> Bool
+packageVersionFixed acc package
   | length parts <= 1 = False  -- No dashes, definitively no version
   | ".rpm" `Text.isSuffixOf` package = True  -- rpm files always have a version
+  | "$" `Text.isInfixOf` package = envDefined acc package
   | otherwise = isVersionLike $ drop 1 parts
   where
     parts = Text.splitOn "-" package
 
+envDefined :: Acc -> Text.Text -> Bool
+envDefined Empty _ = False
+envDefined (Acc stageIdx _ envs args) package =
+  any (`varInText` package) (thisStage envs)
+    || any (`varInText` package) args
+  where
+    thisStage envsMap = Maybe.fromMaybe Set.empty $ Map.lookup stageIdx envsMap
+
+varInText :: Text.Text -> Text.Text -> Bool
+varInText var txt =
+  ( Text.pack "${" <> var <> Text.pack "}" ) `Text.isInfixOf` txt
+
 isVersionLike :: [Text.Text] -> Bool
 isVersionLike parts =
   case parts of
     [] -> False  -- No parts after splitting by hyphen
     _ -> all partIsValid parts && any partStartsWithDigit parts
   where
-    partIsValid part = Text.all isVersionChar part
+    partIsValid = Text.all isVersionChar
     partStartsWithDigit part = case Text.uncons part of
                                  Just (c, _) -> isDigit c
                                  Nothing -> False -- Empty Text
@@ -77,3 +108,74 @@
       arg /= "install",
       arg /= "module"
   ]
+
+registerEnvs :: Pairs -> Acc -> Acc
+registerEnvs pairs Empty =
+  Acc
+    { stageIdx = 0,
+      stageAliases = Map.singleton 0 (Text.pack ""),
+      envs = Map.singleton 0 (Set.fromList (map fst pairs)),
+      args = Set.empty
+    }
+registerEnvs pairs (Acc stageIdx stageAliases envs args) =
+  Acc
+    { stageIdx,
+      stageAliases,
+      envs = Map.adjust stageEnvs stageIdx envs,
+      args
+    }
+  where
+    stageEnvs = Set.union (Set.fromList (map fst pairs))
+
+registerArg :: Text.Text -> Acc -> Acc
+registerArg arg Empty =
+  Acc
+    { stageIdx = 0,
+      stageAliases = Map.singleton 0 (Text.pack ""),
+      envs = Map.singleton 0 Set.empty,
+      args = Set.singleton arg
+    }
+registerArg arg (Acc stageIdx stageAliases envs args) =
+  Acc
+    { stageIdx,
+      stageAliases,
+      envs,
+      args = Set.insert arg args
+    }
+
+newStage :: BaseImage -> Acc -> Acc
+newStage (BaseImage _ _ _ Nothing _) Empty =
+  Acc
+    { stageIdx = 1,
+      stageAliases = Map.singleton 1 (Text.pack ""),
+      envs = Map.singleton 1 Set.empty,
+      args = Set.empty
+    }
+newStage (BaseImage _ _ _ (Just alias) _) Empty =
+  Acc
+    { stageIdx = 1,
+      stageAliases = Map.singleton 1 (unImageAlias alias),
+      envs = Map.singleton 1 Set.empty,
+      args = Set.empty
+    }
+newStage (BaseImage image _ _ alias _) (Acc stageIdx stageAliases envs args) =
+  Acc
+    { stageIdx = stageIdx + 1,
+      stageAliases = Map.insert (stageIdx + 1) (als alias) stageAliases,
+      envs = Map.insert (stageIdx + 1) (stageEnvs stageIdxOfAlias) envs,
+      args
+    }
+  where
+    als :: Maybe ImageAlias -> Text.Text
+    als Nothing = Text.pack ""
+    als (Just a) = unImageAlias a
+
+    stageIdxOfAlias :: Int
+    stageIdxOfAlias = do
+      let l = Map.toList (Map.filter (== imageName image) stageAliases)
+       in case l of
+            [] -> stageIdx + 1
+            x:_ -> fst x
+
+    stageEnvs :: Int -> Set.Set Text.Text
+    stageEnvs idx = Maybe.fromMaybe Set.empty $ Map.lookup idx envs
diff --git a/src/Hadolint/Rule/DL3036.hs b/src/Hadolint/Rule/DL3036.hs
--- a/src/Hadolint/Rule/DL3036.hs
+++ b/src/Hadolint/Rule/DL3036.hs
@@ -20,8 +20,9 @@
     check (Run (RunArgs args flags))
       | foldArguments (Shell.noCommands zypperInstall) args = True
       | Utils.hasCacheOrTmpfsMountWith "/var/cache/zypp" flags = True
-      | foldArguments (Shell.anyCommands zypperInstall) args
-          && foldArguments (Shell.anyCommands zypperClean) args = True
+      | Just True == (
+             (<) <$> foldArguments (Shell.findCommandIndex zypperInstall) args
+                 <*> foldArguments (Shell.findCommandIndex zypperClean) args) = True
       | otherwise = False
     check _ = True
 
diff --git a/src/Hadolint/Rule/DL3040.hs b/src/Hadolint/Rule/DL3040.hs
--- a/src/Hadolint/Rule/DL3040.hs
+++ b/src/Hadolint/Rule/DL3040.hs
@@ -26,12 +26,15 @@
 
     checkMissingClean args cmdName =
       foldArguments (Shell.noCommands $ dnfInstall cmdName) args
-        || ( foldArguments (Shell.anyCommands $ dnfInstall cmdName) args
-               && foldArguments (Shell.anyCommands $ dnfClean cmdName) args
-           )
+        || Just True == (
+             (<) <$> foldArguments (Shell.findCommandIndex $ dnfInstall cmdName) args
+                 <*> foldArguments (Shell.findCommandIndex $ dnfClean cmdName) args)
 
-    dnfInstall cmdName = Shell.cmdHasArgs cmdName ["install"]
+    dnfInstall cmdName args = ( cmdName `elem` dnfCmds )
+      && Shell.cmdHasArgs cmdName installCmds args
     dnfClean cmdName args = Shell.cmdHasArgs cmdName ["clean", "all"] args
       || Shell.cmdHasArgs "rm" ["-rf", "/var/cache/libdnf5*"] args
     dnfCmds = ["dnf", "microdnf"]
+    installCmds = ["install", "in", "upgrade", "up", "upgrade-minimal", "up-min", "reinstall", "rei"]
 {-# INLINEABLE dl3040 #-}
+
diff --git a/src/Hadolint/Rule/DL3041.hs b/src/Hadolint/Rule/DL3041.hs
--- a/src/Hadolint/Rule/DL3041.hs
+++ b/src/Hadolint/Rule/DL3041.hs
@@ -1,28 +1,47 @@
 module Hadolint.Rule.DL3041 (rule) where
 
+import qualified Data.Map as Map
+import qualified Data.Maybe as Maybe
+import qualified Data.Set as Set
 import qualified Data.Text as Text
 import Hadolint.Rule
 import qualified Hadolint.Shell as Shell
 import Language.Docker.Syntax
 import Data.Char (isDigit, isAsciiUpper, isAsciiLower)
 
+
+data Acc
+  = Acc { stageIdx :: Int,
+          stageAliases :: Map.Map Int Text.Text,
+          envs :: Map.Map Int (Set.Set Text.Text),
+          args :: Set.Set Text.Text
+        }
+  | Empty
+  deriving (Show)
+
+
 rule :: Rule Shell.ParsedShell
 rule = dl3041 <> onbuild dl3041
 {-# INLINEABLE rule #-}
 
 dl3041 :: Rule Shell.ParsedShell
-dl3041 = simpleRule code severity message check
+dl3041 = customRule check (emptyState Empty)
   where
     code = "DL3041"
     severity = DLWarningC
     message = "Specify version with `dnf install -y <package>-<version>`."
 
-    check (Run (RunArgs args _)) =
-      foldArguments (all packageVersionFixed . dnfPackages) args
-        && foldArguments (all moduleVersionFixed . dnfModules) args
-    check _ = True
+    check line st (Run (RunArgs a _))
+      | foldArguments (all ( packageVersionFixed ( state st ) ) . dnfPackages) a
+          && foldArguments (all moduleVersionFixed . dnfModules) a = st
+      | otherwise = st |> addFail CheckFailure {..}
+    check _ st (Env pairs) = st |> modify (registerEnvs pairs)
+    check _ st (Arg arg _) = st |> modify (registerArg arg)
+    check _ st (From bi) = st |> modify (newStage bi)
+    check _ st _ = st
 {-# INLINEABLE dl3041 #-}
 
+
 dnfCmds :: [Text.Text]
 dnfCmds = ["dnf", "microdnf"]
 
@@ -30,25 +49,38 @@
 dnfPackages args =
     [ arg
       | cmd <- Shell.presentCommands args,
-        not (Shell.cmdsHaveArgs dnfCmds ["module"] cmd),
+        not (Shell.cmdsHaveArgs dnfCmds ["module", "group"] cmd),
         arg <- installFilter cmd
     ]
 
-packageVersionFixed :: Text.Text -> Bool
-packageVersionFixed package
+packageVersionFixed :: Acc -> Text.Text -> Bool
+packageVersionFixed acc package
   | length parts <= 1 = False  -- No dashes, definitively no version
   | ".rpm" `Text.isSuffixOf` package = True  -- rpm files always have a version
+  | "$" `Text.isInfixOf` package = envDefined acc package
   | otherwise = isVersionLike $ drop 1 parts
   where
     parts = Text.splitOn "-" package
 
+envDefined :: Acc -> Text.Text -> Bool
+envDefined Empty _ = False
+envDefined (Acc stageIdx _ envs args) package =
+  any (`varInText` package) (thisStage envs)
+    || any (`varInText` package) args
+  where
+    thisStage envsMap = Maybe.fromMaybe Set.empty $ Map.lookup stageIdx envsMap
+
+varInText :: Text.Text -> Text.Text -> Bool
+varInText var txt =
+  ( Text.pack "${" <> var <> Text.pack "}" ) `Text.isInfixOf` txt
+
 isVersionLike :: [Text.Text] -> Bool
 isVersionLike parts =
   case parts of
     [] -> False  -- No parts after splitting by hyphen
     _ -> all partIsValid parts && any partStartsWithDigit parts
   where
-    partIsValid part = Text.all isVersionChar part
+    partIsValid = Text.all isVersionChar
     partStartsWithDigit part = case Text.uncons part of
                                  Just (c, _) -> isDigit c
                                  Nothing -> False -- Empty Text
@@ -79,3 +111,74 @@
       arg /= "install",
       arg /= "module"
   ]
+
+registerEnvs :: Pairs -> Acc -> Acc
+registerEnvs pairs Empty =
+  Acc
+    { stageIdx = 0,
+      stageAliases = Map.singleton 0 (Text.pack ""),
+      envs = Map.singleton 0 (Set.fromList (map fst pairs)),
+      args = Set.empty
+    }
+registerEnvs pairs (Acc stageIdx stageAliases envs args) =
+  Acc
+    { stageIdx,
+      stageAliases,
+      envs = Map.adjust stageEnvs stageIdx envs,
+      args
+    }
+  where
+    stageEnvs = Set.union (Set.fromList (map fst pairs))
+
+registerArg :: Text.Text -> Acc -> Acc
+registerArg arg Empty =
+  Acc
+    { stageIdx = 0,
+      stageAliases = Map.singleton 0 (Text.pack ""),
+      envs = Map.singleton 0 Set.empty,
+      args = Set.singleton arg
+    }
+registerArg arg (Acc stageIdx stageAliases envs args) =
+  Acc
+    { stageIdx,
+      stageAliases,
+      envs,
+      args = Set.insert arg args
+    }
+
+newStage :: BaseImage -> Acc -> Acc
+newStage (BaseImage _ _ _ Nothing _) Empty =
+  Acc
+    { stageIdx = 1,
+      stageAliases = Map.singleton 1 (Text.pack ""),
+      envs = Map.singleton 1 Set.empty,
+      args = Set.empty
+    }
+newStage (BaseImage _ _ _ (Just alias) _) Empty =
+  Acc
+    { stageIdx = 1,
+      stageAliases = Map.singleton 1 (unImageAlias alias),
+      envs = Map.singleton 1 Set.empty,
+      args = Set.empty
+    }
+newStage (BaseImage image _ _ alias _) (Acc stageIdx stageAliases envs args) =
+  Acc
+    { stageIdx = stageIdx + 1,
+      stageAliases = Map.insert (stageIdx + 1) (als alias) stageAliases,
+      envs = Map.insert (stageIdx + 1) (stageEnvs stageIdxOfAlias) envs,
+      args
+    }
+  where
+    als :: Maybe ImageAlias -> Text.Text
+    als Nothing = Text.pack ""
+    als (Just a) = unImageAlias a
+
+    stageIdxOfAlias :: Int
+    stageIdxOfAlias = do
+      let l = Map.toList (Map.filter (== imageName image) stageAliases)
+       in case l of
+            [] -> stageIdx + 1
+            x:_ -> fst x
+
+    stageEnvs :: Int -> Set.Set Text.Text
+    stageEnvs idx = Maybe.fromMaybe Set.empty $ Map.lookup idx envs
diff --git a/src/Hadolint/Rule/DL3047.hs b/src/Hadolint/Rule/DL3047.hs
--- a/src/Hadolint/Rule/DL3047.hs
+++ b/src/Hadolint/Rule/DL3047.hs
@@ -24,7 +24,7 @@
 
     forgotProgress cmd = isWget cmd && (not (hasProgressOption cmd) && not (hasSpecialFlags cmd))
     isWget (Shell.Command name _ _) = name == "wget"
-    hasProgressOption cmd = Shell.hasFlag "progress" cmd
+    hasProgressOption = Shell.hasFlag "progress"
 
     hasSpecialFlags cmd =
       hasQuietFlag cmd
@@ -32,9 +32,9 @@
         || 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
+    hasQuietFlag = Shell.hasAnyFlag ["q", "quiet"]
+    hasOutputFlag = Shell.hasAnyFlag ["o", "output-file"]
+    hasAppendOutputFlag = Shell.hasAnyFlag ["a", "append-output"]
     hasNoVerboseFlag cmd =
       Shell.hasAnyFlag ["no-verbose"] cmd
         || Shell.cmdHasArgs "wget" ["-nv"] cmd
diff --git a/src/Hadolint/Rule/DL3049.hs b/src/Hadolint/Rule/DL3049.hs
--- a/src/Hadolint/Rule/DL3049.hs
+++ b/src/Hadolint/Rule/DL3049.hs
@@ -34,7 +34,7 @@
     message = "Label `" <> label <> "` is missing."
     check line state (From img) =
       state |> modify (currentStage (StageID img line))
-    check _ state (Copy (CopyArgs _ _) (CopyFlags _ _ _ (CopySource src) _)) =
+    check _ state (Copy (CopyArgs _ _) (CopyFlags _ _ _ _ (CopySource src) _)) =
       state |> modify (markSilentByAlias src)
     check _ state (Label pairs)
       | label `elem` fmap fst pairs =
diff --git a/src/Hadolint/Rule/DL3060.hs b/src/Hadolint/Rule/DL3060.hs
--- a/src/Hadolint/Rule/DL3060.hs
+++ b/src/Hadolint/Rule/DL3060.hs
@@ -34,6 +34,11 @@
           && foldArguments (Shell.noCommands yarnCacheClean) args
           && not (Utils.hasCacheOrTmpfsMountWith ".cache/yarn" flags) =
         st |> modify (rememberLine line)
+    check line st (Run (RunArgs args _))
+      | foldArguments (Shell.anyCommands yarnInstall) args
+          && foldArguments (Shell.anyCommands yarnCacheClean) args
+          && foldArguments isCleanBeforeInstall args =
+        st |> modify (rememberLine line)
       | otherwise = st
     check _ st _ = st
 
@@ -64,6 +69,14 @@
 
 yarnCacheClean :: Shell.Command -> Bool
 yarnCacheClean = Shell.cmdHasArgs "yarn" ["cache", "clean"]
+
+isCleanBeforeInstall :: Shell.ParsedShell -> Bool
+isCleanBeforeInstall args =
+  case ( Shell.findCommandIndex yarnCacheClean args,
+         Shell.findCommandIndex yarnInstall args
+       ) of
+    (Just cleanIdx, Just installIdx) -> cleanIdx < installIdx
+    _ -> False
 
 -- | This is needed as placeholder when no FROM statement has yet been
 -- encountered.
diff --git a/src/Hadolint/Rule/DL3062.hs b/src/Hadolint/Rule/DL3062.hs
--- a/src/Hadolint/Rule/DL3062.hs
+++ b/src/Hadolint/Rule/DL3062.hs
@@ -16,23 +16,35 @@
     severity = DLWarningC
     message = "Pin versions in go. Instead of `go install <package>` use `go install <package>@<version>`"
 
-    check (Run (RunArgs args _)) = foldArguments (all checkGoPackageVersion . getGoPackage) args
+    check (Run (RunArgs args _)) = foldArguments (all checkGoPackageVersion . goPackageCommand) args
     check _ = True
 {-# INLINEABLE dl3062 #-}
 
-goCommands :: [Text.Text]
-goCommands = ["install", "get", "run"]
+goPackageCommand :: Shell.ParsedShell -> [Text.Text]
+goPackageCommand args
+  | not . null $ runGoPackage args = runGoPackage args
+  | not . null $ getGoPackage args = getGoPackage args
+  | otherwise = []
 
-isGoCommand :: Shell.Command -> Bool
-isGoCommand = Shell.cmdsHaveArgs ["go"] goCommands
+runGoPackage :: Shell.ParsedShell -> [Text.Text]
+runGoPackage args =
+  [ arg
+    | cmd <- Shell.presentCommands args,
+      Shell.cmdsHaveArgs ["go"] ["run"] cmd,
+      (idx, arg) <- enum $ Shell.getArgsNoFlags cmd,
+      arg /= "run" && idx <= 1  -- everything after the package name is an argument to the go program
+  ]
+  where
+    enum :: [Text.Text] -> [(Integer, Text.Text)]
+    enum = zip [0..]
 
 getGoPackage :: Shell.ParsedShell -> [Text.Text]
 getGoPackage args =
   [ arg
     | cmd <- Shell.presentCommands args,
-      isGoCommand cmd,
+      Shell.cmdsHaveArgs ["go"] ["install", "get"] cmd,
       arg <- Shell.getArgsNoFlags cmd,
-      arg `notElem` goCommands
+      arg `notElem` ["install", "get", "tool"]
   ]
 
 hasVersionSymbol :: Text.Text -> Bool
@@ -42,5 +54,13 @@
 isTagsVersion package =
   or [("@" <> tag) `Text.isSuffixOf` package | tag <- ["latest", "none"]]
 
+isLocalPath :: Text.Text -> Bool
+isLocalPath p = p == "."
+  || "/" `Text.isPrefixOf` p
+  || "." `Text.isPrefixOf` p
+
 checkGoPackageVersion :: Text.Text -> Bool
-checkGoPackageVersion package = hasVersionSymbol package && not (isTagsVersion package)
+checkGoPackageVersion package =
+  isLocalPath package
+    || hasVersionSymbol package
+    && not (isTagsVersion package)
diff --git a/src/Hadolint/Rule/DL3063.hs b/src/Hadolint/Rule/DL3063.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3063.hs
@@ -0,0 +1,18 @@
+module Hadolint.Rule.DL3063 (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 = "DL3063"
+    severity = DLWarningC
+    message = "stage name should not be a reserved word"
+
+    check (From BaseImage {alias = Just (ImageAlias als)})
+      | als `elem` ["scratch", "context"] = False
+      | otherwise = True
+    check _ = True
+{-# INLINEABLE rule #-}
diff --git a/src/Hadolint/Rule/DL3064.hs b/src/Hadolint/Rule/DL3064.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3064.hs
@@ -0,0 +1,95 @@
+module Hadolint.Rule.DL3064 (rule) where
+
+import qualified Data.Text as Text
+import Hadolint.Rule
+import Hadolint.Shell (ParsedShell)
+import Language.Docker.Syntax
+
+
+rule :: Rule ParsedShell
+rule = dl3064 <> onbuild dl3064
+{-# INLINEABLE rule #-}
+
+dl3064 :: Rule ParsedShell
+dl3064 = simpleRule code severity message check
+  where
+    code = "DL3064"
+    severity = DLWarningC
+    message = "Potentially sensitive data should not be used in the `ARG` or `ENV` commands"
+
+    check (Arg name _) =
+      Text.toUpper name `notElem` knownSensitiveNames
+        && none (`Text.isInfixOf` Text.toLower name) suspiciousSubstrings
+    check (Env pairs) =
+      none (\s -> (Text.toUpper . fst) s `elem` knownSensitiveNames) pairs
+        && none (\s -> any ((s `Text.isInfixOf`) . Text.toLower . fst) pairs) suspiciousSubstrings
+    check _ = True
+{-# INLINEABLE dl3064 #-}
+
+none :: (a -> Bool) -> [a] -> Bool
+none p xs = not ( any p xs )
+
+knownSensitiveNames :: [Text.Text]
+knownSensitiveNames =
+  [
+    "ACCESS_TOKEN",
+    "APPLICATION_KEY",
+    "APP_SECRET",
+    "AUTH_TOKEN",
+    "AWS_ACCESS_KEY_ID",
+    "AWS_SECRET_ACCESS_KEY",
+    "BITTREX_API_KEY",
+    "BITTREX_API_SECRET",
+    "CF_PASSWORD",
+    "CF_USERNAME",
+    "CIRCLE_TOKEN",
+    "CI_DEPLOY_PASSWORD",
+    "CI_DEPLOY_USER",
+    "DOCKERHUB_PASSWORD",
+    "DOCKER_EMAIL",
+    "DOCKER_PASSWORD",
+    "DOCKER_USERNAME",
+    "FACEBOOK_ACCESS_TOKEN",
+    "FACEBOOK_APP_ID",
+    "FACEBOOK_APP_SECRET",
+    "FIREBASE_API_TOKEN",
+    "FIREBASE_TOKEN",
+    "FOSSA_API_KEY",
+    "GH_ENTERPRISE_TOKEN",
+    "GH_TOKEN",
+    "GITHUB_ENTERPRISE_TOKEN",
+    "GITHUB_TOKEN",
+    "HEROKU_API_KEY",
+    "HEROKU_API_USER",
+    "NPM_AUTH_TOKEN",
+    "NPM_TOKEN",
+    "OKTA_AUTHN_GROUPID",
+    "OKTA_CLIENT_ORGURL",
+    "OKTA_CLIENT_TOKEN",
+    "OKTA_OAUTH2_CLIENTID",
+    "OKTA_OAUTH2_CLIENTSECRET",
+    "OPENAI_API_KEY",
+    "OS_PASSWORD",
+    "OS_USERNAME",
+    "POSTGRES_PASSWORD",
+    "SLACK_TOKEN",
+    "STRIPE_API_KEY",
+    "STRIPE_DEVICE_NAME",
+    "TRAVIS_OS_NAME",
+    "TRAVIS_SECURE_ENV_VARS",
+    "TRAVIS_SUDO",
+    "VAULT_CLIENT_KEY",
+    "VAULT_TOKEN"
+  ]
+
+suspiciousSubstrings :: [Text.Text]
+suspiciousSubstrings =
+  [
+    "api_key",
+    "client_key",
+    "password",
+    "private",
+    "secret",
+    "token",
+    "username"
+  ]
diff --git a/src/Hadolint/Rule/DL3065.hs b/src/Hadolint/Rule/DL3065.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3065.hs
@@ -0,0 +1,18 @@
+module Hadolint.Rule.DL3065 (rule) where
+
+import Hadolint.Rule
+import Language.Docker.Syntax
+
+rule :: Rule args
+rule = simpleRule code severity message check
+  where
+    code = "DL3065"
+    severity = DLWarningC
+    message =
+      "Setting `FROM --platform` to predefined `$TARGETPLATFORM` in is\
+      \ redundant as this is the default behavior"
+
+    check (From (BaseImage _ _ _ _ (Just platform))) =
+      platform /= "$TARGETPLATFORM" && platform /= "${TARGETPLATFORM}"
+    check _ = True
+{-# INLINEABLE rule #-}
diff --git a/src/Hadolint/Rule/DL3066.hs b/src/Hadolint/Rule/DL3066.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3066.hs
@@ -0,0 +1,25 @@
+module Hadolint.Rule.DL3066 (rule) where
+
+import qualified Data.Char as Char
+import qualified Data.Text as Text
+import Hadolint.Rule
+import Language.Docker.Syntax
+
+rule :: Rule args
+rule = simpleRule code severity message check
+  where
+    code = "DL3066"
+    severity = DLInfoC
+    message = "Non-numeric user-id may not be resolvable by host system"
+
+    check (User u) = Text.all Char.isDigit $ getUid u
+    check _ = True
+{-# INLINEABLE rule #-}
+
+getUid :: Text.Text -> Text.Text
+getUid t
+  | ":" `Text.isInfixOf` t = u ( Text.splitOn ":" t )
+  | otherwise = t
+  where
+    u [] = ""
+    u (h:_) = h
diff --git a/src/Hadolint/Rule/DL3067.hs b/src/Hadolint/Rule/DL3067.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Rule/DL3067.hs
@@ -0,0 +1,22 @@
+module Hadolint.Rule.DL3067 (rule) where
+
+import Hadolint.Rule
+import Language.Docker.Syntax
+
+rule :: Rule args
+rule = simpleRule code severity message check
+  where
+    code = "DL3067"
+    severity = DLWarningC
+    message = "Do not copy an entire filesystem from another stage"
+
+    check (Copy (CopyArgs sources target) (CopyFlags _ _ _ _ (CopySource _) _)) =
+      not $ any isRootSource sources && isRootTarget target
+    check _ = True
+{-# INLINEABLE rule #-}
+
+isRootSource :: SourcePath -> Bool
+isRootSource (SourcePath source) = dropQuotes source == "/"
+
+isRootTarget :: TargetPath -> Bool
+isRootTarget (TargetPath target) = dropQuotes target == "/"
diff --git a/src/Hadolint/Rule/DL4006.hs b/src/Hadolint/Rule/DL4006.hs
--- a/src/Hadolint/Rule/DL4006.hs
+++ b/src/Hadolint/Rule/DL4006.hs
@@ -21,13 +21,12 @@
       | foldArguments isNonPosixShell args = st |> replaceWith True
       | otherwise = st |> replaceWith (foldArguments hasPipefailOption args)
     check line st@(State _ False) (Run (RunArgs args _))
-      | foldArguments hasPipes args = st |> addFail CheckFailure {..}
+      | foldArguments Shell.hasPipes args = st |> addFail CheckFailure {..}
       | otherwise = st
     check _ st _ = st
 
     isNonPosixShell (Shell.ParsedShell orig _ _) =
       any (`Text.isPrefixOf` orig) Shell.nonPosixShells
-    hasPipes script = Shell.hasPipes script
     hasPipefailOption script =
       not $
         null
diff --git a/src/Hadolint/Rule/Shellcheck.hs b/src/Hadolint/Rule/Shellcheck.hs
--- a/src/Hadolint/Rule/Shellcheck.hs
+++ b/src/Hadolint/Rule/Shellcheck.hs
@@ -1,5 +1,6 @@
 module Hadolint.Rule.Shellcheck (rule) where
 
+import qualified Data.Map as Map
 import qualified Data.Set as Set
 import qualified Data.Text as Text
 import Hadolint.Rule
@@ -13,7 +14,10 @@
 data Acc
   = Acc
       { opts :: Shell.ShellOpts,
-        defaultOpts :: Shell.ShellOpts
+        defaultOpts :: Shell.ShellOpts,
+        stageIdx :: Int,
+        stageOpts :: Map.Map Int Shell.ShellOpts,
+        stages :: Map.Map Int Text.Text
       }
   | Empty
 
@@ -25,7 +29,7 @@
 scrule :: Rule Shell.ParsedShell
 scrule = customRule check (emptyState Empty)
   where
-    check _ st (From _) = st |> modify newStage
+    check _ st (From bi) = st |> modify (newStage bi)
     check _ st (Arg name _) = st |> modify (addVars [name])
     check _ st (Env pairs) = st |> modify (addVars (map fst pairs))
     check _ st (Shell args) =
@@ -34,93 +38,156 @@
       case Hadolint.Pragma.parseShell com of
         Just sh -> st |> modify (shellPragma sh)
         _ -> st
-    check line st (Run (RunArgs args _)) = getFailures (state st) |> foldr addFail st
-      where
-        getFailures Empty = foldArguments (runShellCheck Shell.defaultShellOpts) args
-        getFailures s = foldArguments (runShellCheck (opts s)) args
-        runShellCheck options script =
-          Set.fromList
-            [ toFailure line c
-              | c <- Shell.shellcheck options script
-            ]
+    check line st (Run (RunArgs args@(ArgumentsText _) _)) = getFailures line args (state st) |> foldr addFail st
+    check _ st (Run (RunArgs (ArgumentsList _) _)) = st
     check _ st _ = st
+
+    getFailures line args Empty = foldArguments (runShellCheck line Shell.defaultShellOpts) args
+    getFailures line args s = foldArguments (runShellCheck line (opts s)) args
+
+    runShellCheck line options script =
+      Set.fromList
+        [ toFailure line c
+          | c <- Shell.shellcheck options script
+        ]
 {-# INLINEABLE scrule #-}
 
-newStage :: Acc -> Acc
-newStage Empty =
+newStage :: BaseImage -> Acc -> Acc
+newStage BaseImage {..} Empty =
   Acc
     { opts = Shell.defaultShellOpts,
-      defaultOpts = Shell.defaultShellOpts
-    }
-newStage Acc {..} =
-  Acc
-    { opts = defaultOpts,
-      defaultOpts
+      defaultOpts = Shell.defaultShellOpts,
+      stageIdx = 0,
+      stageOpts = Map.singleton 0 Shell.defaultShellOpts,
+      stages = fromAlias alias
     }
+  where
+    fromAlias Nothing = Map.empty
+    fromAlias (Just a) = Map.singleton 0 (unImageAlias a)
+newStage BaseImage {..} Acc {..} =
+  if Map.null (Map.filter (== imageName image) stages)
+  then
+    Acc
+      { opts = defaultOpts,
+        defaultOpts,
+        stageIdx = stageIdx + 1,
+        stageOpts = Map.insert (stageIdx + 1) defaultOpts stageOpts,
+        stages = fromAlias (stageIdx + 1) alias
+      }
+  else do
+  let idx = getIdx getList
+      theOpts = toOpts ( Map.lookup idx stageOpts )
+   in
+      Acc
+        { opts = theOpts,
+          defaultOpts,
+          stageIdx = stageIdx + 1,
+          stageOpts = Map.insert (stageIdx + 1) theOpts stageOpts,
+          stages = fromAlias (stageIdx + 1) alias
+        }
+  where
+    fromAlias _ Nothing = stages
+    fromAlias idx (Just a) = Map.insert idx (unImageAlias a) stages
 
+    getIdx [] = 0
+    getIdx [(k, _)] = k
+    getIdx ((k, _):_:_) = k
+
+    getList = Map.toList (Map.filter (== imageName image) stages)
+
+    toOpts Nothing = defaultOpts
+    toOpts (Just o) = o
+
 addVars :: [Text.Text] -> Acc -> Acc
-addVars vars Empty =
-  Acc
-    { opts =
+addVars vars Empty = do
+  let opts =
         Shell.ShellOpts
           { shellName = Shell.shellName Shell.defaultShellOpts,
             envVars = Shell.envVars Shell.defaultShellOpts <> Set.fromList vars
-          },
-      defaultOpts = Shell.defaultShellOpts
-    }
-addVars vars Acc {..} =
-  Acc
-    { opts =
+          }
+   in
+    Acc
+      {
+        opts,
+        defaultOpts = Shell.defaultShellOpts,
+        stageIdx = 0,
+        stageOpts = Map.singleton 0 opts,
+        stages = Map.empty
+      }
+addVars vars Acc {..} = do
+  let newOpts =
         Shell.ShellOpts
-          { shellName = Shell.shellName opts,
+          { shellName = Shell.shellName Shell.defaultShellOpts,
             envVars = Shell.envVars opts <> Set.fromList vars
-          },
-      defaultOpts
-    }
+          }
+   in
+    Acc
+      { opts = newOpts,
+        defaultOpts,
+        stageIdx,
+        stageOpts = Map.update (\_ -> Just newOpts) stageIdx stageOpts,
+        stages
+      }
 
 setShell :: Text.Text -> Acc -> Acc
-setShell sh Empty =
-  Acc
-    { opts =
+setShell sh Empty = do
+  let opts =
         Shell.ShellOpts
           { shellName = sh,
             envVars = Shell.envVars Shell.defaultShellOpts
-          },
-      defaultOpts = Shell.defaultShellOpts
-    }
-setShell sh Acc {..} =
-  Acc
-    { opts =
+          }
+   in
+    Acc
+      { opts,
+        defaultOpts = Shell.defaultShellOpts,
+        stageIdx = 0,
+        stageOpts = Map.singleton 0 opts,
+        stages = Map.empty
+      }
+setShell sh Acc {..} = do
+  let newOpts =
         Shell.ShellOpts
           { shellName = sh,
             envVars = Shell.envVars opts
-          },
-      defaultOpts
-    }
+          }
+   in
+    Acc
+      { opts = newOpts,
+        defaultOpts,
+        stageIdx,
+        stageOpts = Map.update (\_ -> Just newOpts) stageIdx stageOpts,
+        stages
+      }
 
 shellPragma :: Text.Text -> Acc -> Acc
-shellPragma sh Empty =
-  Acc
-    { opts =
-        Shell.ShellOpts
-          { shellName = sh,
-            envVars = Shell.envVars Shell.defaultShellOpts
-          },
-      defaultOpts =
+shellPragma sh Empty = do
+  let newOpts =
         Shell.ShellOpts
           { shellName = sh,
             envVars = Shell.envVars Shell.defaultShellOpts
           }
-    }
-shellPragma sh Acc {..} =
-  Acc
-    { opts =
+   in
+    Acc
+      { opts = newOpts,
+        defaultOpts = newOpts,
+        stageIdx = 0,
+        stageOpts = Map.singleton 0 newOpts,
+        stages = Map.empty
+      }
+shellPragma sh Acc {..} = do
+  let newOpts =
         Shell.ShellOpts
           { shellName = sh,
             envVars = Shell.envVars opts
-          },
-      defaultOpts
-    }
+          }
+   in
+    Acc
+      { opts = newOpts,
+        defaultOpts,
+        stageIdx,
+        stageOpts = Map.update (\_ -> Just newOpts) stageIdx stageOpts,
+        stages
+      }
 
 -- | Converts ShellCheck errors into our own errors type
 toFailure :: Linenumber ->
diff --git a/src/Hadolint/Shell.hs b/src/Hadolint/Shell.hs
--- a/src/Hadolint/Shell.hs
+++ b/src/Hadolint/Shell.hs
@@ -2,12 +2,34 @@
 
 import Control.Monad.Writer (Writer, execWriter, tell)
 import Data.Functor.Identity (runIdentity)
-import Data.List (isInfixOf)
+import Data.List (isInfixOf, findIndex)
 import Data.Maybe (fromMaybe, listToMaybe, mapMaybe)
 import qualified Data.Set as Set
 import Data.Text (Text)
 import qualified Data.Text as Text
-import ShellCheck.AST (Id (..), Token (..), pattern T_Pipe, pattern T_SimpleCommand)
+import ShellCheck.AST
+  ( Id (..),
+    Token (..),
+    pattern T_Pipe,
+    pattern T_SimpleCommand,
+    pattern T_NormalWord,
+    pattern T_DoubleQuoted,
+    pattern T_SingleQuoted,
+    pattern T_DollarBraced,
+    pattern T_DollarArithmetic,
+    pattern T_DollarExpansion,
+    pattern T_Backticked,
+    pattern T_Glob,
+    pattern T_Pipeline,
+    pattern T_Literal,
+    pattern T_ParamSubSpecialChar,
+    pattern T_SimpleCommand,
+    pattern T_Redirecting,
+    pattern T_DollarSingleQuoted,
+    pattern T_Annotation,
+    pattern TA_Sequence,
+    pattern TA_Expansion,
+  )
 import qualified ShellCheck.AST
 import qualified ShellCheck.ASTLib
 import ShellCheck.Checker (checkScript)
@@ -33,6 +55,9 @@
     presentCommands :: [Command]
   }
 
+instance Show ParsedShell where
+  show p = show (presentCommands p)
+
 data ShellOpts = ShellOpts
   { shellName :: Text.Text,
     envVars :: Set.Set Text.Text
@@ -158,6 +183,9 @@
 anyCommands :: (Command -> Bool) -> ParsedShell -> Bool
 anyCommands check script = any check (presentCommands script)
 
+findCommandIndex :: (Command -> Bool) -> ParsedShell -> Maybe Int
+findCommandIndex check script = findIndex check (presentCommands script)
+
 findCommandNames :: ParsedShell -> [Text]
 findCommandNames script = map name (presentCommands script)
 
@@ -179,11 +207,34 @@
   where
     mkPart token =
       CmdPart
-        (Text.concat . fmap Text.pack $ ShellCheck.ASTLib.oversimplify token)
+        (Text.concat . fmap Text.pack $ simplify token)
         (mkId (ShellCheck.AST.getId token))
     mkId (Id i) = i
 extractAllArgs _ = []
 
+-- Modified version of ShellCheck.ASTLib.oversimplify
+-- This version keeps variable names
+simplify :: Token -> [String]
+simplify token =
+  case token of
+    (T_NormalWord _ l) -> [concat (concatMap simplify l)]
+    (T_DoubleQuoted _ l) -> [concat (concatMap simplify l)]
+    (T_SingleQuoted _ s) -> [s]
+    (T_DollarBraced _ _ t) -> ["${" ++ concat ( simplify t ) ++ "}"]
+    (T_DollarArithmetic _ _) -> ["${VAR}"]
+    (T_DollarExpansion _ t) -> ["${" ++ concat ( concatMap simplify t ) ++ "}"]
+    (T_Backticked _ _) -> ["${VAR}"]
+    (T_Glob _ s) -> [s]
+    (T_Pipeline _ _ [x]) -> simplify x
+    (T_Literal _ x) -> [x]
+    (T_ParamSubSpecialChar _ x) -> [x]
+    (T_SimpleCommand _ _ wds) -> concatMap simplify wds
+    (T_Redirecting _ _ foo) -> simplify foo
+    (T_DollarSingleQuoted _ s) -> [s]
+    (T_Annotation _ _ s) -> simplify s
+    (TA_Sequence _ [TA_Expansion _ v]) -> concatMap simplify v
+    _ -> []
+
 getArgs :: Command -> [Text.Text]
 getArgs cmd = map arg (arguments cmd)
 
@@ -216,7 +267,10 @@
 dropFlagArg :: [Text.Text] -> Command -> Command
 dropFlagArg flagsToDrop Command {name, arguments, flags} = Command name filteredArgs flags
   where
-    idsToDrop = Set.fromList [getValueId fId arguments | CmdPart f fId <- flags, f `elem` flagsToDrop]
+    idsToDrop =
+      Set.fromList
+        [getValueId fId arguments | CmdPart f fId <- flags, f `elem` flagsToDrop, not (hasInlineValue fId)]
+    hasInlineValue fId = or ["=" `Text.isInfixOf` a | CmdPart a aId <- arguments, aId == fId]
     filteredArgs = [arg | arg@(CmdPart _ aId) <- arguments, not (aId `Set.member` idsToDrop)]
 
 -- | given a flag and a command, return list of arguments for that particular
diff --git a/test/Hadolint/Config/CommandlineSpec.hs b/test/Hadolint/Config/CommandlineSpec.hs
--- a/test/Hadolint/Config/CommandlineSpec.hs
+++ b/test/Hadolint/Config/CommandlineSpec.hs
@@ -23,6 +23,7 @@
                               Nothing
                               []
                               Nothing
+                              []
                               mempty
 
     describe "parse version flag" $ do
@@ -32,6 +33,7 @@
                                 Nothing
                                 []
                                 Nothing
+                                []
                                 mempty
 
       it "parse --version" $ do
@@ -40,6 +42,7 @@
                                 Nothing
                                 []
                                 Nothing
+                                []
                                 mempty
 
     describe "parse config file option" $ do
@@ -49,6 +52,7 @@
                                 (Just "hadolint.yaml")
                                 []
                                 Nothing
+                                []
                                 mempty
 
       it "parse --config hadolint.yaml" $ do
@@ -57,6 +61,7 @@
                                 (Just "hadolint.yaml")
                                 []
                                 Nothing
+                                []
                                 mempty
 
     describe "parse file arguments" $ do
@@ -66,6 +71,7 @@
                                 Nothing
                                 ["Dockerfile"]
                                 Nothing
+                                []
                                 mempty
 
       it "parse `Dockerfile1 Dockerfile2`" $ do
@@ -74,6 +80,7 @@
                                 Nothing
                                 ["Dockerfile1", "Dockerfile2"]
                                 Nothing
+                                []
                                 mempty
 
       it "parse --file-path-in-report foobar/Dockerfile" $ do
@@ -84,17 +91,55 @@
               Nothing
               []
               (Just "foobar/Dockerfile")
+              []
               mempty
           )
 
+      it "parse single output destination - short form" $ do
+        checkCommandline
+          ["-o", "report.log"]
+          ( CommandlineConfig
+              False
+              Nothing
+              []
+              Nothing
+              ["report.log"]
+              mempty
+          )
+
+      it "parse single output destination - long form" $ do
+        checkCommandline
+          ["--output", "report.log"]
+          ( CommandlineConfig
+              False
+              Nothing
+              []
+              Nothing
+              ["report.log"]
+              mempty
+          )
+
+      it "parse multiple output destinations" $ do
+        checkCommandline
+          ["--output", "report.log", "-o", "other-report.log"]
+          ( CommandlineConfig
+              False
+              Nothing
+              []
+              Nothing
+              ["report.log", "other-report.log"]
+              mempty
+          )
+
     describe "parse general configuration" $ do
       it "parse --no-fail" $ do
         checkCommandline ["--no-fail"] $ CommandlineConfig
-                                          False
-                                          Nothing
-                                          []
-                                          Nothing
-                                          mempty { partialNoFail = Just True }
+                                            False
+                                            Nothing
+                                            []
+                                            Nothing
+                                            []
+                                            mempty { partialNoFail = Just True }
 
       it "parse --no-color" $ do
         checkCommandline ["--no-color"] $ CommandlineConfig
@@ -102,6 +147,7 @@
                                             Nothing
                                             []
                                             Nothing
+                                            []
                                             mempty { partialNoColor = Just True }
 
       it "parse -V" $ do
@@ -110,6 +156,7 @@
                                 Nothing
                                 []
                                 Nothing
+                                []
                                 mempty { partialVerbose = Just True }
 
       it "parse --verbose" $ do
@@ -118,24 +165,37 @@
                                 Nothing
                                 []
                                 Nothing
+                                []
                                 mempty { partialVerbose = Just True }
 
-      it "parse -f json" $ do
-        checkCommandline ["-f", "json"] $ CommandlineConfig
-                                False
-                                Nothing
-                                []
-                                Nothing
-                                mempty { partialFormat = Just Json }
+      describe "parse format options" $ do
+        it "parse -f json" $ do
+          checkCommandline ["-f", "json"] $ CommandlineConfig
+                                  False
+                                  Nothing
+                                  []
+                                  Nothing
+                                  []
+                                  mempty { partialFormats = [Json] }
 
-      it "parse --format" $ do
-        checkCommandline ["--format", "sarif"] $ CommandlineConfig
-                                False
-                                Nothing
-                                []
-                                Nothing
-                                mempty { partialFormat = Just Sarif }
+        it "parse --format" $ do
+          checkCommandline ["--format", "sarif"] $ CommandlineConfig
+                                  False
+                                  Nothing
+                                  []
+                                  Nothing
+                                  []
+                                  mempty { partialFormats = [Sarif] }
 
+        it "parse multiple --format" $ do
+          checkCommandline ["-f", "junit", "--format", "sarif"] $ CommandlineConfig
+                                  False
+                                  Nothing
+                                  []
+                                  Nothing
+                                  []
+                                  mempty { partialFormats = [JUnit, Sarif] }
+
     describe "parse severity overrides" $ do
       it "parse --error=DL3010" $ do
         checkCommandline ["--error", "DL3010"] $ CommandlineConfig
@@ -143,6 +203,7 @@
                                 Nothing
                                 []
                                 Nothing
+                                []
                                 mempty { partialErrorRules = ["DL3010"] }
 
       it "parse --error=DL3010 --error=DL3020" $ do
@@ -153,6 +214,7 @@
               Nothing
               []
               Nothing
+              []
               mempty { partialErrorRules = ["DL3010", "DL3020"] }
           )
 
@@ -162,6 +224,7 @@
                                 Nothing
                                 []
                                 Nothing
+                                []
                                 mempty { partialWarningRules = ["DL3010"] }
 
       it "parse --warning=DL3010 --warning=DL3020" $ do
@@ -172,6 +235,7 @@
               Nothing
               []
               Nothing
+              []
               mempty { partialWarningRules = ["DL3010", "DL3020"] }
           )
 
@@ -181,6 +245,7 @@
                                 Nothing
                                 []
                                 Nothing
+                                []
                                 mempty { partialInfoRules = ["DL3010"] }
 
       it "parse --info=DL3010 --info=DL3020" $ do
@@ -191,6 +256,7 @@
               Nothing
               []
               Nothing
+              []
               mempty { partialInfoRules = ["DL3010", "DL3020"] }
           )
 
@@ -200,6 +266,7 @@
                                 Nothing
                                 []
                                 Nothing
+                                []
                                 mempty { partialStyleRules = ["DL3010"] }
 
       it "parse --style=DL3010 --style=DL3020" $ do
@@ -210,6 +277,7 @@
               Nothing
               []
               Nothing
+              []
               mempty { partialStyleRules = ["DL3010", "DL3020"] }
           )
 
@@ -219,6 +287,7 @@
                                 Nothing
                                 []
                                 Nothing
+                                []
                                 mempty { partialIgnoreRules = ["DL3010"] }
 
       it "parse --ignore=DL3010 --ignore=DL3020" $ do
@@ -229,6 +298,7 @@
               Nothing
               []
               Nothing
+              []
               mempty { partialIgnoreRules = ["DL3010", "DL3020"] }
           )
 
@@ -241,6 +311,7 @@
               Nothing
               []
               Nothing
+              []
               mempty { partialAllowedRegistries = Set.fromList ["foobar.com"] }
           )
 
@@ -256,6 +327,7 @@
               Nothing
               []
               Nothing
+              []
               mempty
                 { partialAllowedRegistries =
                     Set.fromList ["foobar.com", "barfoo.io"]
@@ -271,6 +343,7 @@
               Nothing
               []
               Nothing
+              []
               mempty { partialLabelSchema = Map.fromList [("foo", Email)] }
           )
 
@@ -282,6 +355,7 @@
               Nothing
               []
               Nothing
+              []
               mempty
                 { partialLabelSchema =
                     Map.fromList [("foo", Email), ("bar", RawText)]
@@ -297,6 +371,7 @@
               Nothing
               []
               Nothing
+              []
               mempty { partialStrictLabels = Just True }
           )
 
@@ -309,6 +384,7 @@
               Nothing
               []
               Nothing
+              []
               mempty { partialDisableIgnorePragma = Just True }
           )
 
@@ -319,6 +395,7 @@
                                 Nothing
                                 []
                                 Nothing
+                                []
                                 mempty
                                   { partialFailureThreshold = Just DLWarningC }
 
@@ -328,6 +405,7 @@
                                 Nothing
                                 []
                                 Nothing
+                                []
                                 mempty
                                   { partialFailureThreshold = Just DLStyleC }
 
diff --git a/test/Hadolint/Config/ConfigfileSpec.hs b/test/Hadolint/Config/ConfigfileSpec.hs
--- a/test/Hadolint/Config/ConfigfileSpec.hs
+++ b/test/Hadolint/Config/ConfigfileSpec.hs
@@ -46,15 +46,24 @@
           conf = parseYaml yaml
       conf `shouldBe` Right mempty { partialVerbose = Just False }
 
-    it "parse `format: json`" $ do
-      let yaml = ["format: json"]
-          conf = parseYaml yaml
-      conf `shouldBe` Right mempty { partialFormat = Just Json }
+    describe "parse format options" $ do
+      it "parse `format: json`" $ do
+        let yaml = ["format: json"]
+            conf = parseYaml yaml
+        conf `shouldBe` Right mempty { partialFormats = [Json] }
 
-    it "parse `format: sarif`" $ do
-      let yaml = ["format: sarif"]
-          conf = parseYaml yaml
-      conf `shouldBe` Right mempty { partialFormat = Just Sarif }
+      it "parse `format: sarif`" $ do
+        let yaml = ["format: sarif"]
+            conf = parseYaml yaml
+        conf `shouldBe` Right mempty { partialFormats = [Sarif] }
+
+      it "parse multiple formats" $ do
+        let yaml = [ "formats:",
+                     "  - sarif",
+                     "  - json"
+                   ]
+            conf = parseYaml yaml
+        conf `shouldBe` Right mempty { partialFormats = [Sarif, Json] }
 
     it "parse override error rules" $ do
       let yaml = [ "override:",
diff --git a/test/Hadolint/Config/ConfigurationSpec.hs b/test/Hadolint/Config/ConfigurationSpec.hs
--- a/test/Hadolint/Config/ConfigurationSpec.hs
+++ b/test/Hadolint/Config/ConfigurationSpec.hs
@@ -33,9 +33,9 @@
         `shouldBe` def { verbose = True }
 
     it "override default with specific configuration: output-format json" $ do
-      let config = def { partialFormat = Just Json }
+      let config = def { partialFormats = [Json] }
       applyPartialConfiguration def config
-        `shouldBe` def { format = Json }
+        `shouldBe` def { formats = [Json] }
 
     it "override default with specific configuration: disable ignore pragma" $ do
       let config = def { partialDisableIgnorePragma = Just True }
diff --git a/test/Hadolint/Config/EnvironmentSpec.hs b/test/Hadolint/Config/EnvironmentSpec.hs
--- a/test/Hadolint/Config/EnvironmentSpec.hs
+++ b/test/Hadolint/Config/EnvironmentSpec.hs
@@ -61,15 +61,26 @@
         conf <- getConfigFromEnvironment
         conf `shouldBe` mempty { partialVerbose = Just False }
 
-    withJustEnv "HADOLINT_FORMAT" "json" $ do
-      it "parse HADOLINT_FORMAT=json" $ do
-        conf <- getConfigFromEnvironment
-        conf `shouldBe` mempty { partialFormat = Just Json }
+    describe "parse formats" $ do
+      withJustEnv "HADOLINT_FORMAT" "json" $ do
+        it "parse HADOLINT_FORMAT=json" $ do
+          conf <- getConfigFromEnvironment
+          conf `shouldBe` mempty { partialFormats = [Json] }
 
-    withJustEnv "HADOLINT_FORMAT" "sarif" $ do
-      it "parse HADOLINT_FORMAT=sarif" $ do
-        conf <- getConfigFromEnvironment
-        conf `shouldBe` mempty { partialFormat = Just Sarif }
+      withJustEnv "HADOLINT_FORMAT" "sarif" $ do
+        it "parse HADOLINT_FORMAT=sarif" $ do
+          conf <- getConfigFromEnvironment
+          conf `shouldBe` mempty { partialFormats = [Sarif] }
+
+      withJustEnv "HADOLINT_FORMAT" "sarif,json" $ do
+        it "parse HADOLINT_FORMAT=sarif,json" $ do
+          conf <- getConfigFromEnvironment
+          conf `shouldBe` mempty { partialFormats = [Sarif, Json] }
+
+      withJustEnv "HADOLINT_FORMAT" "junk,json" $ do
+        it "parse HADOLINT_FORMAT=junk,json" $ do
+          conf <- getConfigFromEnvironment
+          conf `shouldBe` mempty { partialFormats = [Json] }
 
     withJustEnv "HADOLINT_OVERRIDE_ERROR" "DL3010" $ do
       it "parse HADOLINT_OVERRIDE_ERROR=DL3010" $ do
diff --git a/test/Hadolint/Formatter/JUnitSpec.hs b/test/Hadolint/Formatter/JUnitSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Formatter/JUnitSpec.hs
@@ -0,0 +1,231 @@
+module Hadolint.Formatter.JUnitSpec ( spec ) where
+
+import Helpers
+import Hadolint
+  ( CheckFailure (..),
+    DLSeverity (..),
+    OutputFormat (..),
+    getShortVersion,
+  )
+import Test.Hspec
+import qualified Data.Map as Map
+import qualified Data.Text as Text
+import qualified Data.Time as Time
+import qualified Text.XML as XML
+
+
+spec :: SpecWith ()
+spec = do
+  time <- runIO Time.getCurrentTime
+
+  let runID = Text.pack $ Time.formatTime Time.defaultTimeLocale "%Y%m%d_%H%M%S" time
+  let runName = Text.pack $ Time.formatTime Time.defaultTimeLocale "Hadolint run at %Y-%m-%d %H:%M:%S" time
+  let providerName = Text.pack $ "Hadolint " <> getShortVersion
+
+  let ?noColor = True
+
+  describe "Formatter: JUnit" $ do
+    it "print empty results" $ do
+      let checkFails = []
+          expectation =
+            XML.Document
+              { documentPrologue = XML.Prologue [] Nothing [],
+                documentRoot =
+                  XML.Element
+                    { elementName = "testsuites",
+                      elementAttributes =
+                        Map.fromList
+                          [ ("id", runID),
+                            ("name", runName),
+                            ("time", "0.001")
+                          ],
+                      elementNodes =
+                        [ XML.NodeElement XML.Element
+                            { elementName = "testsuite",
+                              elementAttributes =
+                                Map.fromList
+                                  [ ("id", "hadolint"),
+                                    ("name", providerName),
+                                    ("time", "0.001"),
+                                    ("failures", "0"),
+                                    ("errors", "0")
+                                  ],
+                              elementNodes = []
+                            }
+                        ]
+                    },
+                documentEpilogue = []
+              }
+      assertFormatterXML JUnit checkFails expectation
+
+    it "print one failed rule" $ do
+      let checkFails =
+            [ CheckFailure
+                { code = "DL2001",
+                  severity = DLWarningC,
+                  message = "test",
+                  line = 1
+                }
+            ]
+          expectation =
+            XML.Document
+              { documentPrologue = XML.Prologue [] Nothing [],
+                documentRoot =
+                  XML.Element
+                    { elementName = "testsuites",
+                      elementAttributes =
+                        Map.fromList
+                          [ ("id", runID),
+                            ("name", runName),
+                            ("time", "0.001")
+                          ],
+                      elementNodes =
+                        [ XML.NodeElement XML.Element
+                            { elementName = "testsuite",
+                              elementAttributes =
+                                Map.fromList
+                                  [ ("id", "hadolint"),
+                                    ("name", providerName),
+                                    ("time", "0.001"),
+                                    ("failures", "1"),
+                                    ("errors", "0")
+                                  ],
+                              elementNodes =
+                                [ XML.NodeElement XML.Element
+                                    { elementName = "testcase",
+                                      elementAttributes =
+                                        Map.fromList
+                                          [ ("id", "hadolint.rule.DL2001"),
+                                            ("time", "0.001")
+                                          ],
+                                      elementNodes =
+                                        [ XML.NodeElement XML.Element
+                                            { elementName = "failure",
+                                              elementAttributes =
+                                                Map.fromList
+                                                  [ ("type", "warning"),
+                                                    ("message", "test"),
+                                                    ("id", "DL2001")
+                                                  ],
+                                              elementNodes =
+                                                [ XML.NodeContent $
+                                                    Text.unlines
+                                                      [ "File: <string>",
+                                                        "Line: 1",
+                                                        "Category: Hadolint - Dockerfile Static Analysis",
+                                                        "warning: test"
+                                                      ]
+                                                ]
+                                            }
+                                        ]
+                                    }
+                                ]
+                            }
+                        ]
+                    },
+                documentEpilogue = []
+              }
+      assertFormatterXML JUnit checkFails expectation
+
+    it "print multiple failed rules" $ do
+      let checkFails =
+            [ CheckFailure
+                { code = "DL2001",
+                  severity = DLWarningC,
+                  message = "test",
+                  line = 1
+                },
+              CheckFailure
+                { code = "DL2003",
+                  severity = DLInfoC,
+                  message = "test 2",
+                  line = 3
+                }
+            ]
+          expectation =
+            XML.Document
+              { documentPrologue = XML.Prologue [] Nothing [],
+                documentRoot =
+                  XML.Element
+                    { elementName = "testsuites",
+                      elementAttributes =
+                        Map.fromList
+                          [ ("id", runID),
+                            ("name", runName),
+                            ("time", "0.001")
+                          ],
+                      elementNodes =
+                        [ XML.NodeElement XML.Element
+                            { elementName = "testsuite",
+                              elementAttributes =
+                                Map.fromList
+                                  [ ("id", "hadolint"),
+                                    ("name", "Hadolint " <> Text.pack getShortVersion),
+                                    ("time", "0.001"),
+                                    ("failures", "2"),
+                                    ("errors", "0")
+                                  ],
+                              elementNodes =
+                                [ XML.NodeElement XML.Element
+                                    { elementName = "testcase",
+                                      elementAttributes =
+                                        Map.fromList
+                                          [ ("id", "hadolint.rule.DL2001"),
+                                            ("time", "0.001")
+                                          ],
+                                      elementNodes =
+                                        [ XML.NodeElement XML.Element
+                                            { elementName = "failure",
+                                              elementAttributes =
+                                                Map.fromList
+                                                  [ ("type", "warning"),
+                                                    ("message", "test"),
+                                                    ("id", "DL2001")
+                                                  ],
+                                              elementNodes =
+                                                [ XML.NodeContent $
+                                                    Text.unlines
+                                                      [ "File: <string>",
+                                                        "Line: 1",
+                                                        "Category: Hadolint - Dockerfile Static Analysis",
+                                                        "warning: test"
+                                                      ]
+                                                ]
+                                            }
+                                        ]
+                                    },
+                                  XML.NodeElement XML.Element
+                                    { elementName = "testcase",
+                                      elementAttributes =
+                                        Map.fromList
+                                          [ ("id", "hadolint.rule.DL2003"),
+                                            ("time", "0.001")
+                                          ],
+                                      elementNodes =
+                                        [ XML.NodeElement XML.Element
+                                            { elementName = "failure",
+                                              elementAttributes =
+                                                Map.fromList
+                                                  [ ("type", "info"),
+                                                    ("message", "test 2"),
+                                                    ("id", "DL2003")
+                                                  ],
+                                              elementNodes =
+                                                [ XML.NodeContent $
+                                                    Text.unlines
+                                                      [ "File: <string>",
+                                                        "Line: 3",
+                                                        "Category: Hadolint - Dockerfile Static Analysis",
+                                                        "info: test 2"
+                                                      ]
+                                                ]
+                                            }
+                                        ]
+                                    }
+                                ]
+                            }
+                        ]
+                    },
+                documentEpilogue = []
+              }
+      assertFormatterXML JUnit checkFails expectation
diff --git a/test/Hadolint/Formatter/SonarQubeSpec.hs b/test/Hadolint/Formatter/SonarQubeSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Formatter/SonarQubeSpec.hs
@@ -0,0 +1,117 @@
+module Hadolint.Formatter.SonarQubeSpec (spec) where
+
+import Data.Aeson
+import qualified Data.ByteString.Lazy.Char8 as BSC
+import qualified Data.List.NonEmpty as NonEmpty
+import qualified Data.Sequence as Seq
+import Hadolint.Formatter (write)
+import Hadolint.Formatter.Format (Result (..))
+import Helpers
+import Hadolint
+  ( CheckFailure (..),
+    DLSeverity (..),
+    OutputFormat (..),
+  )
+import System.IO.Silently (capture)
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?noColor = True
+  let mkIssue ruleId severity issueType msg lineNum =
+        Object
+          [ "engineId" .= String "Hadolint",
+            "ruleId" .= String ruleId,
+            "severity" .= String severity,
+            "type" .= String issueType,
+            "primaryLocation" .=
+              Object
+                [ "message" .= String msg,
+                  "filePath" .= String "<string>",
+                  "textRange" .=
+                    Object
+                      [ "startLine" .= Number lineNum,
+                        "endLine" .= Number lineNum,
+                        "startColumn" .= Number 0.0,
+                        "endColumn" .= Number 1.0
+                      ]
+                ]
+          ]
+
+  describe "Formatter: SonarQube" $ do
+    it "print empty results" $ do
+      assertFormatterJson SonarQube [] (Object ["issues" .= Array []])
+
+    it "print all severity levels with correct mapping" $ do
+      let checks =
+            [ CheckFailure
+                { code = "DL3000",
+                  severity = DLErrorC,
+                  message = "error msg",
+                  line = 1
+                },
+              CheckFailure
+                { code = "DL3001",
+                  severity = DLWarningC,
+                  message = "warning msg",
+                  line = 2
+                },
+              CheckFailure
+                { code = "DL3009",
+                  severity = DLInfoC,
+                  message = "info msg",
+                  line = 3
+                },
+              CheckFailure
+                { code = "DL3015",
+                  severity = DLStyleC,
+                  message = "style msg",
+                  line = 4
+                }
+            ]
+          expected =
+            Object
+              [ "issues" .=
+                  Array
+                    [ mkIssue "DL3000" "CRITICAL" "BUG" "error msg" 1.0,
+                      mkIssue "DL3001" "MAJOR" "CODE_SMELL" "warning msg" 2.0,
+                      mkIssue "DL3009" "MINOR" "CODE_SMELL" "info msg" 3.0,
+                      mkIssue "DL3015" "INFO" "CODE_SMELL" "style msg" 4.0
+                    ]
+              ]
+      assertFormatterJson SonarQube checks expected
+
+    it "use custom file path from --file-path-in-report" $ do
+      let checks =
+            [ CheckFailure
+                { code = "DL3000",
+                  severity = DLErrorC,
+                  message = "error msg",
+                  line = 1
+                }
+            ]
+          results = NonEmpty.fromList [Result "<string>" mempty (Seq.fromList checks)]
+          customPath = Just "path/to/custom/Dockerfile"
+          mkCustomIssue path =
+            Object
+              [ "engineId" .= String "Hadolint",
+                "ruleId" .= String "DL3000",
+                "severity" .= String "CRITICAL",
+                "type" .= String "BUG",
+                "primaryLocation" .=
+                  Object
+                    [ "message" .= String "error msg",
+                      "filePath" .= String path,
+                      "textRange" .=
+                        Object
+                          [ "startLine" .= Number 1.0,
+                            "endLine" .= Number 1.0,
+                            "startColumn" .= Number 0.0,
+                            "endColumn" .= Number 1.0
+                          ]
+                    ]
+              ]
+          expected = Object ["issues" .= Array [mkCustomIssue "path/to/custom/Dockerfile"]]
+      (cap, _) <- capture (write [] [SonarQube] ?noColor customPath results)
+      decode (BSC.pack cap) `shouldBe` Just expected
diff --git a/test/Hadolint/Formatter/TTYSpec.hs b/test/Hadolint/Formatter/TTYSpec.hs
--- a/test/Hadolint/Formatter/TTYSpec.hs
+++ b/test/Hadolint/Formatter/TTYSpec.hs
@@ -1,12 +1,9 @@
 module Hadolint.Formatter.TTYSpec (spec) where
 
 import Helpers
-import Data.List.NonEmpty as NonEmpty
-import Hadolint (OutputFormat (..), printResults)
-import Hadolint.Formatter.Format (Result (..))
+import Hadolint (OutputFormat (..))
 import Hadolint.Rule (CheckFailure (..), DLSeverity (..))
 import Test.Hspec
-import qualified Data.Sequence as Seq
 
 
 spec :: SpecWith ()
@@ -14,8 +11,9 @@
   let ?noColor = True
   describe "Formatter: TTY" $ do
     it "print empty results" $ do
-      let results = NonEmpty.fromList [Result "<string>" mempty Seq.empty]
-      printResults TTY True (Just "<string>") results `shouldReturn` ()
+      let checkFails = []
+          expectation = unlines []
+      assertFormatter TTY checkFails expectation
 
     it "print some result: no colors" $ do
       let checkFails = [ CheckFailure
diff --git a/test/Hadolint/PragmaSpec.hs b/test/Hadolint/PragmaSpec.hs
--- a/test/Hadolint/PragmaSpec.hs
+++ b/test/Hadolint/PragmaSpec.hs
@@ -62,6 +62,45 @@
             ruleCatchesNot "DL3023" $ Text.unlines dockerFile
             ruleCatchesNot "DL3021" $ Text.unlines dockerFile
 
+  describe "Rules can be ignored per stage with stage ignore pragma" $ do
+    it "ignore single rule" $
+      let dockerfile =
+            [ "# hadolint stage ignore=DL3011",
+              "FROM ubuntu",
+              "EXPOSE 80000"
+            ]
+       in ruleCatchesNot "DL3011" $ Text.unlines dockerfile
+
+    it "stage ignore rule, but catch it in different stage 1" $
+      let dockerfile =
+            [ "# hadolint stage ignore=DL3011",
+              "FROM ubuntu",
+              "EXPOSE 80000",
+              "FROM ubuntu",
+              "EXPOSE 80000"
+            ]
+       in ruleCatches "DL3011" $ Text.unlines dockerfile
+
+    it "stage ignore rule, but catch it in different stage 2" $
+      let dockerfile =
+            [ "FROM ubuntu",
+              "EXPOSE 80000",
+              "# hadolint stage ignore=DL3011",
+              "FROM ubuntu",
+              "EXPOSE 80000"
+            ]
+       in ruleCatches "DL3011" $ Text.unlines dockerfile
+
+    it "ignore multiple rules" $
+      let dockerfile =
+            [ "# hadolint stage ignore=DL3011,DL3002",
+              "FROM ubuntu",
+              "USER root",
+              "EXPOSE 80000"
+            ]
+       in do
+            ruleCatchesNot "DL3012" $ Text.unlines dockerfile
+            ruleCatchesNot "DL3011" $ Text.unlines dockerfile
 
   describe "Rules can be ignored globally with global ignore pragma" $ do
 
diff --git a/test/Hadolint/Rule/DL3013Spec.hs b/test/Hadolint/Rule/DL3013Spec.hs
--- a/test/Hadolint/Rule/DL3013Spec.hs
+++ b/test/Hadolint/Rule/DL3013Spec.hs
@@ -168,3 +168,15 @@
     it "pipenv is not pip" $ do
       ruleCatchesNot "DL3013" "RUN pipenv install black"
       onBuildRuleCatchesNot "DL3013" "RUN pipenv install black"
+    it "pipx version not pinned" $ do
+      ruleCatches "DL3013" "RUN pipx install black"
+      onBuildRuleCatches "DL3013" "RUN pipx install black"
+    it "pipx install --python argument is not a package" $ do
+      ruleCatchesNot "DL3013" "RUN pipx install --python \"$(which python)\" \"poetry==1.8.5\""
+      onBuildRuleCatchesNot "DL3013" "RUN pipx install --python \"$(which python)\" \"poetry==1.8.5\""
+    it "pip install --root-user-action argument is not a package" $ do
+      ruleCatchesNot "DL3013" "RUN pip install --no-cache-dir --root-user-action ignore poetry==1.8.5"
+      onBuildRuleCatchesNot "DL3013" "RUN pip install --no-cache-dir --root-user-action ignore poetry==1.8.5"
+    it "pip install flag=value still checks the package" $ do
+      ruleCatches "DL3013" "RUN pip install --root-user-action=ignore mypkg"
+      onBuildRuleCatches "DL3013" "RUN pip install --root-user-action=ignore mypkg"
diff --git a/test/Hadolint/Rule/DL3016Spec.hs b/test/Hadolint/Rule/DL3016Spec.hs
--- a/test/Hadolint/Rule/DL3016Spec.hs
+++ b/test/Hadolint/Rule/DL3016Spec.hs
@@ -122,3 +122,12 @@
     it "don't fire on loglevel flag" $ do
       ruleCatchesNot "DL3016" "RUN npm install --loglevel verbose sax@0.1.1"
       onBuildRuleCatchesNot "DL3016" "RUN npm install --loglevel verbose sax@0.1.1"
+    it "don't fire on registry flag" $ do
+      ruleCatchesNot "DL3016" "RUN npm install --registry https://example.com sax@0.1.1"
+      onBuildRuleCatchesNot "DL3016" "RUN npm install --registry https://example.com sax@0.1.1"
+    it "version not pinned with --registry flag" $ do
+      ruleCatches "DL3016" "RUN npm install --registry https://example.com express"
+      onBuildRuleCatches "DL3016" "RUN npm install --registry https://example.com express"
+    it "version not pinned with --registry=value form" $ do
+      ruleCatches "DL3016" "RUN npm install --registry=https://example.com express"
+      onBuildRuleCatches "DL3016" "RUN npm install --registry=https://example.com express"
diff --git a/test/Hadolint/Rule/DL3018Spec.hs b/test/Hadolint/Rule/DL3018Spec.hs
--- a/test/Hadolint/Rule/DL3018Spec.hs
+++ b/test/Hadolint/Rule/DL3018Spec.hs
@@ -5,7 +5,6 @@
 import Helpers
 import Test.Hspec
 
-
 spec :: SpecWith ()
 spec = do
   let ?config = def
@@ -17,6 +16,17 @@
     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 tilde version pinning single" $ do
+      ruleCatchesNot "DL3018" "RUN apk add --no-cache git~2.52.0"
+      onBuildRuleCatchesNot "DL3018" "RUN apk add --no-cache git~2.52.0"
+
+    it "apk add ~= and =~ version pinning" $ do
+      ruleCatchesNot "DL3018" "RUN apk add --no-cache git~=2.52"
+      ruleCatchesNot "DL3018" "RUN apk add --no-cache git=~2.52"
+      onBuildRuleCatchesNot "DL3018" "RUN apk add --no-cache git~=2.52"
+      onBuildRuleCatchesNot "DL3018" "RUN apk add --no-cache git=~2.52"
+
     it "apk add version pinned chained" $
       let dockerFile =
             [ "RUN apk add --no-cache flex=2.6.4-r1 \\",
@@ -25,6 +35,7 @@
        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 \\",
@@ -36,6 +47,20 @@
        in do
             ruleCatchesNot "DL3018" $ Text.unlines dockerFile
             onBuildRuleCatchesNot "DL3018" $ Text.unlines dockerFile
+
+    it "apk add version pinned with fuzzy versions" $
+      let dockerfile =
+            Text.unlines
+              [ "RUN apk add --no-cache \\",
+                "  \"flex>=2.6.1-r1\" \\",
+                "  \"libffi~3.2.1\" \\",
+                "  \"python3<3.6.12-r2\" \\",
+                "  \"libbz2=~1.0.6-r4\""
+              ]
+       in do
+            ruleCatchesNot "DL3018" dockerfile
+            onBuildRuleCatchesNot "DL3018" dockerfile
+
     it "apk add version pinned regression - one missed" $
       let dockerFile =
             [ "RUN apk add --no-cache \\",
@@ -47,6 +72,7 @@
        in do
             ruleCatches "DL3018" $ Text.unlines dockerFile
             onBuildRuleCatches "DL3018" $ Text.unlines dockerFile
+
     it "apk add virtual package" $
       let dockerFile =
             [ "RUN apk add \\",
@@ -59,6 +85,7 @@
        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 \\",
@@ -68,6 +95,7 @@
        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 \\",
@@ -77,6 +105,7 @@
        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 \\",
diff --git a/test/Hadolint/Rule/DL3025Spec.hs b/test/Hadolint/Rule/DL3025Spec.hs
--- a/test/Hadolint/Rule/DL3025Spec.hs
+++ b/test/Hadolint/Rule/DL3025Spec.hs
@@ -11,24 +11,28 @@
   let ?config = def
 
   describe "DL3025 - Use arguments JSON notation for `CMD` and `ENTRYPOINT` arguments" $ do
+
     it "warn on ENTRYPOINT" $
       let dockerFile =
             [ "FROM node as foo",
               "ENTRYPOINT something"
             ]
        in ruleCatches "DL3025" $ Text.unlines dockerFile
+
     it "don't warn on ENTRYPOINT json notation" $
       let dockerFile =
             [ "FROM scratch as build",
               "ENTRYPOINT [\"foo\", \"bar\"]"
             ]
        in ruleCatchesNot "DL3025" $ Text.unlines dockerFile
+
     it "warn on CMD" $
       let dockerFile =
             [ "FROM node as foo",
               "CMD something"
             ]
        in ruleCatches "DL3025" $ Text.unlines dockerFile
+
     it "don't warn on CMD json notation" $
       let dockerFile =
             [ "FROM scratch as build",
@@ -36,6 +40,14 @@
               "CMD [ \"foo\", \"bar\" ]"
             ]
        in ruleCatchesNot "DL3025" $ Text.unlines dockerFile
+
+    it "warn on HEALTHCHECK CMD shell notation" $
+      let df = Text.unlines [ "HEALTHCHECK CMD foobar" ]
+       in ruleCatches "DL3025" df
+
+    it "don't warn on HEALTHCHECK CMD json notation" $
+      let df = Text.unlines [ "HEALTHCHECK CMD [ \"foobar\" ]" ]
+       in ruleCatchesNot "DL3025" df
 
     -- regression: deal with broken long strings in exec format
     it "don't warn on CMD JSON notation with broken long strings" $
diff --git a/test/Hadolint/Rule/DL3026Spec.hs b/test/Hadolint/Rule/DL3026Spec.hs
--- a/test/Hadolint/Rule/DL3026Spec.hs
+++ b/test/Hadolint/Rule/DL3026Spec.hs
@@ -42,7 +42,7 @@
 
       ruleCatchesNot "DL3026" $ Text.unlines dockerFile
 
-    it "allows boths all forms of docker.io" $ do
+    it "allows all forms of docker.io" $ do
       let dockerFile =
             [ "FROM ubuntu:18.04 AS builder1",
               "FROM zemanlx/ubuntu:18.04 AS builder2",
@@ -86,3 +86,54 @@
       let ?config = def { allowedRegistries = ["*"] }
 
       ruleCatchesNot "DL3026" $ Text.unlines dockerFile
+
+    it "does warn on copy from untrusted registry" $ do
+      let dockerfile =
+            Text.unlines
+              [ "COPY --from=untrusted.com/repo/image:tag /foo /bar" ]
+      let ?config = def { allowedRegistries = [ "trusted.com" ] }
+       in ruleCatches "DL3026" dockerfile
+
+    it "does not warn on copy from untrusted registry" $ do
+      let dockerfile =
+            Text.unlines
+              [ "COPY --from=trusted.com/repo/image:tag /foo /bar" ]
+      let ?config = def { allowedRegistries = [ "trusted.com" ] }
+       in ruleCatchesNot "DL3026" dockerfile
+
+    it "warn on run with bind mount from untrusted registry" $ do
+      let dockerfile =
+            Text.unlines
+              [ "RUN --mount=type=bind,from=untrusted.com/repo/image:tag,target=/foo foobar" ]
+      let ?config = def { allowedRegistries = [ "trusted.com" ] }
+       in ruleCatches "DL3026" dockerfile
+
+    it "warn on run with cache mount from untrusted registry" $ do
+      let dockerfile =
+            Text.unlines
+              [ "RUN --mount=type=cache,from=untrusted.com/repo/image:tag,target=/foo foobar" ]
+      let ?config = def { allowedRegistries = [ "trusted.com" ] }
+       in ruleCatches "DL3026" dockerfile
+
+    it "don't warn on run with bind mount from trusted registry" $ do
+      let dockerfile =
+            Text.unlines
+              [ "RUN --mount=type=bind,from=trusted.com/repo/image:tag,target=/foo foobar" ]
+      let ?config = def { allowedRegistries = [ "trusted.com" ] }
+       in ruleCatchesNot "DL3026" dockerfile
+
+    it "don't warn on run with cache mount from trusted registry" $ do
+      let dockerfile =
+            Text.unlines
+              [ "RUN --mount=type=cache,from=trusted.com/repo/image:tag,target=/foo foobar" ]
+      let ?config = def { allowedRegistries = [ "trusted.com" ] }
+       in ruleCatchesNot "DL3026" dockerfile
+
+    it "distrust all forms of docker.io if trusted registries are given" $ do
+      let dockerFile =
+            [ "FROM ubuntu:18.04 AS builder1",
+              "FROM zemanlx/ubuntu:18.04 AS builder2",
+              "FROM docker.io/zemanlx/ubuntu:18.04 AS builder3"
+            ]
+      let ?config = def { allowedRegistries = ["trusted.com"] }
+       in assertChecks ( Text.unlines dockerFile ) ( failsWith 3 "DL3026" )
diff --git a/test/Hadolint/Rule/DL3032Spec.hs b/test/Hadolint/Rule/DL3032Spec.hs
--- a/test/Hadolint/Rule/DL3032Spec.hs
+++ b/test/Hadolint/Rule/DL3032Spec.hs
@@ -21,3 +21,6 @@
     it "ok with rm -rf /var/cache/yum/*" $ do
       ruleCatchesNot "DL3032" "RUN yum install -y mariadb-10.4 && rm -rf /var/cache/yum/*"
       onBuildRuleCatchesNot "DL3032" "RUN yum install -y mariadb-10.4 && rm -rf /var/cache/yum/*"
+    it "not ok with clean before install" $ do
+      ruleCatchesNot "DL3032" "RUN yum install -y mariadb-10.4 && yum clean all"
+      ruleCatches "DL3032" "RUN yum clean all && yum install -y"
diff --git a/test/Hadolint/Rule/DL3033Spec.hs b/test/Hadolint/Rule/DL3033Spec.hs
--- a/test/Hadolint/Rule/DL3033Spec.hs
+++ b/test/Hadolint/Rule/DL3033Spec.hs
@@ -1,5 +1,6 @@
 module Hadolint.Rule.DL3033Spec (spec) where
 
+import qualified Data.Text as Text
 import Data.Default
 import Helpers
 import Test.Hspec
@@ -41,3 +42,65 @@
       ruleCatchesNot "DL3033" "RUN bash -c `# not even a yum command`"
       onBuildRuleCatchesNot "DL3033" "RUN yum module install -y tomcat:9 && yum clean all"
       onBuildRuleCatchesNot "DL3033" "RUN bash -c `# not even a yum command`"
+
+    -- this is important e.g. when using renovatebot
+    it "ok with version as variable - braced" $ do
+      let
+        rule = "DL3033"
+        snippet =
+          Text.unlines
+            [ "ENV version=2.51.0-2.fc42",
+              "RUN yum -y install git-core-${version}"
+            ]
+       in do
+        ruleCatchesNot rule snippet
+
+    it "ok with version as arg - unbraced" $ do
+      let
+        rule = "DL3033"
+        snippet =
+          Text.unlines
+            [ "ARG version=2.51.0-2.fc42",
+              "RUN yum -y install git-core-$version"
+            ]
+       in do
+        ruleCatchesNot rule snippet
+
+    it "ok with version as arg - different stages" $ do
+      let
+        rule = "DL3033"
+        snippet =
+          Text.unlines
+            [ "FROM fedora:fc42 AS build",
+              "ARG version=2.51.0-2.fc42",
+              "FROM fedora:fc42",
+              "RUN yum -y install git-core-${version}"
+            ]
+       in do
+        ruleCatchesNot rule snippet
+
+    it "ok with version as env - different stages, reused stage" $ do
+      let
+        rule = "DL3033"
+        snippet =
+          Text.unlines
+            [ "FROM fedora:fc42 AS build",
+              "ENV version=2.51.0-2.fc42",
+              "FROM build",
+              "RUN yum -y install git-core-${version}"
+            ]
+       in do
+        ruleCatchesNot rule snippet
+
+    it "not ok with version as variable - different stages, new stage" $ do
+      let
+        rule = "DL3033"
+        snippet =
+          Text.unlines
+            [ "FROM fedora:fc42 AS build",
+              "ENV version=2.51.0-2.fc42",
+              "FROM fedora:fc42",
+              "RUN yum -y install git-core-${version}"
+            ]
+       in do
+        ruleCatches rule snippet
diff --git a/test/Hadolint/Rule/DL3036Spec.hs b/test/Hadolint/Rule/DL3036Spec.hs
--- a/test/Hadolint/Rule/DL3036Spec.hs
+++ b/test/Hadolint/Rule/DL3036Spec.hs
@@ -21,6 +21,9 @@
       onBuildRuleCatchesNot "DL3036" "RUN zypper install -y mariadb=10.4 && zypper clean"
       onBuildRuleCatchesNot "DL3036" "RUN zypper install -y mariadb=10.4 && zypper cc"
 
+    it "not ok with clean before install" $ do
+      ruleCatches "DL3036" "RUN zypper clean && zypper install -y mariadb=10.4"
+
     it "ok when mount type cache is used" $
       let line = "RUN --mount=type=cache,target=/var/cache/zypp zypper install -y mariadb"
       in do
diff --git a/test/Hadolint/Rule/DL3040Spec.hs b/test/Hadolint/Rule/DL3040Spec.hs
--- a/test/Hadolint/Rule/DL3040Spec.hs
+++ b/test/Hadolint/Rule/DL3040Spec.hs
@@ -10,32 +10,79 @@
   let ?config = def
 
   describe "DL3040 - `dnf clean all` missing after dnf command." $ do
+
     it "no ok without dnf clean all" $ do
       ruleCatches "DL3040" "RUN dnf install -y mariadb-10.4"
       ruleCatches "DL3040" "RUN microdnf install -y mariadb-10.4"
+      ruleCatches "DL3040" "RUN dnf in -y mariadb-10.4"
       onBuildRuleCatches "DL3040" "RUN dnf install -y mariadb-10.4"
+      onBuildRuleCatches "DL3040" "RUN dnf in -y mariadb-10.4"
       onBuildRuleCatches "DL3040" "RUN dnf install -y mariadb-10.4 && microdnf clean all"
       onBuildRuleCatches "DL3040" "RUN microdnf install -y mariadb-10.4"
+
     it "ok with dnf clean all" $ do
       ruleCatchesNot "DL3040" "RUN dnf install -y mariadb-10.4 && dnf clean all"
+      ruleCatchesNot "DL3040" "RUN dnf in -y mariadb-10.4 && dnf clean all"
       ruleCatchesNot "DL3040" "RUN microdnf install -y mariadb-10.4 && microdnf clean all"
       ruleCatchesNot "DL3040" "RUN notdnf install mariadb"
       onBuildRuleCatchesNot "DL3040" "RUN dnf install -y mariadb-10.4 && dnf clean all"
       onBuildRuleCatchesNot "DL3040" "RUN microdnf install -y mariadb-10.4 && microdnf clean all"
       onBuildRuleCatchesNot "DL3040" "RUN notdnf install mariadb"
+
     it "ok with rm /var/cache/yum" $ do
-      ruleCatchesNot "DL3040" "RUN dnf install -y mariadb-10.4 && rm -rf /var/cache/libdnf5"
-      ruleCatchesNot "DL3040" "RUN microdnf install -y mariadb-10.4 && rm -rf /var/cache/libdnf5"
-      onBuildRuleCatchesNot "DL3040" "RUN dnf install -y mariadb-10.4 && rm -rf /var/cache/libdnf5"
-      onBuildRuleCatchesNot "DL3040" "RUN microdnf install -y mariadb-10.4 && rm -rf /var/cache/libdnf5"
+      ruleCatchesNot "DL3040" "RUN dnf install -y mariadb-10.4 && rm -rf /var/cache/yum/*"
+      ruleCatchesNot "DL3040" "RUN dnf in -y mariadb-10.4 && rm -rf /var/cache/yum/*"
+      ruleCatchesNot "DL3040" "RUN microdnf install -y mariadb-10.4 && rm -rf /var/cache/yum/*"
+      onBuildRuleCatchesNot "DL3040" "RUN dnf install -y mariadb-10.4 && rm -rf /var/cache/yum/*"
+      onBuildRuleCatchesNot "DL3040" "RUN dnf in -y mariadb-10.4 && rm -rf /var/cache/yum/*"
+      onBuildRuleCatchesNot "DL3040" "RUN microdnf install -y mariadb-10.4 && rm -rf /var/cache/yum/*"
 
+    it "not ok with clean before install" $ do
+      ruleCatches "DL3040" "RUN microdnf clean all && dnf install -y mariadb-10.4"
+      ruleCatches "DL3040" "RUN microdnf clean all && dnf in -y mariadb-10.4"
+      ruleCatches "DL3040" "RUN rm -rf /var/cache/libdnf5 && dnf install -y mariadb-10.4"
+      ruleCatches "DL3040" "RUN rm -rf /var/cache/libdnf5 && dnf in -y mariadb-10.4"
+      ruleCatches "DL3040" "RUN rm -rf /var/cache/libdnf5 && microdnf install -y mariadb-10.4"
+      onBuildRuleCatches "DL3040" "RUN rm -rf /var/cache/libdnf5 && dnf install -y mariadb-10.4"
+      onBuildRuleCatches "DL3040" "RUN rm -rf /var/cache/libdnf5 && dnf in -y mariadb-10.4"
+      onBuildRuleCatches "DL3040" "RUN rm -rf /var/cache/libdnf5 && microdnf install -y mariadb-10.4"
+
     it "ok with cache mount at /var/cache/yum" $ do
       ruleCatchesNot "DL3040" "RUN --mount=type=cache,target=/var/cache/libdnf5 dnf install -y mariadb-10.4"
+      ruleCatchesNot "DL3040" "RUN --mount=type=cache,target=/var/cache/libdnf5 dnf in -y mariadb-10.4"
       ruleCatchesNot "DL3040" "RUN --mount=type=cache,target=/var/cache/libdnf5 microdnf install -y mariadb-10.4"
       onBuildRuleCatchesNot "DL3040" "RUN --mount=type=cache,target=/var/cache/libdnf5 dnf install -y mariadb-10.4"
+      onBuildRuleCatchesNot "DL3040" "RUN --mount=type=cache,target=/var/cache/libdnf5 dnf in -y mariadb-10.4"
       onBuildRuleCatchesNot "DL3040" "RUN --mount=type=cache,target=/var/cache/libdnf5 microdnf install -y mariadb-10.4"
+
     it "ok with tmpfs mount at /var/cache/yum" $ do
       ruleCatchesNot "DL3040" "RUN --mount=type=tmpfs,target=/var/cache/libdnf5 dnf install -y mariadb-10.4"
+      ruleCatchesNot "DL3040" "RUN --mount=type=tmpfs,target=/var/cache/libdnf5 dnf in -y mariadb-10.4"
       ruleCatchesNot "DL3040" "RUN --mount=type=tmpfs,target=/var/cache/libdnf5 microdnf install -y mariadb-10.4"
       onBuildRuleCatchesNot "DL3040" "RUN --mount=type=tmpfs,target=/var/cache/libdnf5 dnf install -y mariadb-10.4"
+      onBuildRuleCatchesNot "DL3040" "RUN --mount=type=tmpfs,target=/var/cache/libdnf5 dnf in -y mariadb-10.4"
       onBuildRuleCatchesNot "DL3040" "RUN --mount=type=tmpfs,target=/var/cache/libdnf5 microdnf install -y mariadb-10.4"
+
+    it "not ok with `dnf upgrade`" $ do
+      ruleCatches "DL3040" "RUN dnf -y upgrade"
+      ruleCatches "DL3040" "RUN dnf -y up"
+      onBuildRuleCatches "DL3040" "RUN dnf -y upgrade"
+      onBuildRuleCatches "DL3040" "RUN dnf -y up"
+
+    it "different install command variants" $ do
+      ruleCatches "DL3040" "RUN dnf -y install"
+      ruleCatches "DL3040" "RUN dnf -y in"
+      ruleCatches "DL3040" "RUN dnf -y upgrade"
+      ruleCatches "DL3040" "RUN dnf -y up"
+      ruleCatches "DL3040" "RUN dnf -y upgrade-minimal"
+      ruleCatches "DL3040" "RUN dnf -y up-min"
+      ruleCatches "DL3040" "RUN dnf -y reinstall"
+      ruleCatches "DL3040" "RUN dnf -y rei"
+      ruleCatchesNot "DL3040" "RUN dnf -y install && dnf clean all"
+      ruleCatchesNot "DL3040" "RUN dnf -y in && dnf clean all"
+      ruleCatchesNot "DL3040" "RUN dnf -y upgrade && dnf clean all"
+      ruleCatchesNot "DL3040" "RUN dnf -y up && dnf clean all"
+      ruleCatchesNot "DL3040" "RUN dnf -y upgrade-minimal && dnf clean all"
+      ruleCatchesNot "DL3040" "RUN dnf -y up-min && dnf clean all"
+      ruleCatchesNot "DL3040" "RUN dnf -y reinstall && dnf clean all"
+      ruleCatchesNot "DL3040" "RUN dnf -y rei && dnf clean all"
diff --git a/test/Hadolint/Rule/DL3041Spec.hs b/test/Hadolint/Rule/DL3041Spec.hs
--- a/test/Hadolint/Rule/DL3041Spec.hs
+++ b/test/Hadolint/Rule/DL3041Spec.hs
@@ -1,5 +1,6 @@
 module Hadolint.Rule.DL3041Spec (spec) where
 
+import qualified Data.Text as Text
 import Data.Default
 import Helpers
 import Test.Hspec
@@ -65,3 +66,72 @@
       onBuildRuleCatchesNot "DL3041" "RUN dnf module install -y tomcat:9 && dnf clean all"
       onBuildRuleCatchesNot "DL3041" "RUN microdnf module install -y tomcat:9 && microdnf clean all"
       onBuildRuleCatchesNot "DL3041" "RUN notdnf module install tomcat"
+
+    it "ok with dnf group install" $ do
+      ruleCatchesNot "DL3041" "RUN dnf -y group install \"Development Tools\""
+      ruleCatchesNot "DL3041" "RUN dnf -y --setopt=group_package_types=\"mandatory\" group install \"Development Tools\""
+      ruleCatchesNot "DL3041" "RUN dnf group install -y \"Development Tools\" && dnf clean all"
+      ruleCatchesNot "DL3041" "RUN microdnf group install -y \"Development Tools\" && microdnf clean all"
+      onBuildRuleCatchesNot "DL3041" "RUN dnf -y group install \"Development Tools\""
+
+    -- this is important e.g. when using renovatebot
+    it "ok with version as variable - braced" $ do
+      let
+        rule = "DL3041"
+        snippet =
+          Text.unlines
+            [ "ENV version=2.51.0-2.fc42",
+              "RUN dnf -y install git-core-${version}"
+            ]
+       in do
+        ruleCatchesNot rule snippet
+
+    it "ok with version as arg - unbraced" $ do
+      let
+        rule = "DL3041"
+        snippet =
+          Text.unlines
+            [ "ARG version=2.51.0-2.fc42",
+              "RUN dnf -y install git-core-$version"
+            ]
+       in do
+        ruleCatchesNot rule snippet
+
+    it "ok with version as arg - different stages" $ do
+      let
+        rule = "DL3041"
+        snippet =
+          Text.unlines
+            [ "FROM fedora:fc42 AS build",
+              "ARG version=2.51.0-2.fc42",
+              "FROM fedora:fc42",
+              "RUN dnf -y install git-core-${version}"
+            ]
+       in do
+        ruleCatchesNot rule snippet
+
+    it "ok with version as env - different stages, reused stage" $ do
+      let
+        rule = "DL3041"
+        snippet =
+          Text.unlines
+            [ "FROM fedora:fc42 AS build",
+              "ENV version=2.51.0-2.fc42",
+              "FROM build",
+              "RUN dnf -y install git-core-${version}"
+            ]
+       in do
+        ruleCatchesNot rule snippet
+
+    it "not ok with version as variable - different stages, new stage" $ do
+      let
+        rule = "DL3041"
+        snippet =
+          Text.unlines
+            [ "FROM fedora:fc42 AS build",
+              "ENV version=2.51.0-2.fc42",
+              "FROM fedora:fc42",
+              "RUN dnf -y install git-core-${version}"
+            ]
+       in do
+        ruleCatches rule snippet
diff --git a/test/Hadolint/Rule/DL3060Spec.hs b/test/Hadolint/Rule/DL3060Spec.hs
--- a/test/Hadolint/Rule/DL3060Spec.hs
+++ b/test/Hadolint/Rule/DL3060Spec.hs
@@ -20,6 +20,8 @@
     it "ok with cache clean" $ do
       ruleCatchesNot "DL3060" "RUN yarn install bar && yarn cache clean"
       onBuildRuleCatchesNot "DL3060" "RUN yarn install bar && yarn cache clean"
+    it "not ok with clean before install" $ do
+      ruleCatches "DL3060" "RUN yarn cache clean && yarn install foo"
 
     it "ok when cache mount is used" $
       let line = "RUN --mount=type=cache,target=/root/.cache/yarn yarn install foobar"
diff --git a/test/Hadolint/Rule/DL3062Spec.hs b/test/Hadolint/Rule/DL3062Spec.hs
--- a/test/Hadolint/Rule/DL3062Spec.hs
+++ b/test/Hadolint/Rule/DL3062Spec.hs
@@ -19,6 +19,17 @@
       it "go version pinned" $ do
         ruleCatchesNot "DL3062" "RUN go install example.com/pkg@v1.2.3"
         onBuildRuleCatchesNot "DL3062" "RUN go install example.com/pkg@v1.2.3"
+
+      it "go install with local absolute path" $ do
+        ruleCatchesNot "DL3062" "RUN go install /go/app/foobar"
+        onBuildRuleCatchesNot "DL3062" "RUN go install /go/app/foobar"
+      it "go install with local relative path" $ do
+        ruleCatchesNot "DL3062" "RUN go install ./app/foobar"
+        onBuildRuleCatchesNot "DL3062" "RUN go install ./app/foobar"
+      it "go install local dir" $ do
+        ruleCatchesNot "DL3062" "RUN go install ."
+        onBuildRuleCatchesNot "DL3062" "RUN go install ."
+
     describe "go get" $ do
       it "go version not pinned" $ do
         ruleCatches "DL3062" "RUN go get example.com/pkg"
@@ -29,6 +40,18 @@
       it "go version pinned" $ do
         ruleCatchesNot "DL3062" "RUN go get example.com/pkg@v1.2.3"
         onBuildRuleCatchesNot "DL3062" "RUN go get example.com/pkg@v1.2.3"
+
+      describe "go get tool" $ do
+        it "version not pinned" $ do
+          ruleCatches "DL3062" "RUN go get -tool foobar"
+          onBuildRuleCatches "DL3062" "RUN go get -tool foobar"
+        it "version pinned as latest" $ do
+          ruleCatches "DL3062" "RUN go get -tool foobar@latest"
+          onBuildRuleCatches "DL3062" "RUN go get -tool foobar@latest"
+        it "version pinned" $ do
+          ruleCatchesNot "DL3062" "RUN go get -tool foobar@v1.2.3"
+          onBuildRuleCatchesNot "DL3062" "RUN go get -tool foobar@v1.2.3"
+
     describe "go run" $ do
       it "go version not pinned" $ do
         ruleCatches "DL3062" "RUN go run example.com/pkg"
@@ -39,3 +62,26 @@
       it "go version pinned" $ do
         ruleCatchesNot "DL3062" "RUN go run example.com/pkg@v1.2.3"
         onBuildRuleCatchesNot "DL3062" "RUN go run example.com/pkg@v1.2.3"
+
+      it "go version pinned with flags" $ do
+        ruleCatchesNot "DL3062" "RUN go run example.com/pkg@v1.2.3 -f"
+        onBuildRuleCatchesNot "DL3062" "RUN go run example.com/pkg@v1.2.3 -f"
+        ruleCatchesNot "DL3062" "RUN go run example.com/pkg@v1.2.3 -foobar"
+        onBuildRuleCatchesNot "DL3062" "RUN go run example.com/pkg@v1.2.3 -foobar"
+        ruleCatchesNot "DL3062" "RUN go run example.com/pkg@v1.2.3 --foobar"
+        onBuildRuleCatchesNot "DL3062" "RUN go run example.com/pkg@v1.2.3 --foobar"
+      it "go version pinned with arguments" $ do
+        ruleCatchesNot "DL3062" "RUN go run example.com/pkg@v1.2.3 foobar"
+        onBuildRuleCatchesNot "DL3062" "RUN go run example.com/pkg@v1.2.3 foobar"
+        ruleCatchesNot "DL3062" "RUN go run example.com/pkg@v1.2.3 --foo bar"
+        onBuildRuleCatchesNot "DL3062" "RUN go run example.com/pkg@v1.2.3 --foo bar"
+
+      it "go run with local absolute path" $ do
+        ruleCatchesNot "DL3062" "RUN go run /go/app/foobar"
+        onBuildRuleCatchesNot "DL3062" "RUN go run /go/app/foobar"
+      it "go run with local relative path" $ do
+        ruleCatchesNot "DL3062" "RUN go run ./app/foobar"
+        onBuildRuleCatchesNot "DL3062" "RUN go run ./app/foobar"
+      it "go run local dir" $ do
+        ruleCatchesNot "DL3062" "RUN go run ."
+        onBuildRuleCatchesNot "DL3062" "RUN go run ."
diff --git a/test/Hadolint/Rule/DL3063Spec.hs b/test/Hadolint/Rule/DL3063Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3063Spec.hs
@@ -0,0 +1,18 @@
+module Hadolint.Rule.DL3063Spec (spec) where
+
+import Data.Default
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+
+  describe "DL3063 - Stage name is a reserved word" $ do
+    it "not ok: stage name `scratch`" $
+      ruleCatches "DL3063" "FROM foo:bar AS scratch"
+    it "not ok: stage name `context`" $
+      ruleCatches "DL3063" "FROM foo:bar AS context"
+    it "ok: stage name `foobar`" $
+      ruleCatchesNot "DL3063" "FROM foo:bar AS foobar"
diff --git a/test/Hadolint/Rule/DL3064Spec.hs b/test/Hadolint/Rule/DL3064Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3064Spec.hs
@@ -0,0 +1,86 @@
+module Hadolint.Rule.DL3064Spec (spec) where
+
+import Data.Default
+import qualified Data.Text as Text
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+
+  describe "DL3064 - Potential sensitive data should not be used in `ARG` or `ENV`" $ do
+    it "ok: no ARG, no ENV" $ do
+      let dockerfile =
+            Text.unlines
+              [ "FROM debian:bullseye",
+                "RUN foobar"
+              ]
+       in do
+          ruleCatchesNot "DL3064" dockerfile
+          onBuildRuleCatchesNot "DL3064" dockerfile
+    it "ok: ARG no sensitive data, no ENV" $ do
+      let dockerfile =
+            Text.unlines
+              [ "ARG foobar",
+                "FROM debian:bullseye",
+                "RUN foobar"
+              ]
+       in do
+          ruleCatchesNot "DL3064" dockerfile
+          onBuildRuleCatchesNot "DL3064" dockerfile
+    it "ok: no ARG, ENV no sensitive data" $ do
+      let dockerfile =
+            Text.unlines
+              [ "FROM debian:bullseye",
+                "ENV foobar=barfoo",
+                "RUN foobar"
+              ]
+       in do
+          ruleCatchesNot "DL3064" dockerfile
+          onBuildRuleCatchesNot "DL3064" dockerfile
+
+    it "not ok: ARG with sensitive data" $ do
+      let dockerfile =
+            Text.unlines
+              [ "ARG AWS_ACCESS_KEY_ID",
+                "FROM debian:bullseye",
+                "RUN foobar"
+              ]
+       in do
+          ruleCatches "DL3064" dockerfile
+          onBuildRuleCatches "DL3064" dockerfile
+
+    it "not ok: ARG with sensitive data, different casing" $ do
+      let dockerfile =
+            Text.unlines
+              [ "ARG openai_api_key",
+                "FROM debian:bullseye",
+                "RUN foobar"
+              ]
+       in do
+          ruleCatches "DL3064" dockerfile
+          onBuildRuleCatches "DL3064" dockerfile
+
+    it "not ok: ENV with sensitive data" $ do
+      let dockerfile =
+            Text.unlines
+              [ "FROM debian:bullseye",
+                "ENV AWS_ACCESS_KEY_ID=abcdefghijklmnopqrstuvwxyz",
+                "RUN foobar"
+              ]
+       in do
+          ruleCatches "DL3064" dockerfile
+          onBuildRuleCatches "DL3064" dockerfile
+
+    it "not ok: ENV with sensitive data, different casing" $ do
+      let dockerfile =
+            Text.unlines
+              [ "FROM debian:bullseye",
+                "ENV my_password=abcdefghijklmnopqrstuvwxyz",
+                "RUN foobar"
+              ]
+       in do
+          ruleCatches "DL3064" dockerfile
+          onBuildRuleCatches "DL3064" dockerfile
diff --git a/test/Hadolint/Rule/DL3065Spec.hs b/test/Hadolint/Rule/DL3065Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3065Spec.hs
@@ -0,0 +1,25 @@
+module Hadolint.Rule.DL3065Spec (spec) where
+
+import Data.Default
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+  let rule = "DL3065"
+
+  describe "DL3065 - Setting `FROM --platform` to predefined `$TARGETPLATFORM` in is redundant as this is the default behavior" $ do
+
+    it "ok: FROM no --platform" $ do
+      ruleCatchesNot rule "FROM alpine:3.24"
+
+    it "ok: FROM --platform not $TARGETPLATFORM" $ do
+      ruleCatchesNot rule "FROM --platform=$FOOBAR alpine:3.24"
+      ruleCatchesNot rule "FROM --platform=foobar alpine:3.24"
+      ruleCatchesNot rule "FROM --platform=foobar/arm64 alpine:3.24"
+
+    it "not ok: FROM --platform=$TARGETPLATFORM" $ do
+      ruleCatches rule "FROM --platform=$TARGETPLATFORM alpine:3.24"
+      ruleCatches rule "FROM --platform=${TARGETPLATFORM} alpine:3.24"
diff --git a/test/Hadolint/Rule/DL3066Spec.hs b/test/Hadolint/Rule/DL3066Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3066Spec.hs
@@ -0,0 +1,25 @@
+module Hadolint.Rule.DL3066Spec (spec) where
+
+import Data.Default
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+  let rule = "DL3066"
+
+  describe "DL3066 Non-numeric user-id may not be resolvable by host system" $ do
+
+    it "ok: numeric UID" $ do
+      ruleCatchesNot rule "USER 12345"
+
+    it "ok: numeric UID and GID" $ do
+      ruleCatchesNot rule "USER 1234:5678"
+
+    it "not ok: non-numeric UID" $ do
+      ruleCatches rule "USER foobar"
+
+    it "not ok: non-numeric UID and GID" $ do
+      ruleCatches rule "USER foobar:barfoo"
diff --git a/test/Hadolint/Rule/DL3067Spec.hs b/test/Hadolint/Rule/DL3067Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hadolint/Rule/DL3067Spec.hs
@@ -0,0 +1,27 @@
+module Hadolint.Rule.DL3067Spec (spec) where
+
+import Data.Default
+import Helpers
+import Test.Hspec
+
+
+spec :: SpecWith ()
+spec = do
+  let ?config = def
+  let rule = "DL3067"
+
+  describe "DL3067 - Do not copy an entire filesystem from another stage" $ do
+    it "warns when copying the root filesystem from another stage to root" $
+      ruleCatches rule "COPY --from=build / /"
+
+    it "warns when copying quoted root paths from another stage to root" $
+      ruleCatches rule "COPY --from=build \"/\" \"/\""
+
+    it "does not warn when copying root without --from" $
+      ruleCatchesNot rule "COPY / /"
+
+    it "does not warn when copying a directory from another stage" $
+      ruleCatchesNot rule "COPY --from=build /app /"
+
+    it "does not warn when copying root from another stage into a subdirectory" $
+      ruleCatchesNot rule "COPY --from=build / /app"
diff --git a/test/Hadolint/Rule/ShellcheckSpec.hs b/test/Hadolint/Rule/ShellcheckSpec.hs
--- a/test/Hadolint/Rule/ShellcheckSpec.hs
+++ b/test/Hadolint/Rule/ShellcheckSpec.hs
@@ -11,13 +11,19 @@
   let ?config = def
 
   describe "Shellcheck" $ do
+
     it "runs shellcheck 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 "no warnings on exec notations" $ do
+      assertChecks "RUN [\"foobar\", \"$f\"]" passesShellcheck
+      assertOnBuildChecks "RUN [\"foobar\", \"$f\"]" passesShellcheck
+
     it "Does not complain on default env vars" $
       let dockerFile =
             Text.unlines
@@ -169,3 +175,53 @@
        in do
             assertChecks dockerFile passesShellcheck
             assertOnBuildChecks dockerFile passesShellcheck
+
+    it "SHELL instructions are propagated in multi-stage builds 1" $
+      let dockerfile =
+            Text.unlines
+              [ "FROM ubuntu:24.04 AS base",
+                "SHELL [\"/bin/bash\", \"-e\", \"-o\", \"pipefail\", \"-c\"]",
+                "",
+                "FROM base",
+                "RUN source /etc/os-release"
+              ]
+       in do
+            assertChecks dockerfile passesShellcheck
+
+    it "SHELL instructions are propagated only propagated if referencing earlier stage" $
+      let dockerfile =
+            Text.unlines
+              [ "FROM ubuntu:24.04 AS base",
+                "SHELL [\"/bin/bash\", \"-e\", \"-o\", \"pipefail\", \"-c\"]",
+                "",
+                "FROM ubuntu:24.04",
+                "RUN source /etc/os-release"
+              ]
+       in do
+            assertChecks dockerfile failsShellcheck
+
+    it "SHELL instructions are propagated in multi-stage builds 2" $
+      let dockerfile =
+            Text.unlines
+              [ "FROM something AS nothing",
+                "FROM ubuntu:24.04 AS base",
+                "SHELL [\"/bin/bash\", \"-e\", \"-o\", \"pipefail\", \"-c\"]",
+                "FROM nothing",
+                "FROM base",
+                "RUN source /etc/os-release"
+              ]
+       in do
+            assertChecks dockerfile passesShellcheck
+
+    it "SHELL instructions are propagated in multi-stage builds 3" $
+      let dockerfile =
+            Text.unlines
+              [ "FROM ubuntu:24.04 AS base",
+                "SHELL [\"/bin/bash\", \"-e\", \"-o\", \"pipefail\", \"-c\"]",
+                "FROM base AS next",
+                "RUN foobar && barfoo | tee logfile",
+                "FROM next",
+                "RUN source /etc/os-release"
+              ]
+       in do
+            assertChecks dockerfile passesShellcheck
diff --git a/test/Hadolint/ShellSpec.hs b/test/Hadolint/ShellSpec.hs
--- a/test/Hadolint/ShellSpec.hs
+++ b/test/Hadolint/ShellSpec.hs
@@ -51,3 +51,8 @@
                         [ CmdPart "loglevel" 1]
        in do
         dropFlagArg ["loglevel"] cmd `shouldBe` res
+  --
+    it "dropFlagArg keeps the next argument when the flag value is inline" $ do
+      let cmds = presentCommands (parseShell "npm --loglevel=info install bla@1.0.0")
+      cmds `shouldNotBe` []
+      map (dropFlagArg ["loglevel"]) cmds `shouldBe` cmds
diff --git a/test/Helpers.hs b/test/Helpers.hs
--- a/test/Helpers.hs
+++ b/test/Helpers.hs
@@ -3,7 +3,7 @@
 import Control.Monad (unless, when)
 import qualified Data.ByteString.Lazy.Char8 as BSC
 import Data.Aeson hiding (Result)
-import Hadolint (Configuration (..), OutputFormat (..), printResults)
+import Hadolint (Configuration (..), OutputFormat (..), write)
 import Hadolint.Formatter.Format (Result (..))
 import Hadolint.Formatter.TTY (formatCheck)
 import Hadolint.Rule (CheckFailure (..), Failures, RuleCode (..))
@@ -17,6 +17,7 @@
 import qualified Data.Sequence as Seq
 import qualified Data.Text as Text
 import qualified Hadolint.Process
+import qualified Text.XML as XML
 
 
 assertChecks ::
@@ -125,18 +126,17 @@
   where
     matched = Seq.filter (\CheckFailure {code = RuleCode rc} -> "SC" `Text.isPrefixOf` rc) checks
 
-
 assertFormatter ::
   (HasCallStack, ?noColor :: Bool) =>
   OutputFormat ->
   [CheckFailure] ->
   String ->
   Assertion
-assertFormatter formatter failures expectation = do
+assertFormatter format failures expectation = do
   let results =
         NonEmpty.fromList [Result "<string>" mempty (Seq.fromList failures)]
   (cap, _) <- capture
-                (printResults formatter ?noColor (Just "<string>") results)
+                (write [] [format] ?noColor (Just "<string>") results)
   cap `shouldBe` expectation
 
 assertFormatterJson
@@ -145,9 +145,22 @@
   [CheckFailure] ->
   Value ->
   Assertion
-assertFormatterJson formatter failures expectation = do
+assertFormatterJson format failures expectation = do
   let results =
         NonEmpty.fromList [ Result "<string>" mempty (Seq.fromList failures) ]
   (cap, _) <- capture
-                (printResults formatter ?noColor (Just "<string>") results)
+                (write [] [format] ?noColor (Just "<string>") results)
   decode (BSC.pack cap) `shouldBe` Just expectation
+
+assertFormatterXML
+  :: (HasCallStack, ?noColor :: Bool) =>
+  OutputFormat ->
+  [CheckFailure] ->
+  XML.Document ->
+  Assertion
+assertFormatterXML format failures expectation = do
+  let results =
+        NonEmpty.fromList [ Result "<string>" mempty (Seq.fromList failures) ]
+  (cap, _) <- capture
+                (write [] [format] ?noColor (Just "<string>") results)
+  XML.parseLBS_ XML.def (BSC.pack cap) `shouldBe` expectation
