packages feed

hadolint 2.14.0 → 2.15.0

raw patch · 75 files changed

+2440/−376 lines, 75 filesdep +ghc-experimentaldep ~language-dockerdep ~timedep ~xml-conduitnew-component:exe:hadolint-embedded

Dependencies added: ghc-experimental

Dependency ranges changed: language-docker, time, xml-conduit

Files

README.md view
@@ -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
app/Main.hs view
@@ -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 ()
+ app/embedded.hs view
@@ -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
hadolint.cabal view
@@ -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
src/Hadolint/Config/Commandline.hs view
@@ -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"                   ]           ) 
src/Hadolint/Config/Configuration.hs view
@@ -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
src/Hadolint/Config/Environment.hs view
@@ -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
src/Hadolint/Formatter.hs view
@@ -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
src/Hadolint/Formatter/Checkstyle.hs view
@@ -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
src/Hadolint/Formatter/Codacy.hs view
@@ -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 )
src/Hadolint/Formatter/Codeclimate.hs view
@@ -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
src/Hadolint/Formatter/Format.hs view
@@ -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  
src/Hadolint/Formatter/Gnu.hs view
@@ -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:"
+ src/Hadolint/Formatter/JUnit.hs view
@@ -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+                      ]+                ]+            }+        ]+    }+
src/Hadolint/Formatter/Json.hs view
@@ -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 
src/Hadolint/Formatter/Sarif.hs view
@@ -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"
src/Hadolint/Formatter/SonarQube.hs view
@@ -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
src/Hadolint/Formatter/TTY.hs view
@@ -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 =
src/Hadolint/Lint.hs view
@@ -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
src/Hadolint/Pragma.hs view
@@ -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
src/Hadolint/Process.hs view
@@ -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
src/Hadolint/Rule/DL3010.hs view
@@ -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,
src/Hadolint/Rule/DL3013.hs view
@@ -80,8 +80,10 @@           "prefix",           "progress-bar",           "proxy",+          "python",           "python-version",           "root",+          "root-user-action",           "src",           "t",           "target",
src/Hadolint/Rule/DL3016.hs view
@@ -81,7 +81,7 @@   ignoreFlags :: [Text.Text]-ignoreFlags = ["loglevel"]+ignoreFlags = ["loglevel", "registry"]  gitPrefixes :: [Text.Text] gitPrefixes = ["git://", "git+ssh://", "git+http://", "git+https://"]
src/Hadolint/Rule/DL3018.hs view
@@ -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"]
src/Hadolint/Rule/DL3022.hs view
@@ -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
src/Hadolint/Rule/DL3023.hs view
@@ -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!
src/Hadolint/Rule/DL3024.hs view
@@ -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 #-}
src/Hadolint/Rule/DL3025.hs view
@@ -12,5 +12,6 @@      check (Cmd (ArgumentsText _)) = False     check (Entrypoint (ArgumentsText _)) = False+    check (Healthcheck (Check (CheckArgs (ArgumentsText _) _ _ _ _ _))) = False     check _ = True {-# INLINEABLE rule #-}
src/Hadolint/Rule/DL3026.hs view
@@ -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 "*"
src/Hadolint/Rule/DL3032.hs view
@@ -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"]
src/Hadolint/Rule/DL3033.hs view
@@ -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
src/Hadolint/Rule/DL3036.hs view
@@ -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 
src/Hadolint/Rule/DL3040.hs view
@@ -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 #-}+
src/Hadolint/Rule/DL3041.hs view
@@ -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
src/Hadolint/Rule/DL3047.hs view
@@ -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
src/Hadolint/Rule/DL3049.hs view
@@ -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 =
src/Hadolint/Rule/DL3060.hs view
@@ -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.
src/Hadolint/Rule/DL3062.hs view
@@ -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)
+ src/Hadolint/Rule/DL3063.hs view
@@ -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 #-}
+ src/Hadolint/Rule/DL3064.hs view
@@ -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"+  ]
+ src/Hadolint/Rule/DL3065.hs view
@@ -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 #-}
+ src/Hadolint/Rule/DL3066.hs view
@@ -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
+ src/Hadolint/Rule/DL3067.hs view
@@ -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 == "/"
src/Hadolint/Rule/DL4006.hs view
@@ -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
src/Hadolint/Rule/Shellcheck.hs view
@@ -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 ->
src/Hadolint/Shell.hs view
@@ -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
test/Hadolint/Config/CommandlineSpec.hs view
@@ -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 } 
test/Hadolint/Config/ConfigfileSpec.hs view
@@ -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:",
test/Hadolint/Config/ConfigurationSpec.hs view
@@ -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 }
test/Hadolint/Config/EnvironmentSpec.hs view
@@ -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
+ test/Hadolint/Formatter/JUnitSpec.hs view
@@ -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
+ test/Hadolint/Formatter/SonarQubeSpec.hs view
@@ -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
test/Hadolint/Formatter/TTYSpec.hs view
@@ -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
test/Hadolint/PragmaSpec.hs view
@@ -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 
test/Hadolint/Rule/DL3013Spec.hs view
@@ -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"
test/Hadolint/Rule/DL3016Spec.hs view
@@ -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"
test/Hadolint/Rule/DL3018Spec.hs view
@@ -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 \\",
test/Hadolint/Rule/DL3025Spec.hs view
@@ -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" $
test/Hadolint/Rule/DL3026Spec.hs view
@@ -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" )
test/Hadolint/Rule/DL3032Spec.hs view
@@ -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"
test/Hadolint/Rule/DL3033Spec.hs view
@@ -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
test/Hadolint/Rule/DL3036Spec.hs view
@@ -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
test/Hadolint/Rule/DL3040Spec.hs view
@@ -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"
test/Hadolint/Rule/DL3041Spec.hs view
@@ -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
test/Hadolint/Rule/DL3060Spec.hs view
@@ -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"
test/Hadolint/Rule/DL3062Spec.hs view
@@ -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 ."
+ test/Hadolint/Rule/DL3063Spec.hs view
@@ -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"
+ test/Hadolint/Rule/DL3064Spec.hs view
@@ -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
+ test/Hadolint/Rule/DL3065Spec.hs view
@@ -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"
+ test/Hadolint/Rule/DL3066Spec.hs view
@@ -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"
+ test/Hadolint/Rule/DL3067Spec.hs view
@@ -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"
test/Hadolint/Rule/ShellcheckSpec.hs view
@@ -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
test/Hadolint/ShellSpec.hs view
@@ -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
test/Helpers.hs view
@@ -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