hadolint 1.7.1 → 1.7.2
raw patch · 4 files changed
+435/−203 lines, 4 filesdep +mtlPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
Dependencies added: mtl
API changes (from Hackage documentation)
- Hadolint.Rules: bashCommands :: [String] -> [[String]]
- Hadolint.Rules: dropOptionsWithArg :: [String] -> [String] -> [String]
- Hadolint.Rules: findPackages :: [String] -> [String]
- Hadolint.Rules: isApkAdd :: [String] -> Bool
- Hadolint.Rules: isAptGetInstall :: [String] -> Bool
+ Hadolint.Bash: ParsedBash :: Text -> ParseResult -> ParsedBash
+ Hadolint.Bash: [original] :: ParsedBash -> Text
+ Hadolint.Bash: [parsed] :: ParsedBash -> ParseResult
+ Hadolint.Bash: allCommands :: (Token -> Bool) -> ParsedBash -> Bool
+ Hadolint.Bash: cmdHasArgs :: String -> [String] -> Token -> Bool
+ Hadolint.Bash: data ParsedBash
+ Hadolint.Bash: dropFlagArg :: [String] -> Token -> Token
+ Hadolint.Bash: findCommandNames :: ParsedBash -> [String]
+ Hadolint.Bash: findCommands :: ParsedBash -> [Token]
+ Hadolint.Bash: getAllArgs :: Token -> [String]
+ Hadolint.Bash: getAllFlags :: Token -> [(Token, String)]
+ Hadolint.Bash: getArgsNoFlags :: Token -> [String]
+ Hadolint.Bash: getCommandName :: Token -> Maybe String
+ Hadolint.Bash: hasFlag :: String -> Token -> Bool
+ Hadolint.Bash: noCommands :: (Token -> Bool) -> ParsedBash -> Bool
+ Hadolint.Bash: parseShell :: Text -> ParsedBash
+ Hadolint.Rules: type ParsedFile = [InstructionPos ParsedBash]
- Hadolint.Bash: shellcheck :: String -> [Comment]
+ Hadolint.Bash: shellcheck :: ParsedBash -> [Comment]
- Hadolint.Rules: allAliasedImages :: Dockerfile -> [(Linenumber, ImageAlias)]
+ Hadolint.Rules: allAliasedImages :: ParsedFile -> [(Linenumber, ImageAlias)]
- Hadolint.Rules: allFromImages :: Dockerfile -> [(Linenumber, BaseImage)]
+ Hadolint.Rules: allFromImages :: ParsedFile -> [(Linenumber, BaseImage)]
- Hadolint.Rules: allImageNames :: Dockerfile -> [(Linenumber, Text)]
+ Hadolint.Rules: allImageNames :: ParsedFile -> [(Linenumber, Text)]
- Hadolint.Rules: apkAddPackages :: [String] -> [String]
+ Hadolint.Rules: apkAddPackages :: ParsedBash -> [String]
- Hadolint.Rules: aptGetPackages :: [String] -> [String]
+ Hadolint.Rules: aptGetPackages :: ParsedBash -> [String]
- Hadolint.Rules: argumentsRule :: ([String] -> a) -> Arguments Text -> a
+ Hadolint.Rules: argumentsRule :: (ParsedBash -> a) -> Arguments ParsedBash -> a
- Hadolint.Rules: instructionRule :: Text -> Severity -> Text -> (Instruction Text -> Bool) -> Rule
+ Hadolint.Rules: instructionRule :: Text -> Severity -> Text -> (Instruction ParsedBash -> Bool) -> Rule
- Hadolint.Rules: instructionRuleLine :: Text -> Severity -> Text -> (Linenumber -> Instruction Text -> Bool) -> Rule
+ Hadolint.Rules: instructionRuleLine :: Text -> Severity -> Text -> (Linenumber -> Instruction ParsedBash -> Bool) -> Rule
- Hadolint.Rules: instructionRuleState :: Text -> Severity -> Text -> (state -> Linenumber -> Instruction Text -> (state, Bool)) -> state -> Rule
+ Hadolint.Rules: instructionRuleState :: Text -> Severity -> Text -> (state -> Linenumber -> Instruction ParsedBash -> (state, Bool)) -> state -> Rule
- Hadolint.Rules: mapInstructions :: Metadata -> (state -> Linenumber -> Instruction Text -> (state, Bool)) -> state -> Rule
+ Hadolint.Rules: mapInstructions :: Metadata -> (state -> Linenumber -> Instruction ParsedBash -> (state, Bool)) -> state -> Rule
- Hadolint.Rules: previouslyDefinedAliases :: Linenumber -> Dockerfile -> [Text]
+ Hadolint.Rules: previouslyDefinedAliases :: Linenumber -> ParsedFile -> [Text]
- Hadolint.Rules: shellcheckBash :: Dockerfile -> [RuleCheck]
+ Hadolint.Rules: shellcheckBash :: ParsedFile -> [RuleCheck]
- Hadolint.Rules: stripInstallPrefix :: (String -> Bool) -> [String] -> Maybe [String]
+ Hadolint.Rules: stripInstallPrefix :: [String] -> [String]
- Hadolint.Rules: type Rule = Dockerfile -> [RuleCheck]
+ Hadolint.Rules: type Rule = ParsedFile -> [RuleCheck]
- Hadolint.Rules: usingProgram :: String -> [String] -> Bool
+ Hadolint.Rules: usingProgram :: String -> ParsedBash -> Bool
Files
- hadolint.cabal +3/−2
- src/Hadolint/Bash.hs +88/−3
- src/Hadolint/Rules.hs +99/−116
- test/Spec.hs +245/−82
hadolint.cabal view
@@ -2,10 +2,10 @@ -- -- see: https://github.com/sol/hpack ----- hash: dee8c05033f3341030b3d3436b7ec3db7ceb472be45eb05a90bce71111365c2a+-- hash: 1968b5091609b7ed9433b56e82cdefd1ad484dce803b5f213ac9a3438de4313f name: hadolint-version: 1.7.1+version: 1.7.2 synopsis: Dockerfile Linter JavaScript API description: A smarter Dockerfile linter that helps you build best practice Docker images. category: Development@@ -36,6 +36,7 @@ , containers , language-docker >=6.0.1 && <7 , megaparsec >=6.4+ , mtl , split >=0.2 , text , void
src/Hadolint/Bash.hs view
@@ -1,16 +1,101 @@ module Hadolint.Bash where +import Control.Monad.Writer (Writer, execWriter, tell) import Data.Functor.Identity (runIdentity)+import Data.List (nub)+import Data.Maybe (mapMaybe)+import qualified Data.Text as Text+import qualified ShellCheck.AST+import ShellCheck.AST (Id(..), Token(..))+import qualified ShellCheck.ASTLib import ShellCheck.Checker import ShellCheck.Interface+import qualified ShellCheck.Parser -shellcheck :: String -> [Comment]-shellcheck bashScript = map comment $ crComments $ runIdentity $ checkScript si spec+data ParsedBash = ParsedBash+ { original :: Text.Text+ , parsed :: ParseResult+ }++shellcheck :: ParsedBash -> [Comment]+shellcheck (ParsedBash txt _) = map comment $ crComments $ runIdentity $ checkScript si spec where comment (PositionedComment _ _ c) = c si = mockedSystemInterface [("", "")] spec = CheckSpec filename script sourced exclusions (Just Bash)- script = "#!/bin/bash\n" ++ bashScript+ script = "#!/bin/bash\n" ++ Text.unpack txt filename = "" -- filename can be ommited because we only want the parse results back sourced = False exclusions = []++parseShell :: Text.Text -> ParsedBash+parseShell txt =+ ParsedBash+ { original = txt+ , parsed =+ runIdentity $+ ShellCheck.Parser.parseScript+ (mockedSystemInterface [("", "")])+ ParseSpec+ { psFilename = "" -- There is no filename+ , psScript = "#!/bin/bash\n" ++ Text.unpack txt+ , psCheckSourced = False+ }+ }++findCommands :: ParsedBash -> [Token]+findCommands (ParsedBash _ ast) =+ case prRoot ast of+ Nothing -> []+ Just script -> nub . execWriter $ ShellCheck.AST.doAnalysis extract script+ where+ extract :: Token -> Writer [Token] ()+ extract t =+ case ShellCheck.ASTLib.getCommand t of+ Nothing -> return ()+ Just cmd -> tell [cmd]++allCommands :: (Token -> Bool) -> ParsedBash -> Bool+allCommands check script = all check (findCommands script)++noCommands :: (Token -> Bool) -> ParsedBash -> Bool+noCommands check = allCommands (not . check)++getCommandName :: Token -> Maybe String+getCommandName = ShellCheck.ASTLib.getCommandName++findCommandNames :: ParsedBash -> [String]+findCommandNames = mapMaybe getCommandName . findCommands++cmdHasArgs :: String -> [String] -> Token -> Bool+cmdHasArgs command arguments token@T_SimpleCommand {}+ | ShellCheck.ASTLib.getCommandName token /= Just command = False+ | otherwise = not $ null [arg | arg <- getAllArgs token, arg `elem` arguments]+cmdHasArgs _ _ _ = False++getAllArgs :: Token -> [String]+getAllArgs (T_SimpleCommand _ _ (_:allArgs)) = concatMap ShellCheck.ASTLib.oversimplify allArgs+getAllArgs _ = []++getArgsNoFlags :: Token -> [String]+getArgsNoFlags cmd@(T_SimpleCommand _ _ (_:allArgs)) = concatMap ShellCheck.ASTLib.oversimplify args+ where+ flags = [t | (t, _) <- getAllFlags cmd]+ args = [a | a <- allArgs, a `notElem` flags]+getArgsNoFlags _ = []++getAllFlags :: Token -> [(Token, String)]+getAllFlags cmd@T_SimpleCommand {} = [(t, f) | (t, f) <- ShellCheck.ASTLib.getAllFlags cmd, f /= ""]+getAllFlags _ = []++hasFlag :: String -> Token -> Bool+hasFlag flag = any (\(_, f) -> f == flag) . getAllFlags++dropFlagArg :: [String] -> Token -> Token+dropFlagArg flags cmd@(T_SimpleCommand cid b allArgs) = T_SimpleCommand cid b filterdArgs+ where+ filterdArgs = [arg | arg <- allArgs, isNotNextToken arg]+ isNotNextToken arg = ShellCheck.AST.getId arg `notElem` findTokensToDrop+ findTokensToDrop = [next (ShellCheck.AST.getId t) | (t, f) <- getAllFlags cmd, f `elem` flags]+ next (Id i) = Id (i + 2)+dropFlagArg _ token = token
src/Hadolint/Rules.hs view
@@ -6,8 +6,7 @@ import Control.Arrow ((&&&)) import Data.List (dropWhile, isInfixOf, isPrefixOf, mapAccumL) import Data.List.NonEmpty (toList)-import Data.List.Split (splitOneOf)-import Hadolint.Bash+import qualified Hadolint.Bash as Bash import Language.Docker.Syntax import Data.Semigroup ((<>))@@ -42,33 +41,49 @@ type IgnoreRuleParser = Megaparsec.Parsec Void Text.Text +type ParsedFile = [InstructionPos Bash.ParsedBash]+ link :: Metadata -> Text.Text link (Metadata code _ _) | "SC" `Text.isPrefixOf` code = "https://github.com/koalaman/shellcheck/wiki/" <> code | "DL" `Text.isPrefixOf` code = "https://github.com/hadolint/hadolint/wiki/" <> code | otherwise = "https://github.com/hadolint/hadolint" --- a Rule takes a Dockerfile and returns the executed checks-type Rule = Dockerfile -> [RuleCheck]+-- a Rule takes a Dockerfile with parsed bash and returns the executed checks+type Rule = ParsedFile -> [RuleCheck] -- Apply a function on each instruction and create a check -- for the according line number mapInstructions ::- Metadata -> (state -> Linenumber -> Instruction Text.Text -> (state, Bool)) -> state -> Rule+ Metadata+ -> (state -> Linenumber -> Instruction Bash.ParsedBash -> (state, Bool))+ -> state+ -> Rule mapInstructions metadata f initialState dockerfile = let (_, results) = mapAccumL applyRule initialState dockerfile in 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 applyRule state (InstructionPos i source linenumber) =- let (newState, res) = f state linenumber i+ 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 metadata source linenumber res) -instructionRule :: Text.Text -> Severity -> Text.Text -> (Instruction Text.Text -> Bool) -> Rule+instructionRule ::+ Text.Text -> Severity -> Text.Text -> (Instruction Bash.ParsedBash -> Bool) -> Rule instructionRule code severity message check = instructionRuleLine code severity message (const check) instructionRuleLine ::- Text.Text -> Severity -> Text.Text -> (Linenumber -> Instruction Text.Text -> Bool) -> Rule+ Text.Text+ -> Severity+ -> Text.Text+ -> (Linenumber -> Instruction Bash.ParsedBash -> Bool)+ -> Rule instructionRuleLine code severity message check = instructionRuleState code severity message checkAndDropState () where@@ -78,7 +93,7 @@ Text.Text -> Severity -> Text.Text- -> (state -> Linenumber -> Instruction Text.Text -> (state, Bool))+ -> (state -> Linenumber -> Instruction Bash.ParsedBash -> (state, Bool)) -> state -> Rule instructionRuleState code severity message = mapInstructions (Metadata code severity message)@@ -86,22 +101,21 @@ withState :: a -> b -> (a, b) withState st res = (st, res) -argumentsRule :: ([String] -> a) -> Arguments Text.Text -> a+argumentsRule :: (Bash.ParsedBash -> a) -> Arguments Bash.ParsedBash -> a argumentsRule applyRule args = case args of- ArgumentsText as -> applyRule . normalizeArgs $ as- ArgumentsList as -> applyRule . normalizeArgs $ as- where- normalizeArgs = words . Text.unpack+ ArgumentsText as -> applyRule as+ ArgumentsList as -> applyRule as -- Enforce rules on a dockerfile and return failed checks analyze :: [Rule] -> Dockerfile -> [RuleCheck]-analyze list dockerfile = filter failed $ concat [r dockerfile | r <- list]+analyze list dockerfile = filter failed $ concat [r parsedFile | r <- list] where failed RuleCheck {metadata = Metadata {code}, linenumber, success} = not success && not (wasIgnored code linenumber) wasIgnored c ln = not $ null [line | (line, codes) <- allIgnores, line == ln, c `elem` codes] allIgnores = ignored dockerfile+ parsedFile = map (fmap Bash.parseShell) dockerfile ignored :: Dockerfile -> [(Linenumber, [Text.Text])] ignored dockerfile =@@ -165,7 +179,7 @@ commentMetadata (ShellCheck.Interface.Comment severity code message) = Metadata (Text.pack ("SC" ++ show code)) severity (Text.pack message) -shellcheckBash :: Dockerfile -> [RuleCheck]+shellcheckBash :: ParsedFile -> [RuleCheck] shellcheckBash = concatMap check where check (InstructionPos (Run args) source linenumber) =@@ -173,32 +187,28 @@ check _ = [] applyRule source linenumber args = rmDup [RuleCheck m source linenumber False | m <- convert args]- convert args = [commentMetadata c | c <- shellcheck $ unwords args]+ convert args = [commentMetadata c | c <- Bash.shellcheck args] rmDup :: [RuleCheck] -> [RuleCheck] rmDup [] = [] rmDup (x:xs) = x : rmDup (filter (\y -> metadata x /= metadata y) xs) --- Split different bash commands-bashCommands :: [String] -> [[String]]-bashCommands = splitOneOf [";", "|", "&&"]--allFromImages :: Dockerfile -> [(Linenumber, BaseImage)]+allFromImages :: ParsedFile -> [(Linenumber, BaseImage)] allFromImages dockerfile = [(l, f) | (l, From f) <- instr] where instr = fmap (lineNumber &&& instruction) dockerfile -allAliasedImages :: Dockerfile -> [(Linenumber, ImageAlias)]+allAliasedImages :: ParsedFile -> [(Linenumber, ImageAlias)] allAliasedImages dockerfile = [(l, alias) | (l, Just alias) <- map extractAlias (allFromImages dockerfile)] where extractAlias (l, f) = (l, fromAlias f) -allImageNames :: Dockerfile -> [(Linenumber, Text.Text)]+allImageNames :: ParsedFile -> [(Linenumber, Text.Text)] allImageNames dockerfile = [(l, fromName baseImage) | (l, baseImage) <- allFromImages dockerfile] -- | Returns a list of all image aliases in FROM instructions that -- are defined before the given line number.-previouslyDefinedAliases :: Linenumber -> Dockerfile -> [Text.Text]+previouslyDefinedAliases :: Linenumber -> ParsedFile -> [Text.Text] previouslyDefinedAliases line dockerfile = [i | (l, ImageAlias i) <- allAliasedImages dockerfile, l < line] @@ -245,8 +255,8 @@ check _ = True -- Check if a command contains a program call in the Run instruction-usingProgram :: String -> [String] -> Bool-usingProgram prog args = or [True | cmd:_ <- bashCommands args, cmd == prog]+usingProgram :: String -> Bash.ParsedBash -> Bool+usingProgram prog args = not $ null [cmd | cmd <- Bash.findCommandNames args, cmd == prog] multipleCmds :: Rule multipleCmds = instructionRuleState code severity message check Nothing@@ -284,7 +294,8 @@ let newArgs = extractCommands args newState = Set.union state newArgs in withState newState (Set.size newState < 2)- extractCommands args = Set.fromList [w | w <- args, w == "curl" || w == "wget"]+ extractCommands args =+ Set.fromList [w | w <- Bash.findCommandNames args, w == "curl" || w == "wget"] invalidCmd :: Rule invalidCmd = instructionRule code severity message check@@ -294,10 +305,9 @@ message = "For some bash commands it makes no sense running them in a Docker container like `ssh`, \ \`vim`, `shutdown`, `service`, `ps`, `free`, `top`, `kill`, `mount`, `ifconfig`"- check (Run args) = argumentsRule applyRule args+ check (Run args) = argumentsRule detectInvalid args check _ = True- applyRule (arg:_) = arg `notElem` invalidCmds- applyRule _ = True+ detectInvalid args = null [arg | arg <- Bash.findCommandNames args, arg `elem` invalidCmds] invalidCmds = ["ssh", "vim", "shutdown", "service", "ps", "free", "top", "kill", "mount"] noRootUser :: Rule@@ -338,7 +348,7 @@ code = "DL3005" severity = ErrorC message = "Do not use apt-get upgrade or dist-upgrade"- check (Run args) = argumentsRule (not . isInfixOf ["apt-get", "upgrade"]) args+ check (Run args) = argumentsRule (Bash.noCommands (Bash.cmdHasArgs "apt-get" ["upgrade"])) args check _ = True noUntagged :: Rule@@ -371,19 +381,20 @@ message = "Pin versions in apt get install. Instead of `apt-get install <package>` use `apt-get \ \install <package>=<version>`"- check (Run args) = argumentsRule (\as -> and [versionFixed p | p <- aptGetPackages as]) args+ check (Run args) = argumentsRule (all versionFixed . aptGetPackages) args check _ = True versionFixed package = "=" `isInfixOf` package -aptGetPackages :: [String] -> [String]+aptGetPackages :: Bash.ParsedBash -> [String] aptGetPackages args =- concat- [ filter noOption (dropOptionsWithArg ["-t", "--target-release"] cmd)- | cmd <- bashCommands args- , isAptGetInstall cmd- ]+ [ arg+ | cmd <- dropTarget <$> Bash.findCommands args+ , arg <- Bash.getArgsNoFlags cmd+ , Bash.cmdHasArgs "apt-get" ["install"] cmd+ , arg /= "install"+ ] where- noOption arg = arg `notElem` ["apt-get", "install"] && not ("-" `isPrefixOf` arg)+ dropTarget = Bash.dropFlagArg ["t", "target-release"] aptGetCleanup :: Rule aptGetCleanup dockerfile = instructionRuleState code severity message check Nothing dockerfile@@ -398,13 +409,16 @@ -- 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 args) =- withState st (argumentsRule (applyRule line baseimage) args)+ withState st (argumentsRule (didNotForgetToCleanup line baseimage) args) check st _ _ = withState st True- applyRule line baseimage args+ -- Check all commands in the script for the presence of apt-get update+ -- If the command is there, then we need to verify that the user is also removing the lists folder+ didNotForgetToCleanup line baseimage args | not (hasUpdate args) || not (imageIsUsed line baseimage) = True | otherwise = hasCleanup args- hasCleanup cmd = ["rm", "-rf", "/var/lib/apt/lists/*"] `isInfixOf` cmd- hasUpdate cmd = ["apt-get", "update"] `isInfixOf` cmd+ hasCleanup args =+ any (Bash.cmdHasArgs "rm" ["-rf", "/var/lib/apt/lists/*"]) (Bash.findCommands args)+ hasUpdate args = any (Bash.cmdHasArgs "apt-get" ["update"]) (Bash.findCommands args) imageIsUsed line baseimage = isLastImage line baseimage || imageIsUsedLater line baseimage isLastImage line baseimage = case reverse (allFromImages dockerfile) of@@ -416,24 +430,15 @@ Just (ImageAlias alias) -> alias `elem` [i | (l, i) <- allImageNames dockerfile, l > line] -dropOptionsWithArg :: [String] -> [String] -> [String]-dropOptionsWithArg _ [] = []-dropOptionsWithArg os (x:xs)- | x `elem` os = dropOptionsWithArg os (drop 1 xs)- | otherwise = x : dropOptionsWithArg os xs- noApkUpgrade :: Rule noApkUpgrade = instructionRule code severity message check where code = "DL3017" severity = ErrorC message = "Do not use apk upgrade"- check (Run args) = argumentsRule (not . isInfixOf ["apk", "upgrade"]) args+ check (Run args) = argumentsRule (Bash.noCommands (Bash.cmdHasArgs "apk" ["upgrade"])) args check _ = True -isApkAdd :: [String] -> Bool-isApkAdd cmd = ["apk"] `isInfixOf` cmd && ["add"] `isInfixOf` cmd- apkAddVersionPinned :: Rule apkAddVersionPinned = instructionRule code severity message check where@@ -445,16 +450,16 @@ check _ = True versionFixed package = "=" `isInfixOf` package -apkAddPackages :: [String] -> [String]+apkAddPackages :: Bash.ParsedBash -> [String] apkAddPackages args =- concat- [ filter noOption (dropOptionsWithArg ["-t", "--virtual"] cmd)- | cmd <- bashCommands args- , isApkAdd cmd- ]+ [ arg+ | cmd <- dropTarget <$> Bash.findCommands args+ , arg <- Bash.getArgsNoFlags cmd+ , Bash.cmdHasArgs "apk" ["add"] cmd+ , arg /= "add"+ ] where- noOption arg = arg `notElem` options && not ("--" `isPrefixOf` arg)- options = ["apk", "add", "-q", "-p", "-v", "-f", "-t"]+ dropTarget = Bash.dropFlagArg ["t", "virtual"] apkAddNoCache :: Rule apkAddNoCache = instructionRule code severity message check@@ -464,9 +469,9 @@ message = "Use the `--no-cache` switch to avoid the need to use `--update` and remove \ \`/var/cache/apk/*` when done installing packages"- check (Run args) = argumentsRule (\as -> not (isApkAdd as) || hasNoCacheOption as) args+ check (Run args) = argumentsRule (Bash.noCommands forgotCacheOption) args check _ = True- hasNoCacheOption cmd = ["--no-cache"] `isInfixOf` cmd+ forgotCacheOption cmd = Bash.cmdHasArgs "apk" ["add"] cmd && not (Bash.hasFlag "no-cache" cmd) useAdd :: Rule useAdd = instructionRule code severity message check@@ -502,46 +507,27 @@ message = "Pin versions in pip. Instead of `pip install <package>` use `pip install \ \<package>==<version>`"- check (Run args) =- argumentsRule (\as -> not (isPipInstall as) || all versionFixed (packages as)) args+ check (Run args) = argumentsRule (Bash.noCommands forgotToPinVersion) args check _ = True- isPipInstall :: [String] -> Bool+ forgotToPinVersion cmd = isPipInstall cmd && not (all versionFixed (packages cmd))+ -- Check if the command is a pip* install command, and that specific pacakges are being listed isPipInstall cmd =- case getInstallArgs cmd of- Nothing -> False- Just args -> not (["-r"] `isInfixOf` args || ["."] `isInfixOf` args)- packages cmd =- case getInstallArgs cmd of- Nothing -> []- Just args -> findPackages args- getInstallArgs = stripInstallPrefix isInstallCommand- isInstallCommand ('p':'i':'p':_) = True- isInstallCommand _ = False+ case Bash.getCommandName cmd of+ Just ('p':'i':'p':_) -> relevantInstall cmd+ _ -> False+ -- If the user is installing requirements from a file or just the local module, then we are not interested+ -- in running this rule+ relevantInstall cmd =+ ["install"] `isInfixOf` Bash.getAllArgs cmd &&+ not (["-r"] `isInfixOf` Bash.getAllArgs cmd || ["."] `isInfixOf` Bash.getAllArgs cmd)+ packages cmd = stripInstallPrefix (Bash.getArgsNoFlags cmd) versionFixed package = hasVersionSymbol package || isVersionedGit package isVersionedGit package = "git+http" `isInfixOf` package && "@" `isInfixOf` package- versionSymbols = ["==", ">=", "<=", ">", "<", "!="]+ versionSymbols = ["==", ">=", "<=", ">", "<", "!=", "~=", "==="] hasVersionSymbol package = or [s `isInfixOf` package | s <- versionSymbols] --- | Returns all the packages after pip install-findPackages :: [String] -> [String]-findPackages = takeWhile (not . isEnd) . dropWhile isFlag- where- isEnd word = isFlag word || word `elem` ["&&", "||", ";", "|"]- isFlag ('-':_) = True- isFlag _ = False--stripInstallPrefix :: (String -> Bool) -> [String] -> Maybe [String]-stripInstallPrefix isCommand args =- if ["install"] `isPrefixOf` dropUntilInstall- then dropUntilInstall |> drop 1 |> Just- else Nothing- where- dropUntilInstall =- args |> -- using a pipiline for readability- dropWhile (not . isCommand) |>- drop 1 |>- dropWhile (/= "install")- a |> f = f a+stripInstallPrefix :: [String] -> [String]+stripInstallPrefix = dropWhile (== "install") {-| Rule for pinning NPM packages to version, tag, or commit@@ -561,13 +547,11 @@ message = "Pin versions in npm. Instead of `npm install <package>` use `npm install \ \<package>@<version>`"- check (Run args) = argumentsRule (all versionFixed . packages) args+ check (Run args) = argumentsRule (Bash.noCommands forgotToPinVersion) args check _ = True- packages cmd =- case getInstallArgs cmd of- Nothing -> []- Just args -> findPackages args- getInstallArgs = stripInstallPrefix (== "npm")+ forgotToPinVersion cmd = isNpmInstall cmd && not (all versionFixed (packages cmd))+ isNpmInstall = Bash.cmdHasArgs "npm" ["install"]+ packages cmd = stripInstallPrefix (Bash.getArgsNoFlags cmd) versionFixed package = if hasGitPrefix package then isVersionedGit package@@ -582,21 +566,20 @@ then dropWhile ('/' <) pkg else pkg -isAptGetInstall :: [String] -> Bool-isAptGetInstall cmd = ["apt-get"] `isInfixOf` cmd && ["install"] `isInfixOf` cmd- aptGetYes :: Rule aptGetYes = instructionRule code severity message check where code = "DL3014" severity = WarningC message = "Use the `-y` switch to avoid manual input `apt-get -y install <package>`"- check (Run args) = argumentsRule (\as -> not (isAptGetInstall as) || hasYesOption as) args+ check (Run args) = argumentsRule (Bash.noCommands forgotAptYesOption) args check _ = True+ forgotAptYesOption cmd = isAptGetInstall cmd && not (hasYesOption cmd)+ isAptGetInstall = Bash.cmdHasArgs "apt-get" ["install"] hasYesOption cmd =- ["-y"] `isInfixOf` cmd ||- ["--yes"] `isInfixOf` cmd || ["-qq"] `isInfixOf` cmd || startsWithYesFlag cmd- startsWithYesFlag cmd = True `elem` ["-y" `isInfixOf` arg | arg <- cmd]+ "y" `elem` allFlags cmd ||+ "yes" `elem` allFlags cmd || length (filter (== "q") (allFlags cmd)) > 1+ allFlags cmd = snd <$> Bash.getAllFlags cmd aptGetNoRecommends :: Rule aptGetNoRecommends = instructionRule code severity message check@@ -604,10 +587,11 @@ code = "DL3015" severity = InfoC message = "Avoid additional packages by specifying `--no-install-recommends`"- check (Run args) =- argumentsRule (\as -> not (isAptGetInstall as) || hasNoRecommendsOption as) args+ check (Run args) = argumentsRule (Bash.noCommands forgotNoInstallRecommends) args check _ = True- hasNoRecommendsOption cmd = ["--no-install-recommends"] `isInfixOf` cmd+ forgotNoInstallRecommends cmd = isAptGetInstall cmd && not (hasRecommendsOption cmd)+ isAptGetInstall = Bash.cmdHasArgs "apt-get" ["install"]+ hasRecommendsOption = Bash.hasFlag "no-install-recommends" isArchive :: Text.Text -> Bool isArchive path =@@ -695,6 +679,5 @@ code = "DL4005" severity = WarningC message = "Use SHELL to change the default shell"- check (Run args) = argumentsRule (not . any shellSymlink . bashCommands) args+ check (Run args) = argumentsRule (Bash.noCommands (Bash.cmdHasArgs "ln" ["/bin/sh"])) args check _ = True- shellSymlink args = usingProgram "ln" args && isInfixOf ["/bin/sh"] args
test/Spec.hs view
@@ -6,6 +6,7 @@ import Hadolint.Rules import Language.Docker.Parser+import Language.Docker.Syntax import Data.Semigroup ((<>)) import qualified Data.Text as Text @@ -23,7 +24,7 @@ "FROM hub.docker.io/debian@sha256:\ \7959ed6f7e35f8b1aaa06d1d8259d4ee25aa85a086d5c125480c333183f9deeb" it "explicit tagged with name" $- ruleCatchesNot noLatestTag "FROM debian:jessie AS builder"+ ruleCatchesNot noLatestTag "FROM debian:jessie AS builder" it "local aliases are OK to be untagged" $ let dockerFile = [ "FROM golang:1.9.3-alpine3.7 AS build"@@ -33,7 +34,9 @@ , "FROM alpine:3.7" , "RUN baz" ]- in ruleCatchesNot noUntagged $ Text.unlines dockerFile+ 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"@@ -43,33 +46,49 @@ , "FROM alpine:3.7" , "RUN baz" ]- in ruleCatches noUntagged $ Text.unlines dockerFile+ in do+ ruleCatches noUntagged $ Text.unlines dockerFile+ onBuildRuleCatches noUntagged $ Text.unlines dockerFile -- describe "no root or sudo rules" $ do- it "sudo" $ ruleCatches noSudo "RUN sudo apt-get update"+ it "sudo" $ do+ ruleCatches noSudo "RUN sudo apt-get update"+ onBuildRuleCatches noSudo "RUN sudo apt-get update" it "no root" $ ruleCatches noRootUser "USER root" it "no root" $ ruleCatchesNot noRootUser "USER foo" it "no root UID" $ ruleCatches noRootUser "USER 0" it "no root:root" $ ruleCatches noRootUser "USER root:root" it "no root UID:GID" $ ruleCatches noRootUser "USER 0:0"- it "install sudo" $ ruleCatchesNot noSudo "RUN apt-get install sudo"- it "sudo chained programs" $+ 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" $ ruleCatches invalidCmd "RUN top"- it "install ssh" $ ruleCatchesNot invalidCmd "RUN apt-get install ssh"+ 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 "apt-get rules" $ do- it "apt upgrade" $ ruleCatches noAptGetUpgrade "RUN apt-get update && apt-get upgrade"- it "apt-get version pinning" $+ it "apt 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 ruleCatches aptGetCleanup $ Text.unlines dockerFile+ 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"@@ -77,7 +96,9 @@ , "FROM scratch" , "RUN echo hey!" ]- in ruleCatchesNot aptGetCleanup $ Text.unlines dockerFile+ 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"@@ -85,7 +106,9 @@ , "FROM scratch" , "RUN apt-get update && apt-get install python" ]- in ruleCatches aptGetCleanup $ Text.unlines dockerFile+ 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"@@ -93,15 +116,19 @@ , "FROM foo" , "RUN hey!" ]- in ruleCatches aptGetCleanup $ Text.unlines dockerFile- it "now warn apt-get cleanup in intermediate stage that cleans lists" $+ 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 ruleCatchesNot aptGetCleanup $ Text.unlines dockerFile+ 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"@@ -109,13 +136,17 @@ , "FROM scratch" , "RUN hey!" ]- in ruleCatchesNot aptGetCleanup $ Text.unlines dockerFile+ 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 ruleCatchesNot aptGetCleanup $ Text.unlines dockerFile+ in do+ ruleCatchesNot aptGetCleanup $ Text.unlines dockerFile+ onBuildRuleCatchesNot aptGetCleanup $ Text.unlines dockerFile it "apt-get pinned chained" $ let dockerFile =@@ -123,7 +154,10 @@ , " && apt-get -yqq --no-install-recommends install nodejs=0.10 \\" , " && rm -rf /var/lib/apt/lists/*" ]- in ruleCatchesNot aptGetVersionPinned $ Text.unlines dockerFile+ 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 \\"@@ -132,21 +166,31 @@ , "git=1:2.5.0* \\" , "ruby=1:2.1.*" ]- in ruleCatchesNot aptGetVersionPinned $ Text.unlines dockerFile+ 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" $ ruleCatches noApkUpgrade "RUN apk update && apk upgrade"- it "apk add version pinning single" $ ruleCatches apkAddVersionPinned "RUN apk add flex"- it "apk add no version pinning single" $+ 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 ruleCatchesNot apkAddVersionPinned $ Text.unlines dockerFile+ 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 \\"@@ -155,7 +199,9 @@ , "python2=2.7.13-r1 \\" , "libbz2=1.0.6-r5" ]- in ruleCatchesNot apkAddVersionPinned $ Text.unlines dockerFile+ 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 \\"@@ -164,10 +210,15 @@ , "python2=2.7.13-r1 \\" , "libbz2=1.0.6-r5" ]- in ruleCatches apkAddVersionPinned $ Text.unlines dockerFile- it "apk add with --no-cache" $ ruleCatches apkAddNoCache "RUN apk add flex=2.6.4-r1"- it "apk add without --no-cache" $+ 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 \\"@@ -177,139 +228,226 @@ , "&& python setup.py install \\" , "&& apk del build-dependencies" ]- in ruleCatchesNot apkAddVersionPinned $ Text.unlines dockerFile+ 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" $+ it "pip2 version not pinned" $ do ruleCatches pipVersionPinned "RUN pip2 install MySQL_python"- it "pip3 version not pinned" $+ onBuildRuleCatches pipVersionPinned "RUN pip2 install MySQL_python"+ it "pip3 version not pinned" $ do ruleCatches pipVersionPinned "RUN pip3 install MySQL_python"- it "pip3 version pinned" $+ onBuildRuleCatches pipVersionPinned "RUN pip2 install MySQL_python"+ it "pip3 version pinned" $ do ruleCatchesNot pipVersionPinned "RUN pip3 install MySQL_python==1.2.2"- it "pip install requirements" $+ onBuildRuleCatchesNot pipVersionPinned "RUN pip3 install MySQL_python==1.2.2"+ it "pip install requirements" $ do ruleCatchesNot pipVersionPinned "RUN pip install -r requirements.txt"- it "pip install use setup.py" $+ onBuildRuleCatchesNot pipVersionPinned "RUN pip install -r requirements.txt"+ it "pip install use setup.py" $ do ruleCatchesNot pipVersionPinned "RUN pip install ."- it "pip version not pinned" $+ onBuildRuleCatchesNot pipVersionPinned "RUN pip install ."+ it "pip version not pinned" $ do ruleCatches pipVersionPinned "RUN pip install MySQL_python"- it "pip version pinned" $+ onBuildRuleCatches pipVersionPinned "RUN pip install MySQL_python"+ it "pip version pinned" $ do ruleCatchesNot pipVersionPinned "RUN pip install MySQL_python==1.2.2"- it "pip version pinned with flag" $+ 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" $ do ruleCatchesNot pipVersionPinned "RUN pip install --ignore-installed MySQL_python==1.2.2"- it "pip version pinned with python -m" $+ onBuildRuleCatchesNot pipVersionPinned "RUN pip install --ignore-installed MySQL_python==1.2.2"+ it "pip version pinned with python -m" $ do ruleCatchesNot pipVersionPinned "RUN python -m pip install example==1.2.2"- it "pip install git" $+ onBuildRuleCatchesNot pipVersionPinned "RUN python -m pip install example==1.2.2"+ it "pip install git" $ do ruleCatchesNot pipVersionPinned "RUN pip install git+https://github.com/rtfd/r-ext.git@0.6-alpha#egg=r-ext"- it "pip install unversioned git" $+ 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"- it "pip install upper bound" $+ 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'"- it "pip install lower bound" $+ onBuildRuleCatchesNot pipVersionPinned "RUN pip install 'alabaster>=0.7'"+ it "pip install lower bound" $ do ruleCatchesNot pipVersionPinned "RUN pip install 'alabaster<0.7'"- it "pip install excluded version" $+ onBuildRuleCatchesNot pipVersionPinned "RUN pip install 'alabaster<0.7'"+ it "pip install excluded version" $ do ruleCatchesNot pipVersionPinned "RUN pip install 'alabaster!=0.7'"- it "pip install user directory" $+ 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"- it "pip install no pip version check" $+ 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"- it "pip install no cache dir" $+ onBuildRuleCatchesNot+ pipVersionPinned+ "RUN pip install MySQL_python==1.2.2 --disable-pip-version-check"+ 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" -- describe "npm pinning" $ do- it "version pinned in package.json" $ ruleCatchesNot npmVersionPinned "RUN npm install"- it "version pinned in package.json with arguments" $+ 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"- it "version pinned" $ ruleCatchesNot npmVersionPinned "RUN npm install express@4.1.1"- it "version pinned with scope" $+ 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\""- it "version pinned multiple packages" $+ 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"- it "version pinned with --global" $+ 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\""- it "version pinned with -g" $+ 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\""- it "commit pinned for git+ssh" $+ onBuildRuleCatchesNot npmVersionPinned "RUN npm install -g express@\"4.1.1\""+ it "commit pinned for git+ssh" $ do ruleCatchesNot npmVersionPinned "RUN npm install git+ssh://git@github.com:npm/npm.git#v1.0.27"- it "commit pinned for git+http" $+ 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"- it "commit pinned for git+https" $+ 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"- it "commit pinned for git" $+ 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"+ --version range is not supported- --it "version pinned with scope" $- -- ruleCatchesNot npmVersionPinned "RUN npm install @myorg/privatepackage@\">=0.1.0 <0.2.0\""- it "version not pinned" $ ruleCatches npmVersionPinned "RUN npm install express"- it "version not pinned with scope" $+ 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"- it "version not pinned multiple packages" $+ onBuildRuleCatches npmVersionPinned "RUN npm install @myorg/privatepackage"+ it "version not pinned multiple packages" $ do ruleCatches npmVersionPinned "RUN npm install express sax@0.1.1"- it "version not pinned with --global" $+ onBuildRuleCatches npmVersionPinned "RUN npm install express sax@0.1.1"+ it "version not pinned with --global" $ do ruleCatches npmVersionPinned "RUN npm install --global express"- it "commit not pinned for git+ssh" $+ 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"- it "commit not pinned for git+http" $+ 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"- it "commit not pinned for git+https" $+ 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"- it "commit not pinned for 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" $ ruleCatches useShell "RUN ln -sfv /bin/bash /bin/sh"- it "RUN ln with unrelated symlinks" $+ 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"- it "RUN ln with multiple acceptable commands" $+ 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 "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" $ ruleCatches aptGetYes "RUN apt-get install python"- it "apt-get yes shortflag" $ ruleCatchesNot aptGetYes "RUN apt-get install -yq python"- it "apt-get yes quiet level 2 implies -y" $+ 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"- it "apt-get yes different pos" $+ onBuildRuleCatchesNot aptGetYes "RUN apt-get install -qq python"+ it "apt-get yes different pos" $ do ruleCatchesNot aptGetYes "RUN apt-get install -y python"- it "apt-get with auto yes" $ ruleCatchesNot aptGetYes "RUN apt-get -y install python"- it "apt-get with auto expanded yes" $+ 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"- it "apt-get install recommends" $+ onBuildRuleCatchesNot aptGetYes "RUN apt-get --yes install python"+ it "apt-get install recommends" $ do ruleCatchesNot aptGetNoRecommends "RUN apt-get install --no-install-recommends python"- it "apt-get no install recommends" $+ onBuildRuleCatchesNot+ aptGetNoRecommends+ "RUN apt-get install --no-install-recommends python"+ it "apt-get no install recommends" $ do ruleCatches aptGetNoRecommends "RUN apt-get install python"- it "apt-get no install recommends" $+ onBuildRuleCatches aptGetNoRecommends "RUN apt-get install python"+ it "apt-get no install recommends" $ do ruleCatches aptGetNoRecommends "RUN apt-get -y install python"- it "apt-get version" $+ onBuildRuleCatches aptGetNoRecommends "RUN apt-get -y install python"+ it "apt-get version" $ do ruleCatchesNot aptGetVersionPinned "RUN apt-get install -y python=1.2.2"- it "apt-get pinned" $+ onBuildRuleCatchesNot aptGetVersionPinned "RUN apt-get install -y python=1.2.2"+ 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 &&\\"@@ -320,7 +458,9 @@ \openjdk-8-jdk=8u131-b11-1~bpo8+1 &&\\" , " rm -rf /var/lib/apt/lists/*" ]- in ruleCatchesNot aptGetVersionPinned $ Text.unlines dockerFile+ 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"@@ -464,6 +604,17 @@ 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+ -- Assert a failed check exists for rule ruleCatches :: HasCallStack => Rule -> Text.Text -> Assertion ruleCatches rule s = assertChecks rule s f@@ -472,7 +623,19 @@ assertEqual "No check for rule found" 1 $ length checks assertBool "Incorrect line number for result" $ null [c | c <- checks, linenumber c <= 0] +onBuildRuleCatches :: HasCallStack => Rule -> Text.Text -> Assertion+onBuildRuleCatches rule s = assertOnBuildChecks rule s f+ where+ f checks = do+ assertEqual "No check for rule found" 1 $ length checks+ assertBool "Incorrect line number for result" $ null [c | c <- checks, linenumber c <= 0]+ ruleCatchesNot :: HasCallStack => Rule -> Text.Text -> Assertion ruleCatchesNot rule s = assertChecks rule s f+ where+ f checks = assertEqual "Found check of rule" 0 $ length checks++onBuildRuleCatchesNot :: HasCallStack => Rule -> Text.Text -> Assertion+onBuildRuleCatchesNot rule s = assertOnBuildChecks rule s f where f checks = assertEqual "Found check of rule" 0 $ length checks