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 +32/−16
- app/Main.hs +5/−4
- app/embedded.hs +79/−0
- hadolint.cabal +46/−3
- src/Hadolint/Config/Commandline.hs +20/−6
- src/Hadolint/Config/Configuration.hs +12/−6
- src/Hadolint/Config/Environment.hs +11/−3
- src/Hadolint/Formatter.hs +42/−13
- src/Hadolint/Formatter/Checkstyle.hs +13/−17
- src/Hadolint/Formatter/Codacy.hs +6/−8
- src/Hadolint/Formatter/Codeclimate.hs +40/−23
- src/Hadolint/Formatter/Format.hs +18/−15
- src/Hadolint/Formatter/Gnu.hs +14/−23
- src/Hadolint/Formatter/JUnit.hs +165/−0
- src/Hadolint/Formatter/Json.hs +5/−7
- src/Hadolint/Formatter/Sarif.hs +6/−8
- src/Hadolint/Formatter/SonarQube.hs +28/−18
- src/Hadolint/Formatter/TTY.hs +9/−10
- src/Hadolint/Lint.hs +4/−5
- src/Hadolint/Pragma.hs +23/−0
- src/Hadolint/Process.hs +31/−4
- src/Hadolint/Rule/DL3010.hs +1/−1
- src/Hadolint/Rule/DL3013.hs +2/−0
- src/Hadolint/Rule/DL3016.hs +1/−1
- src/Hadolint/Rule/DL3018.hs +8/−9
- src/Hadolint/Rule/DL3022.hs +1/−1
- src/Hadolint/Rule/DL3023.hs +1/−1
- src/Hadolint/Rule/DL3024.hs +3/−5
- src/Hadolint/Rule/DL3025.hs +1/−0
- src/Hadolint/Rule/DL3026.hs +29/−10
- src/Hadolint/Rule/DL3032.hs +3/−3
- src/Hadolint/Rule/DL3033.hs +110/−8
- src/Hadolint/Rule/DL3036.hs +3/−2
- src/Hadolint/Rule/DL3040.hs +7/−4
- src/Hadolint/Rule/DL3041.hs +112/−9
- src/Hadolint/Rule/DL3047.hs +4/−4
- src/Hadolint/Rule/DL3049.hs +1/−1
- src/Hadolint/Rule/DL3060.hs +13/−0
- src/Hadolint/Rule/DL3062.hs +28/−8
- src/Hadolint/Rule/DL3063.hs +18/−0
- src/Hadolint/Rule/DL3064.hs +95/−0
- src/Hadolint/Rule/DL3065.hs +18/−0
- src/Hadolint/Rule/DL3066.hs +25/−0
- src/Hadolint/Rule/DL3067.hs +22/−0
- src/Hadolint/Rule/DL4006.hs +1/−2
- src/Hadolint/Rule/Shellcheck.hs +126/−59
- src/Hadolint/Shell.hs +58/−4
- test/Hadolint/Config/CommandlineSpec.hs +97/−19
- test/Hadolint/Config/ConfigfileSpec.hs +17/−8
- test/Hadolint/Config/ConfigurationSpec.hs +2/−2
- test/Hadolint/Config/EnvironmentSpec.hs +19/−8
- test/Hadolint/Formatter/JUnitSpec.hs +231/−0
- test/Hadolint/Formatter/SonarQubeSpec.hs +117/−0
- test/Hadolint/Formatter/TTYSpec.hs +4/−6
- test/Hadolint/PragmaSpec.hs +39/−0
- test/Hadolint/Rule/DL3013Spec.hs +12/−0
- test/Hadolint/Rule/DL3016Spec.hs +9/−0
- test/Hadolint/Rule/DL3018Spec.hs +30/−1
- test/Hadolint/Rule/DL3025Spec.hs +12/−0
- test/Hadolint/Rule/DL3026Spec.hs +52/−1
- test/Hadolint/Rule/DL3032Spec.hs +3/−0
- test/Hadolint/Rule/DL3033Spec.hs +63/−0
- test/Hadolint/Rule/DL3036Spec.hs +3/−0
- test/Hadolint/Rule/DL3040Spec.hs +51/−4
- test/Hadolint/Rule/DL3041Spec.hs +70/−0
- test/Hadolint/Rule/DL3060Spec.hs +2/−0
- test/Hadolint/Rule/DL3062Spec.hs +46/−0
- test/Hadolint/Rule/DL3063Spec.hs +18/−0
- test/Hadolint/Rule/DL3064Spec.hs +86/−0
- test/Hadolint/Rule/DL3065Spec.hs +25/−0
- test/Hadolint/Rule/DL3066Spec.hs +25/−0
- test/Hadolint/Rule/DL3067Spec.hs +27/−0
- test/Hadolint/Rule/ShellcheckSpec.hs +56/−0
- test/Hadolint/ShellSpec.hs +5/−0
- test/Helpers.hs +19/−6
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 || 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