packages feed

hadolint 1.18.1 → 1.18.2

raw patch · 15 files changed

+2155/−2073 lines, 15 filesdep +asyncdep +parallel

Dependencies added: async, parallel

Files

app/Main.hs view
@@ -1,6 +1,6 @@-{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE TemplateHaskell #-}  module Main where @@ -8,22 +8,44 @@ import qualified Data.List.NonEmpty as NonEmpty import Data.Semigroup ((<>)) import qualified Data.Set as Set-import Data.String+import Data.String (IsString (fromString)) import qualified Data.Version import qualified Development.GitRev-import Options.Applicative hiding (ParseError)-import qualified Paths_hadolint -- version from hadolint.cabal file-import System.Exit (exitFailure, exitSuccess)- import qualified Hadolint+import Options.Applicative+  ( Parser,+    action,+    argument,+    completeWith,+    execParser,+    fullDesc,+    header,+    help,+    helper,+    info,+    long,+    maybeReader,+    metavar,+    option,+    progDesc,+    short,+    showDefaultWith,+    str,+    strOption,+    switch,+    value,+  )+-- version from hadolint.cabal file+import qualified Paths_hadolint as Meta+import System.Exit (exitFailure, exitSuccess)  data CommandOptions = CommandOptions-    { showVersion :: Bool-    , configFile :: Maybe FilePath-    , format :: Hadolint.OutputFormat-    , dockerfiles :: [String]-    , lintingOptions :: Hadolint.LintOptions-    }+  { showVersion :: Bool,+    configFile :: Maybe FilePath,+    format :: Hadolint.OutputFormat,+    dockerfiles :: [String],+    lintingOptions :: Hadolint.LintOptions+  }  toOutputFormat :: String -> Maybe Hadolint.OutputFormat toOutputFormat "json" = Just Hadolint.Json@@ -42,81 +64,85 @@  parseOptions :: Parser CommandOptions parseOptions =-    CommandOptions <$> -- CLI options parser definition-    version <*>-    configFile <*>-    outputFormat <*>-    files <*>-    lintOptions+  CommandOptions+    <$> version -- CLI options parser definition+    <*> configFile+    <*> outputFormat+    <*> files+    <*> lintOptions   where     version = switch (long "version" <> short 'v' <> help "Show version")-    ---    -- | Parse the config filename to use+     configFile =-        optional-            (strOption-                 (long "config" <> short 'c' <> metavar "FILENAME" <>-                  help "Path to the configuration file"))-    ---    -- | Parse the output format option+      optional+        ( strOption+            ( long "config" <> short 'c' <> metavar "FILENAME"+                <> help "Path to the configuration file"+            )+        )+     outputFormat =-        option-            (maybeReader toOutputFormat)-            (long "format" <> -- options for the output format-             short 'f' <>-             help-                 "The output format for the results [tty | json | checkstyle | codeclimate | codacy]" <>-             value Hadolint.TTY <> -- The default value-             showDefaultWith showFormat <>-             completeWith ["tty", "json", "checkstyle", "codeclimate", "codacy"])-    ---    -- | Parse a list of ignored rules+      option+        (maybeReader toOutputFormat)+        ( long "format"+            <> short 'f' -- options for the output format+            <> help+              "The output format for the results [tty | json | checkstyle | codeclimate | codacy]"+            <> value Hadolint.TTY+            <> showDefaultWith showFormat -- The default value+            <> completeWith ["tty", "json", "checkstyle", "codeclimate", "codacy"]+        )+     ignoreList =-        many-            (strOption-                 (long "ignore" <>-                  help "A rule to ignore. If present, the ignore list in the config file is ignored" <>-                  metavar "RULECODE"))-    ---    -- | Parse a list of dockerfile names+      many+        ( strOption+            ( long "ignore"+                <> help "A rule to ignore. If present, the ignore list in the config file is ignored"+                <> metavar "RULECODE"+            )+        )+     files = many (argument str (metavar "DOCKERFILE..." <> action "file"))-    ---    -- | Parse the rule ignore list and the rules configuration into a LintOptions+     lintOptions = Hadolint.LintOptions <$> ignoreList <*> parseRulesConfig-    ---    -- | Parse all the optional rules configuration+     parseRulesConfig =-        Hadolint.RulesConfig . Set.fromList . fmap fromString <$>-        many-            (strOption-                 (long "trusted-registry" <>-                  help "A docker registry to allow to appear in FROM instructions" <>-                  metavar "REGISTRY (e.g. docker.io)"))+      Hadolint.RulesConfig . Set.fromList . fmap fromString+        <$> many+          ( strOption+              ( long "trusted-registry"+                  <> help "A docker registry to allow to appear in FROM instructions"+                  <> metavar "REGISTRY (e.g. docker.io)"+              )+          )  main :: IO () main = do-    cmd <- execParser opts-    execute cmd+  cmd <- execParser opts+  execute cmd   where     execute CommandOptions {showVersion = True} = putStrLn getVersion >> exitSuccess     execute CommandOptions {dockerfiles = []} =-        putStrLn "Please provide a Dockerfile" >> exitFailure+      putStrLn "Please provide a Dockerfile" >> exitFailure     execute cmd = do-        lintConfig <- Hadolint.applyConfig (configFile cmd) (lintingOptions cmd)-        let files = NonEmpty.fromList (dockerfiles cmd)-        case lintConfig of-            Left err -> error err-            Right conf -> do-                res <- Hadolint.lint conf files-                Hadolint.printResultsAndExit (format cmd) res+      lintConfig <- Hadolint.applyConfig (configFile cmd) (lintingOptions cmd)+      let files = NonEmpty.fromList (dockerfiles cmd)+      case lintConfig of+        Left err -> error err+        Right conf -> do+          res <- Hadolint.lint conf files+          Hadolint.printResultsAndExit (format cmd) res     opts =-        info-            (helper <*> parseOptions)-            (fullDesc <> progDesc "Lint Dockerfile for errors and best practices" <>-             header "hadolint - Dockerfile Linter written in Haskell")+      info+        (helper <*> parseOptions)+        ( fullDesc <> progDesc "Lint Dockerfile for errors and best practices"+            <> header "hadolint - Dockerfile Linter written in Haskell"+        )  getVersion :: String getVersion-    | $(Development.GitRev.gitDescribe) == "UNKNOWN" =-        "Haskell Dockerfile Linter " ++ Data.Version.showVersion Paths_hadolint.version ++ "-no-git"-    | otherwise = "Haskell Dockerfile Linter " ++ $(Development.GitRev.gitDescribe)+  | version == "UNKNOWN" =+    "Haskell Dockerfile Linter " ++ Data.Version.showVersion Meta.version ++ "-no-git"+  | otherwise = "Haskell Dockerfile Linter " ++ version+  where+    version = $(Development.GitRev.gitDescribe)
hadolint.cabal view
@@ -1,13 +1,13 @@-cabal-version: 1.12+cabal-version: 2.0  -- This file has been generated from package.yaml by hpack version 0.33.0. -- -- see: https://github.com/sol/hpack ----- hash: f2454ac92e402e2007f598a079c8998996473f8fc6501b81e3eb1f9cd98d5f56+-- hash: 418683858197c94a7d90196b639708548b3c5966bc20506c43b2383a836a281b  name:           hadolint-version:        1.18.1+version:        1.18.2 synopsis:       Dockerfile Linter JavaScript API description:    A smarter Dockerfile linter that helps you build best practice Docker images. category:       Development@@ -45,6 +45,8 @@       Hadolint.Shell   other-modules:       Paths_hadolint+  autogen-modules:+      Paths_hadolint   hs-source-dirs:       src   ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints -optP-Wno-nonportable-include-path@@ -52,6 +54,7 @@       HsYAML     , ShellCheck >=0.7.1     , aeson+    , async     , base >=4.8 && <5     , bytestring     , containers@@ -60,6 +63,7 @@     , language-docker >=9.1.2 && <10     , megaparsec >=7.0     , mtl+    , parallel     , split >=0.2     , text     , void@@ -71,7 +75,7 @@       Paths_hadolint   hs-source-dirs:       app-  ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints -optP-Wno-nonportable-include-path+  ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints -optP-Wno-nonportable-include-path -O2 -threaded -rtsopts "-with-rtsopts=-N5 -A4m"   build-depends:       base >=4.8 && <5     , containers
src/Hadolint.hs view
@@ -1,8 +1,9 @@ module Hadolint-    ( module Hadolint.Lint-    , module Hadolint.Rules-    , module Hadolint.Config-    ) where+  ( module Hadolint.Lint,+    module Hadolint.Rules,+    module Hadolint.Config,+  )+where  import Hadolint.Config import Hadolint.Lint
src/Hadolint/Config.hs view
@@ -1,35 +1,39 @@ {-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings #-} -module Hadolint.Config (applyConfig, ConfigFile(..)) where+module Hadolint.Config (applyConfig, ConfigFile (..)) where  import Control.Monad (filterM)+import qualified Data.ByteString as Bytes import Data.Coerce (coerce) import Data.Maybe (fromMaybe, listToMaybe)-import qualified Data.ByteString as Bytes import qualified Data.Set as Set-import qualified Data.YAML as Yaml import Data.YAML ((.:?))-import GHC.Generics+import qualified Data.YAML as Yaml+import GHC.Generics (Generic)+import qualified Hadolint.Lint as Lint+import qualified Hadolint.Rules as Rules import qualified Language.Docker as Docker import System.Directory-       (XdgDirectory(..), doesFileExist, getCurrentDirectory,-        getXdgDirectory)+  ( XdgDirectory (..),+    doesFileExist,+    getCurrentDirectory,+    getXdgDirectory,+  ) import System.FilePath ((</>)) -import qualified Hadolint.Lint as Lint-import qualified Hadolint.Rules as Rules- data ConfigFile = ConfigFile-    { ignoredRules :: Maybe [Lint.IgnoreRule]-    , trustedRegistries :: Maybe [Lint.TrustedRegistry]-    } deriving (Show, Eq, Generic)+  { ignoredRules :: Maybe [Lint.IgnoreRule],+    trustedRegistries :: Maybe [Lint.TrustedRegistry]+  }+  deriving (Show, Eq, Generic)  instance Yaml.FromYAML ConfigFile where-  parseYAML = Yaml.withMap "ConfigFile" $ \m -> ConfigFile-       <$> m .:? "ignored"-       <*> m .:? "trustedRegistries"+  parseYAML = Yaml.withMap "ConfigFile" $ \m ->+    ConfigFile+      <$> m .:? "ignored"+      <*> m .:? "trustedRegistries"  -- | If both the ignoreRules and rulesConfig properties of Lint options are empty -- then this function will fill them with the default found in the passed config@@ -37,50 +41,53 @@ -- return the error string. applyConfig :: Maybe FilePath -> Lint.LintOptions -> IO (Either String Lint.LintOptions) applyConfig maybeConfig o-    | not (null (Lint.ignoreRules o)) && Lint.rulesConfig o /= mempty = return (Right o)-    | otherwise = do-        theConfig <--            case maybeConfig of-                Nothing -> findConfig-                c -> return c-        case theConfig of-            Nothing -> return (Right o)-            Just config -> parseAndApply config+  | not (null (Lint.ignoreRules o)) && Lint.rulesConfig o /= mempty = return (Right o)+  | otherwise = do+    theConfig <-+      case maybeConfig of+        Nothing -> findConfig+        c -> return c+    case theConfig of+      Nothing -> return (Right o)+      Just config -> parseAndApply config   where     findConfig = do-        localConfigFile <- (</> ".hadolint.yaml") <$> getCurrentDirectory-        configFile <- getXdgDirectory XdgConfig "hadolint.yaml"-        listToMaybe <$> filterM doesFileExist [localConfigFile, configFile]+      localConfigFile <- (</> ".hadolint.yaml") <$> getCurrentDirectory+      configFile <- getXdgDirectory XdgConfig "hadolint.yaml"+      listToMaybe <$> filterM doesFileExist [localConfigFile, configFile]+     parseAndApply :: FilePath -> IO (Either String Lint.LintOptions)     parseAndApply configFile = do-        contents <- Bytes.readFile configFile-        case Yaml.decode1Strict contents of-            Left (_, err) -> return $ Left (formatError err configFile)-            Right (ConfigFile ignore trusted) -> return (Right (override ignore trusted))-    -- | Applies the configuration found in the file to the passed Lint.LintOptions+      contents <- Bytes.readFile configFile+      case Yaml.decode1Strict contents of+        Left (_, err) -> return $ Left (formatError err configFile)+        Right (ConfigFile ignore trusted) -> return (Right (override ignore trusted))+     override ignore trusted = applyTrusted trusted . applyIgnore ignore $ o     applyIgnore ignore opts =-        case Lint.ignoreRules opts of-            [] -> opts {Lint.ignoreRules = fromMaybe [] ignore}-            _ -> opts+      case Lint.ignoreRules opts of+        [] -> opts {Lint.ignoreRules = fromMaybe [] ignore}+        _ -> opts+     applyTrusted trusted opts-        | null (Rules.allowedRegistries (Lint.rulesConfig opts)) =-            opts {Lint.rulesConfig = toRules trusted <> Lint.rulesConfig opts}-        | otherwise = opts-    -- | Converts a list of TrustedRegistry to a RulesConfig record+      | null (Rules.allowedRegistries (Lint.rulesConfig opts)) =+        opts {Lint.rulesConfig = toRules trusted <> Lint.rulesConfig opts}+      | otherwise = opts+     toRules (Just trusted) = Rules.RulesConfig (Set.fromList . coerce $ trusted)     toRules _ = mempty+     formatError err config =       unlines-          [ "Error parsing your config file in  '" ++ config ++ "':"-          , "It should contain one of the keys 'ignored' or 'trustedRegistries'. For example:\n"-          , "ignored:"-          , "\t- DL3000"-          , "\t- SC1099\n\n"-          , "The key 'trustedRegistries' should contain the names of the allowed docker registries:\n"-          , "allowedRegistries:"-          , "\t- docker.io"-          , "\t- my-company.com"-          , ""-          , err-          ]+        [ "Error parsing your config file in  '" ++ config ++ "':",+          "It should contain one of the keys 'ignored' or 'trustedRegistries'. For example:\n",+          "ignored:",+          "\t- DL3000",+          "\t- SC1099\n\n",+          "The key 'trustedRegistries' should contain the names of the allowed docker registries:\n",+          "allowedRegistries:",+          "\t- docker.io",+          "\t- my-company.com",+          "",+          err+        ]
src/Hadolint/Formatter/Checkstyle.hs view
@@ -1,74 +1,75 @@-{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}  module Hadolint.Formatter.Checkstyle-    ( printResult-    , formatResult-    ) where+  ( printResult,+    formatResult,+  )+where  import qualified Data.ByteString.Builder as Builder import qualified Data.ByteString.Lazy.Char8 as B import Data.Char import Data.Foldable (toList) import Data.List (groupBy)-import Data.Monoid ((<>), mconcat)+import Data.Monoid (mconcat, (<>)) import qualified Data.Text as Text import Hadolint.Formatter.Format-import Hadolint.Rules (Metadata(..), RuleCheck(..))+import Hadolint.Rules (Metadata (..), RuleCheck (..)) import ShellCheck.Interface import Text.Megaparsec (Stream) import Text.Megaparsec.Error import Text.Megaparsec.Pos (sourceColumn, sourceLine, sourceName, unPos)  data CheckStyle = CheckStyle-    { file :: String-    , line :: Int-    , column :: Int-    , impact :: String-    , msg :: String-    , source :: String-    }+  { file :: String,+    line :: Int,+    column :: Int,+    impact :: String,+    msg :: String,+    source :: String+  }  errorToCheckStyle :: (Stream s, ShowErrorComponent e) => ParseErrorBundle s e -> CheckStyle errorToCheckStyle err =-    CheckStyle-        { file = sourceName pos-        , line = unPos (sourceLine pos)-        , column = unPos (sourceColumn pos)-        , impact = severityText ErrorC-        , msg = errorBundlePretty err-        , source = "DL1000"-        }+  CheckStyle+    { file = sourceName pos,+      line = unPos (sourceLine pos),+      column = unPos (sourceColumn pos),+      impact = severityText ErrorC,+      msg = errorBundlePretty err,+      source = "DL1000"+    }   where     pos = errorPosition err  ruleToCheckStyle :: RuleCheck -> CheckStyle ruleToCheckStyle RuleCheck {..} =-    CheckStyle-        { file = Text.unpack filename-        , line = linenumber-        , column = 1-        , impact = severityText (severity metadata)-        , msg = Text.unpack (message metadata)-        , source = Text.unpack (code metadata)-        }+  CheckStyle+    { file = Text.unpack filename,+      line = linenumber,+      column = 1,+      impact = severityText (severity metadata),+      msg = Text.unpack (message metadata),+      source = Text.unpack (code metadata)+    }  toXml :: [CheckStyle] -> Builder.Builder toXml checks = wrap fileName (foldMap convert checks)   where     wrap name innerNode = "<file " <> attr "name" name <> ">" <> innerNode <> "</file>"     convert CheckStyle {..} =-        "<error " <> -- Beging the node construction-        attr "line" (show line) <>-        attr "column" (show column) <>-        attr "severity" impact <>-        attr "message" msg <>-        attr "source" source <>-        "/>"+      "<error "+        <> attr "line" (show line) -- Beging the node construction+        <> attr "column" (show column)+        <> attr "severity" impact+        <> attr "message" msg+        <> attr "source" source+        <> "/>"     fileName =-        case checks of-            [] -> ""-            h:_ -> file h+      case checks of+        [] -> ""+        h : _ -> file h  attr :: String -> String -> Builder.Builder attr name value = Builder.string8 name <> "='" <> Builder.string8 (escape value) <> "' "@@ -77,14 +78,14 @@ escape = concatMap doEscape   where     doEscape c =-        if isOk c-            then [c]-            else "&#" ++ show (ord c) ++ ";"+      if isOk c+        then [c]+        else "&#" ++ show (ord c) ++ ";"     isOk x = any (\check -> check x) [isAsciiUpper, isAsciiLower, isDigit, (`elem` [' ', '.', '/'])]  formatResult :: (Stream s, ShowErrorComponent e) => Result s e -> Builder.Builder formatResult (Result errors checks) =-    "<?xml version='1.0' encoding='UTF-8'?><checkstyle version='4.3'>" <> xmlBody <> "</checkstyle>"+  "<?xml version='1.0' encoding='UTF-8'?><checkstyle version='4.3'>" <> xmlBody <> "</checkstyle>"   where     xmlBody = mconcat xmlChunks     xmlChunks = fmap toXml (groupBy sameFileName flatten)
src/Hadolint/Formatter/Codacy.hs view
@@ -1,53 +1,54 @@-{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}  module Hadolint.Formatter.Codacy-    ( printResult-    , formatResult-    ) where+  ( printResult,+    formatResult,+  )+where  import Data.Aeson hiding (Result) import qualified Data.ByteString.Lazy.Char8 as B import Data.Monoid ((<>)) import Data.Sequence (Seq) import qualified Data.Text as Text-import Hadolint.Formatter.Format (Result(..), errorPosition)-import Hadolint.Rules (Metadata(..), RuleCheck(..))+import Hadolint.Formatter.Format (Result (..), errorPosition)+import Hadolint.Rules (Metadata (..), RuleCheck (..)) import Text.Megaparsec (Stream) import Text.Megaparsec.Error import Text.Megaparsec.Pos (sourceLine, sourceName, unPos)  data Issue = Issue-    { filename :: String-    , msg :: String-    , patternId :: String-    , line :: Int-    }+  { filename :: String,+    msg :: String,+    patternId :: String,+    line :: Int+  }  instance ToJSON Issue where-    toJSON Issue {..} =-        object ["filename" .= filename, "patternId" .= patternId, "message" .= msg, "line" .= line]+  toJSON Issue {..} =+    object ["filename" .= filename, "patternId" .= patternId, "message" .= msg, "line" .= line]  errorToIssue :: (Stream s, ShowErrorComponent e) => ParseErrorBundle s e -> Issue errorToIssue err =-    Issue-        { filename = sourceName pos-        , patternId = "DL1000"-        , msg = errorBundlePretty err-        , line = linenumber-        }+  Issue+    { filename = sourceName pos,+      patternId = "DL1000",+      msg = errorBundlePretty err,+      line = linenumber+    }   where     pos = errorPosition err     linenumber = unPos (sourceLine pos)  checkToIssue :: RuleCheck -> Issue checkToIssue RuleCheck {..} =-    Issue-        { filename = Text.unpack filename-        , patternId = Text.unpack (code metadata)-        , msg = Text.unpack (message metadata)-        , line = linenumber-        }+  Issue+    { filename = Text.unpack filename,+      patternId = Text.unpack (code metadata),+      msg = Text.unpack (message metadata),+      line = linenumber+    }  formatResult :: (Stream s, ShowErrorComponent e) => Result s e -> Seq Issue formatResult (Result errors checks) = allIssues
src/Hadolint/Formatter/Codeclimate.hs view
@@ -1,11 +1,12 @@-{-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}  module Hadolint.Formatter.Codeclimate-    ( printResult-    , formatResult-    ) where+  ( printResult,+    formatResult,+  )+where  import Data.Aeson hiding (Result) import qualified Data.ByteString.Lazy as B@@ -13,57 +14,60 @@ import Data.Sequence (Seq) import qualified Data.Text as Text import GHC.Generics-import Hadolint.Formatter.Format (Result(..), errorPosition)-import Hadolint.Rules (Metadata(..), RuleCheck(..))+import Hadolint.Formatter.Format (Result (..), errorPosition)+import Hadolint.Rules (Metadata (..), RuleCheck (..)) import ShellCheck.Interface import Text.Megaparsec (Stream) import Text.Megaparsec.Error import Text.Megaparsec.Pos (sourceColumn, sourceLine, sourceName, unPos)  data Issue = Issue-    { checkName :: String-    , description :: String-    , location :: Location-    , impact :: String-    }+  { checkName :: String,+    description :: String,+    location :: Location,+    impact :: String+  }  data Location-    = LocLine String-              Int-    | LocPos String-             Pos+  = LocLine+      String+      Int+  | LocPos+      String+      Pos  instance ToJSON Location where-    toJSON (LocLine path l) = object ["path" .= path, "lines" .= object ["begin" .= l, "end" .= l]]-    toJSON (LocPos path pos) =-        object ["path" .= path, "positions" .= object ["begin" .= pos, "end" .= pos]]+  toJSON (LocLine path l) = object ["path" .= path, "lines" .= object ["begin" .= l, "end" .= l]]+  toJSON (LocPos path pos) =+    object ["path" .= path, "positions" .= object ["begin" .= pos, "end" .= pos]]  data Pos = Pos-    { line :: Int-    , column :: Int-    } deriving (Generic)+  { line :: Int,+    column :: Int+  }+  deriving (Generic)  instance ToJSON Pos  instance ToJSON Issue where-    toJSON Issue {..} =-        object-            [ "type" .= ("issue" :: String)-            , "check_name" .= checkName-            , "description" .= description-            , "categories" .= (["Bug Risk"] :: [String])-            , "location" .= location-            , "severity" .= impact-            ]+  toJSON Issue {..} =+    object+      [ "type" .= ("issue" :: String),+        "check_name" .= checkName,+        "description" .= description,+        "categories" .= (["Bug Risk"] :: [String]),+        "location" .= location,+        "severity" .= impact+      ]  errorToIssue :: (Stream s, ShowErrorComponent e) => ParseErrorBundle s e -> Issue errorToIssue err =-    Issue-        { checkName = "DL1000"-        , description = errorBundlePretty err-        , location = LocPos (sourceName pos) Pos {..}-        , impact = severityText ErrorC-        }+  Issue+    { checkName = "DL1000",+      description = errorBundlePretty err,+      location = LocPos (sourceName pos) Pos {..},+      impact = severityText ErrorC+    }   where     pos = errorPosition err     line = unPos (sourceLine pos)@@ -71,20 +75,20 @@  checkToIssue :: RuleCheck -> Issue checkToIssue RuleCheck {..} =-    Issue-        { checkName = Text.unpack (code metadata)-        , description = Text.unpack (message metadata)-        , location = LocLine (Text.unpack filename) linenumber-        , impact = severityText (severity metadata)-        }+  Issue+    { checkName = Text.unpack (code metadata),+      description = Text.unpack (message metadata),+      location = LocLine (Text.unpack filename) linenumber,+      impact = severityText (severity metadata)+    }  severityText :: Severity -> String severityText severity =-    case severity of-        ErrorC -> "blocker"-        WarningC -> "major"-        InfoC -> "info"-        StyleC -> "minor"+  case severity of+    ErrorC -> "blocker"+    WarningC -> "major"+    InfoC -> "info"+    StyleC -> "minor"  formatResult :: (Stream s, ShowErrorComponent e) => Result s e -> Seq Issue formatResult (Result errors checks) = allIssues@@ -97,5 +101,5 @@ printResult result = mapM_ output (formatResult result)   where     output value = do-        B.putStr (encode value)-        B.putStr (B.singleton 0x00)+      B.putStr (encode value)+      B.putStr (B.singleton 0x00)
src/Hadolint/Formatter/Format.hs view
@@ -1,38 +1,39 @@ module Hadolint.Formatter.Format-    ( severityText-    , stripNewlines-    , errorMessageLine-    , errorPosition-    , errorPositionPretty-    , Text.Megaparsec.Error.errorBundlePretty-    , Result(..)-    , isEmpty-    , toResult-    ) where+  ( severityText,+    stripNewlines,+    errorMessageLine,+    errorPosition,+    errorPositionPretty,+    Text.Megaparsec.Error.errorBundlePretty,+    Result (..),+    isEmpty,+    toResult,+  )+where  import Data.List (sort) import qualified Data.List.NonEmpty as NE import Data.Monoid (Monoid) import Data.Semigroup-import qualified Data.Sequence as Seq import Data.Sequence (Seq)+import qualified Data.Sequence as Seq import Hadolint.Rules import ShellCheck.Interface-import Text.Megaparsec (Stream(..), pstateSourcePos)+import Text.Megaparsec (Stream (..), pstateSourcePos) import Text.Megaparsec.Error import Text.Megaparsec.Pos (SourcePos, sourcePosPretty)  data Result s e = Result-    { errors :: Seq (ParseErrorBundle s e)-    , checks :: Seq RuleCheck-    }+  { errors :: !(Seq (ParseErrorBundle s e)),+    checks :: !(Seq RuleCheck)+  }  instance Semigroup (Result s e) where-    (Result e1 c1) <> (Result e2 c2) = Result (e1 <> e2) (c1 <> c2)+  (Result e1 c1) <> (Result e2 c2) = Result (e1 <> e2) (c1 <> c2)  instance Monoid (Result s e) where-    mappend = (<>)-    mempty = Result mempty mempty+  mappend = (<>)+  mempty = Result mempty mempty  isEmpty :: Result s e -> Bool isEmpty (Result Seq.Empty Seq.Empty) = True@@ -40,33 +41,35 @@  toResult :: Either (ParseErrorBundle s e) [RuleCheck] -> Result s e toResult res =-    case res of-        Left err -> Result (Seq.singleton err) mempty-        Right c -> Result mempty (Seq.fromList (sort c))+  case res of+    Left err -> Result (Seq.singleton err) mempty+    Right c -> Result mempty (Seq.fromList (sort c))  severityText :: Severity -> String severityText s =-    case s of-        ErrorC -> "error"-        WarningC -> "warning"-        InfoC -> "info"-        StyleC -> "style"+  case s of+    ErrorC -> "error"+    WarningC -> "warning"+    InfoC -> "info"+    StyleC -> "style"  stripNewlines :: String -> String stripNewlines =-    map (\c ->-             if c == '\n'-                 then ' '-                 else c)+  map+    ( \c ->+        if c == '\n'+          then ' '+          else c+    )  errorMessageLine :: (Stream s, ShowErrorComponent e) => ParseErrorBundle s e -> String errorMessageLine err@(ParseErrorBundle e _) =-    errorPositionPretty err ++ " " ++ parseErrorTextPretty (NE.head e)+  errorPositionPretty err ++ " " ++ parseErrorTextPretty (NE.head e)  errorPositionPretty :: Stream s => ParseErrorBundle s e -> String errorPositionPretty err = sourcePosPretty (errorPosition err)  errorPosition :: Stream s => ParseErrorBundle s e -> Text.Megaparsec.Pos.SourcePos errorPosition (ParseErrorBundle e s) =-    let (_, posState) = reachOffset (errorOffset (NE.head e)) s-     in pstateSourcePos posState+  let (_, posState) = reachOffset (errorOffset (NE.head e)) s+   in pstateSourcePos posState
src/Hadolint/Formatter/Json.hs view
@@ -1,46 +1,47 @@-{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}  module Hadolint.Formatter.Json-    ( printResult-    , formatResult-    ) where+  ( printResult,+    formatResult,+  )+where  import Data.Aeson hiding (Result) import qualified Data.ByteString.Lazy.Char8 as B import Data.Monoid ((<>))-import Hadolint.Formatter.Format (Result(..), errorPosition, severityText)-import Hadolint.Rules (Metadata(..), RuleCheck(..))+import Hadolint.Formatter.Format (Result (..), errorPosition, severityText)+import Hadolint.Rules (Metadata (..), RuleCheck (..)) import ShellCheck.Interface import Text.Megaparsec (Stream) import Text.Megaparsec.Error import Text.Megaparsec.Pos (sourceColumn, sourceLine, sourceName, unPos)  data JsonFormat s e-    = JsonCheck RuleCheck-    | JsonParseError (ParseErrorBundle s e)+  = JsonCheck RuleCheck+  | JsonParseError (ParseErrorBundle s e)  instance (Stream s, ShowErrorComponent e) => ToJSON (JsonFormat s e) where-    toJSON (JsonCheck RuleCheck {..}) =-        object-            [ "file" .= filename-            , "line" .= linenumber-            , "column" .= (1 :: Int)-            , "level" .= severityText (severity metadata)-            , "code" .= code metadata-            , "message" .= message metadata-            ]-    toJSON (JsonParseError err) =-        object-            [ "file" .= sourceName pos-            , "line" .= unPos (sourceLine pos)-            , "column" .= unPos (sourceColumn pos)-            , "level" .= severityText ErrorC-            , "code" .= ("DL1000" :: String)-            , "message" .= errorBundlePretty err-            ]-      where-        pos = errorPosition err+  toJSON (JsonCheck RuleCheck {..}) =+    object+      [ "file" .= filename,+        "line" .= linenumber,+        "column" .= (1 :: Int),+        "level" .= severityText (severity metadata),+        "code" .= code metadata,+        "message" .= message metadata+      ]+  toJSON (JsonParseError err) =+    object+      [ "file" .= sourceName pos,+        "line" .= unPos (sourceLine pos),+        "column" .= unPos (sourceColumn pos),+        "level" .= severityText ErrorC,+        "code" .= ("DL1000" :: String),+        "message" .= errorBundlePretty err+      ]+    where+      pos = errorPosition err  formatResult :: (Stream s, ShowErrorComponent e) => Result s e -> Value formatResult (Result errors checks) = toJSON allMessages
src/Hadolint/Formatter/TTY.hs view
@@ -2,17 +2,18 @@ {-# LANGUAGE OverloadedStrings #-}  module Hadolint.Formatter.TTY-    ( printResult-    , formatError-    , formatChecks-    ) where+  ( printResult,+    formatError,+    formatChecks,+  )+where  import Data.Semigroup ((<>)) import qualified Data.Text as Text import Hadolint.Formatter.Format import Hadolint.Rules import Language.Docker.Syntax-import Text.Megaparsec (Stream(..))+import Text.Megaparsec (Stream (..)) import Text.Megaparsec.Error  formatErrors :: (Stream s, ShowErrorComponent e, Functor f) => f (ParseErrorBundle s e) -> f String@@ -25,7 +26,7 @@ formatChecks = fmap formatCheck   where     formatCheck (RuleCheck meta source line _) =-        formatPos source line <> code meta <> " " <> message meta+      formatPos source line <> code meta <> " " <> message meta  formatPos :: Filename -> Linenumber -> Text.Text formatPos source line = source <> ":" <> Text.pack (show line) <> " "
src/Hadolint/Lint.hs view
@@ -2,13 +2,11 @@  module Hadolint.Lint where +import qualified Control.Concurrent.Async as Async+import Control.Parallel.Strategies (parListChunk, rseq, using) import qualified Data.List.NonEmpty as NonEmpty import Data.Text (Text)-import qualified Language.Docker as Docker-import Language.Docker.Parser (DockerfileError, Error)-import Language.Docker.Syntax (Dockerfile)-import System.Exit (exitFailure, exitSuccess)-+import GHC.Conc (numCapabilities) import qualified Hadolint.Formatter.Checkstyle as Checkstyle import qualified Hadolint.Formatter.Codacy as Codacy import qualified Hadolint.Formatter.Codeclimate as Codeclimate@@ -16,64 +14,67 @@ import qualified Hadolint.Formatter.Json as Json import qualified Hadolint.Formatter.TTY as TTY import qualified Hadolint.Rules as Rules+import qualified Language.Docker as Docker+import Language.Docker.Parser (DockerfileError, Error)+import Language.Docker.Syntax (Dockerfile)+import System.Exit (exitFailure, exitSuccess)  type IgnoreRule = Text  type TrustedRegistry = Text  data LintOptions = LintOptions-    { ignoreRules :: [IgnoreRule]-    , rulesConfig :: Rules.RulesConfig-    } deriving (Show)+  { ignoreRules :: [IgnoreRule],+    rulesConfig :: Rules.RulesConfig+  }+  deriving (Show)  data OutputFormat-    = Json-    | TTY-    | CodeclimateJson-    | Checkstyle-    | Codacy-    deriving (Show, Eq)+  = Json+  | TTY+  | CodeclimateJson+  | Checkstyle+  | Codacy+  deriving (Show, Eq)  printResultsAndExit :: OutputFormat -> Format.Result Text DockerfileError -> IO () printResultsAndExit format allResults = do-    printResult allResults-    if not . Format.isEmpty $ allResults-        then exitFailure-        else exitSuccess+  printResult allResults+  if not . Format.isEmpty $ allResults+    then exitFailure+    else exitSuccess   where     printResult res =-        case format of-            TTY -> TTY.printResult res-            Json -> Json.printResult res-            Checkstyle -> Checkstyle.printResult res-            CodeclimateJson -> Codeclimate.printResult res >> exitSuccess-            Codacy -> Codacy.printResult res >> exitSuccess+      case format of+        TTY -> TTY.printResult res+        Json -> Json.printResult res+        Checkstyle -> Checkstyle.printResult res+        CodeclimateJson -> Codeclimate.printResult res >> exitSuccess+        Codacy -> Codacy.printResult res >> exitSuccess  -- | Performs the process of parsing the dockerfile and analyzing it with all the applicable -- rules, depending on the list of ignored rules. -- Depending on the preferred printing format, it will output the results to stdout lint :: LintOptions -> NonEmpty.NonEmpty String -> IO (Format.Result Text DockerfileError) lint LintOptions {ignoreRules = ignoreList, rulesConfig} dFiles = do-    processedFiles <- mapM (lintDockerfile ignoreList) (NonEmpty.toList dFiles)-    return (results processedFiles)+  parsedFiles <- Async.mapConcurrently parseFile (NonEmpty.toList dFiles)+  let results = lintAll parsedFiles `using` parListChunk (div numCapabilities 2) rseq+  return $ mconcat results   where-    results = foldMap Format.toResult -- Parse and check rules for each dockerfile,-                                      -- then convert them to a Result and combine with-                                      -- the result of the previous dockerfile results-    lintDockerfile ignoreRules dockerFile = do-        ast <- parseFilename dockerFile-        return (processedFile ast)+    parseFile :: String -> IO (Either Error Dockerfile)+    parseFile "-" = Docker.parseStdin+    parseFile s = Docker.parseFile s++    lintAll = fmap (lintDockerfile ignoreList)++    lintDockerfile ignoreRules ast = processedFile ast       where-        processedFile = fmap processRules+        processedFile = Format.toResult . fmap processRules         processRules fileLines = filter ignoredRules (analyzeAll rulesConfig fileLines)         ignoredRules = ignoreFilter ignoreRules-        -- | Returns true if the rule should be ignored+         ignoreFilter :: [IgnoreRule] -> Rules.RuleCheck -> Bool         ignoreFilter rules (Rules.RuleCheck (Rules.Metadata code _ _) _ _ _) = code `notElem` rules-        -- | Support UNIX convention of passing "-" instead of "/dev/stdin"-        parseFilename :: String -> IO (Either Error Dockerfile)-        parseFilename "-" = Docker.parseStdin-        parseFilename s = Docker.parseFile s  -- | Returns the result of applying all the rules to the given dockerfile analyzeAll :: Rules.RulesConfig -> Dockerfile -> [Rules.RuleCheck]
src/Hadolint/Rules.hs view
@@ -6,24 +6,24 @@ import Control.Arrow ((&&&)) import Data.List (foldl', isInfixOf, isPrefixOf, mapAccumL, nub) import Data.List.NonEmpty (toList)-import qualified Hadolint.Shell as Shell-import Language.Docker.Syntax- import qualified Data.Map as Map import Data.Semigroup (Semigroup, (<>)) import qualified Data.Set as Set import qualified Data.Text as Text import Data.Void (Void)+import qualified Hadolint.Shell as Shell+import Language.Docker.Syntax+import ShellCheck.Interface (Severity (..)) import qualified ShellCheck.Interface-import ShellCheck.Interface (Severity(..)) import qualified Text.Megaparsec as Megaparsec import qualified Text.Megaparsec.Char as Megaparsec  data Metadata = Metadata-    { code :: Text.Text-    , severity :: Severity-    , message :: Text.Text-    } deriving (Eq)+  { code :: Text.Text,+    severity :: Severity,+    message :: Text.Text+  }+  deriving (Eq, Show)  -- a check is the application of a rule on a specific part of code -- the enforced result and the affected position@@ -31,26 +31,29 @@ -- and simple to develop new rules -- line numbers in the negative range are meant for the global context data RuleCheck = RuleCheck-    { metadata :: Metadata-    , filename :: Filename-    , linenumber :: Linenumber-    , success :: Bool-    } deriving (Eq)+  { metadata :: Metadata,+    filename :: Filename,+    linenumber :: Linenumber,+    success :: Bool+  }+  deriving (Eq, Show)  -- | Contains the required parameters for optional rules newtype RulesConfig = RulesConfig-    { allowedRegistries :: Set.Set Registry -- ^ The docker registries that are allowed in FROM-    } deriving (Show, Eq)+  { -- | The docker registries that are allowed in FROM+    allowedRegistries :: Set.Set Registry+  }+  deriving (Show, Eq)  instance Ord RuleCheck where-    a `compare` b = linenumber a `compare` linenumber b+  a `compare` b = linenumber a `compare` linenumber b  instance Semigroup RulesConfig where-    RulesConfig a <> RulesConfig b = RulesConfig (a <> b)+  RulesConfig a <> RulesConfig b = RulesConfig (a <> b)  instance Monoid RulesConfig where-    mempty = RulesConfig mempty-    mappend = (<>)+  mempty = RulesConfig mempty+  mappend = (<>)  type IgnoreRuleParser = Megaparsec.Parsec Void Text.Text @@ -59,8 +62,8 @@ -- | A function to check individual dockerfile instructions. -- It gets the current state and a line number. -- It should return the new state and whether or not the check passes for the given instruction.-type SimpleCheckerWithState state-     = state -> Linenumber -> Instruction Shell.ParsedShell -> (state, Bool)+type SimpleCheckerWithState state =+  state -> Linenumber -> Instruction Shell.ParsedShell -> (state, Bool)  -- | A function to check individual dockerfile instructions. -- It gets the current line number.@@ -70,14 +73,14 @@ -- | A function to check individual dockerfile instructions. -- It should return the new state and a list of Metadata records. -- Each Metadata record signifies a failing check for the given instruction.-type CheckerWithState state-     = state -> Linenumber -> Instruction Shell.ParsedShell -> (state, [Metadata])+type CheckerWithState state =+  state -> Linenumber -> Instruction Shell.ParsedShell -> (state, [Metadata])  link :: Metadata -> Text.Text link (Metadata code _ _)-    | "SC" `Text.isPrefixOf` code = "https://github.com/koalaman/shellcheck/wiki/" <> code-    | "DL" `Text.isPrefixOf` code = "https://github.com/hadolint/hadolint/wiki/" <> code-    | otherwise = "https://github.com/hadolint/hadolint"+  | "SC" `Text.isPrefixOf` code = "https://github.com/koalaman/shellcheck/wiki/" <> code+  | "DL" `Text.isPrefixOf` code = "https://github.com/hadolint/hadolint/wiki/" <> code+  | otherwise = "https://github.com/hadolint/hadolint"  -- a Rule takes a Dockerfile with parsed shell and returns the executed checks type Rule = ParsedFile -> [RuleCheck]@@ -86,58 +89,58 @@ -- for the according line number mapInstructions :: CheckerWithState state -> state -> Rule mapInstructions f initialState dockerfile =-    let (_, results) = mapAccumL applyRule initialState dockerfile-     in concat results+  let (_, results) = mapAccumL applyRule initialState dockerfile+   in concat results   where     applyRule state (InstructionPos (OnBuild i) source linenumber) =-        applyWithState state source linenumber i -- All rules applying to instructions also apply to ONBUILD,-                                                 -- so we unwrap the OnBuild constructor and check directly the inner-                                                 -- instruction+      applyWithState state source linenumber i -- All rules applying to instructions also apply to ONBUILD,+      -- so we unwrap the OnBuild constructor and check directly the inner+      -- instruction     applyRule state (InstructionPos i source linenumber) =-        applyWithState state source linenumber i -- Otherwise, normal instructions are not unwrapped+      applyWithState state source linenumber i -- Otherwise, normal instructions are not unwrapped     applyWithState state source linenumber instruction =-        let (newState, res) = f state linenumber instruction-         in (newState, [RuleCheck m source linenumber False | m <- res])+      let (newState, res) = f state linenumber instruction+       in (newState, [RuleCheck m source linenumber False | m <- res])  instructionRule ::-       Text.Text -> Severity -> Text.Text -> (Instruction Shell.ParsedShell -> Bool) -> Rule+  Text.Text -> Severity -> Text.Text -> (Instruction Shell.ParsedShell -> Bool) -> Rule instructionRule code severity message check =-    instructionRuleLine code severity message (const check)+  instructionRuleLine code severity message (const check)  instructionRuleLine :: Text.Text -> Severity -> Text.Text -> SimpleCheckerWithLine -> Rule instructionRuleLine code severity message check =-    instructionRuleState code severity message checkAndDropState ()+  instructionRuleState code severity message checkAndDropState ()   where     checkAndDropState state line instr = (state, check line instr)  instructionRuleState ::-       Text.Text -> Severity -> Text.Text -> SimpleCheckerWithState state -> state -> Rule+  Text.Text -> Severity -> Text.Text -> SimpleCheckerWithState state -> state -> Rule instructionRuleState code severity message f = mapInstructions constMetadataCheck   where     meta = Metadata code severity message     constMetadataCheck st ln instr =-        let (newSt, success) = f st ln instr-         in if not success-                then (newSt, [meta])-                else (newSt, [])+      let (newSt, success) = f st ln instr+       in if not success+            then (newSt, [meta])+            else (newSt, [])  withState :: a -> b -> (a, b) withState st res = (st, res)  argumentsRule :: (Shell.ParsedShell -> a) -> Arguments Shell.ParsedShell -> a argumentsRule applyRule args =-    case args of-        ArgumentsText as -> applyRule as-        ArgumentsList as -> applyRule as+  case args of+    ArgumentsText as -> applyRule as+    ArgumentsList as -> applyRule as  -- Enforce rules on a dockerfile and return failed checks analyze :: [Rule] -> Dockerfile -> [RuleCheck] analyze list dockerfile =-    [ result -- Keep the result-    | rule <- list -- for each rule in the list-    , result <- rule parsedFile -- after applying the rule to the file-    , notIgnored result -- and only keep failures that were not ignored-    ]+  [ result -- Keep the result+    | rule <- list, -- for each rule in the list+      result <- rule parsedFile, -- after applying the rule to the file+      notIgnored result -- and only keep failures that were not ignored+  ]   where     notIgnored RuleCheck {metadata = Metadata {code}, linenumber} = not (wasIgnored code linenumber)     wasIgnored c ln = not $ null [line | (line, codes) <- allIgnores, line == ln, c `elem` codes]@@ -146,22 +149,21 @@  ignored :: Dockerfile -> [(Linenumber, [Text.Text])] ignored dockerfile =-    [(l + 1, ignores) | (l, Just ignores) <- map (lineNumber &&& extractIgnored) dockerfile]+  [(l + 1, ignores) | (l, Just ignores) <- map (lineNumber &&& extractIgnored) dockerfile]   where     extractIgnored = ignoreFromInstruction . instruction     ignoreFromInstruction (Comment comment) = parseComment comment     ignoreFromInstruction _ = Nothing-    -- | Parses the comment text and extracts the ignored rule names     parseComment :: Text.Text -> Maybe [Text.Text]     parseComment = Megaparsec.parseMaybe commentParser     commentParser :: IgnoreRuleParser [Text.Text]     commentParser =-        spaces >> -- The parser for the ignored rules-        string "hadolint" >>-        spaces1 >>-        string "ignore=" >>-        spaces >>-        Megaparsec.sepBy1 ruleName (spaces >> string "," >> spaces)+      spaces+        >> string "hadolint" -- The parser for the ignored rules+        >> spaces1+        >> string "ignore="+        >> spaces+        >> Megaparsec.sepBy1 ruleName (spaces >> string "," >> spaces)     string = Megaparsec.string     spaces = Megaparsec.takeWhileP Nothing space     spaces1 = Megaparsec.takeWhile1P Nothing space@@ -170,46 +172,46 @@  rules :: [Rule] rules =-    [ absoluteWorkdir-    , shellcheck-    , invalidCmd-    , copyInsteadAdd-    , copyEndingSlash-    , copyFromExists-    , copyFromAnother-    , fromAliasUnique-    , noRootUser-    , noCd-    , noSudo-    , noAptGetUpgrade-    , noApkUpgrade-    , noLatestTag-    , noUntagged-    , noPlatformFlag-    , aptGetVersionPinned-    , aptGetCleanup-    , apkAddVersionPinned-    , apkAddNoCache-    , useAdd-    , pipVersionPinned-    , npmVersionPinned-    , invalidPort-    , aptGetNoRecommends-    , aptGetYes-    , wgetOrCurl-    , hasNoMaintainer-    , multipleCmds-    , multipleEntrypoints-    , useShell-    , useJsonArgs-    , usePipefail-    , noApt-    , gemVersionPinned-    , yumYes-    , noYumUpdate-    , yumCleanup-    , yumVersionPinned-    ]+  [ absoluteWorkdir,+    shellcheck,+    invalidCmd,+    copyInsteadAdd,+    copyEndingSlash,+    copyFromExists,+    copyFromAnother,+    fromAliasUnique,+    noRootUser,+    noCd,+    noSudo,+    noAptGetUpgrade,+    noApkUpgrade,+    noLatestTag,+    noUntagged,+    noPlatformFlag,+    aptGetVersionPinned,+    aptGetCleanup,+    apkAddVersionPinned,+    apkAddNoCache,+    useAdd,+    pipVersionPinned,+    npmVersionPinned,+    invalidPort,+    aptGetNoRecommends,+    aptGetYes,+    wgetOrCurl,+    hasNoMaintainer,+    multipleCmds,+    multipleEntrypoints,+    useShell,+    useJsonArgs,+    usePipefail,+    noApt,+    gemVersionPinned,+    yumYes,+    noYumUpdate,+    yumCleanup,+    yumVersionPinned+  ]  optionalRules :: RulesConfig -> [Rule] optionalRules RulesConfig {allowedRegistries} = [registryIsAllowed allowedRegistries]@@ -221,7 +223,7 @@  allAliasedImages :: ParsedFile -> [(Linenumber, ImageAlias)] allAliasedImages dockerfile =-    [(l, alias) | (l, Just alias) <- map extractAlias (allFromImages dockerfile)]+  [(l, alias) | (l, Just alias) <- map extractAlias (allFromImages dockerfile)]   where     extractAlias (l, f) = (l, fromAlias f) @@ -232,16 +234,16 @@ --  are defined before the given line number. previouslyDefinedAliases :: Linenumber -> ParsedFile -> [Text.Text] previouslyDefinedAliases line dockerfile =-    [i | (l, ImageAlias i) <- allAliasedImages dockerfile, l < line]+  [i | (l, ImageAlias i) <- allAliasedImages dockerfile, l < line]  -- | Returns the result of running the check function on the image alias --   name, if the passed instruction is a FROM instruction with a stage alias. --   Otherwise, returns True. aliasMustBe :: (Text.Text -> Bool) -> Instruction a -> Bool aliasMustBe predicate fromInstr =-    case fromInstr of-        From BaseImage {alias = Just (ImageAlias as)} -> predicate as-        _ -> True+  case fromInstr of+    From BaseImage {alias = Just (ImageAlias as)} -> predicate as+    _ -> True  fromName :: BaseImage -> Text.Text fromName BaseImage {image = Image {imageName}} = imageName@@ -269,7 +271,7 @@ -- | Converts ShellCheck errors into our own errors type commentMetadata :: ShellCheck.Interface.PositionedComment -> Metadata commentMetadata c =-    Metadata (Text.pack ("SC" ++ show (code c))) (severity c) (Text.pack (message c))+  Metadata (Text.pack ("SC" ++ show (code c))) (severity c) (Text.pack (message c))   where     severity pc = ShellCheck.Interface.cSeverity $ ShellCheck.Interface.pcComment pc     code pc = ShellCheck.Interface.cCode $ ShellCheck.Interface.pcComment pc@@ -282,9 +284,9 @@     severity = ErrorC     message = "Use absolute WORKDIR"     check (Workdir loc)-        | "$" `Text.isPrefixOf` loc = True-        | "/" `Text.isPrefixOf` loc = True-        | otherwise = False+      | "$" `Text.isPrefixOf` loc = True+      | "/" `Text.isPrefixOf` loc = True+      | otherwise = False     check _ = True  hasNoMaintainer :: Rule@@ -306,8 +308,8 @@     code = "DL4003"     severity = WarningC     message =-        "Multiple `CMD` instructions found. If you list more than one `CMD` then only the last \-        \`CMD` will take effect"+      "Multiple `CMD` instructions found. If you list more than one `CMD` then only the last \+      \`CMD` will take effect"     check _ _ From {} = withState False True -- Reset the state each time we find a FROM     check st _ Cmd {} = withState True (not st) -- Remember we found a CMD, fail if we found a CMD before     check st _ _ = withState st True@@ -318,11 +320,11 @@     code = "DL4004"     severity = ErrorC     message =-        "Multiple `ENTRYPOINT` instructions found. If you list more than one `ENTRYPOINT` then \-        \only the last `ENTRYPOINT` will take effect"+      "Multiple `ENTRYPOINT` instructions found. If you list more than one `ENTRYPOINT` then \+      \only the last `ENTRYPOINT` will take effect"     check _ _ From {} = withState False True -- Reset the state each time we find a FROM     check st _ Entrypoint {} = withState True (not st) -- Remember we found an ENTRYPOINT-                                                       -- and fail if we found another one before+    -- and fail if we found another one before     check st _ _ = withState st True  wgetOrCurl :: Rule@@ -335,11 +337,11 @@     check _ _ (From _) = withState Set.empty True -- Reset the state for each stage     check state _ _ = withState state True     detectDoubleUsage state args =-        let newArgs = extractCommands args-            newState = Set.union state newArgs-         in withState newState (Set.null newArgs || Set.size newState < 2)+      let newArgs = extractCommands args+          newState = Set.union state newArgs+       in withState newState (Set.null newArgs || Set.size newState < 2)     extractCommands args =-        Set.fromList [w | w <- Shell.findCommandNames args, w == "curl" || w == "wget"]+      Set.fromList [w | w <- Shell.findCommandNames args, w == "curl" || w == "wget"]  invalidCmd :: Rule invalidCmd = instructionRule code severity message check@@ -347,8 +349,8 @@     code = "DL3001"     severity = InfoC     message =-        "For some bash commands it makes no sense running them in a Docker container like `ssh`, \-        \`vim`, `shutdown`, `service`, `ps`, `free`, `top`, `kill`, `mount`, `ifconfig`"+      "For some bash commands it makes no sense running them in a Docker container like `ssh`, \+      \`vim`, `shutdown`, `service`, `ps`, `free`, `top`, `kill`, `mount`, `ifconfig`"     check (Run (RunArgs args _)) = argumentsRule detectInvalid args     check _ = True     detectInvalid args = null [arg | arg <- Shell.findCommandNames args, arg `elem` invalidCmds]@@ -362,8 +364,8 @@     message = "Last USER should not be root"     check _ _ (From from) = withState (Just from) True -- Remember the last FROM instruction found     check st@(Just from) line (User user)-        | isRoot user && lastUserIsRoot from line = withState st False-        | otherwise = withState st True+      | isRoot user && lastUserIsRoot from line = withState st False+      | otherwise = withState st True     check st _ _ = withState st True     --     --@@ -372,20 +374,20 @@     --     rootStages :: Map.Map BaseImage Linenumber     rootStages =-        let indexedInstructions = map (instruction &&& lineNumber) dockerfile-            (_, usersMap) = foldl' buildMap (Nothing, Map.empty) indexedInstructions-         in usersMap+      let indexedInstructions = map (instruction &&& lineNumber) dockerfile+          (_, usersMap) = foldl' buildMap (Nothing, Map.empty) indexedInstructions+       in usersMap     --     --     buildMap (_, st) (From from, _) = (Just from, st) -- Remember the FROM we are currently inspecting     buildMap (Just from, st) (User user, line)-        | isRoot user = (Just from, Map.insert from line st) -- Remember the line with a root user-        | otherwise = (Just from, Map.delete from st) -- Forget there was a root used for this FROM+      | isRoot user = (Just from, Map.insert from line st) -- Remember the line with a root user+      | otherwise = (Just from, Map.delete from st) -- Forget there was a root used for this FROM     buildMap st _ = st     --     --     isRoot user =-        Text.isPrefixOf "root:" user || Text.isPrefixOf "0:" user || user == "root" || user == "0"+      Text.isPrefixOf "root:" user || Text.isPrefixOf "0:" user || user == "root" || user == "0"  noCd :: Rule noCd = instructionRule code severity message check@@ -402,8 +404,8 @@     code = "DL3004"     severity = ErrorC     message =-        "Do not use sudo as it leads to unpredictable behavior. Use a tool like gosu to enforce \-        \root"+      "Do not use sudo as it leads to unpredictable behavior. Use a tool like gosu to enforce \+      \root"     check (Run (RunArgs args _)) = argumentsRule (not . usingProgram "sudo") args     check _ = True @@ -414,7 +416,7 @@     severity = ErrorC     message = "Do not use apt-get upgrade or dist-upgrade"     check (Run (RunArgs args _)) =-        argumentsRule (Shell.noCommands (Shell.cmdHasArgs "apt-get" ["upgrade"])) args+      argumentsRule (Shell.noCommands (Shell.cmdHasArgs "apt-get" ["upgrade"])) args     check _ = True  noUntagged :: Rule@@ -426,7 +428,7 @@     check _ (From BaseImage {image = (Image _ "scratch")}) = True     check _ (From BaseImage {digest = Just _}) = True     check line (From BaseImage {image = (Image _ i), tag = Nothing}) =-        i `elem` previouslyDefinedAliases line dockerfile+      i `elem` previouslyDefinedAliases line dockerfile     check _ _ = True  noLatestTag :: Rule@@ -435,8 +437,8 @@     code = "DL3007"     severity = WarningC     message =-        "Using latest is prone to errors if the image will ever update. Pin the version explicitly \-        \to a release tag"+      "Using latest is prone to errors if the image will ever update. Pin the version explicitly \+      \to a release tag"     check (From BaseImage {tag = Just t}) = t /= "latest"     check _ = True @@ -446,20 +448,20 @@     code = "DL3008"     severity = WarningC     message =-        "Pin versions in apt get install. Instead of `apt-get install <package>` use `apt-get \-        \install <package>=<version>`"+      "Pin versions in apt get install. Instead of `apt-get install <package>` use `apt-get \+      \install <package>=<version>`"     check (Run (RunArgs args _)) = argumentsRule (all versionFixed . aptGetPackages) args     check _ = True     versionFixed package = "=" `Text.isInfixOf` package || ("/" `Text.isInfixOf` package || ".deb" `Text.isSuffixOf` package)  aptGetPackages :: Shell.ParsedShell -> [Text.Text] aptGetPackages args =-    [ arg-    | cmd <- Shell.presentCommands args-    , Shell.cmdHasArgs "apt-get" ["install"] cmd-    , arg <- Shell.getArgsNoFlags (dropTarget cmd)-    , arg /= "install"-    ]+  [ arg+    | cmd <- Shell.presentCommands args,+      Shell.cmdHasArgs "apt-get" ["install"] cmd,+      arg <- Shell.getArgsNoFlags (dropTarget cmd),+      arg /= "install"+  ]   where     dropTarget = Shell.dropFlagArg ["t", "target-release"] @@ -469,33 +471,29 @@     code = "DL3009"     severity = InfoC     message = "Delete the apt-get lists after installing something"-    -- | 'check' returns a tuple (state, check_result)-    --   The state in this case is the FROM instruction where the current instruction we are-    --   inspecting is nested in.-    --   We only care for users to delete the lists folder if the FROM clase we're is is the last one-    --   or if it is used as the base image for another FROM clause.+     check _ line f@(From _) = withState (Just (line, f)) True -- Remember the last FROM instruction found     check st@(Just (line, From baseimage)) _ (Run (RunArgs args _)) =-        withState st (argumentsRule (didNotForgetToCleanup line baseimage) args)+      withState st (argumentsRule (didNotForgetToCleanup line baseimage) args)     check st _ _ = withState st True     -- Check all commands in the script for the presence of apt-get update     -- If the command is there, then we need to verify that the user is also removing the lists folder     didNotForgetToCleanup line baseimage args-        | not (hasUpdate args) || not (imageIsUsed line baseimage) = True-        | otherwise = hasCleanup args+      | not (hasUpdate args) || not (imageIsUsed line baseimage) = True+      | otherwise = hasCleanup args     hasCleanup args =-        any (Shell.cmdHasArgs "rm" ["-rf", "/var/lib/apt/lists/*"]) (Shell.presentCommands args)+      any (Shell.cmdHasArgs "rm" ["-rf", "/var/lib/apt/lists/*"]) (Shell.presentCommands args)     hasUpdate args = any (Shell.cmdHasArgs "apt-get" ["update"]) (Shell.presentCommands args)     imageIsUsed line baseimage = isLastImage line baseimage || imageIsUsedLater line baseimage     isLastImage line baseimage =-        case reverse (allFromImages dockerfile) of-            lst:_ -> (line, baseimage) == lst-            _ -> True+      case reverse (allFromImages dockerfile) of+        lst : _ -> (line, baseimage) == lst+        _ -> True     imageIsUsedLater line baseimage =-        case fromAlias baseimage of-            Nothing -> True-            Just (ImageAlias alias) ->-                alias `elem` [i | (l, i) <- allImageNames dockerfile, l > line]+      case fromAlias baseimage of+        Nothing -> True+        Just (ImageAlias alias) ->+          alias `elem` [i | (l, i) <- allImageNames dockerfile, l > line]  noApkUpgrade :: Rule noApkUpgrade = instructionRule code severity message check@@ -512,19 +510,19 @@     code = "DL3018"     severity = WarningC     message =-        "Pin versions in apk add. Instead of `apk add <package>` use `apk add <package>=<version>`"+      "Pin versions in apk add. Instead of `apk add <package>` use `apk add <package>=<version>`"     check (Run (RunArgs args _)) = argumentsRule (\as -> and [versionFixed p | p <- apkAddPackages as]) args     check _ = True     versionFixed package = "=" `Text.isInfixOf` package  apkAddPackages :: Shell.ParsedShell -> [Text.Text] apkAddPackages args =-    [ arg-    | cmd <- Shell.presentCommands args-    , Shell.cmdHasArgs "apk" ["add"] cmd-    , arg <- Shell.getArgsNoFlags (dropTarget cmd)-    , arg /= "add"-    ]+  [ arg+    | cmd <- Shell.presentCommands args,+      Shell.cmdHasArgs "apk" ["add"] cmd,+      arg <- Shell.getArgsNoFlags (dropTarget cmd),+      arg /= "add"+  ]   where     dropTarget = Shell.dropFlagArg ["t", "virtual", "repository", "X"] @@ -534,8 +532,8 @@     code = "DL3019"     severity = InfoC     message =-        "Use the `--no-cache` switch to avoid the need to use `--update` and remove \-        \`/var/cache/apk/*` when done installing packages"+      "Use the `--no-cache` switch to avoid the need to use `--update` and remove \+      \`/var/cache/apk/*` when done installing packages"     check (Run (RunArgs args _)) = argumentsRule (Shell.noCommands forgotCacheOption) args     check _ = True     forgotCacheOption cmd = Shell.cmdHasArgs "apk" ["add"] cmd && not (Shell.hasFlag "no-cache" cmd)@@ -547,29 +545,29 @@     severity = InfoC     message = "Use ADD for extracting archives into an image"     check (Copy (CopyArgs srcs _ _ _)) =-        and-            [ not (format `Text.isSuffixOf` src)-            | SourcePath src <- toList srcs-            , format <- archiveFormats-            ]+      and+        [ not (format `Text.isSuffixOf` src)+          | SourcePath src <- toList srcs,+            format <- archiveFormats+        ]     check _ = True     archiveFormats =-        [ ".tar"-        , ".tar.bz2"-        , ".tb2"-        , ".tbz"-        , ".tbz2"-        , ".tar.gz"-        , ".tgz"-        , ".tpz"-        , ".tar.lz"-        , ".tar.lzma"-        , ".tlz"-        , ".tar.xz"-        , ".txz"-        , ".tar.Z"-        , ".tZ"-        ]+      [ ".tar",+        ".tar.bz2",+        ".tb2",+        ".tbz",+        ".tbz2",+        ".tar.gz",+        ".tgz",+        ".tpz",+        ".tar.lz",+        ".tar.lzma",+        ".tlz",+        ".tar.xz",+        ".txz",+        ".tar.Z",+        ".tZ"+      ]  invalidPort :: Rule invalidPort = instructionRule code severity message check@@ -578,8 +576,8 @@     severity = ErrorC     message = "Valid UNIX ports range from 0 to 65535"     check (Expose (Ports ports)) =-        and [p <= 65535 | Port p _ <- ports] &&-        and [l <= 65535 && m <= 65535 | PortRange l m _ <- ports]+      and [p <= 65535 | Port p _ <- ports]+        && and [l <= 65535 && m <= 65535 | PortRange l m _ <- ports]     check _ = True  pipVersionPinned :: Rule@@ -588,46 +586,55 @@     code = "DL3013"     severity = WarningC     message =-        "Pin versions in pip. Instead of `pip install <package>` use `pip install \-        \<package>==<version>`"+      "Pin versions in pip. Instead of `pip install <package>` use `pip install \+      \<package>==<version>`"     check (Run (RunArgs args _)) = argumentsRule (Shell.noCommands forgotToPinVersion) args     check _ = True     forgotToPinVersion cmd =-        isPipInstall cmd && not (hasBuildConstraint cmd) && not (all versionFixed (packages cmd))+      isPipInstall cmd && not (hasBuildConstraint cmd) && not (all versionFixed (packages cmd))     -- Check if the command is a pip* install command, and that specific packages are being listed     isPipInstall cmd = (isStdPipInstall cmd || isPythonPipInstall cmd) && not (requirementInstall cmd)     isStdPipInstall cmd@(Shell.Command name _ _) = "pip" `Text.isPrefixOf` name && ["install"] `isInfixOf` Shell.getArgs cmd-    isPythonPipInstall cmd@(Shell.Command name _ _) = "python" `Text.isPrefixOf` name &&-        ["-m", "pip", "install"] `isInfixOf` Shell.getArgs cmd+    isPythonPipInstall cmd@(Shell.Command name _ _) =+      "python" `Text.isPrefixOf` name+        && ["-m", "pip", "install"] `isInfixOf` Shell.getArgs cmd     -- If the user is installing requirements from a file or just the local module, then we are not interested     -- in running this rule-    requirementInstall cmd = ["--requirement"] `isInfixOf` Shell.getArgs cmd ||-        ["-r"] `isInfixOf` Shell.getArgs cmd ||-        ["."] `isInfixOf` Shell.getArgs cmd+    requirementInstall cmd =+      ["--requirement"] `isInfixOf` Shell.getArgs cmd+        || ["-r"] `isInfixOf` Shell.getArgs cmd+        || ["."] `isInfixOf` Shell.getArgs cmd     hasBuildConstraint cmd = Shell.hasFlag "constraint" cmd || Shell.hasFlag "c" cmd     packages cmd =-        stripInstallPrefix $-        Shell.getArgsNoFlags $ Shell.dropFlagArg-            [ "abi"-            , "b", "build"-            , "e", "editable"-            , "extra-index-url"-            , "f", "find-links"-            , "i", "index-url"-            , "implementation"-            , "no-binary"-            , "only-binary"-            , "platform"-            , "prefix"-            , "progress-bar"-            , "proxy"-            , "python-version"-            , "root"-            , "src"-            , "t", "target"-            , "trusted-host"-            , "upgrade-strategy"-            ] cmd+      stripInstallPrefix $+        Shell.getArgsNoFlags $+          Shell.dropFlagArg+            [ "abi",+              "b",+              "build",+              "e",+              "editable",+              "extra-index-url",+              "f",+              "find-links",+              "i",+              "index-url",+              "implementation",+              "no-binary",+              "only-binary",+              "platform",+              "prefix",+              "progress-bar",+              "proxy",+              "python-version",+              "root",+              "src",+              "t",+              "target",+              "trusted-host",+              "upgrade-strategy"+            ]+            cmd     versionFixed package = hasVersionSymbol package || isVersionedGit package     isVersionedGit package = "git+http" `Text.isInfixOf` package && "@" `Text.isInfixOf` package     versionSymbols = ["==", ">=", "<=", ">", "<", "!=", "~=", "==="]@@ -636,36 +643,35 @@ stripInstallPrefix :: [Text.Text] -> [Text.Text] stripInstallPrefix cmd = dropWhile (== "install") (dropWhile (/= "install") cmd) -{-|-  Rule for pinning NPM packages to version, tag, or commit-  supported formats by Hadolint-    npm install (with no args, in package dir)-    npm install [<@scope>/]<name>-    npm install [<@scope>/]<name>@<tag>-    npm install [<@scope>/]<name>@<version>-    npm install git[+http|+https]://<git-host>/<git-user>/<repo-name>[#<commit>|#semver:<semver>]-    npm install git+ssh://<git-host>:<git-user>/<repo-name>[#<commit>|#semver:<semver>]--}+-- |+--  Rule for pinning NPM packages to version, tag, or commit+--  supported formats by Hadolint+--    npm install (with no args, in package dir)+--    npm install [<@scope>/]<name>+--    npm install [<@scope>/]<name>@<tag>+--    npm install [<@scope>/]<name>@<version>+--    npm install git[+http|+https]://<git-host>/<git-user>/<repo-name>[#<commit>|#semver:<semver>]+--    npm install git+ssh://<git-host>:<git-user>/<repo-name>[#<commit>|#semver:<semver>] npmVersionPinned :: Rule npmVersionPinned = instructionRule code severity message check   where     code = "DL3016"     severity = WarningC     message =-        "Pin versions in npm. Instead of `npm install <package>` use `npm install \-        \<package>@<version>`"+      "Pin versions in npm. Instead of `npm install <package>` use `npm install \+      \<package>@<version>`"     check (Run (RunArgs args _)) = argumentsRule (Shell.noCommands forgotToPinVersion) args     check _ = True     forgotToPinVersion cmd =-        isNpmInstall cmd && installIsFirst cmd && not (all versionFixed (packages cmd))+      isNpmInstall cmd && installIsFirst cmd && not (all versionFixed (packages cmd))     isNpmInstall = Shell.cmdHasArgs "npm" ["install"]     installIsFirst cmd = ["install"] `isPrefixOf` Shell.getArgsNoFlags cmd     packages cmd = stripInstallPrefix (Shell.getArgsNoFlags cmd)     versionFixed package-        | hasGitPrefix package = isVersionedGit package-        | hasTarballSuffix package = True-        | isFolder package = True-        | otherwise = hasVersionSymbol package+      | hasGitPrefix package = isVersionedGit package+      | hasTarballSuffix package = True+      | isFolder package = True+      | otherwise = hasVersionSymbol package     gitPrefixes = ["git://", "git+ssh://", "git+http://", "git+https://"]     hasGitPrefix package = or [p `Text.isPrefixOf` package | p <- gitPrefixes]     tarballSuffixes = [".tar", ".tar.gz", ".tgz"]@@ -676,9 +682,9 @@     hasVersionSymbol package = "@" `Text.isInfixOf` dropScope package       where         dropScope pkg =-            if "@" `Text.isPrefixOf` pkg-                then Text.dropWhile ('/' <) pkg-                else pkg+          if "@" `Text.isPrefixOf` pkg+            then Text.dropWhile ('/' <) pkg+            else pkg  aptGetYes :: Rule aptGetYes = instructionRule code severity message check@@ -703,31 +709,32 @@     forgotNoInstallRecommends cmd = isAptGetInstall cmd && not (disablesRecommendOption cmd)     isAptGetInstall = Shell.cmdHasArgs "apt-get" ["install"]     disablesRecommendOption cmd =-        Shell.hasFlag "no-install-recommends" cmd ||-        Shell.hasArg "APT::Install-Recommends=false" cmd+      Shell.hasFlag "no-install-recommends" cmd+        || Shell.hasArg "APT::Install-Recommends=false" cmd  isArchive :: Text.Text -> Bool isArchive path =-    or-    ([ ftype `Text.isSuffixOf` path-    | ftype <--          [ ".tar"-          , ".gz"-          , ".bz2"-          , ".xz"-          , ".zip"-          , ".tgz"-          , ".tb2"-          , ".tbz"-          , ".tbz2"-          , ".lz"-          , ".lzma"-          , ".tlz"-          , ".txz"-          , ".Z"-          , ".tZ"-          ]-    ])+  or+    ( [ ftype `Text.isSuffixOf` path+        | ftype <-+            [ ".tar",+              ".gz",+              ".bz2",+              ".xz",+              ".zip",+              ".tgz",+              ".tb2",+              ".tbz",+              ".tbz2",+              ".lz",+              ".lzma",+              ".tlz",+              ".txz",+              ".Z",+              ".tZ"+            ]+      ]+    )  isUrl :: Text.Text -> Bool isUrl path = or ([proto `Text.isPrefixOf` path | proto <- ["https://", "http://"]])@@ -739,7 +746,7 @@     severity = ErrorC     message = "Use COPY instead of ADD for files and folders"     check (Add (AddArgs srcs _ _)) =-        and [isArchive src || isUrl src | SourcePath src <- toList srcs]+      and [isArchive src || isUrl src | SourcePath src <- toList srcs]     check _ = True  copyEndingSlash :: Rule@@ -749,8 +756,8 @@     severity = ErrorC     message = "COPY with more than 2 arguments requires the last argument to end with /"     check (Copy (CopyArgs sources t _ _))-        | length sources > 1 = endsWithSlash t-        | otherwise = True+      | length sources > 1 = endsWithSlash t+      | otherwise = True     check _ = True     endsWithSlash (TargetPath t) = not (Text.null t) && Text.last t == '/' @@ -769,12 +776,10 @@     code = "DL3023"     severity = ErrorC     message = "COPY --from should reference a previously defined FROM alias"-    -- | 'check' returns a tuple (state, check_result)-    --   The state in this case is the FROM instruction where the current instruction we are-    --   inspecting is nested in.+     check _ _ f@(From _) = withState (Just f) True -- Remember the last FROM instruction found     check st@(Just fromInstr) _ (Copy (CopyArgs _ _ _ (CopySource stageName))) =-        withState st (aliasMustBe (/= stageName) fromInstr) -- Cannot copy from itself!+      withState st (aliasMustBe (/= stageName) fromInstr) -- Cannot copy from itself!     check state _ _ = withState state True  fromAliasUnique :: Rule@@ -811,7 +816,7 @@     code = "DL3027"     severity = WarningC     message =-        "Do not use apt as it is meant to be a end-user tool, use apt-get or apt-cache instead"+      "Do not use apt as it is meant to be a end-user tool, use apt-get or apt-cache instead"     check (Run (RunArgs args _)) = argumentsRule (not . usingProgram "apt") args     check _ = True @@ -820,28 +825,29 @@   where     code = "DL4006"     severity = WarningC-    message = "Set the SHELL option -o pipefail before RUN with a pipe in it. If you are using \-              \/bin/sh in an alpine image or if your shell is symlinked to busybox then consider \-              \explicitly setting your SHELL to /bin/ash, or disable this check"+    message =+      "Set the SHELL option -o pipefail before RUN with a pipe in it. If you are using \+      \/bin/sh in an alpine image or if your shell is symlinked to busybox then consider \+      \explicitly setting your SHELL to /bin/ash, or disable this check"     check _ _ From {} = (False, True) -- Reset the state each time we find a new FROM     check _ _ (Shell args)-        | argumentsRule isPowerShell args = (True, True)-        | otherwise = (argumentsRule hasPipefailOption args, True)+      | argumentsRule isPowerShell args = (True, True)+      | otherwise = (argumentsRule hasPipefailOption args, True)     check False _ (Run (RunArgs args _)) = (False, argumentsRule notHasPipes args)     check st _ _ = (st, True)     isPowerShell (Shell.ParsedShell orig _ _) = "pwsh" `Text.isPrefixOf` orig     notHasPipes script = not (Shell.hasPipes script)     hasPipefailOption script =-        not $+      not $         null-            [ True-            | cmd@(Shell.Command name arguments _) <- Shell.presentCommands script-            , validShell <- ["/bin/bash", "/bin/zsh", "/bin/ash", "bash", "zsh", "ash"]-            , name == validShell-            , Shell.hasFlag "o" cmd-            , arg <- Shell.arg <$> arguments-            , arg == "pipefail"-            ]+          [ True+            | cmd@(Shell.Command name arguments _) <- Shell.presentCommands script,+              validShell <- ["/bin/bash", "/bin/zsh", "/bin/ash", "bash", "zsh", "ash"],+              name == validShell,+              Shell.hasFlag "o" cmd,+              arg <- Shell.arg <$> arguments,+              arg == "pipefail"+          ]  registryIsAllowed :: Set.Set Registry -> Rule registryIsAllowed allowed = instructionRuleState code severity message check Set.empty@@ -851,15 +857,14 @@     message = "Use only an allowed registry in the FROM image"     check st _ (From BaseImage {image, alias}) = withState (Set.insert alias st) (doCheck st image)     check st _ _ = (st, True)-    -- |Transforms an Image into a Maybe ImageAlias by using the Image name     toImageAlias = Just . ImageAlias . imageName-    -- | Returns True if the image being used is a previous aliased image-    -- or if the image registry is in the set of allowed registries+     doCheck st img = Set.member (toImageAlias img) st || Set.null allowed || isAllowed img     isAllowed Image {registryName = Just registry} = Set.member registry allowed     isAllowed Image {registryName = Nothing, imageName} =-        imageName == "scratch" ||-        Set.member "docker.io" allowed || Set.member "hub.docker.com" allowed+      imageName == "scratch"+        || Set.member "docker.io" allowed+        || Set.member "hub.docker.com" allowed  gemVersionPinned :: Rule gemVersionPinned = instructionRule code severity message check@@ -867,8 +872,8 @@     code = "DL3028"     severity = WarningC     message =-        "Pin versions in gem install. Instead of `gem install <gem>` use `gem \-        \install <gem>:<version>`"+      "Pin versions in gem install. Instead of `gem install <gem>` use `gem \+      \install <gem>:<version>`"     check (Run (RunArgs args _)) = argumentsRule (all versionFixed . gems) args     check _ = True     versionFixed package = ":" `Text.isInfixOf` package@@ -901,11 +906,18 @@     severity = ErrorC     message = "Do not use yum update."     check (Run (RunArgs args _)) =-      argumentsRule (Shell.noCommands (-                       Shell.cmdHasArgs "yum" ["update",-                                               "update-to",-                                               "upgrade",-                                               "upgrade-to"])) args+      argumentsRule+        ( Shell.noCommands+            ( Shell.cmdHasArgs+                "yum"+                [ "update",+                  "update-to",+                  "upgrade",+                  "upgrade-to"+                ]+            )+        )+        args     check _ = True  yumCleanup :: Rule@@ -914,9 +926,11 @@     code = "DL3032"     severity = WarningC     message = "`yum clean all` missing after yum command."-    check (Run (RunArgs args _)) = argumentsRule (Shell.noCommands yumInstall) args ||-                                   (argumentsRule (Shell.anyCommands yumInstall) args &&-                                    argumentsRule (Shell.anyCommands yumClean) args)+    check (Run (RunArgs args _)) =+      argumentsRule (Shell.noCommands yumInstall) args+        || ( argumentsRule (Shell.anyCommands yumInstall) args+               && argumentsRule (Shell.anyCommands yumClean) args+           )     check _ = True     yumInstall = Shell.cmdHasArgs "yum" ["install"]     yumClean = Shell.cmdHasArgs "yum" ["clean", "all"]@@ -929,25 +943,25 @@     message = "Specify version with `yum install -y <package>-<version>`."     check (Run (RunArgs args _)) = argumentsRule (all versionFixed . yumPackages) args     check _ = True-    versionFixed package = "-" `Text.isInfixOf` package-                        || ".rpm" `Text.isSuffixOf` package+    versionFixed package =+      "-" `Text.isInfixOf` package+        || ".rpm" `Text.isSuffixOf` package  yumPackages :: Shell.ParsedShell -> [Text.Text]-yumPackages args = [arg | cmd <- Shell.presentCommands args,-                          Shell.cmdHasArgs "yum" ["install"] cmd,-                          arg <- Shell.getArgsNoFlags cmd,-                          arg /= "install"]+yumPackages args =+  [ arg | cmd <- Shell.presentCommands args, Shell.cmdHasArgs "yum" ["install"] cmd, arg <- Shell.getArgsNoFlags cmd, arg /= "install"+  ]  gems :: Shell.ParsedShell -> [Text.Text] gems shell =-    [ arg-    | cmd <- Shell.presentCommands shell-    , Shell.cmdHasArgs "gem" ["install", "i"] cmd-    , not (Shell.cmdHasArgs "gem" ["-v"] cmd)-    , not (Shell.cmdHasArgs "gem" ["--version"] cmd)-    , not (Shell.cmdHasPrefixArg "gem" "--version=" cmd)-    , arg <- Shell.getArgsNoFlags cmd-    , arg /= "install"-    , arg /= "i"-    , arg /= "--"-    ]+  [ arg+    | cmd <- Shell.presentCommands shell,+      Shell.cmdHasArgs "gem" ["install", "i"] cmd,+      not (Shell.cmdHasArgs "gem" ["-v"] cmd),+      not (Shell.cmdHasArgs "gem" ["--version"] cmd),+      not (Shell.cmdHasPrefixArg "gem" "--version=" cmd),+      arg <- Shell.getArgsNoFlags cmd,+      arg /= "install",+      arg /= "i",+      arg /= "--"+  ]
src/Hadolint/Shell.hs view
@@ -1,7 +1,7 @@-{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE NamedFieldPuns #-}-{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PatternSynonyms #-}+{-# LANGUAGE ScopedTypeVariables #-}  module Hadolint.Shell where @@ -10,51 +10,53 @@ import Data.Maybe (listToMaybe, mapMaybe) import Data.Semigroup ((<>)) import qualified Data.Set as Set-import qualified Data.Text as Text import Data.Text (Text)+import qualified Data.Text as Text+import ShellCheck.AST (Id (..), Token (..), pattern T_Pipe, pattern T_SimpleCommand) import qualified ShellCheck.AST-import ShellCheck.AST (Id(..), Token(..), pattern T_SimpleCommand, pattern T_Pipe) import qualified ShellCheck.ASTLib-import ShellCheck.Checker+import ShellCheck.Checker (checkScript) import ShellCheck.Interface import qualified ShellCheck.Parser  data CmdPart = CmdPart-    { arg :: !Text-    , partId :: !Int-    } deriving (Show)+  { arg :: !Text,+    partId :: !Int+  }+  deriving (Show)  data Command = Command-    { name :: !Text.Text-    , arguments :: [CmdPart]-    , flags :: [CmdPart]-    } deriving (Show)+  { name :: !Text.Text,+    arguments :: [CmdPart],+    flags :: [CmdPart]+  }+  deriving (Show)  data ParsedShell = ParsedShell-    { original :: !Text.Text-    , parsed :: !ParseResult-    , presentCommands :: ![Command]-    }+  { original :: !Text.Text,+    parsed :: !ParseResult,+    presentCommands :: ![Command]+  }  data ShellOpts = ShellOpts-    { shellName :: Text.Text-    , envVars :: Set.Set Text.Text-    }+  { shellName :: Text.Text,+    envVars :: Set.Set Text.Text+  }  defaultShellOpts :: ShellOpts defaultShellOpts = ShellOpts "/bin/sh -c" defaultVars   where     defaultVars =-        Set.fromList-            [ "HTTP_PROXY"-            , "http_proxy"-            , "HTTPS_PROXY"-            , "https_proxy"-            , "FTP_PROXY"-            , "ftp_proxy"-            , "NO_PROXY"-            , "no_proxy"-            ]+      Set.fromList+        [ "HTTP_PROXY",+          "http_proxy",+          "HTTPS_PROXY",+          "https_proxy",+          "FTP_PROXY",+          "ftp_proxy",+          "NO_PROXY",+          "no_proxy"+        ]  addVars :: [Text.Text] -> ShellOpts -> ShellOpts addVars vars (ShellOpts n v) = ShellOpts n (v <> Set.fromList vars)@@ -64,50 +66,49 @@  shellcheck :: ShellOpts -> ParsedShell -> [PositionedComment] shellcheck (ShellOpts sh env) (ParsedShell txt _ _) =-    if "pwsh" `Text.isPrefixOf` sh-        then [] -- Do no run for powershell-        else runShellCheck+  if "pwsh" `Text.isPrefixOf` sh+    then [] -- Do no run for powershell+    else runShellCheck   where     runShellCheck = crComments $ runIdentity $ checkScript si spec     si = mockedSystemInterface [("", "")]     spec =-        emptyCheckSpec-            { csFilename = "" -- filename can be ommited because we only want the parse results back-            , csScript = script-            , csCheckSourced = False-            , csExcludedWarnings = exclusions-            , csShellTypeOverride = Nothing-            , csMinSeverity = StyleC-            }+      emptyCheckSpec+        { csFilename = "", -- filename can be ommited because we only want the parse results back+          csScript = script,+          csCheckSourced = False,+          csExcludedWarnings = exclusions,+          csShellTypeOverride = Nothing,+          csMinSeverity = StyleC+        }     script = "#!" ++ extractShell sh ++ "\n" ++ printVars ++ Text.unpack txt     exclusions =-        [ 2187 -- exclude the warning about the ash shell not being supported-        , 1090 -- requires a directive (shell comment) that can't be expressed in a Dockerfile-        ]-    -- | Shellcheck complains when the shebang has more than one argument, so we only take the first+      [ 2187, -- exclude the warning about the ash shell not being supported+        1090 -- requires a directive (shell comment) that can't be expressed in a Dockerfile+      ]+     extractShell s =-        maybe "" Text.unpack (listToMaybe . Text.words $ s)-    -- | Inject all the collected env vars as exported variables so they can be used+      maybe "" Text.unpack (listToMaybe . Text.words $ s)     printVars = Text.unpack . Text.unlines . Set.toList $ Set.map (\v -> "export " <> v <> "=1") env  parseShell :: Text.Text -> ParsedShell parseShell txt = ParsedShell {original = txt, parsed = parsedResult, presentCommands = commands}   where     parsedResult =-        runIdentity $+      runIdentity $         ShellCheck.Parser.parseScript-            (mockedSystemInterface [("", "")])-            newParseSpec-                { psFilename = "" -- There is no filename-                , psScript = "#!/bin/bash\n" ++ Text.unpack txt-                , psCheckSourced = False-                }-    -- | Extract all commands with their name+          (mockedSystemInterface [("", "")])+          newParseSpec+            { psFilename = "", -- There is no filename+              psScript = "#!/bin/bash\n" ++ Text.unpack txt,+              psCheckSourced = False+            }+     commands = mapMaybe extractNames (findCommandsInResult parsedResult)     extractNames token =-        case ShellCheck.ASTLib.getCommandName token of-            Nothing -> Nothing-            Just n -> Just $ Command (Text.pack n) allArgs (getAllFlags allArgs)+      case ShellCheck.ASTLib.getCommandName token of+        Nothing -> Nothing+        Just n -> Just $ Command (Text.pack n) allArgs (getAllFlags allArgs)       where         allArgs = extractAllArgs token @@ -118,15 +119,15 @@  extractTokensWith :: forall a. (Token -> Maybe a) -> ParseResult -> [a] extractTokensWith extractor ast =-    case prRoot ast of-        Nothing -> []-        Just script -> execWriter $ ShellCheck.AST.doAnalysis extract script+  case prRoot ast of+    Nothing -> []+    Just script -> execWriter $ ShellCheck.AST.doAnalysis extract script   where     extract :: Token -> Writer [a] ()     extract token =-        case extractor token of-            Nothing -> return ()-            Just t -> tell [t]+      case extractor token of+        Nothing -> return ()+        Just t -> tell [t]  findPipes :: ParsedShell -> [Token] findPipes (ParsedShell _ ast _) = extractTokensWith pipesExtractor ast@@ -151,21 +152,21 @@  cmdHasArgs :: Text.Text -> [Text.Text] -> Command -> Bool cmdHasArgs expectedName expectedArgs (Command n args _)-    | expectedName /= n = False-    | otherwise = not $ null [arg | CmdPart arg _ <- args, arg `elem` expectedArgs]+  | expectedName /= n = False+  | otherwise = not $ null [arg | CmdPart arg _ <- args, arg `elem` expectedArgs]  cmdHasPrefixArg :: Text.Text -> Text.Text -> Command -> Bool cmdHasPrefixArg expectedName expectedArg (Command n args _)-    | expectedName /= n = False-    | otherwise = not $ null [arg | CmdPart arg _ <- args, expectedArg `Text.isPrefixOf` arg]+  | expectedName /= n = False+  | otherwise = not $ null [arg | CmdPart arg _ <- args, expectedArg `Text.isPrefixOf` arg]  extractAllArgs :: Token -> [CmdPart]-extractAllArgs (T_SimpleCommand _ _ (_:allArgs)) = map mkPart allArgs+extractAllArgs (T_SimpleCommand _ _ (_ : allArgs)) = map mkPart allArgs   where     mkPart token =-        CmdPart-            (Text.pack . concat $ ShellCheck.ASTLib.oversimplify token)-            (mkId (ShellCheck.AST.getId token))+      CmdPart+        (Text.pack . concat $ ShellCheck.ASTLib.oversimplify token)+        (mkId (ShellCheck.AST.getId token))     mkId (Id i) = i extractAllArgs _ = [] @@ -176,10 +177,10 @@ getAllFlags = concatMap flag   where     flag (CmdPart arg pId)-        | arg == "--" || arg == "-" = []-        | "--" `Text.isPrefixOf` arg = [CmdPart (Text.drop 2 . Text.takeWhile (/= '=') $ arg) pId]-        | "-" `Text.isPrefixOf` arg = map (`CmdPart` pId) (Text.chunksOf 1 (Text.tail arg))-        | otherwise = []+      | arg == "--" || arg == "-" = []+      | "--" `Text.isPrefixOf` arg = [CmdPart (Text.drop 2 . Text.takeWhile (/= '=') $ arg) pId]+      | "-" `Text.isPrefixOf` arg = map (`CmdPart` pId) (Text.chunksOf 1 (Text.tail arg))+      | otherwise = []  getArgsNoFlags :: Command -> [Text.Text] getArgsNoFlags args = map arg $ filter (notAFlagId . partId) (arguments args)@@ -200,4 +201,6 @@   where     idsToDrop = Set.fromList [getValueId fId arguments | CmdPart f fId <- flags, f `elem` flagsToDrop]     filterdArgs = [arg | arg@(CmdPart _ aId) <- arguments, not (aId `Set.member` idsToDrop)]-getValueId fId flags = foldl min (maxBound :: Int) $ filter (>fId) $ map partId flags ++getValueId :: Int -> [CmdPart] -> Int+getValueId fId flags = foldl min (maxBound :: Int) $ filter (> fId) $ map partId flags
test/ConfigSpec.hs view
@@ -1,54 +1,54 @@ {-# LANGUAGE OverloadedStrings #-}+ module ConfigSpec where -import Test.HUnit-import Test.Hspec import Control.Monad (unless) import qualified Data.ByteString.Char8 as Bytes import qualified Data.YAML as Yaml- import Hadolint.Config+import Test.HUnit+import Test.Hspec  tests :: SpecWith () tests =-    describe "Config" $ do-        it "Parses config with only ignores" $-            let configFile =-                    [ "ignored:"-                    , "- DL3000"-                    , "- SC1010"-                    ]-                expected = ConfigFile (Just ["DL3000", "SC1010"]) Nothing-            in assertConfig expected (Bytes.unlines configFile)+  describe "Config" $ do+    it "Parses config with only ignores" $+      let configFile =+            [ "ignored:",+              "- DL3000",+              "- SC1010"+            ]+          expected = ConfigFile (Just ["DL3000", "SC1010"]) Nothing+       in assertConfig expected (Bytes.unlines configFile) -        it "Parses config with only trustedRegistries" $-            let configFile =-                    [ "trustedRegistries:"-                    , "- hub.docker.com"-                    , "- my.shady.xyz"-                    ]-                expected = ConfigFile Nothing (Just ["hub.docker.com", "my.shady.xyz"])-            in assertConfig expected (Bytes.unlines configFile)+    it "Parses config with only trustedRegistries" $+      let configFile =+            [ "trustedRegistries:",+              "- hub.docker.com",+              "- my.shady.xyz"+            ]+          expected = ConfigFile Nothing (Just ["hub.docker.com", "my.shady.xyz"])+       in assertConfig expected (Bytes.unlines configFile) -        it "Parses full file" $-            let configFile =-                    [ "trustedRegistries:"-                    , "- hub.docker.com"-                    , ""-                    , "ignored:"-                    , "- DL3000"-                    ]-                expected = ConfigFile (Just ["DL3000"]) (Just ["hub.docker.com"])-            in assertConfig expected (Bytes.unlines configFile)+    it "Parses full file" $+      let configFile =+            [ "trustedRegistries:",+              "- hub.docker.com",+              "",+              "ignored:",+              "- DL3000"+            ]+          expected = ConfigFile (Just ["DL3000"]) (Just ["hub.docker.com"])+       in assertConfig expected (Bytes.unlines configFile)  assertConfig :: HasCallStack => ConfigFile -> Bytes.ByteString -> Assertion assertConfig config s =-    case Yaml.decode1Strict s of-        Left (_, err) ->-            assertFailure err-        Right result ->-            checkResult result+  case Yaml.decode1Strict s of+    Left (_, err) ->+      assertFailure err+    Right result ->+      checkResult result   where     checkResult result =-        unless (result == config) $-            assertFailure ("Config \n\n" ++ show config  ++ "\n\n is not \n\n" ++ show result)+      unless (result == config) $+        assertFailure ("Config \n\n" ++ show config ++ "\n\n is not \n\n" ++ show result)
test/Spec.hs view
@@ -1,1292 +1,1307 @@-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE OverloadedLists #-}-import Test.HUnit hiding (Label)-import Test.Hspec-import Control.Monad (when, unless)--import Hadolint.Formatter.TTY (formatError, formatChecks)-import Hadolint.Rules--import Language.Docker.Parser-import Language.Docker.Syntax-import Data.Semigroup ((<>))-import qualified Data.Text as Text--import qualified ConfigSpec--main :: IO ()-main =-    hspec $ do-        describe "FROM rules" $ do-            it "no untagged" $ ruleCatches noUntagged "FROM debian"-            it "no untagged with name" $ ruleCatches noUntagged "FROM debian AS builder"-            it "explicit latest" $ ruleCatches noLatestTag "FROM debian:latest"-            it "explicit latest with name" $ ruleCatches noLatestTag "FROM debian:latest AS builder"-            it "explicit tagged" $ ruleCatchesNot noLatestTag "FROM debian:jessie"-            it "explicit platform flag" $ ruleCatches noPlatformFlag "FROM --platform=linux debian:jessie"-            it "no platform flag" $ ruleCatchesNot noPlatformFlag "FROM debian:jessie"-            it "explicit SHA" $-                ruleCatchesNot noLatestTag-                    "FROM hub.docker.io/debian@sha256:\-                    \7959ed6f7e35f8b1aaa06d1d8259d4ee25aa85a086d5c125480c333183f9deeb"-            it "explicit tagged with name" $-                ruleCatchesNot noLatestTag "FROM debian:jessie AS builder"-            it "untagged digest is not an error" $-                ruleCatchesNot noUntagged "FROM ruby@sha256:f1dbca0f5dbc9"-            it "untagged digest is not an error" $-                ruleCatchesNot noUntagged "FROM ruby:2"-            it "local aliases are OK to be untagged" $-                let dockerFile =-                        [ "FROM golang:1.9.3-alpine3.7 AS build"-                        , "RUN foo"-                        , "FROM build as unit-test"-                        , "RUN bar"-                        , "FROM alpine:3.7"-                        , "RUN baz"-                        ]-                in do-                  ruleCatchesNot noUntagged $ Text.unlines dockerFile-                  onBuildRuleCatchesNot noUntagged $ Text.unlines dockerFile-            it "other untagged cases are not ok" $-                let dockerFile =-                        [ "FROM golang:1.9.3-alpine3.7 AS build"-                        , "RUN foo"-                        , "FROM node as unit-test"-                        , "RUN bar"-                        , "FROM alpine:3.7"-                        , "RUN baz"-                        ]-                in do-                  ruleCatches noUntagged $ Text.unlines dockerFile-                  onBuildRuleCatches noUntagged $ Text.unlines dockerFile-        ---        describe "no root or sudo rules" $ do-            it "sudo" $ do-              ruleCatches noSudo "RUN sudo apt-get update"-              onBuildRuleCatches noSudo "RUN sudo apt-get update"--            it "last user should not be root" $-                let dockerFile =-                        [ "FROM scratch"-                        , "USER root"-                        ]-                in ruleCatches noRootUser $ Text.unlines dockerFile--            it "no root" $-                let dockerFile =-                        [ "FROM scratch"-                        , "USER foo"-                        ]-                in ruleCatchesNot noRootUser $ Text.unlines dockerFile--            it "no root UID" $-                let dockerFile =-                        [ "FROM scratch"-                        , "USER 0"-                        ]-                in ruleCatches noRootUser $ Text.unlines dockerFile--            it "no root:root" $-                let dockerFile =-                        [ "FROM scratch"-                        , "USER root:root"-                        ]-                in ruleCatches noRootUser $ Text.unlines dockerFile--            it "no UID:GID" $-                let dockerFile =-                        [ "FROM scratch"-                        , "USER 0:0"-                        ]-                in ruleCatches noRootUser $ Text.unlines dockerFile--            it "can switch back to non root" $-                let dockerFile =-                        [ "FROM scratch"-                        , "USER root"-                        , "RUN something"-                        , "USER foo"-                        ]-                in ruleCatchesNot noRootUser $ Text.unlines dockerFile--            it "warns on transitive root user" $-                let dockerFile =-                        [ "FROM debian as base"-                        , "USER root"-                        , "RUN something"-                        , "FROM base"-                        , "RUN something else"-                        ]-                in ruleCatches noRootUser $ Text.unlines dockerFile--            it "warns on multiple stages" $-                let dockerFile =-                        [ "FROM debian as base"-                        , "USER root"-                        , "RUN something"-                        , "FROM scratch"-                        , "USER foo"-                        , "RUN something else"-                        ]-                in ruleCatches noRootUser $ Text.unlines dockerFile--            it "does not warn when switching in multiple stages" $-                let dockerFile =-                        [ "FROM debian as base"-                        , "USER root"-                        , "RUN something"-                        , "USER foo"-                        , "FROM scratch"-                        , "RUN something else"-                        ]-                in ruleCatchesNot noRootUser $ Text.unlines dockerFile--            it "install sudo" $ do-                ruleCatchesNot noSudo "RUN apt-get install sudo"-                onBuildRuleCatchesNot noSudo "RUN apt-get install sudo"-            it "sudo chained programs" $ do-                ruleCatches noSudo "RUN apt-get update && sudo apt-get install"-                onBuildRuleCatches noSudo "RUN apt-get update && sudo apt-get install"-        ---        describe "invalid CMD rules" $ do-            it "invalid cmd" $ do-                ruleCatches invalidCmd "RUN top"-                onBuildRuleCatches invalidCmd "RUN top"-            it "install ssh" $ do-                ruleCatchesNot invalidCmd "RUN apt-get install ssh"-                onBuildRuleCatchesNot invalidCmd "RUN apt-get install ssh"-        ---        describe "gem" $-          describe "version pinning" $ do-            describe "i" $ do-              it "unpinned" $ do-                ruleCatches gemVersionPinned "RUN gem i bundler"-                onBuildRuleCatches gemVersionPinned "RUN gem i bundler"-              it "pinned" $ do-                ruleCatchesNot gemVersionPinned "RUN gem i bundler:1"-                onBuildRuleCatchesNot gemVersionPinned "RUN gem i bundler:1"-              it "multi" $ do-                ruleCatches gemVersionPinned "RUN gem i bunlder:1 nokogiri"-                onBuildRuleCatches gemVersionPinned "RUN gem i bunlder:1 nokogiri"-                ruleCatchesNot gemVersionPinned "RUN gem i bunlder:1 nokogirii:1"-                onBuildRuleCatchesNot gemVersionPinned "RUN gem i bunlder:1 nokogiri:1"-            describe "install" $ do-              it "unpinned" $ do-                ruleCatches gemVersionPinned "RUN gem install bundler"-                onBuildRuleCatches gemVersionPinned "RUN gem install bundler"-              it "pinned" $ do-                ruleCatchesNot gemVersionPinned "RUN gem install bundler:1"-                onBuildRuleCatchesNot gemVersionPinned "RUN gem install bundler:1"-              it "does not warn on -v" $ do-                ruleCatchesNot gemVersionPinned "RUN gem install bundler -v '2.0.1'"-                onBuildRuleCatchesNot gemVersionPinned "RUN gem install bundler -v '2.0.1'"-              it "does not warn on --version without =" $ do-                ruleCatchesNot gemVersionPinned "RUN gem install bundler --version '2.0.1'"-                onBuildRuleCatchesNot gemVersionPinned "RUN gem install bundler --version '2.0.1'"-              it "does not warn on --version with =" $ do-                ruleCatchesNot gemVersionPinned "RUN gem install bundler --version='2.0.1'"-                onBuildRuleCatchesNot gemVersionPinned "RUN gem install bundler --version='2.0.1'"-              it "does not warn on extra flags" $ do-                ruleCatchesNot gemVersionPinned "RUN gem install bundler:2.0.1 -- --use-system-libraries=true"-                onBuildRuleCatchesNot gemVersionPinned "RUN gem install bundler:2.0.1 -- --use-system-libraries=true"-        ---        describe "yum rules" $ do-            it "yum update" $ do-                ruleCatches noYumUpdate "RUN yum update"-                onBuildRuleCatches noYumUpdate "RUN yum update"-            it "yum version pinning" $ do-                ruleCatches yumVersionPinned "RUN yum install -y tomcat && yum clean all"-                onBuildRuleCatches yumVersionPinned "RUN yum install -y tomcat && yum clean all"-            it "yum no clean all" $ do-                ruleCatches yumCleanup "RUN yum install -y mariadb-10.4"-                onBuildRuleCatches yumCleanup "RUN yum install -y mariadb-10.4"-            it "yum non-interactive" $ do-                ruleCatches yumYes "RUN yum install httpd-2.4.24 && yum clean all"-                onBuildRuleCatches yumYes "RUN yum install httpd-2.4.24 && yum clean all"-        ---        describe "apt-get rules" $ do-            it "apt" $-                let dockerFile =-                        [ "FROM ubuntu"-                        , "RUN apt install python"-                        ]-                in do-                  ruleCatches noApt $ Text.unlines dockerFile-                  onBuildRuleCatches noApt $ Text.unlines dockerFile-            it "apt-get upgrade" $ do-                ruleCatches noAptGetUpgrade "RUN apt-get update && apt-get upgrade"-                onBuildRuleCatches noAptGetUpgrade "RUN apt-get update && apt-get upgrade"-            it "apt-get version pinning" $ do-                ruleCatches aptGetVersionPinned "RUN apt-get update && apt-get install python"-                onBuildRuleCatches aptGetVersionPinned "RUN apt-get update && apt-get install python"-            it "apt-get no cleanup" $-                let dockerFile =-                        [ "FROM scratch"-                        , "RUN apt-get update && apt-get install python"-                        ]-                in do-                  ruleCatches aptGetCleanup $ Text.unlines dockerFile-                  onBuildRuleCatches aptGetCleanup $ Text.unlines dockerFile-            it "apt-get cleanup in stage image" $-                let dockerFile =-                        [ "FROM ubuntu as foo"-                        , "RUN apt-get update && apt-get install python"-                        , "FROM scratch"-                        , "RUN echo hey!"-                        ]-                in do-                  ruleCatchesNot aptGetCleanup $ Text.unlines dockerFile-                  onBuildRuleCatchesNot aptGetCleanup $ Text.unlines dockerFile-            it "apt-get no cleanup in last stage" $-                let dockerFile =-                        [ "FROM ubuntu as foo"-                        , "RUN hey!"-                        , "FROM scratch"-                        , "RUN apt-get update && apt-get install python"-                        ]-                in do-                  ruleCatches aptGetCleanup $ Text.unlines dockerFile-                  onBuildRuleCatches aptGetCleanup $ Text.unlines dockerFile-            it "apt-get no cleanup in intermediate stage" $-                let dockerFile =-                        [ "FROM ubuntu as foo"-                        , "RUN apt-get update && apt-get install python"-                        , "FROM foo"-                        , "RUN hey!"-                        ]-                in do-                  ruleCatches aptGetCleanup $ Text.unlines dockerFile-                  onBuildRuleCatches aptGetCleanup $ Text.unlines dockerFile-            it "no warn apt-get cleanup in intermediate stage that cleans lists" $-                let dockerFile =-                        [ "FROM ubuntu as foo"-                        , "RUN apt-get update && apt-get install python && rm -rf /var/lib/apt/lists/*"-                        , "FROM foo"-                        , "RUN hey!"-                        ]-                in do-                  ruleCatchesNot aptGetCleanup $ Text.unlines dockerFile-                  onBuildRuleCatchesNot aptGetCleanup $ Text.unlines dockerFile-            it "no warn apt-get cleanup in intermediate stage when stage not used later" $-                let dockerFile =-                        [ "FROM ubuntu as foo"-                        , "RUN apt-get update && apt-get install python"-                        , "FROM scratch"-                        , "RUN hey!"-                        ]-                in do-                  ruleCatchesNot aptGetCleanup $ Text.unlines dockerFile-                  onBuildRuleCatchesNot aptGetCleanup $ Text.unlines dockerFile-            it "apt-get cleanup" $-                let dockerFile =-                        [ "FROM scratch"-                        , "RUN apt-get update && apt-get install python && rm -rf /var/lib/apt/lists/*"-                        ]-                in do-                  ruleCatchesNot aptGetCleanup $ Text.unlines dockerFile-                  onBuildRuleCatchesNot aptGetCleanup $ Text.unlines dockerFile--            it "apt-get pinned chained" $-                let dockerFile =-                        [ "RUN apt-get update \\"-                        , " && apt-get -yqq --no-install-recommends install nodejs=0.10 \\"-                        , " && rm -rf /var/lib/apt/lists/*"-                        ]-                in do-                  ruleCatchesNot aptGetVersionPinned $ Text.unlines dockerFile-                  onBuildRuleCatchesNot aptGetVersionPinned $ Text.unlines dockerFile--            it "apt-get pinned regression" $-                let dockerFile =-                        [ "RUN apt-get update && apt-get install --no-install-recommends -y \\"-                        , "python-demjson=2.2.2* \\"-                        , "wget=1.16.1* \\"-                        , "git=1:2.5.0* \\"-                        , "ruby=1:2.1.*"-                        ]-                in do-                  ruleCatchesNot aptGetVersionPinned $ Text.unlines dockerFile-                  onBuildRuleCatchesNot aptGetVersionPinned $ Text.unlines dockerFile--            it "has deprecated maintainer" $-                ruleCatches hasNoMaintainer "FROM busybox\nMAINTAINER hudu@mail.com"-        ---        describe "apk add rules" $ do-            it "apk upgrade" $ do-                ruleCatches noApkUpgrade "RUN apk update && apk upgrade"-                onBuildRuleCatches noApkUpgrade "RUN apk update && apk upgrade"-            it "apk add version pinning single" $ do-                ruleCatches apkAddVersionPinned "RUN apk add flex"-                onBuildRuleCatches apkAddVersionPinned "RUN apk add flex"-            it "apk add no version pinning single" $ do-                ruleCatchesNot apkAddVersionPinned "RUN apk add flex=2.6.4-r1"-                onBuildRuleCatchesNot apkAddVersionPinned "RUN apk add flex=2.6.4-r1"-            it "apk add version pinned chained" $-                let dockerFile =-                        [ "RUN apk add --no-cache flex=2.6.4-r1 \\"-                        , " && pip install -r requirements.txt"-                        ]-                in do-                  ruleCatchesNot apkAddVersionPinned $ Text.unlines dockerFile-                  onBuildRuleCatchesNot apkAddVersionPinned $ Text.unlines dockerFile-            it "apk add version pinned regression" $-                let dockerFile =-                        [ "RUN apk add --no-cache \\"-                        , "flex=2.6.4-r1 \\"-                        , "libffi=3.2.1-r3 \\"-                        , "python2=2.7.13-r1 \\"-                        , "libbz2=1.0.6-r5"-                        ]-                in do-                  ruleCatchesNot apkAddVersionPinned $ Text.unlines dockerFile-                  onBuildRuleCatchesNot apkAddVersionPinned $ Text.unlines dockerFile-            it "apk add version pinned regression - one missed" $-                let dockerFile =-                        [ "RUN apk add --no-cache \\"-                        , "flex=2.6.4-r1 \\"-                        , "libffi \\"-                        , "python2=2.7.13-r1 \\"-                        , "libbz2=1.0.6-r5"-                        ]-                in do-                  ruleCatches apkAddVersionPinned $ Text.unlines dockerFile-                  onBuildRuleCatches apkAddVersionPinned $ Text.unlines dockerFile-            it "apk add with --no-cache" $ do-                ruleCatches apkAddNoCache "RUN apk add flex=2.6.4-r1"-                onBuildRuleCatches apkAddNoCache "RUN apk add flex=2.6.4-r1"-            it "apk add without --no-cache" $ do-                ruleCatchesNot apkAddNoCache "RUN apk add --no-cache flex=2.6.4-r1"-                onBuildRuleCatchesNot apkAddNoCache "RUN apk add --no-cache flex=2.6.4-r1"-            it "apk add virtual package" $-                let dockerFile =-                        [ "RUN apk add \\"-                        , "--virtual build-dependencies \\"-                        , "python-dev=1.1.1 build-base=2.2.2 wget=3.3.3 \\"-                        , "&& pip install -r requirements.txt \\"-                        , "&& python setup.py install \\"-                        , "&& apk del build-dependencies"-                        ]-                in do-                  ruleCatchesNot apkAddVersionPinned $ Text.unlines dockerFile-                  onBuildRuleCatchesNot apkAddVersionPinned $ Text.unlines dockerFile-            it "apk add with repository without equal sign" $-                let dockerFile =-                        [ "RUN apk add --no-cache \\"-                        , "--repository https://nl.alpinelinux.org/alpine/edge/testing \\"-                        , "flow=0.78.0-r0"-                        ]-                in do-                ruleCatchesNot apkAddVersionPinned $ Text.unlines dockerFile-                onBuildRuleCatchesNot apkAddVersionPinned $ Text.unlines dockerFile-            it "apk add with repository with equal sign" $-                let dockerFile =-                        [ "RUN apk add --no-cache \\"-                        , "--repository=https://nl.alpinelinux.org/alpine/edge/testing \\"-                        , "flow=0.78.0-r0"-                        ]-                in do-                ruleCatchesNot apkAddVersionPinned $ Text.unlines dockerFile-                onBuildRuleCatchesNot apkAddVersionPinned $ Text.unlines dockerFile-            it "apk add with repository (-X) without equal sign" $-                let dockerFile =-                        [ "RUN apk add --no-cache \\"-                        , "-X https://nl.alpinelinux.org/alpine/edge/testing \\"-                        , "flow=0.78.0-r0"-                        ]-                in do-                ruleCatchesNot apkAddVersionPinned $ Text.unlines dockerFile-                onBuildRuleCatchesNot apkAddVersionPinned $ Text.unlines dockerFile-        ---        describe "EXPOSE rules" $ do-            it "invalid port" $ ruleCatches invalidPort "EXPOSE 80000"-            it "valid port" $ ruleCatchesNot invalidPort "EXPOSE 60000"-        ---        describe "pip pinning" $ do-            it "pip2 version not pinned" $ do-                ruleCatches pipVersionPinned "RUN pip2 install MySQL_python"-                onBuildRuleCatches pipVersionPinned "RUN pip2 install MySQL_python"-            it "pip3 version not pinned" $ do-                ruleCatches pipVersionPinned "RUN pip3 install MySQL_python"-                onBuildRuleCatches pipVersionPinned "RUN pip2 install MySQL_python"-            it "pip3 version pinned" $ do-                ruleCatchesNot pipVersionPinned "RUN pip3 install MySQL_python==1.2.2"-                onBuildRuleCatchesNot pipVersionPinned "RUN pip3 install MySQL_python==1.2.2"-            it "pip install requirements" $ do-                ruleCatchesNot pipVersionPinned "RUN pip install -r requirements.txt"-                onBuildRuleCatchesNot pipVersionPinned "RUN pip install -r requirements.txt"-            it "pip install requirements with long flag" $ do-                ruleCatchesNot pipVersionPinned "RUN pip install --requirement requirements.txt"-                onBuildRuleCatchesNot pipVersionPinned "RUN pip install --requirement requirements.txt"-            it "pip install use setup.py" $ do-                ruleCatchesNot pipVersionPinned "RUN pip install ."-                onBuildRuleCatchesNot pipVersionPinned "RUN pip install ."-            it "pip version not pinned" $ do-                ruleCatches pipVersionPinned "RUN pip install MySQL_python"-                onBuildRuleCatches pipVersionPinned "RUN pip install MySQL_python"-            it "pip version pinned" $ do-                ruleCatchesNot pipVersionPinned "RUN pip install MySQL_python==1.2.2"-                onBuildRuleCatchesNot pipVersionPinned "RUN pip install MySQL_python==1.2.2"-            it "pip version pinned with ~= operator" $ do-                ruleCatchesNot pipVersionPinned "RUN pip install MySQL_python~=1.2.2"-                onBuildRuleCatchesNot pipVersionPinned "RUN pip install MySQL_python~=1.2.2"-            it "pip version pinned with === operator" $ do-                ruleCatchesNot pipVersionPinned "RUN pip install MySQL_python===1.2.2"-                onBuildRuleCatchesNot pipVersionPinned "RUN pip install MySQL_python===1.2.2"-            it "pip version pinned with flag --ignore-installed" $ do-                ruleCatchesNot pipVersionPinned "RUN pip install --ignore-installed MySQL_python==1.2.2"-                onBuildRuleCatchesNot pipVersionPinned "RUN pip install --ignore-installed MySQL_python==1.2.2"-            it "pip version pinned with flag --build" $ do-                ruleCatchesNot pipVersionPinned "RUN pip3 install --build /opt/yamllint yamllint==1.20.0"-                onBuildRuleCatchesNot pipVersionPinned "RUN pip3 install --build /opt/yamllint yamllint==1.20.0"-            it "pip version pinned with flag --prefix" $ do-                ruleCatchesNot pipVersionPinned "RUN pip3 install --prefix /opt/yamllint yamllint==1.20.0"-                onBuildRuleCatchesNot pipVersionPinned "RUN pip3 install --prefix /opt/yamllint yamllint==1.20.0"-            it "pip version pinned with flag --root" $ do-                ruleCatchesNot pipVersionPinned "RUN pip3 install --root /opt/yamllint yamllint==1.20.0"-                onBuildRuleCatchesNot pipVersionPinned "RUN pip3 install --root /opt/yamllint yamllint==1.20.0"-            it "pip version pinned with flag --target" $ do-                ruleCatchesNot pipVersionPinned "RUN pip3 install --target /opt/yamllint yamllint==1.20.0"-                onBuildRuleCatchesNot pipVersionPinned "RUN pip3 install --target /opt/yamllint yamllint==1.20.0"-            it "pip version pinned with flag --trusted-host" $ do-                ruleCatchesNot pipVersionPinned "RUN pip3 install --trusted-host host example==1.2.2"-                onBuildRuleCatchesNot pipVersionPinned "RUN pip3 install --trusted-host host example==1.2.2"-            it "pip version pinned with python -m" $ do-                ruleCatchesNot pipVersionPinned "RUN python -m pip install example==1.2.2"-                onBuildRuleCatchesNot pipVersionPinned "RUN python -m pip install example==1.2.2"-            it "pip version not pinned with python -m" $ do-                ruleCatches pipVersionPinned "RUN python -m pip install example"-                onBuildRuleCatches pipVersionPinned "RUN python -m pip install --index-url url example"-            it "pip install git" $ do-                ruleCatchesNot-                    pipVersionPinned-                    "RUN pip install git+https://github.com/rtfd/r-ext.git@0.6-alpha#egg=r-ext"-                onBuildRuleCatchesNot-                    pipVersionPinned-                    "RUN pip install git+https://github.com/rtfd/r-ext.git@0.6-alpha#egg=r-ext"-            it "pip install unversioned git" $ do-                ruleCatches-                    pipVersionPinned-                    "RUN pip install git+https://github.com/rtfd/read-ext.git#egg=read-ext"-                onBuildRuleCatches-                    pipVersionPinned-                    "RUN pip install git+https://github.com/rtfd/read-ext.git#egg=read-ext"-            it "pip install upper bound" $ do-                ruleCatchesNot pipVersionPinned "RUN pip install 'alabaster>=0.7'"-                onBuildRuleCatchesNot pipVersionPinned "RUN pip install 'alabaster>=0.7'"-            it "pip install lower bound" $ do-                ruleCatchesNot pipVersionPinned "RUN pip install 'alabaster<0.7'"-                onBuildRuleCatchesNot pipVersionPinned "RUN pip install 'alabaster<0.7'"-            it "pip install excluded version" $ do-                ruleCatchesNot pipVersionPinned "RUN pip install 'alabaster!=0.7'"-                onBuildRuleCatchesNot pipVersionPinned "RUN pip install 'alabaster!=0.7'"-            it "pip install user directory" $ do-                ruleCatchesNot pipVersionPinned "RUN pip install MySQL_python==1.2.2 --user"-                onBuildRuleCatchesNot pipVersionPinned "RUN pip install MySQL_python==1.2.2 --user"-            it "pip install no pip version check" $ do-                ruleCatchesNot-                    pipVersionPinned-                    "RUN pip install MySQL_python==1.2.2 --disable-pip-version-check"-                onBuildRuleCatchesNot-                    pipVersionPinned-                    "RUN pip install MySQL_python==1.2.2 --disable-pip-version-check"-            it "pip install --index-url" $ do-                ruleCatchesNot-                    pipVersionPinned-                    "RUN pip install --index-url https://eg.com/foo foobar==1.0.0"-                onBuildRuleCatchesNot-                    pipVersionPinned-                    "RUN pip install --index-url https://eg.com/foo foobar==1.0.0"-            it "pip install index-url with -i flag" $ do-                ruleCatchesNot-                    pipVersionPinned-                    "RUN pip install --index-url https://eg.com/foo foobar==1.0.0"-                onBuildRuleCatchesNot-                    pipVersionPinned-                    "RUN pip install --index-url https://eg.com/foo foobar==1.0.0"-            it "pip install --index-url with --extra-index-url" $ do-                ruleCatchesNot-                    pipVersionPinned-                    "RUN pip install --index-url https://eg.com/foo --extra-index-url https://ex-eg.io/foo foobar==1.0.0"-                onBuildRuleCatchesNot-                    pipVersionPinned-                    "RUN pip install --index-url https://eg.com/foo --extra-index-url https://ex-eg.io/foo foobar==1.0.0"-            it "pip install no cache dir" $ do-                ruleCatchesNot pipVersionPinned "RUN pip install MySQL_python==1.2.2 --no-cache-dir"-                onBuildRuleCatchesNot pipVersionPinned "RUN pip install MySQL_python==1.2.2 --no-cache-dir"-            it "pip install constraints file - long version argument" $ do-                ruleCatchesNot pipVersionPinned "RUN pip install pykafka --constraint http://foo.bar.baz"-                onBuildRuleCatchesNot pipVersionPinned "RUN pip install pykafka --constraint http://foo.bar.baz"-            it "pip install constraints file - short version argument" $ do-                ruleCatchesNot pipVersionPinned "RUN pip install pykafka -c http://foo.bar.baz"-                onBuildRuleCatchesNot pipVersionPinned "RUN pip install pykafka -c http://foo.bar.baz"-            it "pip install --index-url with --extra-index-url with basic auth" $ do-                ruleCatchesNot-                    pipVersionPinned-                    "RUN pip install --index-url https://user:pass@eg.com/foo --extra-index-url https://user:pass@ex-eg.io/foo foobar==1.0.0"-                onBuildRuleCatchesNot-                    pipVersionPinned-                    "RUN pip install --index-url https://user:pass@eg.com/foo --extra-index-url https://user:pass@ex-eg.io/foo foobar==1.0.0"-        ---        describe "npm pinning" $ do-            it "version pinned in package.json" $ do-                ruleCatchesNot npmVersionPinned "RUN npm install"-                onBuildRuleCatchesNot npmVersionPinned "RUN npm install"-            it "version pinned in package.json with arguments" $ do-                ruleCatchesNot npmVersionPinned "RUN npm install --progress=false"-                onBuildRuleCatchesNot npmVersionPinned "RUN npm install --progress=false"-            it "version pinned" $ do-                ruleCatchesNot npmVersionPinned "RUN npm install express@4.1.1"-                onBuildRuleCatchesNot npmVersionPinned "RUN npm install express@4.1.1"-            it "version pinned with scope" $ do-                ruleCatchesNot npmVersionPinned "RUN npm install @myorg/privatepackage@\">=0.1.0\""-                onBuildRuleCatchesNot npmVersionPinned "RUN npm install @myorg/privatepackage@\">=0.1.0\""-            it "version pinned multiple packages" $ do-                ruleCatchesNot npmVersionPinned "RUN npm install express@\"4.1.1\" sax@0.1.1"-                onBuildRuleCatchesNot npmVersionPinned "RUN npm install express@\"4.1.1\" sax@0.1.1"-            it "version pinned with --global" $ do-                ruleCatchesNot npmVersionPinned "RUN npm install --global express@\"4.1.1\""-                onBuildRuleCatchesNot npmVersionPinned "RUN npm install --global express@\"4.1.1\""-            it "version pinned with -g" $ do-                ruleCatchesNot npmVersionPinned "RUN npm install -g express@\"4.1.1\""-                onBuildRuleCatchesNot npmVersionPinned "RUN npm install -g express@\"4.1.1\""-            it "version does not have to be pinned for tarball suffix .tar" $ do-                ruleCatchesNot npmVersionPinned "RUN npm install package-v1.2.3.tar"-                onBuildRuleCatchesNot npmVersionPinned "RUN npm install package-v1.2.3.tar"-            it "version does not have to be pinned for tarball suffix .tar.gz" $ do-                ruleCatchesNot npmVersionPinned "RUN npm install package-v1.2.3.tar.gz"-                onBuildRuleCatchesNot npmVersionPinned "RUN npm install package-v1.2.3.tar.gz"-            it "version does not have to be pinned for tarball suffix .tgz" $ do-                ruleCatchesNot npmVersionPinned "RUN npm install package-v1.2.3.tgz"-                onBuildRuleCatchesNot npmVersionPinned "RUN npm install package-v1.2.3.tgz"-            it "version does not have to be pinned for folder - absolute path" $ do-                ruleCatchesNot npmVersionPinned "RUN npm install /folder"-                onBuildRuleCatchesNot npmVersionPinned "RUN npm install /folder"-            it "version does not have to be pinned for folder - relative path from current folder" $ do-                ruleCatchesNot npmVersionPinned "RUN npm install ./folder"-                onBuildRuleCatchesNot npmVersionPinned "RUN npm install ./folder"-            it "version does not have to be pinned for folder - relative path to parent folder" $ do-                ruleCatchesNot npmVersionPinned "RUN npm install ../folder"-                onBuildRuleCatchesNot npmVersionPinned "RUN npm install ../folder"-            it "version does not have to be pinned for folder - relative path from home" $ do-                ruleCatchesNot npmVersionPinned "RUN npm install ~/folder"-                onBuildRuleCatchesNot npmVersionPinned "RUN npm install ~/folder"-            it "commit pinned for git+ssh" $ do-                ruleCatchesNot-                    npmVersionPinned-                    "RUN npm install git+ssh://git@github.com:npm/npm.git#v1.0.27"-                onBuildRuleCatchesNot-                    npmVersionPinned-                    "RUN npm install git+ssh://git@github.com:npm/npm.git#v1.0.27"-            it "commit pinned for git+http" $ do-                ruleCatchesNot-                    npmVersionPinned-                    "RUN npm install git+http://isaacs@github.com/npm/npm#semver:^5.0"-                onBuildRuleCatchesNot-                    npmVersionPinned-                    "RUN npm install git+http://isaacs@github.com/npm/npm#semver:^5.0"-            it "commit pinned for git+https" $ do-                ruleCatchesNot-                    npmVersionPinned-                    "RUN npm install git+https://isaacs@github.com/npm/npm.git#v1.0.27"-                onBuildRuleCatchesNot-                    npmVersionPinned-                    "RUN npm install git+https://isaacs@github.com/npm/npm.git#v1.0.27"-            it "commit pinned for git" $ do-                ruleCatchesNot-                    npmVersionPinned-                    "RUN npm install git://github.com/npm/npm.git#v1.0.27"-                onBuildRuleCatchesNot-                    npmVersionPinned-                    "RUN npm install git://github.com/npm/npm.git#v1.0.27"-            it "npm run install is fine" $ do-                ruleCatchesNot-                    npmVersionPinned-                    "RUN npm run --crazy install"-                onBuildRuleCatchesNot-                    npmVersionPinned-                    "RUN npm run --crazy install"--      --version range is not supported-            it "version pinned with scope" $ do-                ruleCatchesNot npmVersionPinned "RUN npm install @myorg/privatepackage@\">=0.1.0 <0.2.0\""-                onBuildRuleCatchesNot npmVersionPinned "RUN npm install @myorg/privatepackage@\">=0.1.0 <0.2.0\""-            it "version not pinned" $ do-                ruleCatches npmVersionPinned "RUN npm install express"-                onBuildRuleCatches npmVersionPinned "RUN npm install express"-            it "version not pinned with scope" $ do-                ruleCatches npmVersionPinned "RUN npm install @myorg/privatepackage"-                onBuildRuleCatches npmVersionPinned "RUN npm install @myorg/privatepackage"-            it "version not pinned multiple packages" $ do-                ruleCatches npmVersionPinned "RUN npm install express sax@0.1.1"-                onBuildRuleCatches npmVersionPinned "RUN npm install express sax@0.1.1"-            it "version not pinned with --global" $ do-                ruleCatches npmVersionPinned "RUN npm install --global express"-                onBuildRuleCatches npmVersionPinned "RUN npm install --global express"-            it "commit not pinned for git+ssh" $ do-                ruleCatches npmVersionPinned "RUN npm install git+ssh://git@github.com:npm/npm.git"-                onBuildRuleCatches npmVersionPinned "RUN npm install git+ssh://git@github.com:npm/npm.git"-            it "commit not pinned for git+http" $ do-                ruleCatches npmVersionPinned "RUN npm install git+http://isaacs@github.com/npm/npm"-                onBuildRuleCatches npmVersionPinned "RUN npm install git+http://isaacs@github.com/npm/npm"-            it "commit not pinned for git+https" $ do-                ruleCatches-                    npmVersionPinned-                    "RUN npm install git+https://isaacs@github.com/npm/npm.git"-                onBuildRuleCatches-                    npmVersionPinned-                    "RUN npm install git+https://isaacs@github.com/npm/npm.git"-            it "commit not pinned for git" $ do-                ruleCatches npmVersionPinned "RUN npm install git://github.com/npm/npm.git"-                onBuildRuleCatches npmVersionPinned "RUN npm install git://github.com/npm/npm.git"-        ---        describe "use SHELL" $ do-            it "RUN ln" $ do-                ruleCatches useShell "RUN ln -sfv /bin/bash /bin/sh"-                onBuildRuleCatches useShell "RUN ln -sfv /bin/bash /bin/sh"-            it "RUN ln with unrelated symlinks" $ do-                ruleCatchesNot useShell "RUN ln -sf /bin/true /sbin/initctl"-                onBuildRuleCatchesNot useShell "RUN ln -sf /bin/true /sbin/initctl"-            it "RUN ln with multiple acceptable commands" $ do-                ruleCatchesNot useShell "RUN ln -s foo bar && unrelated && something_with /bin/sh"-                onBuildRuleCatchesNot useShell "RUN ln -s foo bar && unrelated && something_with /bin/sh"-        ---        ---        describe "Shellcheck" $ do-            it "runs shellchek on RUN instructions" $ do-                ruleCatches shellcheck "RUN echo $MISSING_QUOTES"-                onBuildRuleCatches shellcheck "RUN echo $MISSING_QUOTES"-            it "not warns on valid scripts" $ do-                ruleCatchesNot shellcheck "RUN echo foo"-                onBuildRuleCatchesNot shellcheck "RUN echo foo"--            it "Does not complain on default env vars" $-                let dockerFile = Text.unlines-                        [ "RUN echo \"$HTTP_PROXY\""-                        , "RUN echo \"$http_proxy\""-                        , "RUN echo \"$HTTPS_PROXY\""-                        , "RUN echo \"$https_proxy\""-                        , "RUN echo \"$FTP_PROXY\""-                        , "RUN echo \"$ftp_proxy\""-                        , "RUN echo \"$NO_PROXY\""-                        , "RUN echo \"$no_proxy\""-                        ]-                in do-                  ruleCatchesNot shellcheck dockerFile-                  onBuildRuleCatchesNot shellcheck dockerFile--            it "Complain on missing env vars" $-                let dockerFile = Text.unlines-                        [ "RUN echo \"$RTTP_PROXY\""-                        ]-                in do-                  ruleCatches shellcheck dockerFile-                  onBuildRuleCatches shellcheck dockerFile--            it "Is aware of ARGS and ENV" $-                let dockerFile = Text.unlines-                        [ "ARG foo=bar"-                        , "ARG another_foo"-                        , "ENV bar=10 baz=20"-                        , "RUN echo \"$foo\""-                        , "RUN echo \"$another_foo\""-                        , "RUN echo \"$bar\""-                        , "RUN echo \"$baz\""-                        ]-                in do-                  ruleCatchesNot shellcheck dockerFile-                  onBuildRuleCatchesNot shellcheck dockerFile--            it "Resets env vars after a FROM" $-                let dockerFile = Text.unlines-                        [ "ARG foo=bar"-                        , "ARG another_foo"-                        , "ENV bar=10 baz=20"-                        , "FROM debian"-                        , "RUN echo \"$foo\""-                        ]-                in do-                  ruleCatches shellcheck dockerFile-                  onBuildRuleCatches shellcheck dockerFile--            it "Defaults the shell to sh" $-                let dockerFile = Text.unlines-                        [ "RUN echo $RANDOM"-                        ]-                in do-                  ruleCatches shellcheck dockerFile-                  onBuildRuleCatches shellcheck dockerFile--            it "Can change the shell check to bash" $-                let dockerFile = Text.unlines-                        [ "SHELL [\"/bin/bash\", \"-eo\", \"pipefail\", \"-c\"]"-                        , "RUN echo $RANDOM"-                        ]-                in do-                  ruleCatchesNot shellcheck dockerFile-                  onBuildRuleCatchesNot shellcheck dockerFile--            it "Resets the SHELL to sh after a FROM" $-                let dockerFile = Text.unlines-                        [ "SHELL [\"/bin/bash\", \"-eo\", \"pipefail\", \"-c\"]"-                        , "FROM debian"-                        , "RUN echo $RANDOM"-                        ]-                in do-                  ruleCatches shellcheck dockerFile-                  onBuildRuleCatches shellcheck dockerFile--            it "Does not complain on ash shell" $-                let dockerFile = Text.unlines-                        [ "SHELL [\"/bin/ash\", \"-o\", \"pipefail\", \"-c\"]"-                        , "RUN echo hello"-                        ]-                in do-                  ruleCatchesNot shellcheck dockerFile-                  onBuildRuleCatchesNot shellcheck dockerFile--            it "Does not complain on powershell" $-                let dockerFile = Text.unlines-                        [ "SHELL [\"pwsh\", \"-c\"]"-                        , "RUN Get-Variable PSVersionTable | Select-Object -ExpandProperty Value"-                        ]-                in do-                  ruleCatchesNot shellcheck dockerFile-                  onBuildRuleCatchesNot shellcheck dockerFile-        ---        ---        describe "COPY rules" $ do-            it "use add" $ ruleCatches useAdd "COPY packaged-app.tar /usr/src/app"-            it "use not add" $ ruleCatchesNot useAdd "COPY package.json /usr/src/app"-        ---        describe "other rules" $ do-            it "apt-get auto yes" $ do-              ruleCatches aptGetYes "RUN apt-get install python"-              onBuildRuleCatches aptGetYes "RUN apt-get install python"-            it "apt-get yes shortflag" $ do-              ruleCatchesNot aptGetYes "RUN apt-get install -yq python"-              onBuildRuleCatchesNot aptGetYes "RUN apt-get install -yq python"-            it "apt-get yes quiet level 2 implies -y" $ do-                ruleCatchesNot aptGetYes "RUN apt-get install -qq python"-                onBuildRuleCatchesNot aptGetYes "RUN apt-get install -qq python"-            it "apt-get yes different pos" $ do-                ruleCatchesNot aptGetYes "RUN apt-get install -y python"-                onBuildRuleCatchesNot aptGetYes "RUN apt-get install -y python"-            it "apt-get with auto yes" $ do-                ruleCatchesNot aptGetYes "RUN apt-get -y install python"-                onBuildRuleCatchesNot aptGetYes "RUN apt-get -y install python"-            it "apt-get with auto expanded yes" $ do-                ruleCatchesNot aptGetYes "RUN apt-get --yes install python"-                onBuildRuleCatchesNot aptGetYes "RUN apt-get --yes install python"-            it "apt-get with assume-yes" $ do-                ruleCatchesNot aptGetYes "RUN apt-get --assume-yes install python"-                onBuildRuleCatchesNot aptGetYes "RUN apt-get --assume-yes install python"-            it "apt-get install recommends" $ do-                ruleCatchesNot-                    aptGetNoRecommends-                    "RUN apt-get install --no-install-recommends python"-                onBuildRuleCatchesNot-                    aptGetNoRecommends-                    "RUN apt-get install --no-install-recommends python"-            it "apt-get no install recommends" $ do-                ruleCatches aptGetNoRecommends "RUN apt-get install python"-                onBuildRuleCatches aptGetNoRecommends "RUN apt-get install python"-            it "apt-get no install recommends" $ do-                ruleCatches aptGetNoRecommends "RUN apt-get -y install python"-                onBuildRuleCatches aptGetNoRecommends "RUN apt-get -y install python"-            it "apt-get no install recommends via option" $ do-                ruleCatchesNot aptGetNoRecommends "RUN apt-get -o APT::Install-Recommends=false install python"-                onBuildRuleCatchesNot aptGetNoRecommends "RUN apt-get -o APT::Install-Recommends=false install python"-            it "apt-get version" $ do-                ruleCatchesNot aptGetVersionPinned "RUN apt-get install -y python=1.2.2"-                onBuildRuleCatchesNot aptGetVersionPinned "RUN apt-get install -y python=1.2.2"-            it "apt-get version" $ do-                ruleCatchesNot aptGetVersionPinned "RUN apt-get install ./wkhtmltox_0.12.5-1.bionic_amd64.deb"-                onBuildRuleCatchesNot aptGetVersionPinned "RUN apt-get install ./wkhtmltox_0.12.5-1.bionic_amd64.deb"-            it "apt-get pinned" $ do-                ruleCatchesNot-                    aptGetVersionPinned-                    "RUN apt-get -y --no-install-recommends install nodejs=0.10"-                onBuildRuleCatchesNot-                    aptGetVersionPinned-                    "RUN apt-get -y --no-install-recommends install nodejs=0.10"-            it "apt-get tolerate target-release" $-                let dockerFile =-                        [ "RUN set -e &&\\"-                        , " echo \"deb http://http.debian.net/debian jessie-backports main\" \-                          \> /etc/apt/sources.list.d/jessie-backports.list &&\\"-                        , " apt-get update &&\\"-                        , " apt-get install -y --no-install-recommends -t jessie-backports \-                          \openjdk-8-jdk=8u131-b11-1~bpo8+1 &&\\"-                        , " rm -rf /var/lib/apt/lists/*"-                        ]-                in do-                  ruleCatchesNot aptGetVersionPinned $ Text.unlines dockerFile-                  onBuildRuleCatchesNot aptGetVersionPinned $ Text.unlines dockerFile--            it "has maintainer" $ ruleCatches hasNoMaintainer "FROM debian\nMAINTAINER Lukas"-            it "has maintainer first" $ ruleCatches hasNoMaintainer "MAINTAINER Lukas\nFROM DEBIAN"-            it "has no maintainer" $ ruleCatchesNot hasNoMaintainer "FROM debian"-            it "using add" $ ruleCatches copyInsteadAdd "ADD file /usr/src/app/"--            it "many cmds" $-                let dockerFile =-                        [ "FROM debian"-                        , "CMD bash"-                        , "RUN foo"-                        , "CMD another"-                        ]-                in ruleCatches multipleCmds $ Text.unlines dockerFile--            it "single cmds, different stages" $-                let dockerFile =-                        [ "FROM debian as distro1"-                        , "CMD bash"-                        , "RUN foo"-                        , "FROM debian as distro2"-                        , "CMD another"-                        ]-                in ruleCatchesNot multipleCmds $ Text.unlines dockerFile--            it "many cmds, different stages" $-                let dockerFile =-                        [ "FROM debian as distro1"-                        , "CMD bash"-                        , "RUN foo"-                        , "CMD another"-                        , "FROM debian as distro2"-                        , "CMD another"-                        ]-                in ruleCatches multipleCmds $ Text.unlines dockerFile--            it "single cmd" $ ruleCatchesNot multipleCmds "CMD /bin/true"-            it "no cmd" $ ruleCatchesNot multipleEntrypoints "FROM busybox"--            it "many entrypoints" $-                let dockerFile =-                        [ "FROM debian"-                        , "ENTRYPOINT bash"-                        , "RUN foo"-                        , "ENTRYPOINT another"-                        ]-                in ruleCatches multipleEntrypoints $ Text.unlines dockerFile--            it "single entrypoint, different stages" $-                let dockerFile =-                        [ "FROM debian as distro1"-                        , "ENTRYPOINT bash"-                        , "RUN foo"-                        , "FROM debian as distro2"-                        , "ENTRYPOINT another"-                        ]-                in ruleCatchesNot multipleEntrypoints $ Text.unlines dockerFile--            it "many entrypoints, different stages" $-                let dockerFile =-                        [ "FROM debian as distro1"-                        , "ENTRYPOINT bash"-                        , "RUN foo"-                        , "ENTRYPOINT another"-                        , "FROM debian as distro2"-                        , "ENTRYPOINT another"-                        ]-                in ruleCatches multipleEntrypoints $ Text.unlines dockerFile-            it "single entry" $ ruleCatchesNot multipleEntrypoints "ENTRYPOINT /bin/true"-            it "no entry" $ ruleCatchesNot multipleEntrypoints "FROM busybox"-            it "workdir variable" $ ruleCatchesNot absoluteWorkdir "WORKDIR ${work}"-            it "scratch" $ ruleCatchesNot noUntagged "FROM scratch"-        ---        describe "add files and archives" $ do-            it "add for tar" $ ruleCatchesNot copyInsteadAdd "ADD file.tar /usr/src/app/"-            it "add for zip" $ ruleCatchesNot copyInsteadAdd "ADD file.zip /usr/src/app/"-            it "add for gzip" $ ruleCatchesNot copyInsteadAdd "ADD file.gz /usr/src/app/"-            it "add for bz2" $ ruleCatchesNot copyInsteadAdd "ADD file.bz2 /usr/src/app/"-            it "add for xz" $ ruleCatchesNot copyInsteadAdd "ADD file.xz /usr/src/app/"-            it "add for tgz" $ ruleCatchesNot copyInsteadAdd "ADD file.tgz /usr/src/app/"-            it "add for url" $ ruleCatchesNot copyInsteadAdd "ADD http://file.com /usr/src/app/"-        ---        describe "copy last argument" $ do-            it "no warn on 2 args" $ ruleCatchesNot copyEndingSlash "COPY foo bar"-            it "warn on 3 args" $ ruleCatches copyEndingSlash "COPY foo bar baz"-            it "no warn on 3 args" $ ruleCatchesNot copyEndingSlash "COPY foo bar baz/"-        ---        describe "copy from existing alias" $ do-            it "warn on missing alias" $ ruleCatches copyFromExists "COPY --from=foo bar ."-            it "warn on alias defined after" $-                let dockerFile =-                        [ "FROM scratch"-                        , "COPY --from=build foo ."-                        , "FROM node as build"-                        , "RUN baz"-                        ]-                in ruleCatches copyFromExists $ Text.unlines dockerFile-            it "don't warn on correctly defined aliases" $-                let dockerFile =-                        [ "FROM scratch as build"-                        , "RUN foo"-                        , "FROM node"-                        , "COPY --from=build foo ."-                        , "RUN baz"-                        ]-                in ruleCatchesNot copyFromExists $ Text.unlines dockerFile-        ---        describe "copy from own FROM" $ do-            it "warn on copying from your the same FROM" $-                let dockerFile =-                        [ "FROM node as foo"-                        , "COPY --from=foo bar ."-                        ]-                in ruleCatches copyFromAnother $ Text.unlines dockerFile-            it "don't warn on copying form other sources" $-                let dockerFile =-                        [ "FROM scratch as build"-                        , "RUN foo"-                        , "FROM node as run"-                        , "COPY --from=build foo ."-                        , "RUN baz"-                        ]-                in ruleCatchesNot copyFromAnother $ Text.unlines dockerFile-        ---        describe "Duplicate aliases" $ do-            it "warn on duplicate aliases" $-                let dockerFile =-                        [ "FROM node as foo"-                        , "RUN something"-                        , "FROM scratch as foo"-                        , "RUN something"-                        ]-                in ruleCatches fromAliasUnique $ Text.unlines dockerFile-            it "don't warn on unique aliases" $-                let dockerFile =-                        [ "FROM scratch as build"-                        , "RUN foo"-                        , "FROM node as run"-                        , "RUN baz"-                        ]-                in ruleCatchesNot fromAliasUnique $ Text.unlines dockerFile-        ---        describe "format error" $-            it "display error after line pos" $ do-                let ast = parseText "FOM debian:jessie"-                    expectedMsg = "<string>:1:1 unexpected 'F' expecting '#', ADD, ARG, CMD, COPY, ENTRYPOINT, " <>-                                  "ENV, EXPOSE, FROM, HEALTHCHECK, LABEL, MAINTAINER, ONBUILD, RUN, SHELL, STOPSIGNAL, " <>-                                  "USER, VOLUME, WORKDIR, or end of input "-                case ast of-                    Left err -> assertEqual "Unexpected error msg" expectedMsg (formatError err)-                    Right _ -> assertFailure "AST should fail parsing"-        ---        describe "Rules can be ignored with inline comments" $ do-            it "ignores single rule" $-                let dockerFile =-                        [ "FROM ubuntu"-                        , "# hadolint ignore=DL3002"-                        , "USER root"-                        ]-                in ruleCatchesNot noRootUser $ Text.unlines dockerFile-            it "ignores only the given rule" $-                let dockerFile =-                        [ "FROM scratch"-                        , "# hadolint ignore=DL3001"-                        , "USER root"-                        ]-                in ruleCatches noRootUser $ Text.unlines dockerFile-            it "ignores only the given rule, when multiple passed" $-                let dockerFile =-                        [ "FROM scratch"-                        , "# hadolint ignore=DL3001,DL3002"-                        , "USER root"-                        ]-                in ruleCatchesNot noRootUser $ Text.unlines dockerFile-            it "ignores the rule only if directly above the instruction" $-                let dockerFile =-                        [ "# hadolint ignore=DL3001,DL3002"-                        , "FROM ubuntu"-                        , "USER root"-                        ]-                in ruleCatches noRootUser $ Text.unlines dockerFile-            it "won't ignore the rule if passed invalid rule names" $-                let dockerFile =-                        [ "FROM scratch"-                        , "# hadolint ignore=crazy,DL3002"-                        , "USER root"-                        ]-                in ruleCatches noRootUser $ Text.unlines dockerFile-            it "ignores multiple rules correctly, even with some extra whitespace" $-                let dockerFile =-                        [ "FROM node as foo"-                        , "# hadolint ignore=DL3023, DL3021"-                        , "COPY --from=foo bar baz ."-                        ]-                in do-                  ruleCatchesNot copyFromAnother $ Text.unlines dockerFile-                  ruleCatchesNot copyEndingSlash $ Text.unlines dockerFile-        ---        describe "JSON notation in ENTRYPOINT and CMD" $ do-            it "warn on ENTRYPOINT" $-                let dockerFile =-                        [ "FROM node as foo"-                        , "ENTRYPOINT something"-                        ]-                in ruleCatches useJsonArgs $ Text.unlines dockerFile-            it "don't warn on ENTRYPOINT json notation" $-                let dockerFile =-                        [ "FROM scratch as build"-                        , "ENTRYPOINT [\"foo\", \"bar\"]"-                        ]-                in ruleCatchesNot useJsonArgs $ Text.unlines dockerFile-            it "warn on CMD" $-                let dockerFile =-                        [ "FROM node as foo"-                        , "CMD something"-                        ]-                in ruleCatches useJsonArgs $ Text.unlines dockerFile-            it "don't warn on CMD json notation" $-                let dockerFile =-                        [ "FROM scratch as build"-                        , "CMD [\"foo\", \"bar\"]"-                        , "CMD [ \"foo\", \"bar\" ]"-                        ]-                in ruleCatchesNot useJsonArgs $ Text.unlines dockerFile--        ---        describe "Detects missing pipefail option" $ do-            it "warn on missing pipefail" $-                let dockerFile =-                        [ "FROM scratch"-                        , "RUN wget -O - https://some.site | wc -l > /number"-                        ]-                in ruleCatches usePipefail $ Text.unlines dockerFile-            it "don't warn on commands with no pipes" $-                let dockerFile =-                        [ "FROM scratch as build"-                        , "RUN wget -O - https://some.site && wc -l file > /number"-                        ]-                in ruleCatchesNot usePipefail $ Text.unlines dockerFile-            it "don't warn on commands with pipes and the pipefail option" $-                let dockerFile =-                        [ "FROM scratch as build"-                        , "SHELL [\"/bin/bash\", \"-eo\", \"pipefail\", \"-c\"]"-                        , "RUN wget -O - https://some.site | wc -l file > /number"-                        ]-                in ruleCatchesNot usePipefail $ Text.unlines dockerFile-            it "don't warn on commands with pipes and the pipefail option 2" $-                let dockerFile =-                        [ "FROM scratch as build"-                        , "SHELL [\"/bin/bash\", \"-e\", \"-o\", \"pipefail\", \"-c\"]"-                        , "RUN wget -O - https://some.site | wc -l file > /number"-                        ]-                in ruleCatchesNot usePipefail $ Text.unlines dockerFile-            it "don't warn on commands with pipes and the pipefail option 3" $-                let dockerFile =-                        [ "FROM scratch as build"-                        , "SHELL [\"/bin/bash\", \"-o\", \"errexit\", \"-o\", \"pipefail\", \"-c\"]"-                        , "RUN wget -O - https://some.site | wc -l file > /number"-                        ]-                in ruleCatchesNot usePipefail $ Text.unlines dockerFile-            it "don't warn on commands with pipes and the pipefail zsh" $-                let dockerFile =-                        [ "FROM scratch as build"-                        , "SHELL [\"/bin/zsh\", \"-o\", \"pipefail\", \"-c\"]"-                        , "RUN wget -O - https://some.site | wc -l file > /number"-                        ]-                in ruleCatchesNot usePipefail $ Text.unlines dockerFile-            it "don't warn on powershell" $-                let dockerFile =-                        [ "FROM scratch as build"-                        , "SHELL [\"pwsh\", \"-c\"]"-                        , "RUN Get-Variable PSVersionTable | Select-Object -ExpandProperty Value"-                        ]-                in ruleCatchesNot usePipefail $ Text.unlines dockerFile-            it "warns when using plain sh" $-                let dockerFile =-                        [ "FROM scratch as build"-                        , "SHELL [\"/bin/sh\", \"-o\", \"pipefail\", \"-c\"]"-                        , "RUN wget -O - https://some.site | wc -l file > /number"-                        ]-                in ruleCatches usePipefail $ Text.unlines dockerFile-            it "warn on missing pipefail in the next image" $-                let dockerFile =-                        [ "FROM scratch as build"-                        , "SHELL [\"/bin/bash\", \"-o\", \"pipefail\", \"-c\"]"-                        , "RUN wget -O - https://some.site | wc -l file > /number"-                        , "FROM scratch as build2"-                        , "RUN wget -O - https://some.site | wc -l file > /number"-                        ]-                in ruleCatches usePipefail $ Text.unlines dockerFile-            it "warn on missing pipefail if next SHELL is not using it" $-                let dockerFile =-                        [ "FROM scratch as build"-                        , "SHELL [\"/bin/bash\", \"-o\", \"pipefail\", \"-c\"]"-                        , "RUN wget -O - https://some.site | wc -l file > /number"-                        , "SHELL [\"/bin/sh\", \"-c\"]"-                        , "RUN wget -O - https://some.site | wc -l file > /number"-                        ]-                in ruleCatches usePipefail $ Text.unlines dockerFile-        ---        describe "Allowed docker registries" $ do-            it "warn on non-allowed registry" $-                let dockerFile =-                        [ "FROM random.com/debian"-                        ]-                in ruleCatches (registryIsAllowed ["docker.io"]) $ Text.unlines dockerFile-            it "don't warn on empty allowed registries" $-                let dockerFile =-                        [ "FROM random.com/debian"-                        ]-                in ruleCatchesNot (registryIsAllowed []) $ Text.unlines dockerFile-            it "don't warn on allowed registries" $-                let dockerFile =-                        [ "FROM random.com/debian"-                        ]-                in ruleCatchesNot (registryIsAllowed ["x.com", "random.com"]) $ Text.unlines dockerFile-            it "doesn't warn on scratch image" $-                let dockerFile =-                        [ "FROM scratch"-                        ]-                in ruleCatchesNot (registryIsAllowed ["x.com", "random.com"]) $ Text.unlines dockerFile-            it "allows boths all forms of docker.io" $-                let dockerFile =-                        [ "FROM ubuntu:18.04 AS builder1"-                        , "FROM zemanlx/ubuntu:18.04 AS builder2"-                        , "FROM docker.io/zemanlx/ubuntu:18.04 AS builder3"-                        ]-                in ruleCatchesNot (registryIsAllowed ["docker.io"]) $ Text.unlines dockerFile--            it "allows using previous stages" $-                let dockerFile =-                        [ "FROM random.com/foo AS builder1"-                        , "FROM builder1 AS builder2"-                        ]-                in ruleCatchesNot (registryIsAllowed ["random.com"]) $ Text.unlines dockerFile-        ---        describe "Wget or Curl" $ do-            it "warns when using both wget and curl" $-                let dockerFile =-                        [ "FROM node as foo"-                        , "RUN wget my.xyz"-                        , "RUN curl localhost"-                        ]-                in ruleCatches wgetOrCurl $ Text.unlines dockerFile-            it "warns when using both wget and curl in same instruction" $-                let dockerFile =-                        [ "FROM node as foo"-                        , "RUN wget my.xyz && curl localhost"-                        ]-                in ruleCatches wgetOrCurl $ Text.unlines dockerFile-            it "does not warn when using only wget" $-                let dockerFile =-                        [ "FROM node as foo"-                        , "RUN wget my.xyz"-                        ]-                in ruleCatchesNot wgetOrCurl $ Text.unlines dockerFile-            it "does not warn when using both curl and wget in different stages" $-                let dockerFile =-                        [ "FROM node as foo"-                        , "RUN wget my.xyz"-                        , "FROM scratch"-                        , "RUN curl localhost"-                        ]-                in ruleCatchesNot wgetOrCurl $ Text.unlines dockerFile-            it "does not warns when using both, on a single stage" $-                let dockerFile =-                        [ "FROM node as foo"-                        , "RUN wget my.xyz"-                        , "RUN curl localhost"-                        , "FROM scratch"-                        , "RUN curl localhost"-                        ]-                in ruleCatches wgetOrCurl $ Text.unlines dockerFile-            it "only warns on the relevant RUN instruction" $-                let dockerFile =-                        [ "FROM node as foo"-                        , "RUN wget my.xyz"-                        , "RUN curl my.xyz"-                        , "RUN echo hello"-                        ]-                in assertChecks wgetOrCurl-                                (Text.unlines dockerFile)-                                (\checks -> assertBool-                                                    "Expecting warnings only in 1 RUN instruction"-                                                    (length checks == 1)-                                )-            it "only warns on many relevant RUN instructions" $-                let dockerFile =-                        [ "FROM node as foo"-                        , "RUN wget my.xyz"-                        , "RUN curl my.xyz"-                        , "RUN echo hello"-                        , "RUN wget foo.com"-                        ]-                in assertChecks wgetOrCurl-                                (Text.unlines dockerFile)-                                (\checks -> assertBool-                                                    "Expecting warnings only in 2 RUN instructions"-                                                    (length checks == 2)-                                )-        ---        describe "Regression Tests" $-            it "Comments with backslashes at the end are just comments" $-                let dockerFile =-                        [ "FROM alpine:3.6"-                        , "# The following comment makes hadolint still complain about DL4006"-                        , "# \\"-                        , "# should solve DL4006"-                        , "SHELL [\"/bin/sh\", \"-o\", \"pipefail\", \"-c\"]"-                        , "# RUN with pipe. causes DL4006, but should be fixed by above SHELL"-                        , "RUN echo \"kaka\" | sed 's/a/o/g' >> /root/afile"-                        ]-                in ruleCatches usePipefail $ Text.unlines dockerFile--        -- Run tests for the Config module-        ConfigSpec.tests--assertChecks :: HasCallStack => Rule -> Text.Text -> ([RuleCheck] -> IO a) -> IO a-assertChecks rule s makeAssertions =-    case parseText (s <> "\n") of-        Left err -> assertFailure $ show err-        Right dockerFile -> makeAssertions $ analyze [rule] dockerFile--assertOnBuildChecks :: HasCallStack => Rule -> Text.Text -> ([RuleCheck] -> IO a) -> IO a-assertOnBuildChecks rule s makeAssertions =-    case parseText (s <> "\n") of-        Left err -> assertFailure $ show err-        Right dockerFile -> checkOnBuild dockerFile-  where-    checkOnBuild dockerFile = makeAssertions $ analyze [rule] (fmap wrapInOnBuild dockerFile)-    wrapInOnBuild (InstructionPos (Run args) so li) = InstructionPos (OnBuild (Run args)) so li-    wrapInOnBuild i = i--selectChecksWithLines :: [RuleCheck] -> [RuleCheck]-selectChecksWithLines checks = [c | c <- checks, linenumber c <= 0]---- Assert a failed check exists for rule-ruleCatches :: HasCallStack => Rule -> Text.Text -> Assertion-ruleCatches rule s = assertChecks rule s f-  where-    f checks = do-      when (null checks) $-        assertFailure "I was expecting to catch at least one error"-      assertBool "Incorrect line number for result" $ null $ selectChecksWithLines checks--onBuildRuleCatches :: HasCallStack => Rule -> Text.Text -> Assertion-onBuildRuleCatches rule s = assertOnBuildChecks rule s f-  where-    f checks = do-      when (length checks /= 1) $-        assertFailure (Text.unpack . Text.unlines . formatChecks $ checks)-      assertBool "Incorrect line number for result" $ null $ selectChecksWithLines checks--ruleCatchesNot :: HasCallStack => Rule -> Text.Text -> Assertion-ruleCatchesNot rule s = assertChecks rule s f-  where-    f checks =-      unless (null checks) $-        assertFailure $ "Not expecting the following errors: \n" ++-                        (Text.unpack . Text.unlines . formatChecks $ checks)--onBuildRuleCatchesNot :: HasCallStack => Rule -> Text.Text -> Assertion-onBuildRuleCatchesNot rule s = assertOnBuildChecks rule s f-  where-    f checks =-      unless (null checks) $-        assertFailure $ "Not expecting the following errors: \n" ++-                        (Text.unpack . Text.unlines . formatChecks $ checks)+{-# LANGUAGE OverloadedLists #-}+{-# LANGUAGE OverloadedStrings #-}++import qualified ConfigSpec+import Control.Monad (unless, when)+import Data.Semigroup ((<>))+import qualified Data.Text as Text+import Hadolint.Formatter.TTY (formatChecks, formatError)+import Hadolint.Rules+import Language.Docker.Parser+import Language.Docker.Syntax+import Test.HUnit hiding (Label)+import Test.Hspec++main :: IO ()+main =+  hspec $ do+    describe "FROM rules" $ do+      it "no untagged" $ ruleCatches noUntagged "FROM debian"+      it "no untagged with name" $ ruleCatches noUntagged "FROM debian AS builder"+      it "explicit latest" $ ruleCatches noLatestTag "FROM debian:latest"+      it "explicit latest with name" $ ruleCatches noLatestTag "FROM debian:latest AS builder"+      it "explicit tagged" $ ruleCatchesNot noLatestTag "FROM debian:jessie"+      it "explicit platform flag" $ ruleCatches noPlatformFlag "FROM --platform=linux debian:jessie"+      it "no platform flag" $ ruleCatchesNot noPlatformFlag "FROM debian:jessie"+      it "explicit SHA" $+        ruleCatchesNot+          noLatestTag+          "FROM hub.docker.io/debian@sha256:\+          \7959ed6f7e35f8b1aaa06d1d8259d4ee25aa85a086d5c125480c333183f9deeb"+      it "explicit tagged with name" $+        ruleCatchesNot noLatestTag "FROM debian:jessie AS builder"+      it "untagged digest is not an error" $+        ruleCatchesNot noUntagged "FROM ruby@sha256:f1dbca0f5dbc9"+      it "untagged digest is not an error" $+        ruleCatchesNot noUntagged "FROM ruby:2"+      it "local aliases are OK to be untagged" $+        let dockerFile =+              [ "FROM golang:1.9.3-alpine3.7 AS build",+                "RUN foo",+                "FROM build as unit-test",+                "RUN bar",+                "FROM alpine:3.7",+                "RUN baz"+              ]+         in do+              ruleCatchesNot noUntagged $ Text.unlines dockerFile+              onBuildRuleCatchesNot noUntagged $ Text.unlines dockerFile+      it "other untagged cases are not ok" $+        let dockerFile =+              [ "FROM golang:1.9.3-alpine3.7 AS build",+                "RUN foo",+                "FROM node as unit-test",+                "RUN bar",+                "FROM alpine:3.7",+                "RUN baz"+              ]+         in do+              ruleCatches noUntagged $ Text.unlines dockerFile+              onBuildRuleCatches noUntagged $ Text.unlines dockerFile+    --+    describe "no root or sudo rules" $ do+      it "sudo" $ do+        ruleCatches noSudo "RUN sudo apt-get update"+        onBuildRuleCatches noSudo "RUN sudo apt-get update"++      it "last user should not be root" $+        let dockerFile =+              [ "FROM scratch",+                "USER root"+              ]+         in ruleCatches noRootUser $ Text.unlines dockerFile++      it "no root" $+        let dockerFile =+              [ "FROM scratch",+                "USER foo"+              ]+         in ruleCatchesNot noRootUser $ Text.unlines dockerFile++      it "no root UID" $+        let dockerFile =+              [ "FROM scratch",+                "USER 0"+              ]+         in ruleCatches noRootUser $ Text.unlines dockerFile++      it "no root:root" $+        let dockerFile =+              [ "FROM scratch",+                "USER root:root"+              ]+         in ruleCatches noRootUser $ Text.unlines dockerFile++      it "no UID:GID" $+        let dockerFile =+              [ "FROM scratch",+                "USER 0:0"+              ]+         in ruleCatches noRootUser $ Text.unlines dockerFile++      it "can switch back to non root" $+        let dockerFile =+              [ "FROM scratch",+                "USER root",+                "RUN something",+                "USER foo"+              ]+         in ruleCatchesNot noRootUser $ Text.unlines dockerFile++      it "warns on transitive root user" $+        let dockerFile =+              [ "FROM debian as base",+                "USER root",+                "RUN something",+                "FROM base",+                "RUN something else"+              ]+         in ruleCatches noRootUser $ Text.unlines dockerFile++      it "warns on multiple stages" $+        let dockerFile =+              [ "FROM debian as base",+                "USER root",+                "RUN something",+                "FROM scratch",+                "USER foo",+                "RUN something else"+              ]+         in ruleCatches noRootUser $ Text.unlines dockerFile++      it "does not warn when switching in multiple stages" $+        let dockerFile =+              [ "FROM debian as base",+                "USER root",+                "RUN something",+                "USER foo",+                "FROM scratch",+                "RUN something else"+              ]+         in ruleCatchesNot noRootUser $ Text.unlines dockerFile++      it "install sudo" $ do+        ruleCatchesNot noSudo "RUN apt-get install sudo"+        onBuildRuleCatchesNot noSudo "RUN apt-get install sudo"+      it "sudo chained programs" $ do+        ruleCatches noSudo "RUN apt-get update && sudo apt-get install"+        onBuildRuleCatches noSudo "RUN apt-get update && sudo apt-get install"+    --+    describe "invalid CMD rules" $ do+      it "invalid cmd" $ do+        ruleCatches invalidCmd "RUN top"+        onBuildRuleCatches invalidCmd "RUN top"+      it "install ssh" $ do+        ruleCatchesNot invalidCmd "RUN apt-get install ssh"+        onBuildRuleCatchesNot invalidCmd "RUN apt-get install ssh"+    --+    describe "gem" $+      describe "version pinning" $ do+        describe "i" $ do+          it "unpinned" $ do+            ruleCatches gemVersionPinned "RUN gem i bundler"+            onBuildRuleCatches gemVersionPinned "RUN gem i bundler"+          it "pinned" $ do+            ruleCatchesNot gemVersionPinned "RUN gem i bundler:1"+            onBuildRuleCatchesNot gemVersionPinned "RUN gem i bundler:1"+          it "multi" $ do+            ruleCatches gemVersionPinned "RUN gem i bunlder:1 nokogiri"+            onBuildRuleCatches gemVersionPinned "RUN gem i bunlder:1 nokogiri"+            ruleCatchesNot gemVersionPinned "RUN gem i bunlder:1 nokogirii:1"+            onBuildRuleCatchesNot gemVersionPinned "RUN gem i bunlder:1 nokogiri:1"+        describe "install" $ do+          it "unpinned" $ do+            ruleCatches gemVersionPinned "RUN gem install bundler"+            onBuildRuleCatches gemVersionPinned "RUN gem install bundler"+          it "pinned" $ do+            ruleCatchesNot gemVersionPinned "RUN gem install bundler:1"+            onBuildRuleCatchesNot gemVersionPinned "RUN gem install bundler:1"+          it "does not warn on -v" $ do+            ruleCatchesNot gemVersionPinned "RUN gem install bundler -v '2.0.1'"+            onBuildRuleCatchesNot gemVersionPinned "RUN gem install bundler -v '2.0.1'"+          it "does not warn on --version without =" $ do+            ruleCatchesNot gemVersionPinned "RUN gem install bundler --version '2.0.1'"+            onBuildRuleCatchesNot gemVersionPinned "RUN gem install bundler --version '2.0.1'"+          it "does not warn on --version with =" $ do+            ruleCatchesNot gemVersionPinned "RUN gem install bundler --version='2.0.1'"+            onBuildRuleCatchesNot gemVersionPinned "RUN gem install bundler --version='2.0.1'"+          it "does not warn on extra flags" $ do+            ruleCatchesNot gemVersionPinned "RUN gem install bundler:2.0.1 -- --use-system-libraries=true"+            onBuildRuleCatchesNot gemVersionPinned "RUN gem install bundler:2.0.1 -- --use-system-libraries=true"+    --+    describe "yum rules" $ do+      it "yum update" $ do+        ruleCatches noYumUpdate "RUN yum update"+        onBuildRuleCatches noYumUpdate "RUN yum update"+      it "yum version pinning" $ do+        ruleCatches yumVersionPinned "RUN yum install -y tomcat && yum clean all"+        onBuildRuleCatches yumVersionPinned "RUN yum install -y tomcat && yum clean all"+      it "yum no clean all" $ do+        ruleCatches yumCleanup "RUN yum install -y mariadb-10.4"+        onBuildRuleCatches yumCleanup "RUN yum install -y mariadb-10.4"+      it "yum non-interactive" $ do+        ruleCatches yumYes "RUN yum install httpd-2.4.24 && yum clean all"+        onBuildRuleCatches yumYes "RUN yum install httpd-2.4.24 && yum clean all"+    --+    describe "apt-get rules" $ do+      it "apt" $+        let dockerFile =+              [ "FROM ubuntu",+                "RUN apt install python"+              ]+         in do+              ruleCatches noApt $ Text.unlines dockerFile+              onBuildRuleCatches noApt $ Text.unlines dockerFile+      it "apt-get upgrade" $ do+        ruleCatches noAptGetUpgrade "RUN apt-get update && apt-get upgrade"+        onBuildRuleCatches noAptGetUpgrade "RUN apt-get update && apt-get upgrade"+      it "apt-get version pinning" $ do+        ruleCatches aptGetVersionPinned "RUN apt-get update && apt-get install python"+        onBuildRuleCatches aptGetVersionPinned "RUN apt-get update && apt-get install python"+      it "apt-get no cleanup" $+        let dockerFile =+              [ "FROM scratch",+                "RUN apt-get update && apt-get install python"+              ]+         in do+              ruleCatches aptGetCleanup $ Text.unlines dockerFile+              onBuildRuleCatches aptGetCleanup $ Text.unlines dockerFile+      it "apt-get cleanup in stage image" $+        let dockerFile =+              [ "FROM ubuntu as foo",+                "RUN apt-get update && apt-get install python",+                "FROM scratch",+                "RUN echo hey!"+              ]+         in do+              ruleCatchesNot aptGetCleanup $ Text.unlines dockerFile+              onBuildRuleCatchesNot aptGetCleanup $ Text.unlines dockerFile+      it "apt-get no cleanup in last stage" $+        let dockerFile =+              [ "FROM ubuntu as foo",+                "RUN hey!",+                "FROM scratch",+                "RUN apt-get update && apt-get install python"+              ]+         in do+              ruleCatches aptGetCleanup $ Text.unlines dockerFile+              onBuildRuleCatches aptGetCleanup $ Text.unlines dockerFile+      it "apt-get no cleanup in intermediate stage" $+        let dockerFile =+              [ "FROM ubuntu as foo",+                "RUN apt-get update && apt-get install python",+                "FROM foo",+                "RUN hey!"+              ]+         in do+              ruleCatches aptGetCleanup $ Text.unlines dockerFile+              onBuildRuleCatches aptGetCleanup $ Text.unlines dockerFile+      it "no warn apt-get cleanup in intermediate stage that cleans lists" $+        let dockerFile =+              [ "FROM ubuntu as foo",+                "RUN apt-get update && apt-get install python && rm -rf /var/lib/apt/lists/*",+                "FROM foo",+                "RUN hey!"+              ]+         in do+              ruleCatchesNot aptGetCleanup $ Text.unlines dockerFile+              onBuildRuleCatchesNot aptGetCleanup $ Text.unlines dockerFile+      it "no warn apt-get cleanup in intermediate stage when stage not used later" $+        let dockerFile =+              [ "FROM ubuntu as foo",+                "RUN apt-get update && apt-get install python",+                "FROM scratch",+                "RUN hey!"+              ]+         in do+              ruleCatchesNot aptGetCleanup $ Text.unlines dockerFile+              onBuildRuleCatchesNot aptGetCleanup $ Text.unlines dockerFile+      it "apt-get cleanup" $+        let dockerFile =+              [ "FROM scratch",+                "RUN apt-get update && apt-get install python && rm -rf /var/lib/apt/lists/*"+              ]+         in do+              ruleCatchesNot aptGetCleanup $ Text.unlines dockerFile+              onBuildRuleCatchesNot aptGetCleanup $ Text.unlines dockerFile++      it "apt-get pinned chained" $+        let dockerFile =+              [ "RUN apt-get update \\",+                " && apt-get -yqq --no-install-recommends install nodejs=0.10 \\",+                " && rm -rf /var/lib/apt/lists/*"+              ]+         in do+              ruleCatchesNot aptGetVersionPinned $ Text.unlines dockerFile+              onBuildRuleCatchesNot aptGetVersionPinned $ Text.unlines dockerFile++      it "apt-get pinned regression" $+        let dockerFile =+              [ "RUN apt-get update && apt-get install --no-install-recommends -y \\",+                "python-demjson=2.2.2* \\",+                "wget=1.16.1* \\",+                "git=1:2.5.0* \\",+                "ruby=1:2.1.*"+              ]+         in do+              ruleCatchesNot aptGetVersionPinned $ Text.unlines dockerFile+              onBuildRuleCatchesNot aptGetVersionPinned $ Text.unlines dockerFile++      it "has deprecated maintainer" $+        ruleCatches hasNoMaintainer "FROM busybox\nMAINTAINER hudu@mail.com"+    --+    describe "apk add rules" $ do+      it "apk upgrade" $ do+        ruleCatches noApkUpgrade "RUN apk update && apk upgrade"+        onBuildRuleCatches noApkUpgrade "RUN apk update && apk upgrade"+      it "apk add version pinning single" $ do+        ruleCatches apkAddVersionPinned "RUN apk add flex"+        onBuildRuleCatches apkAddVersionPinned "RUN apk add flex"+      it "apk add no version pinning single" $ do+        ruleCatchesNot apkAddVersionPinned "RUN apk add flex=2.6.4-r1"+        onBuildRuleCatchesNot apkAddVersionPinned "RUN apk add flex=2.6.4-r1"+      it "apk add version pinned chained" $+        let dockerFile =+              [ "RUN apk add --no-cache flex=2.6.4-r1 \\",+                " && pip install -r requirements.txt"+              ]+         in do+              ruleCatchesNot apkAddVersionPinned $ Text.unlines dockerFile+              onBuildRuleCatchesNot apkAddVersionPinned $ Text.unlines dockerFile+      it "apk add version pinned regression" $+        let dockerFile =+              [ "RUN apk add --no-cache \\",+                "flex=2.6.4-r1 \\",+                "libffi=3.2.1-r3 \\",+                "python2=2.7.13-r1 \\",+                "libbz2=1.0.6-r5"+              ]+         in do+              ruleCatchesNot apkAddVersionPinned $ Text.unlines dockerFile+              onBuildRuleCatchesNot apkAddVersionPinned $ Text.unlines dockerFile+      it "apk add version pinned regression - one missed" $+        let dockerFile =+              [ "RUN apk add --no-cache \\",+                "flex=2.6.4-r1 \\",+                "libffi \\",+                "python2=2.7.13-r1 \\",+                "libbz2=1.0.6-r5"+              ]+         in do+              ruleCatches apkAddVersionPinned $ Text.unlines dockerFile+              onBuildRuleCatches apkAddVersionPinned $ Text.unlines dockerFile+      it "apk add with --no-cache" $ do+        ruleCatches apkAddNoCache "RUN apk add flex=2.6.4-r1"+        onBuildRuleCatches apkAddNoCache "RUN apk add flex=2.6.4-r1"+      it "apk add without --no-cache" $ do+        ruleCatchesNot apkAddNoCache "RUN apk add --no-cache flex=2.6.4-r1"+        onBuildRuleCatchesNot apkAddNoCache "RUN apk add --no-cache flex=2.6.4-r1"+      it "apk add virtual package" $+        let dockerFile =+              [ "RUN apk add \\",+                "--virtual build-dependencies \\",+                "python-dev=1.1.1 build-base=2.2.2 wget=3.3.3 \\",+                "&& pip install -r requirements.txt \\",+                "&& python setup.py install \\",+                "&& apk del build-dependencies"+              ]+         in do+              ruleCatchesNot apkAddVersionPinned $ Text.unlines dockerFile+              onBuildRuleCatchesNot apkAddVersionPinned $ Text.unlines dockerFile+      it "apk add with repository without equal sign" $+        let dockerFile =+              [ "RUN apk add --no-cache \\",+                "--repository https://nl.alpinelinux.org/alpine/edge/testing \\",+                "flow=0.78.0-r0"+              ]+         in do+              ruleCatchesNot apkAddVersionPinned $ Text.unlines dockerFile+              onBuildRuleCatchesNot apkAddVersionPinned $ Text.unlines dockerFile+      it "apk add with repository with equal sign" $+        let dockerFile =+              [ "RUN apk add --no-cache \\",+                "--repository=https://nl.alpinelinux.org/alpine/edge/testing \\",+                "flow=0.78.0-r0"+              ]+         in do+              ruleCatchesNot apkAddVersionPinned $ Text.unlines dockerFile+              onBuildRuleCatchesNot apkAddVersionPinned $ Text.unlines dockerFile+      it "apk add with repository (-X) without equal sign" $+        let dockerFile =+              [ "RUN apk add --no-cache \\",+                "-X https://nl.alpinelinux.org/alpine/edge/testing \\",+                "flow=0.78.0-r0"+              ]+         in do+              ruleCatchesNot apkAddVersionPinned $ Text.unlines dockerFile+              onBuildRuleCatchesNot apkAddVersionPinned $ Text.unlines dockerFile+    --+    describe "EXPOSE rules" $ do+      it "invalid port" $ ruleCatches invalidPort "EXPOSE 80000"+      it "valid port" $ ruleCatchesNot invalidPort "EXPOSE 60000"+    --+    describe "pip pinning" $ do+      it "pip2 version not pinned" $ do+        ruleCatches pipVersionPinned "RUN pip2 install MySQL_python"+        onBuildRuleCatches pipVersionPinned "RUN pip2 install MySQL_python"+      it "pip3 version not pinned" $ do+        ruleCatches pipVersionPinned "RUN pip3 install MySQL_python"+        onBuildRuleCatches pipVersionPinned "RUN pip2 install MySQL_python"+      it "pip3 version pinned" $ do+        ruleCatchesNot pipVersionPinned "RUN pip3 install MySQL_python==1.2.2"+        onBuildRuleCatchesNot pipVersionPinned "RUN pip3 install MySQL_python==1.2.2"+      it "pip install requirements" $ do+        ruleCatchesNot pipVersionPinned "RUN pip install -r requirements.txt"+        onBuildRuleCatchesNot pipVersionPinned "RUN pip install -r requirements.txt"+      it "pip install requirements with long flag" $ do+        ruleCatchesNot pipVersionPinned "RUN pip install --requirement requirements.txt"+        onBuildRuleCatchesNot pipVersionPinned "RUN pip install --requirement requirements.txt"+      it "pip install use setup.py" $ do+        ruleCatchesNot pipVersionPinned "RUN pip install ."+        onBuildRuleCatchesNot pipVersionPinned "RUN pip install ."+      it "pip version not pinned" $ do+        ruleCatches pipVersionPinned "RUN pip install MySQL_python"+        onBuildRuleCatches pipVersionPinned "RUN pip install MySQL_python"+      it "pip version pinned" $ do+        ruleCatchesNot pipVersionPinned "RUN pip install MySQL_python==1.2.2"+        onBuildRuleCatchesNot pipVersionPinned "RUN pip install MySQL_python==1.2.2"+      it "pip version pinned with ~= operator" $ do+        ruleCatchesNot pipVersionPinned "RUN pip install MySQL_python~=1.2.2"+        onBuildRuleCatchesNot pipVersionPinned "RUN pip install MySQL_python~=1.2.2"+      it "pip version pinned with === operator" $ do+        ruleCatchesNot pipVersionPinned "RUN pip install MySQL_python===1.2.2"+        onBuildRuleCatchesNot pipVersionPinned "RUN pip install MySQL_python===1.2.2"+      it "pip version pinned with flag --ignore-installed" $ do+        ruleCatchesNot pipVersionPinned "RUN pip install --ignore-installed MySQL_python==1.2.2"+        onBuildRuleCatchesNot pipVersionPinned "RUN pip install --ignore-installed MySQL_python==1.2.2"+      it "pip version pinned with flag --build" $ do+        ruleCatchesNot pipVersionPinned "RUN pip3 install --build /opt/yamllint yamllint==1.20.0"+        onBuildRuleCatchesNot pipVersionPinned "RUN pip3 install --build /opt/yamllint yamllint==1.20.0"+      it "pip version pinned with flag --prefix" $ do+        ruleCatchesNot pipVersionPinned "RUN pip3 install --prefix /opt/yamllint yamllint==1.20.0"+        onBuildRuleCatchesNot pipVersionPinned "RUN pip3 install --prefix /opt/yamllint yamllint==1.20.0"+      it "pip version pinned with flag --root" $ do+        ruleCatchesNot pipVersionPinned "RUN pip3 install --root /opt/yamllint yamllint==1.20.0"+        onBuildRuleCatchesNot pipVersionPinned "RUN pip3 install --root /opt/yamllint yamllint==1.20.0"+      it "pip version pinned with flag --target" $ do+        ruleCatchesNot pipVersionPinned "RUN pip3 install --target /opt/yamllint yamllint==1.20.0"+        onBuildRuleCatchesNot pipVersionPinned "RUN pip3 install --target /opt/yamllint yamllint==1.20.0"+      it "pip version pinned with flag --trusted-host" $ do+        ruleCatchesNot pipVersionPinned "RUN pip3 install --trusted-host host example==1.2.2"+        onBuildRuleCatchesNot pipVersionPinned "RUN pip3 install --trusted-host host example==1.2.2"+      it "pip version pinned with python -m" $ do+        ruleCatchesNot pipVersionPinned "RUN python -m pip install example==1.2.2"+        onBuildRuleCatchesNot pipVersionPinned "RUN python -m pip install example==1.2.2"+      it "pip version not pinned with python -m" $ do+        ruleCatches pipVersionPinned "RUN python -m pip install example"+        onBuildRuleCatches pipVersionPinned "RUN python -m pip install --index-url url example"+      it "pip install git" $ do+        ruleCatchesNot+          pipVersionPinned+          "RUN pip install git+https://github.com/rtfd/r-ext.git@0.6-alpha#egg=r-ext"+        onBuildRuleCatchesNot+          pipVersionPinned+          "RUN pip install git+https://github.com/rtfd/r-ext.git@0.6-alpha#egg=r-ext"+      it "pip install unversioned git" $ do+        ruleCatches+          pipVersionPinned+          "RUN pip install git+https://github.com/rtfd/read-ext.git#egg=read-ext"+        onBuildRuleCatches+          pipVersionPinned+          "RUN pip install git+https://github.com/rtfd/read-ext.git#egg=read-ext"+      it "pip install upper bound" $ do+        ruleCatchesNot pipVersionPinned "RUN pip install 'alabaster>=0.7'"+        onBuildRuleCatchesNot pipVersionPinned "RUN pip install 'alabaster>=0.7'"+      it "pip install lower bound" $ do+        ruleCatchesNot pipVersionPinned "RUN pip install 'alabaster<0.7'"+        onBuildRuleCatchesNot pipVersionPinned "RUN pip install 'alabaster<0.7'"+      it "pip install excluded version" $ do+        ruleCatchesNot pipVersionPinned "RUN pip install 'alabaster!=0.7'"+        onBuildRuleCatchesNot pipVersionPinned "RUN pip install 'alabaster!=0.7'"+      it "pip install user directory" $ do+        ruleCatchesNot pipVersionPinned "RUN pip install MySQL_python==1.2.2 --user"+        onBuildRuleCatchesNot pipVersionPinned "RUN pip install MySQL_python==1.2.2 --user"+      it "pip install no pip version check" $ do+        ruleCatchesNot+          pipVersionPinned+          "RUN pip install MySQL_python==1.2.2 --disable-pip-version-check"+        onBuildRuleCatchesNot+          pipVersionPinned+          "RUN pip install MySQL_python==1.2.2 --disable-pip-version-check"+      it "pip install --index-url" $ do+        ruleCatchesNot+          pipVersionPinned+          "RUN pip install --index-url https://eg.com/foo foobar==1.0.0"+        onBuildRuleCatchesNot+          pipVersionPinned+          "RUN pip install --index-url https://eg.com/foo foobar==1.0.0"+      it "pip install index-url with -i flag" $ do+        ruleCatchesNot+          pipVersionPinned+          "RUN pip install --index-url https://eg.com/foo foobar==1.0.0"+        onBuildRuleCatchesNot+          pipVersionPinned+          "RUN pip install --index-url https://eg.com/foo foobar==1.0.0"+      it "pip install --index-url with --extra-index-url" $ do+        ruleCatchesNot+          pipVersionPinned+          "RUN pip install --index-url https://eg.com/foo --extra-index-url https://ex-eg.io/foo foobar==1.0.0"+        onBuildRuleCatchesNot+          pipVersionPinned+          "RUN pip install --index-url https://eg.com/foo --extra-index-url https://ex-eg.io/foo foobar==1.0.0"+      it "pip install no cache dir" $ do+        ruleCatchesNot pipVersionPinned "RUN pip install MySQL_python==1.2.2 --no-cache-dir"+        onBuildRuleCatchesNot pipVersionPinned "RUN pip install MySQL_python==1.2.2 --no-cache-dir"+      it "pip install constraints file - long version argument" $ do+        ruleCatchesNot pipVersionPinned "RUN pip install pykafka --constraint http://foo.bar.baz"+        onBuildRuleCatchesNot pipVersionPinned "RUN pip install pykafka --constraint http://foo.bar.baz"+      it "pip install constraints file - short version argument" $ do+        ruleCatchesNot pipVersionPinned "RUN pip install pykafka -c http://foo.bar.baz"+        onBuildRuleCatchesNot pipVersionPinned "RUN pip install pykafka -c http://foo.bar.baz"+      it "pip install --index-url with --extra-index-url with basic auth" $ do+        ruleCatchesNot+          pipVersionPinned+          "RUN pip install --index-url https://user:pass@eg.com/foo --extra-index-url https://user:pass@ex-eg.io/foo foobar==1.0.0"+        onBuildRuleCatchesNot+          pipVersionPinned+          "RUN pip install --index-url https://user:pass@eg.com/foo --extra-index-url https://user:pass@ex-eg.io/foo foobar==1.0.0"+    --+    describe "npm pinning" $ do+      it "version pinned in package.json" $ do+        ruleCatchesNot npmVersionPinned "RUN npm install"+        onBuildRuleCatchesNot npmVersionPinned "RUN npm install"+      it "version pinned in package.json with arguments" $ do+        ruleCatchesNot npmVersionPinned "RUN npm install --progress=false"+        onBuildRuleCatchesNot npmVersionPinned "RUN npm install --progress=false"+      it "version pinned" $ do+        ruleCatchesNot npmVersionPinned "RUN npm install express@4.1.1"+        onBuildRuleCatchesNot npmVersionPinned "RUN npm install express@4.1.1"+      it "version pinned with scope" $ do+        ruleCatchesNot npmVersionPinned "RUN npm install @myorg/privatepackage@\">=0.1.0\""+        onBuildRuleCatchesNot npmVersionPinned "RUN npm install @myorg/privatepackage@\">=0.1.0\""+      it "version pinned multiple packages" $ do+        ruleCatchesNot npmVersionPinned "RUN npm install express@\"4.1.1\" sax@0.1.1"+        onBuildRuleCatchesNot npmVersionPinned "RUN npm install express@\"4.1.1\" sax@0.1.1"+      it "version pinned with --global" $ do+        ruleCatchesNot npmVersionPinned "RUN npm install --global express@\"4.1.1\""+        onBuildRuleCatchesNot npmVersionPinned "RUN npm install --global express@\"4.1.1\""+      it "version pinned with -g" $ do+        ruleCatchesNot npmVersionPinned "RUN npm install -g express@\"4.1.1\""+        onBuildRuleCatchesNot npmVersionPinned "RUN npm install -g express@\"4.1.1\""+      it "version does not have to be pinned for tarball suffix .tar" $ do+        ruleCatchesNot npmVersionPinned "RUN npm install package-v1.2.3.tar"+        onBuildRuleCatchesNot npmVersionPinned "RUN npm install package-v1.2.3.tar"+      it "version does not have to be pinned for tarball suffix .tar.gz" $ do+        ruleCatchesNot npmVersionPinned "RUN npm install package-v1.2.3.tar.gz"+        onBuildRuleCatchesNot npmVersionPinned "RUN npm install package-v1.2.3.tar.gz"+      it "version does not have to be pinned for tarball suffix .tgz" $ do+        ruleCatchesNot npmVersionPinned "RUN npm install package-v1.2.3.tgz"+        onBuildRuleCatchesNot npmVersionPinned "RUN npm install package-v1.2.3.tgz"+      it "version does not have to be pinned for folder - absolute path" $ do+        ruleCatchesNot npmVersionPinned "RUN npm install /folder"+        onBuildRuleCatchesNot npmVersionPinned "RUN npm install /folder"+      it "version does not have to be pinned for folder - relative path from current folder" $ do+        ruleCatchesNot npmVersionPinned "RUN npm install ./folder"+        onBuildRuleCatchesNot npmVersionPinned "RUN npm install ./folder"+      it "version does not have to be pinned for folder - relative path to parent folder" $ do+        ruleCatchesNot npmVersionPinned "RUN npm install ../folder"+        onBuildRuleCatchesNot npmVersionPinned "RUN npm install ../folder"+      it "version does not have to be pinned for folder - relative path from home" $ do+        ruleCatchesNot npmVersionPinned "RUN npm install ~/folder"+        onBuildRuleCatchesNot npmVersionPinned "RUN npm install ~/folder"+      it "commit pinned for git+ssh" $ do+        ruleCatchesNot+          npmVersionPinned+          "RUN npm install git+ssh://git@github.com:npm/npm.git#v1.0.27"+        onBuildRuleCatchesNot+          npmVersionPinned+          "RUN npm install git+ssh://git@github.com:npm/npm.git#v1.0.27"+      it "commit pinned for git+http" $ do+        ruleCatchesNot+          npmVersionPinned+          "RUN npm install git+http://isaacs@github.com/npm/npm#semver:^5.0"+        onBuildRuleCatchesNot+          npmVersionPinned+          "RUN npm install git+http://isaacs@github.com/npm/npm#semver:^5.0"+      it "commit pinned for git+https" $ do+        ruleCatchesNot+          npmVersionPinned+          "RUN npm install git+https://isaacs@github.com/npm/npm.git#v1.0.27"+        onBuildRuleCatchesNot+          npmVersionPinned+          "RUN npm install git+https://isaacs@github.com/npm/npm.git#v1.0.27"+      it "commit pinned for git" $ do+        ruleCatchesNot+          npmVersionPinned+          "RUN npm install git://github.com/npm/npm.git#v1.0.27"+        onBuildRuleCatchesNot+          npmVersionPinned+          "RUN npm install git://github.com/npm/npm.git#v1.0.27"+      it "npm run install is fine" $ do+        ruleCatchesNot+          npmVersionPinned+          "RUN npm run --crazy install"+        onBuildRuleCatchesNot+          npmVersionPinned+          "RUN npm run --crazy install"++      --version range is not supported+      it "version pinned with scope" $ do+        ruleCatchesNot npmVersionPinned "RUN npm install @myorg/privatepackage@\">=0.1.0 <0.2.0\""+        onBuildRuleCatchesNot npmVersionPinned "RUN npm install @myorg/privatepackage@\">=0.1.0 <0.2.0\""+      it "version not pinned" $ do+        ruleCatches npmVersionPinned "RUN npm install express"+        onBuildRuleCatches npmVersionPinned "RUN npm install express"+      it "version not pinned with scope" $ do+        ruleCatches npmVersionPinned "RUN npm install @myorg/privatepackage"+        onBuildRuleCatches npmVersionPinned "RUN npm install @myorg/privatepackage"+      it "version not pinned multiple packages" $ do+        ruleCatches npmVersionPinned "RUN npm install express sax@0.1.1"+        onBuildRuleCatches npmVersionPinned "RUN npm install express sax@0.1.1"+      it "version not pinned with --global" $ do+        ruleCatches npmVersionPinned "RUN npm install --global express"+        onBuildRuleCatches npmVersionPinned "RUN npm install --global express"+      it "commit not pinned for git+ssh" $ do+        ruleCatches npmVersionPinned "RUN npm install git+ssh://git@github.com:npm/npm.git"+        onBuildRuleCatches npmVersionPinned "RUN npm install git+ssh://git@github.com:npm/npm.git"+      it "commit not pinned for git+http" $ do+        ruleCatches npmVersionPinned "RUN npm install git+http://isaacs@github.com/npm/npm"+        onBuildRuleCatches npmVersionPinned "RUN npm install git+http://isaacs@github.com/npm/npm"+      it "commit not pinned for git+https" $ do+        ruleCatches+          npmVersionPinned+          "RUN npm install git+https://isaacs@github.com/npm/npm.git"+        onBuildRuleCatches+          npmVersionPinned+          "RUN npm install git+https://isaacs@github.com/npm/npm.git"+      it "commit not pinned for git" $ do+        ruleCatches npmVersionPinned "RUN npm install git://github.com/npm/npm.git"+        onBuildRuleCatches npmVersionPinned "RUN npm install git://github.com/npm/npm.git"+    --+    describe "use SHELL" $ do+      it "RUN ln" $ do+        ruleCatches useShell "RUN ln -sfv /bin/bash /bin/sh"+        onBuildRuleCatches useShell "RUN ln -sfv /bin/bash /bin/sh"+      it "RUN ln with unrelated symlinks" $ do+        ruleCatchesNot useShell "RUN ln -sf /bin/true /sbin/initctl"+        onBuildRuleCatchesNot useShell "RUN ln -sf /bin/true /sbin/initctl"+      it "RUN ln with multiple acceptable commands" $ do+        ruleCatchesNot useShell "RUN ln -s foo bar && unrelated && something_with /bin/sh"+        onBuildRuleCatchesNot useShell "RUN ln -s foo bar && unrelated && something_with /bin/sh"+    --+    --+    describe "Shellcheck" $ do+      it "runs shellchek on RUN instructions" $ do+        ruleCatches shellcheck "RUN echo $MISSING_QUOTES"+        onBuildRuleCatches shellcheck "RUN echo $MISSING_QUOTES"+      it "not warns on valid scripts" $ do+        ruleCatchesNot shellcheck "RUN echo foo"+        onBuildRuleCatchesNot shellcheck "RUN echo foo"++      it "Does not complain on default env vars" $+        let dockerFile =+              Text.unlines+                [ "RUN echo \"$HTTP_PROXY\"",+                  "RUN echo \"$http_proxy\"",+                  "RUN echo \"$HTTPS_PROXY\"",+                  "RUN echo \"$https_proxy\"",+                  "RUN echo \"$FTP_PROXY\"",+                  "RUN echo \"$ftp_proxy\"",+                  "RUN echo \"$NO_PROXY\"",+                  "RUN echo \"$no_proxy\""+                ]+         in do+              ruleCatchesNot shellcheck dockerFile+              onBuildRuleCatchesNot shellcheck dockerFile++      it "Complain on missing env vars" $+        let dockerFile =+              Text.unlines+                [ "RUN echo \"$RTTP_PROXY\""+                ]+         in do+              ruleCatches shellcheck dockerFile+              onBuildRuleCatches shellcheck dockerFile++      it "Is aware of ARGS and ENV" $+        let dockerFile =+              Text.unlines+                [ "ARG foo=bar",+                  "ARG another_foo",+                  "ENV bar=10 baz=20",+                  "RUN echo \"$foo\"",+                  "RUN echo \"$another_foo\"",+                  "RUN echo \"$bar\"",+                  "RUN echo \"$baz\""+                ]+         in do+              ruleCatchesNot shellcheck dockerFile+              onBuildRuleCatchesNot shellcheck dockerFile++      it "Resets env vars after a FROM" $+        let dockerFile =+              Text.unlines+                [ "ARG foo=bar",+                  "ARG another_foo",+                  "ENV bar=10 baz=20",+                  "FROM debian",+                  "RUN echo \"$foo\""+                ]+         in do+              ruleCatches shellcheck dockerFile+              onBuildRuleCatches shellcheck dockerFile++      it "Defaults the shell to sh" $+        let dockerFile =+              Text.unlines+                [ "RUN echo $RANDOM"+                ]+         in do+              ruleCatches shellcheck dockerFile+              onBuildRuleCatches shellcheck dockerFile++      it "Can change the shell check to bash" $+        let dockerFile =+              Text.unlines+                [ "SHELL [\"/bin/bash\", \"-eo\", \"pipefail\", \"-c\"]",+                  "RUN echo $RANDOM"+                ]+         in do+              ruleCatchesNot shellcheck dockerFile+              onBuildRuleCatchesNot shellcheck dockerFile++      it "Resets the SHELL to sh after a FROM" $+        let dockerFile =+              Text.unlines+                [ "SHELL [\"/bin/bash\", \"-eo\", \"pipefail\", \"-c\"]",+                  "FROM debian",+                  "RUN echo $RANDOM"+                ]+         in do+              ruleCatches shellcheck dockerFile+              onBuildRuleCatches shellcheck dockerFile++      it "Does not complain on ash shell" $+        let dockerFile =+              Text.unlines+                [ "SHELL [\"/bin/ash\", \"-o\", \"pipefail\", \"-c\"]",+                  "RUN echo hello"+                ]+         in do+              ruleCatchesNot shellcheck dockerFile+              onBuildRuleCatchesNot shellcheck dockerFile++      it "Does not complain on powershell" $+        let dockerFile =+              Text.unlines+                [ "SHELL [\"pwsh\", \"-c\"]",+                  "RUN Get-Variable PSVersionTable | Select-Object -ExpandProperty Value"+                ]+         in do+              ruleCatchesNot shellcheck dockerFile+              onBuildRuleCatchesNot shellcheck dockerFile+    --+    --+    describe "COPY rules" $ do+      it "use add" $ ruleCatches useAdd "COPY packaged-app.tar /usr/src/app"+      it "use not add" $ ruleCatchesNot useAdd "COPY package.json /usr/src/app"+    --+    describe "other rules" $ do+      it "apt-get auto yes" $ do+        ruleCatches aptGetYes "RUN apt-get install python"+        onBuildRuleCatches aptGetYes "RUN apt-get install python"+      it "apt-get yes shortflag" $ do+        ruleCatchesNot aptGetYes "RUN apt-get install -yq python"+        onBuildRuleCatchesNot aptGetYes "RUN apt-get install -yq python"+      it "apt-get yes quiet level 2 implies -y" $ do+        ruleCatchesNot aptGetYes "RUN apt-get install -qq python"+        onBuildRuleCatchesNot aptGetYes "RUN apt-get install -qq python"+      it "apt-get yes different pos" $ do+        ruleCatchesNot aptGetYes "RUN apt-get install -y python"+        onBuildRuleCatchesNot aptGetYes "RUN apt-get install -y python"+      it "apt-get with auto yes" $ do+        ruleCatchesNot aptGetYes "RUN apt-get -y install python"+        onBuildRuleCatchesNot aptGetYes "RUN apt-get -y install python"+      it "apt-get with auto expanded yes" $ do+        ruleCatchesNot aptGetYes "RUN apt-get --yes install python"+        onBuildRuleCatchesNot aptGetYes "RUN apt-get --yes install python"+      it "apt-get with assume-yes" $ do+        ruleCatchesNot aptGetYes "RUN apt-get --assume-yes install python"+        onBuildRuleCatchesNot aptGetYes "RUN apt-get --assume-yes install python"+      it "apt-get install recommends" $ do+        ruleCatchesNot+          aptGetNoRecommends+          "RUN apt-get install --no-install-recommends python"+        onBuildRuleCatchesNot+          aptGetNoRecommends+          "RUN apt-get install --no-install-recommends python"+      it "apt-get no install recommends" $ do+        ruleCatches aptGetNoRecommends "RUN apt-get install python"+        onBuildRuleCatches aptGetNoRecommends "RUN apt-get install python"+      it "apt-get no install recommends" $ do+        ruleCatches aptGetNoRecommends "RUN apt-get -y install python"+        onBuildRuleCatches aptGetNoRecommends "RUN apt-get -y install python"+      it "apt-get no install recommends via option" $ do+        ruleCatchesNot aptGetNoRecommends "RUN apt-get -o APT::Install-Recommends=false install python"+        onBuildRuleCatchesNot aptGetNoRecommends "RUN apt-get -o APT::Install-Recommends=false install python"+      it "apt-get version" $ do+        ruleCatchesNot aptGetVersionPinned "RUN apt-get install -y python=1.2.2"+        onBuildRuleCatchesNot aptGetVersionPinned "RUN apt-get install -y python=1.2.2"+      it "apt-get version" $ do+        ruleCatchesNot aptGetVersionPinned "RUN apt-get install ./wkhtmltox_0.12.5-1.bionic_amd64.deb"+        onBuildRuleCatchesNot aptGetVersionPinned "RUN apt-get install ./wkhtmltox_0.12.5-1.bionic_amd64.deb"+      it "apt-get pinned" $ do+        ruleCatchesNot+          aptGetVersionPinned+          "RUN apt-get -y --no-install-recommends install nodejs=0.10"+        onBuildRuleCatchesNot+          aptGetVersionPinned+          "RUN apt-get -y --no-install-recommends install nodejs=0.10"+      it "apt-get tolerate target-release" $+        let dockerFile =+              [ "RUN set -e &&\\",+                " echo \"deb http://http.debian.net/debian jessie-backports main\" \+                \> /etc/apt/sources.list.d/jessie-backports.list &&\\",+                " apt-get update &&\\",+                " apt-get install -y --no-install-recommends -t jessie-backports \+                \openjdk-8-jdk=8u131-b11-1~bpo8+1 &&\\",+                " rm -rf /var/lib/apt/lists/*"+              ]+         in do+              ruleCatchesNot aptGetVersionPinned $ Text.unlines dockerFile+              onBuildRuleCatchesNot aptGetVersionPinned $ Text.unlines dockerFile++      it "has maintainer" $ ruleCatches hasNoMaintainer "FROM debian\nMAINTAINER Lukas"+      it "has maintainer first" $ ruleCatches hasNoMaintainer "MAINTAINER Lukas\nFROM DEBIAN"+      it "has no maintainer" $ ruleCatchesNot hasNoMaintainer "FROM debian"+      it "using add" $ ruleCatches copyInsteadAdd "ADD file /usr/src/app/"++      it "many cmds" $+        let dockerFile =+              [ "FROM debian",+                "CMD bash",+                "RUN foo",+                "CMD another"+              ]+         in ruleCatches multipleCmds $ Text.unlines dockerFile++      it "single cmds, different stages" $+        let dockerFile =+              [ "FROM debian as distro1",+                "CMD bash",+                "RUN foo",+                "FROM debian as distro2",+                "CMD another"+              ]+         in ruleCatchesNot multipleCmds $ Text.unlines dockerFile++      it "many cmds, different stages" $+        let dockerFile =+              [ "FROM debian as distro1",+                "CMD bash",+                "RUN foo",+                "CMD another",+                "FROM debian as distro2",+                "CMD another"+              ]+         in ruleCatches multipleCmds $ Text.unlines dockerFile++      it "single cmd" $ ruleCatchesNot multipleCmds "CMD /bin/true"+      it "no cmd" $ ruleCatchesNot multipleEntrypoints "FROM busybox"++      it "many entrypoints" $+        let dockerFile =+              [ "FROM debian",+                "ENTRYPOINT bash",+                "RUN foo",+                "ENTRYPOINT another"+              ]+         in ruleCatches multipleEntrypoints $ Text.unlines dockerFile++      it "single entrypoint, different stages" $+        let dockerFile =+              [ "FROM debian as distro1",+                "ENTRYPOINT bash",+                "RUN foo",+                "FROM debian as distro2",+                "ENTRYPOINT another"+              ]+         in ruleCatchesNot multipleEntrypoints $ Text.unlines dockerFile++      it "many entrypoints, different stages" $+        let dockerFile =+              [ "FROM debian as distro1",+                "ENTRYPOINT bash",+                "RUN foo",+                "ENTRYPOINT another",+                "FROM debian as distro2",+                "ENTRYPOINT another"+              ]+         in ruleCatches multipleEntrypoints $ Text.unlines dockerFile+      it "single entry" $ ruleCatchesNot multipleEntrypoints "ENTRYPOINT /bin/true"+      it "no entry" $ ruleCatchesNot multipleEntrypoints "FROM busybox"+      it "workdir variable" $ ruleCatchesNot absoluteWorkdir "WORKDIR ${work}"+      it "scratch" $ ruleCatchesNot noUntagged "FROM scratch"+    --+    describe "add files and archives" $ do+      it "add for tar" $ ruleCatchesNot copyInsteadAdd "ADD file.tar /usr/src/app/"+      it "add for zip" $ ruleCatchesNot copyInsteadAdd "ADD file.zip /usr/src/app/"+      it "add for gzip" $ ruleCatchesNot copyInsteadAdd "ADD file.gz /usr/src/app/"+      it "add for bz2" $ ruleCatchesNot copyInsteadAdd "ADD file.bz2 /usr/src/app/"+      it "add for xz" $ ruleCatchesNot copyInsteadAdd "ADD file.xz /usr/src/app/"+      it "add for tgz" $ ruleCatchesNot copyInsteadAdd "ADD file.tgz /usr/src/app/"+      it "add for url" $ ruleCatchesNot copyInsteadAdd "ADD http://file.com /usr/src/app/"+    --+    describe "copy last argument" $ do+      it "no warn on 2 args" $ ruleCatchesNot copyEndingSlash "COPY foo bar"+      it "warn on 3 args" $ ruleCatches copyEndingSlash "COPY foo bar baz"+      it "no warn on 3 args" $ ruleCatchesNot copyEndingSlash "COPY foo bar baz/"+    --+    describe "copy from existing alias" $ do+      it "warn on missing alias" $ ruleCatches copyFromExists "COPY --from=foo bar ."+      it "warn on alias defined after" $+        let dockerFile =+              [ "FROM scratch",+                "COPY --from=build foo .",+                "FROM node as build",+                "RUN baz"+              ]+         in ruleCatches copyFromExists $ Text.unlines dockerFile+      it "don't warn on correctly defined aliases" $+        let dockerFile =+              [ "FROM scratch as build",+                "RUN foo",+                "FROM node",+                "COPY --from=build foo .",+                "RUN baz"+              ]+         in ruleCatchesNot copyFromExists $ Text.unlines dockerFile+    --+    describe "copy from own FROM" $ do+      it "warn on copying from your the same FROM" $+        let dockerFile =+              [ "FROM node as foo",+                "COPY --from=foo bar ."+              ]+         in ruleCatches copyFromAnother $ Text.unlines dockerFile+      it "don't warn on copying form other sources" $+        let dockerFile =+              [ "FROM scratch as build",+                "RUN foo",+                "FROM node as run",+                "COPY --from=build foo .",+                "RUN baz"+              ]+         in ruleCatchesNot copyFromAnother $ Text.unlines dockerFile+    --+    describe "Duplicate aliases" $ do+      it "warn on duplicate aliases" $+        let dockerFile =+              [ "FROM node as foo",+                "RUN something",+                "FROM scratch as foo",+                "RUN something"+              ]+         in ruleCatches fromAliasUnique $ Text.unlines dockerFile+      it "don't warn on unique aliases" $+        let dockerFile =+              [ "FROM scratch as build",+                "RUN foo",+                "FROM node as run",+                "RUN baz"+              ]+         in ruleCatchesNot fromAliasUnique $ Text.unlines dockerFile+    --+    describe "format error" $+      it "display error after line pos" $ do+        let ast = parseText "FOM debian:jessie"+            expectedMsg =+              "<string>:1:1 unexpected 'F' expecting '#', ADD, ARG, CMD, COPY, ENTRYPOINT, "+                <> "ENV, EXPOSE, FROM, HEALTHCHECK, LABEL, MAINTAINER, ONBUILD, RUN, SHELL, STOPSIGNAL, "+                <> "USER, VOLUME, WORKDIR, or end of input "+        case ast of+          Left err -> assertEqual "Unexpected error msg" expectedMsg (formatError err)+          Right _ -> assertFailure "AST should fail parsing"+    --+    describe "Rules can be ignored with inline comments" $ do+      it "ignores single rule" $+        let dockerFile =+              [ "FROM ubuntu",+                "# hadolint ignore=DL3002",+                "USER root"+              ]+         in ruleCatchesNot noRootUser $ Text.unlines dockerFile+      it "ignores only the given rule" $+        let dockerFile =+              [ "FROM scratch",+                "# hadolint ignore=DL3001",+                "USER root"+              ]+         in ruleCatches noRootUser $ Text.unlines dockerFile+      it "ignores only the given rule, when multiple passed" $+        let dockerFile =+              [ "FROM scratch",+                "# hadolint ignore=DL3001,DL3002",+                "USER root"+              ]+         in ruleCatchesNot noRootUser $ Text.unlines dockerFile+      it "ignores the rule only if directly above the instruction" $+        let dockerFile =+              [ "# hadolint ignore=DL3001,DL3002",+                "FROM ubuntu",+                "USER root"+              ]+         in ruleCatches noRootUser $ Text.unlines dockerFile+      it "won't ignore the rule if passed invalid rule names" $+        let dockerFile =+              [ "FROM scratch",+                "# hadolint ignore=crazy,DL3002",+                "USER root"+              ]+         in ruleCatches noRootUser $ Text.unlines dockerFile+      it "ignores multiple rules correctly, even with some extra whitespace" $+        let dockerFile =+              [ "FROM node as foo",+                "# hadolint ignore=DL3023, DL3021",+                "COPY --from=foo bar baz ."+              ]+         in do+              ruleCatchesNot copyFromAnother $ Text.unlines dockerFile+              ruleCatchesNot copyEndingSlash $ Text.unlines dockerFile+    --+    describe "JSON notation in ENTRYPOINT and CMD" $ do+      it "warn on ENTRYPOINT" $+        let dockerFile =+              [ "FROM node as foo",+                "ENTRYPOINT something"+              ]+         in ruleCatches useJsonArgs $ Text.unlines dockerFile+      it "don't warn on ENTRYPOINT json notation" $+        let dockerFile =+              [ "FROM scratch as build",+                "ENTRYPOINT [\"foo\", \"bar\"]"+              ]+         in ruleCatchesNot useJsonArgs $ Text.unlines dockerFile+      it "warn on CMD" $+        let dockerFile =+              [ "FROM node as foo",+                "CMD something"+              ]+         in ruleCatches useJsonArgs $ Text.unlines dockerFile+      it "don't warn on CMD json notation" $+        let dockerFile =+              [ "FROM scratch as build",+                "CMD [\"foo\", \"bar\"]",+                "CMD [ \"foo\", \"bar\" ]"+              ]+         in ruleCatchesNot useJsonArgs $ Text.unlines dockerFile++    --+    describe "Detects missing pipefail option" $ do+      it "warn on missing pipefail" $+        let dockerFile =+              [ "FROM scratch",+                "RUN wget -O - https://some.site | wc -l > /number"+              ]+         in ruleCatches usePipefail $ Text.unlines dockerFile+      it "don't warn on commands with no pipes" $+        let dockerFile =+              [ "FROM scratch as build",+                "RUN wget -O - https://some.site && wc -l file > /number"+              ]+         in ruleCatchesNot usePipefail $ Text.unlines dockerFile+      it "don't warn on commands with pipes and the pipefail option" $+        let dockerFile =+              [ "FROM scratch as build",+                "SHELL [\"/bin/bash\", \"-eo\", \"pipefail\", \"-c\"]",+                "RUN wget -O - https://some.site | wc -l file > /number"+              ]+         in ruleCatchesNot usePipefail $ Text.unlines dockerFile+      it "don't warn on commands with pipes and the pipefail option 2" $+        let dockerFile =+              [ "FROM scratch as build",+                "SHELL [\"/bin/bash\", \"-e\", \"-o\", \"pipefail\", \"-c\"]",+                "RUN wget -O - https://some.site | wc -l file > /number"+              ]+         in ruleCatchesNot usePipefail $ Text.unlines dockerFile+      it "don't warn on commands with pipes and the pipefail option 3" $+        let dockerFile =+              [ "FROM scratch as build",+                "SHELL [\"/bin/bash\", \"-o\", \"errexit\", \"-o\", \"pipefail\", \"-c\"]",+                "RUN wget -O - https://some.site | wc -l file > /number"+              ]+         in ruleCatchesNot usePipefail $ Text.unlines dockerFile+      it "don't warn on commands with pipes and the pipefail zsh" $+        let dockerFile =+              [ "FROM scratch as build",+                "SHELL [\"/bin/zsh\", \"-o\", \"pipefail\", \"-c\"]",+                "RUN wget -O - https://some.site | wc -l file > /number"+              ]+         in ruleCatchesNot usePipefail $ Text.unlines dockerFile+      it "don't warn on powershell" $+        let dockerFile =+              [ "FROM scratch as build",+                "SHELL [\"pwsh\", \"-c\"]",+                "RUN Get-Variable PSVersionTable | Select-Object -ExpandProperty Value"+              ]+         in ruleCatchesNot usePipefail $ Text.unlines dockerFile+      it "warns when using plain sh" $+        let dockerFile =+              [ "FROM scratch as build",+                "SHELL [\"/bin/sh\", \"-o\", \"pipefail\", \"-c\"]",+                "RUN wget -O - https://some.site | wc -l file > /number"+              ]+         in ruleCatches usePipefail $ Text.unlines dockerFile+      it "warn on missing pipefail in the next image" $+        let dockerFile =+              [ "FROM scratch as build",+                "SHELL [\"/bin/bash\", \"-o\", \"pipefail\", \"-c\"]",+                "RUN wget -O - https://some.site | wc -l file > /number",+                "FROM scratch as build2",+                "RUN wget -O - https://some.site | wc -l file > /number"+              ]+         in ruleCatches usePipefail $ Text.unlines dockerFile+      it "warn on missing pipefail if next SHELL is not using it" $+        let dockerFile =+              [ "FROM scratch as build",+                "SHELL [\"/bin/bash\", \"-o\", \"pipefail\", \"-c\"]",+                "RUN wget -O - https://some.site | wc -l file > /number",+                "SHELL [\"/bin/sh\", \"-c\"]",+                "RUN wget -O - https://some.site | wc -l file > /number"+              ]+         in ruleCatches usePipefail $ Text.unlines dockerFile+    --+    describe "Allowed docker registries" $ do+      it "warn on non-allowed registry" $+        let dockerFile =+              [ "FROM random.com/debian"+              ]+         in ruleCatches (registryIsAllowed ["docker.io"]) $ Text.unlines dockerFile+      it "don't warn on empty allowed registries" $+        let dockerFile =+              [ "FROM random.com/debian"+              ]+         in ruleCatchesNot (registryIsAllowed []) $ Text.unlines dockerFile+      it "don't warn on allowed registries" $+        let dockerFile =+              [ "FROM random.com/debian"+              ]+         in ruleCatchesNot (registryIsAllowed ["x.com", "random.com"]) $ Text.unlines dockerFile+      it "doesn't warn on scratch image" $+        let dockerFile =+              [ "FROM scratch"+              ]+         in ruleCatchesNot (registryIsAllowed ["x.com", "random.com"]) $ Text.unlines dockerFile+      it "allows boths all forms of docker.io" $+        let dockerFile =+              [ "FROM ubuntu:18.04 AS builder1",+                "FROM zemanlx/ubuntu:18.04 AS builder2",+                "FROM docker.io/zemanlx/ubuntu:18.04 AS builder3"+              ]+         in ruleCatchesNot (registryIsAllowed ["docker.io"]) $ Text.unlines dockerFile++      it "allows using previous stages" $+        let dockerFile =+              [ "FROM random.com/foo AS builder1",+                "FROM builder1 AS builder2"+              ]+         in ruleCatchesNot (registryIsAllowed ["random.com"]) $ Text.unlines dockerFile+    --+    describe "Wget or Curl" $ do+      it "warns when using both wget and curl" $+        let dockerFile =+              [ "FROM node as foo",+                "RUN wget my.xyz",+                "RUN curl localhost"+              ]+         in ruleCatches wgetOrCurl $ Text.unlines dockerFile+      it "warns when using both wget and curl in same instruction" $+        let dockerFile =+              [ "FROM node as foo",+                "RUN wget my.xyz && curl localhost"+              ]+         in ruleCatches wgetOrCurl $ Text.unlines dockerFile+      it "does not warn when using only wget" $+        let dockerFile =+              [ "FROM node as foo",+                "RUN wget my.xyz"+              ]+         in ruleCatchesNot wgetOrCurl $ Text.unlines dockerFile+      it "does not warn when using both curl and wget in different stages" $+        let dockerFile =+              [ "FROM node as foo",+                "RUN wget my.xyz",+                "FROM scratch",+                "RUN curl localhost"+              ]+         in ruleCatchesNot wgetOrCurl $ Text.unlines dockerFile+      it "does not warns when using both, on a single stage" $+        let dockerFile =+              [ "FROM node as foo",+                "RUN wget my.xyz",+                "RUN curl localhost",+                "FROM scratch",+                "RUN curl localhost"+              ]+         in ruleCatches wgetOrCurl $ Text.unlines dockerFile+      it "only warns on the relevant RUN instruction" $+        let dockerFile =+              [ "FROM node as foo",+                "RUN wget my.xyz",+                "RUN curl my.xyz",+                "RUN echo hello"+              ]+         in assertChecks+              wgetOrCurl+              (Text.unlines dockerFile)+              ( \checks ->+                  assertBool+                    "Expecting warnings only in 1 RUN instruction"+                    (length checks == 1)+              )+      it "only warns on many relevant RUN instructions" $+        let dockerFile =+              [ "FROM node as foo",+                "RUN wget my.xyz",+                "RUN curl my.xyz",+                "RUN echo hello",+                "RUN wget foo.com"+              ]+         in assertChecks+              wgetOrCurl+              (Text.unlines dockerFile)+              ( \checks ->+                  assertBool+                    "Expecting warnings only in 2 RUN instructions"+                    (length checks == 2)+              )+    --+    describe "Regression Tests" $+      it "Comments with backslashes at the end are just comments" $+        let dockerFile =+              [ "FROM alpine:3.6",+                "# The following comment makes hadolint still complain about DL4006",+                "# \\",+                "# should solve DL4006",+                "SHELL [\"/bin/sh\", \"-o\", \"pipefail\", \"-c\"]",+                "# RUN with pipe. causes DL4006, but should be fixed by above SHELL",+                "RUN echo \"kaka\" | sed 's/a/o/g' >> /root/afile"+              ]+         in ruleCatches usePipefail $ Text.unlines dockerFile++    -- Run tests for the Config module+    ConfigSpec.tests++assertChecks :: HasCallStack => Rule -> Text.Text -> ([RuleCheck] -> IO a) -> IO a+assertChecks rule s makeAssertions =+  case parseText (s <> "\n") of+    Left err -> assertFailure $ show err+    Right dockerFile -> makeAssertions $ analyze [rule] dockerFile++assertOnBuildChecks :: HasCallStack => Rule -> Text.Text -> ([RuleCheck] -> IO a) -> IO a+assertOnBuildChecks rule s makeAssertions =+  case parseText (s <> "\n") of+    Left err -> assertFailure $ show err+    Right dockerFile -> checkOnBuild dockerFile+  where+    checkOnBuild dockerFile = makeAssertions $ analyze [rule] (fmap wrapInOnBuild dockerFile)+    wrapInOnBuild (InstructionPos (Run args) so li) = InstructionPos (OnBuild (Run args)) so li+    wrapInOnBuild i = i++selectChecksWithLines :: [RuleCheck] -> [RuleCheck]+selectChecksWithLines checks = [c | c <- checks, linenumber c <= 0]++-- Assert a failed check exists for rule+ruleCatches :: HasCallStack => Rule -> Text.Text -> Assertion+ruleCatches rule s = assertChecks rule s f+  where+    f checks = do+      when (null checks) $+        assertFailure "I was expecting to catch at least one error"+      assertBool "Incorrect line number for result" $ null $ selectChecksWithLines checks++onBuildRuleCatches :: HasCallStack => Rule -> Text.Text -> Assertion+onBuildRuleCatches rule s = assertOnBuildChecks rule s f+  where+    f checks = do+      when (length checks /= 1) $+        assertFailure (Text.unpack . Text.unlines . formatChecks $ checks)+      assertBool "Incorrect line number for result" $ null $ selectChecksWithLines checks++ruleCatchesNot :: HasCallStack => Rule -> Text.Text -> Assertion+ruleCatchesNot rule s = assertChecks rule s f+  where+    f checks =+      unless (null checks) $+        assertFailure $+          "Not expecting the following errors: \n"+            ++ (Text.unpack . Text.unlines . formatChecks $ checks)++onBuildRuleCatchesNot :: HasCallStack => Rule -> Text.Text -> Assertion+onBuildRuleCatchesNot rule s = assertOnBuildChecks rule s f+  where+    f checks =+      unless (null checks) $+        assertFailure $+          "Not expecting the following errors: \n"+            ++ (Text.unpack . Text.unlines . formatChecks $ checks)