packages feed

hadolint 1.23.0 → 2.0.0

raw patch · 85 files changed

+5069/−3147 lines, 85 filesdep +Cabaldep +foldldep +network-uridep −asyncPVP ok

version bump matches the API change (PVP)

Dependencies added: Cabal, foldl, network-uri, parsec, semver, spdx, time, timerep

Dependencies removed: async

API changes (from Hackage documentation)

- Hadolint.Formatter.Checkstyle: printResult :: (VisualStream s, TraversableStream s, ShowErrorComponent e) => Result s e -> IO ()
- Hadolint.Formatter.Codacy: printResult :: (VisualStream s, TraversableStream s, ShowErrorComponent e) => Result s e -> IO ()
- Hadolint.Formatter.Codeclimate: printGitlabResult :: (VisualStream s, TraversableStream s, ShowErrorComponent e) => Result s e -> IO ()
- Hadolint.Formatter.Codeclimate: printResult :: (VisualStream s, TraversableStream s, ShowErrorComponent e) => Result s e -> IO ()
- Hadolint.Formatter.Format: instance GHC.Base.Monoid (Hadolint.Formatter.Format.Result s e)
- Hadolint.Formatter.Format: instance GHC.Base.Semigroup (Hadolint.Formatter.Format.Result s e)
- Hadolint.Formatter.Json: printResult :: (VisualStream s, TraversableStream s, ShowErrorComponent e) => Result s e -> IO ()
- Hadolint.Formatter.TTY: formatChecks :: Functor f => f RuleCheck -> Bool -> f Text
- Hadolint.Formatter.TTY: printResult :: (VisualStream s, TraversableStream s, ShowErrorComponent e) => Result s e -> Bool -> IO ()
- Hadolint.Lint: Checkstyle :: OutputFormat
- Hadolint.Lint: Codacy :: OutputFormat
- Hadolint.Lint: CodeclimateJson :: OutputFormat
- Hadolint.Lint: GitlabCodeclimateJson :: OutputFormat
- Hadolint.Lint: Json :: OutputFormat
- Hadolint.Lint: TTY :: OutputFormat
- Hadolint.Lint: analyzeAll :: RulesConfig -> Dockerfile -> [RuleCheck]
- Hadolint.Lint: analyzeEither :: RulesConfig -> Either t Dockerfile -> [RuleCheck]
- Hadolint.Lint: data OutputFormat
- Hadolint.Lint: instance GHC.Classes.Eq Hadolint.Lint.OutputFormat
- Hadolint.Lint: instance GHC.Show.Show Hadolint.Lint.OutputFormat
- Hadolint.Lint: printResults :: OutputFormat -> Bool -> Result Text DockerfileError -> IO ()
- Hadolint.Lint: shallSkipErrorStatus :: OutputFormat -> Bool
- Hadolint.Rules: DLErrorC :: DLSeverity
- Hadolint.Rules: DLIgnoreC :: DLSeverity
- Hadolint.Rules: DLInfoC :: DLSeverity
- Hadolint.Rules: DLStyleC :: DLSeverity
- Hadolint.Rules: DLWarningC :: DLSeverity
- Hadolint.Rules: Metadata :: Text -> DLSeverity -> Text -> Metadata
- Hadolint.Rules: RuleCheck :: Metadata -> Filename -> Linenumber -> Bool -> RuleCheck
- Hadolint.Rules: RulesConfig :: Set Registry -> RulesConfig
- Hadolint.Rules: [allowedRegistries] :: RulesConfig -> Set Registry
- Hadolint.Rules: [code] :: Metadata -> Text
- Hadolint.Rules: [filename] :: RuleCheck -> Filename
- Hadolint.Rules: [linenumber] :: RuleCheck -> Linenumber
- Hadolint.Rules: [message] :: Metadata -> Text
- Hadolint.Rules: [metadata] :: RuleCheck -> Metadata
- Hadolint.Rules: [severity] :: Metadata -> DLSeverity
- Hadolint.Rules: [success] :: RuleCheck -> Bool
- Hadolint.Rules: absoluteWorkdir :: Rule
- Hadolint.Rules: aliasMustBe :: (Text -> Bool) -> Instruction a -> Bool
- Hadolint.Rules: allAliasedImages :: ParsedFile -> [(Linenumber, ImageAlias)]
- Hadolint.Rules: allFromImages :: ParsedFile -> [(Linenumber, BaseImage)]
- Hadolint.Rules: allImageNames :: ParsedFile -> [(Linenumber, Text)]
- Hadolint.Rules: analyze :: [Rule] -> Dockerfile -> [RuleCheck]
- Hadolint.Rules: apkAddNoCache :: Rule
- Hadolint.Rules: apkAddPackages :: ParsedShell -> [Text]
- Hadolint.Rules: apkAddVersionPinned :: Rule
- Hadolint.Rules: aptGetCleanup :: Rule
- Hadolint.Rules: aptGetNoRecommends :: Rule
- Hadolint.Rules: aptGetPackages :: ParsedShell -> [Text]
- Hadolint.Rules: aptGetVersionPinned :: Rule
- Hadolint.Rules: aptGetYes :: Rule
- Hadolint.Rules: argumentsRule :: (ParsedShell -> a) -> Arguments ParsedShell -> a
- Hadolint.Rules: commentMetadata :: PositionedComment -> Metadata
- Hadolint.Rules: copyEndingSlash :: Rule
- Hadolint.Rules: copyFromAnother :: Rule
- Hadolint.Rules: copyFromExists :: Rule
- Hadolint.Rules: copyInsteadAdd :: Rule
- Hadolint.Rules: data DLSeverity
- Hadolint.Rules: data Metadata
- Hadolint.Rules: data RuleCheck
- Hadolint.Rules: dnfCleanup :: Rule
- Hadolint.Rules: dnfPackages :: ParsedShell -> [Text]
- Hadolint.Rules: dnfVersionPinned :: Rule
- Hadolint.Rules: dnfYes :: Rule
- Hadolint.Rules: fromAlias :: BaseImage -> Maybe ImageAlias
- Hadolint.Rules: fromAliasUnique :: Rule
- Hadolint.Rules: fromName :: BaseImage -> Text
- Hadolint.Rules: gemVersionPinned :: Rule
- Hadolint.Rules: gems :: ParsedShell -> [Text]
- Hadolint.Rules: hasNoMaintainer :: Rule
- Hadolint.Rules: ignored :: Dockerfile -> [(Linenumber, [Text])]
- Hadolint.Rules: instance Control.DeepSeq.NFData Hadolint.Rules.DLSeverity
- Hadolint.Rules: instance GHC.Base.Monoid Hadolint.Rules.RulesConfig
- Hadolint.Rules: instance GHC.Base.Semigroup Hadolint.Rules.RulesConfig
- Hadolint.Rules: instance GHC.Classes.Eq Hadolint.Rules.DLSeverity
- Hadolint.Rules: instance GHC.Classes.Eq Hadolint.Rules.Metadata
- Hadolint.Rules: instance GHC.Classes.Eq Hadolint.Rules.RuleCheck
- Hadolint.Rules: instance GHC.Classes.Eq Hadolint.Rules.RulesConfig
- Hadolint.Rules: instance GHC.Classes.Ord Hadolint.Rules.DLSeverity
- Hadolint.Rules: instance GHC.Classes.Ord Hadolint.Rules.RuleCheck
- Hadolint.Rules: instance GHC.Generics.Generic Hadolint.Rules.DLSeverity
- Hadolint.Rules: instance GHC.Show.Show Hadolint.Rules.DLSeverity
- Hadolint.Rules: instance GHC.Show.Show Hadolint.Rules.Metadata
- Hadolint.Rules: instance GHC.Show.Show Hadolint.Rules.RuleCheck
- Hadolint.Rules: instance GHC.Show.Show Hadolint.Rules.RulesConfig
- Hadolint.Rules: instructionRule :: Text -> DLSeverity -> Text -> (Instruction ParsedShell -> Bool) -> Rule
- Hadolint.Rules: instructionRuleLine :: Text -> DLSeverity -> Text -> SimpleCheckerWithLine -> Rule
- Hadolint.Rules: instructionRuleState :: Text -> DLSeverity -> Text -> SimpleCheckerWithState state -> state -> Rule
- Hadolint.Rules: invalidCmd :: Rule
- Hadolint.Rules: invalidPort :: Rule
- Hadolint.Rules: isArchive :: Text -> Bool
- Hadolint.Rules: isPipInstall :: Command -> Bool
- Hadolint.Rules: isPipWrapper :: Command -> Bool
- Hadolint.Rules: isUrl :: Text -> Bool
- Hadolint.Rules: link :: Metadata -> Text
- Hadolint.Rules: mapInstructions :: CheckerWithState state -> state -> Rule
- Hadolint.Rules: multipleCmds :: Rule
- Hadolint.Rules: multipleEntrypoints :: Rule
- Hadolint.Rules: newtype RulesConfig
- Hadolint.Rules: noApkUpgrade :: Rule
- Hadolint.Rules: noApt :: Rule
- Hadolint.Rules: noAptGetUpgrade :: Rule
- Hadolint.Rules: noCd :: Rule
- Hadolint.Rules: noDnfUpdate :: Rule
- Hadolint.Rules: noIllegalInstructionInOnbuild :: Rule
- Hadolint.Rules: noLatestTag :: Rule
- Hadolint.Rules: noPlatformFlag :: Rule
- Hadolint.Rules: noRootUser :: Rule
- Hadolint.Rules: noSelfreferencingEnv :: Rule
- Hadolint.Rules: noSudo :: Rule
- Hadolint.Rules: noUntagged :: Rule
- Hadolint.Rules: noYumUpdate :: Rule
- Hadolint.Rules: noZypperUpdate :: Rule
- Hadolint.Rules: npmVersionPinned :: Rule
- Hadolint.Rules: optionalRules :: RulesConfig -> [Rule]
- Hadolint.Rules: pipNoCacheDir :: Rule
- Hadolint.Rules: pipVersionPinned :: Rule
- Hadolint.Rules: previouslyDefinedAliases :: Linenumber -> ParsedFile -> [Text]
- Hadolint.Rules: registryIsAllowed :: Set Registry -> Rule
- Hadolint.Rules: rules :: [Rule]
- Hadolint.Rules: shellcheck :: Rule
- Hadolint.Rules: stripInstallPrefix :: [Text] -> [Text]
- Hadolint.Rules: type CheckerWithState state = state -> Linenumber -> Instruction ParsedShell -> (state, [Metadata])
- Hadolint.Rules: type IgnoreRuleParser = Parsec Void Text
- Hadolint.Rules: type ParsedFile = [InstructionPos ParsedShell]
- Hadolint.Rules: type Rule = ParsedFile -> [RuleCheck]
- Hadolint.Rules: type SimpleCheckerWithLine = (Linenumber -> Instruction ParsedShell -> Bool)
- Hadolint.Rules: type SimpleCheckerWithState state = state -> Linenumber -> Instruction ParsedShell -> (state, Bool)
- Hadolint.Rules: useAdd :: Rule
- Hadolint.Rules: useJsonArgs :: Rule
- Hadolint.Rules: usePipefail :: Rule
- Hadolint.Rules: useShell :: Rule
- Hadolint.Rules: usingProgram :: Text -> ParsedShell -> Bool
- Hadolint.Rules: wgetOrCurl :: Rule
- Hadolint.Rules: withState :: a -> b -> (a, b)
- Hadolint.Rules: yumCleanup :: Rule
- Hadolint.Rules: yumPackages :: ParsedShell -> [Text]
- Hadolint.Rules: yumVersionPinned :: Rule
- Hadolint.Rules: yumYes :: Rule
- Hadolint.Rules: zypperCleanup :: Rule
- Hadolint.Rules: zypperPackages :: ParsedShell -> [Text]
- Hadolint.Rules: zypperVersionPinned :: Rule
- Hadolint.Rules: zypperYes :: Rule
+ Hadolint: Checkstyle :: OutputFormat
+ Hadolint: Codacy :: OutputFormat
+ Hadolint: CodeclimateJson :: OutputFormat
+ Hadolint: GitlabCodeclimateJson :: OutputFormat
+ Hadolint: Json :: OutputFormat
+ Hadolint: TTY :: OutputFormat
+ Hadolint: [fileName] :: Result s e -> Text
+ Hadolint: data OutputFormat
+ Hadolint: instance GHC.Classes.Eq Hadolint.OutputFormat
+ Hadolint: instance GHC.Show.Show Hadolint.OutputFormat
+ Hadolint: printResults :: Foldable f => OutputFormat -> Bool -> f (Result Text DockerfileError) -> IO ()
+ Hadolint: shallSkipErrorStatus :: OutputFormat -> Bool
+ Hadolint.Config: [labelSchemaConfig] :: ConfigFile -> Maybe LabelSchema
+ Hadolint.Config: [strictLabelSchema] :: ConfigFile -> Maybe Bool
+ Hadolint.Config: instance GHC.Base.Monoid Hadolint.Config.OverrideConfig
+ Hadolint.Config: instance GHC.Base.Semigroup Hadolint.Config.OverrideConfig
+ Hadolint.Formatter.Checkstyle: printResults :: (Foldable f, VisualStream s, TraversableStream s, ShowErrorComponent e) => f (Result s e) -> IO ()
+ Hadolint.Formatter.Codacy: printResults :: (Foldable f, VisualStream s, TraversableStream s, ShowErrorComponent e) => f (Result s e) -> IO ()
+ Hadolint.Formatter.Codeclimate: formatGitlabResult :: (VisualStream s, TraversableStream s, ShowErrorComponent e) => Result s e -> Seq FingerprintIssue
+ Hadolint.Formatter.Codeclimate: printGitlabResults :: (Foldable f, VisualStream s, TraversableStream s, ShowErrorComponent e) => f (Result s e) -> IO ()
+ Hadolint.Formatter.Codeclimate: printResults :: (VisualStream s, TraversableStream s, ShowErrorComponent e, Foldable f) => f (Result s e) -> IO ()
+ Hadolint.Formatter.Format: [fileName] :: Result s e -> Text
+ Hadolint.Formatter.Json: printResults :: (VisualStream s, TraversableStream s, ShowErrorComponent e, Foldable f) => f (Result s e) -> IO ()
+ Hadolint.Formatter.TTY: formatCheck :: Bool -> Text -> CheckFailure -> Text
+ Hadolint.Formatter.TTY: printResults :: (VisualStream s, TraversableStream s, ShowErrorComponent e, Foldable f) => f (Result s e) -> Bool -> IO ()
+ Hadolint.Ignore: ignored :: Fold (InstructionPos Text) (IntMap (Set RuleCode))
+ Hadolint.Lint: analyze :: LintOptions -> Dockerfile -> Seq CheckFailure
+ Hadolint.Lint: instance GHC.Base.Monoid Hadolint.Lint.LintOptions
+ Hadolint.Lint: instance GHC.Base.Semigroup Hadolint.Lint.LintOptions
+ Hadolint.Lint: lintIO :: LintOptions -> NonEmpty FilePath -> IO (NonEmpty (Result Text DockerfileError))
+ Hadolint.Process: RulesConfig :: Set Registry -> LabelSchema -> Bool -> RulesConfig
+ Hadolint.Process: [allowedRegistries] :: RulesConfig -> Set Registry
+ Hadolint.Process: [labelSchema] :: RulesConfig -> LabelSchema
+ Hadolint.Process: [strictLabels] :: RulesConfig -> Bool
+ Hadolint.Process: data RulesConfig
+ Hadolint.Process: instance GHC.Base.Monoid Hadolint.Process.RulesConfig
+ Hadolint.Process: instance GHC.Base.Semigroup Hadolint.Process.RulesConfig
+ Hadolint.Process: instance GHC.Classes.Eq Hadolint.Process.RulesConfig
+ Hadolint.Process: instance GHC.Show.Show Hadolint.Process.RulesConfig
+ Hadolint.Process: run :: RulesConfig -> [InstructionPos Text] -> Failures
+ Hadolint.Rule: (|>) :: a -> (a -> b) -> b
+ Hadolint.Rule: CheckFailure :: RuleCode -> DLSeverity -> Text -> Linenumber -> CheckFailure
+ Hadolint.Rule: DLErrorC :: DLSeverity
+ Hadolint.Rule: DLIgnoreC :: DLSeverity
+ Hadolint.Rule: DLInfoC :: DLSeverity
+ Hadolint.Rule: DLStyleC :: DLSeverity
+ Hadolint.Rule: DLWarningC :: DLSeverity
+ Hadolint.Rule: GitHash :: LabelType
+ Hadolint.Rule: RawText :: LabelType
+ Hadolint.Rule: Rfc3339 :: LabelType
+ Hadolint.Rule: RuleCode :: Text -> RuleCode
+ Hadolint.Rule: SemVer :: LabelType
+ Hadolint.Rule: Spdx :: LabelType
+ Hadolint.Rule: State :: Failures -> a -> State a
+ Hadolint.Rule: Url :: LabelType
+ Hadolint.Rule: [code] :: CheckFailure -> RuleCode
+ Hadolint.Rule: [failures] :: State a -> Failures
+ Hadolint.Rule: [line] :: CheckFailure -> Linenumber
+ Hadolint.Rule: [message] :: CheckFailure -> Text
+ Hadolint.Rule: [severity] :: CheckFailure -> DLSeverity
+ Hadolint.Rule: [state] :: State a -> a
+ Hadolint.Rule: [unRuleCode] :: RuleCode -> Text
+ Hadolint.Rule: addFail :: CheckFailure -> State a -> State a
+ Hadolint.Rule: aliasMustBe :: (Text -> Bool) -> Instruction a -> Bool
+ Hadolint.Rule: customRule :: (Linenumber -> State a -> Instruction args -> State a) -> State a -> Rule args
+ Hadolint.Rule: data CheckFailure
+ Hadolint.Rule: data DLSeverity
+ Hadolint.Rule: data LabelType
+ Hadolint.Rule: data State a
+ Hadolint.Rule: emptyState :: a -> State a
+ Hadolint.Rule: foldArguments :: (a -> b) -> Arguments a -> b
+ Hadolint.Rule: infixl 0 |>
+ Hadolint.Rule: instance Control.DeepSeq.NFData Hadolint.Rule.DLSeverity
+ Hadolint.Rule: instance Data.String.IsString Hadolint.Rule.RuleCode
+ Hadolint.Rule: instance Data.YAML.FromYAML Hadolint.Rule.LabelType
+ Hadolint.Rule: instance GHC.Classes.Eq Hadolint.Rule.CheckFailure
+ Hadolint.Rule: instance GHC.Classes.Eq Hadolint.Rule.DLSeverity
+ Hadolint.Rule: instance GHC.Classes.Eq Hadolint.Rule.LabelType
+ Hadolint.Rule: instance GHC.Classes.Eq Hadolint.Rule.RuleCode
+ Hadolint.Rule: instance GHC.Classes.Ord Hadolint.Rule.CheckFailure
+ Hadolint.Rule: instance GHC.Classes.Ord Hadolint.Rule.DLSeverity
+ Hadolint.Rule: instance GHC.Classes.Ord Hadolint.Rule.RuleCode
+ Hadolint.Rule: instance GHC.Generics.Generic Hadolint.Rule.DLSeverity
+ Hadolint.Rule: instance GHC.Read.Read Hadolint.Rule.LabelType
+ Hadolint.Rule: instance GHC.Show.Show Hadolint.Rule.CheckFailure
+ Hadolint.Rule: instance GHC.Show.Show Hadolint.Rule.DLSeverity
+ Hadolint.Rule: instance GHC.Show.Show Hadolint.Rule.LabelType
+ Hadolint.Rule: instance GHC.Show.Show Hadolint.Rule.RuleCode
+ Hadolint.Rule: instance GHC.Show.Show a => GHC.Show.Show (Hadolint.Rule.State a)
+ Hadolint.Rule: modify :: (a -> a) -> State a -> State a
+ Hadolint.Rule: newtype RuleCode
+ Hadolint.Rule: read :: Text -> Either Text LabelType
+ Hadolint.Rule: replaceWith :: a -> State a -> State a
+ Hadolint.Rule: simpleRule :: RuleCode -> DLSeverity -> Text -> (Instruction args -> Bool) -> Rule args
+ Hadolint.Rule: simpleState :: State ()
+ Hadolint.Rule: type Failures = Seq CheckFailure
+ Hadolint.Rule: type LabelName = Text
+ Hadolint.Rule: type LabelSchema = Map LabelName LabelType
+ Hadolint.Rule: type Rule args = Fold (InstructionPos args) Failures
+ Hadolint.Rule: veryCustomRule :: (Linenumber -> State a -> Instruction args -> State a) -> State a -> (State a -> Failures) -> Rule args
+ Hadolint.Rule: withLabelType :: (LabelType -> Parser a) -> Node Pos -> Parser a
+ Hadolint.Rule: withLineNumber :: (Linenumber -> t1 -> Instruction args -> t2) -> t1 -> InstructionPos args -> t2
+ Hadolint.Rule.DL3000: rule :: Rule args
+ Hadolint.Rule.DL3001: rule :: Rule ParsedShell
+ Hadolint.Rule.DL3002: instance GHC.Show.Show Hadolint.Rule.DL3002.Acc
+ Hadolint.Rule.DL3002: rule :: Rule args
+ Hadolint.Rule.DL3003: rule :: Rule ParsedShell
+ Hadolint.Rule.DL3004: rule :: Rule ParsedShell
+ Hadolint.Rule.DL3005: rule :: Rule ParsedShell
+ Hadolint.Rule.DL3006: rule :: Rule args
+ Hadolint.Rule.DL3007: rule :: Rule args
+ Hadolint.Rule.DL3008: rule :: Rule ParsedShell
+ Hadolint.Rule.DL3009: instance GHC.Show.Show Hadolint.Rule.DL3009.Acc
+ Hadolint.Rule.DL3009: rule :: Rule ParsedShell
+ Hadolint.Rule.DL3010: rule :: Rule args
+ Hadolint.Rule.DL3011: rule :: Rule args
+ Hadolint.Rule.DL3012: rule :: Rule args
+ Hadolint.Rule.DL3013: rule :: Rule ParsedShell
+ Hadolint.Rule.DL3014: rule :: Rule ParsedShell
+ Hadolint.Rule.DL3015: rule :: Rule ParsedShell
+ Hadolint.Rule.DL3016: rule :: Rule ParsedShell
+ Hadolint.Rule.DL3017: rule :: Rule ParsedShell
+ Hadolint.Rule.DL3018: rule :: Rule ParsedShell
+ Hadolint.Rule.DL3019: rule :: Rule ParsedShell
+ Hadolint.Rule.DL3020: rule :: Rule args
+ Hadolint.Rule.DL3021: rule :: Rule args
+ Hadolint.Rule.DL3022: rule :: Rule args
+ Hadolint.Rule.DL3023: rule :: Rule args
+ Hadolint.Rule.DL3024: rule :: Rule args
+ Hadolint.Rule.DL3025: rule :: Rule args
+ Hadolint.Rule.DL3026: rule :: Set Registry -> Rule args
+ Hadolint.Rule.DL3027: rule :: Rule ParsedShell
+ Hadolint.Rule.DL3028: rule :: Rule ParsedShell
+ Hadolint.Rule.DL3029: rule :: Rule args
+ Hadolint.Rule.DL3030: rule :: Rule ParsedShell
+ Hadolint.Rule.DL3031: rule :: Rule ParsedShell
+ Hadolint.Rule.DL3032: rule :: Rule ParsedShell
+ Hadolint.Rule.DL3033: rule :: Rule ParsedShell
+ Hadolint.Rule.DL3034: rule :: Rule ParsedShell
+ Hadolint.Rule.DL3035: rule :: Rule ParsedShell
+ Hadolint.Rule.DL3036: rule :: Rule ParsedShell
+ Hadolint.Rule.DL3037: rule :: Rule ParsedShell
+ Hadolint.Rule.DL3038: rule :: Rule ParsedShell
+ Hadolint.Rule.DL3039: rule :: Rule ParsedShell
+ Hadolint.Rule.DL3040: rule :: Rule ParsedShell
+ Hadolint.Rule.DL3041: rule :: Rule ParsedShell
+ Hadolint.Rule.DL3042: rule :: Rule ParsedShell
+ Hadolint.Rule.DL3043: rule :: Rule args
+ Hadolint.Rule.DL3044: rule :: Rule args
+ Hadolint.Rule.DL3045: instance GHC.Classes.Eq Hadolint.Rule.DL3045.Stage
+ Hadolint.Rule.DL3045: instance GHC.Classes.Ord Hadolint.Rule.DL3045.Stage
+ Hadolint.Rule.DL3045: rule :: Rule args
+ Hadolint.Rule.DL3046: rule :: Rule ParsedShell
+ Hadolint.Rule.DL3047: rule :: Rule ParsedShell
+ Hadolint.Rule.DL3048: rule :: Rule args
+ Hadolint.Rule.DL3049: instance GHC.Classes.Eq Hadolint.Rule.DL3049.StageID
+ Hadolint.Rule.DL3049: instance GHC.Classes.Ord Hadolint.Rule.DL3049.StageID
+ Hadolint.Rule.DL3049: instance GHC.Show.Show Hadolint.Rule.DL3049.Acc
+ Hadolint.Rule.DL3049: instance GHC.Show.Show Hadolint.Rule.DL3049.StageID
+ Hadolint.Rule.DL3049: rule :: LabelSchema -> Rule args
+ Hadolint.Rule.DL3050: rule :: LabelSchema -> Bool -> Rule args
+ Hadolint.Rule.DL3051: rule :: LabelSchema -> Rule args
+ Hadolint.Rule.DL3052: rule :: LabelSchema -> Rule args
+ Hadolint.Rule.DL3053: rule :: LabelSchema -> Rule args
+ Hadolint.Rule.DL3054: rule :: LabelSchema -> Rule args
+ Hadolint.Rule.DL3055: rule :: LabelSchema -> Rule args
+ Hadolint.Rule.DL3056: rule :: LabelSchema -> Rule args
+ Hadolint.Rule.DL3057: instance GHC.Classes.Eq Hadolint.Rule.DL3057.StageID
+ Hadolint.Rule.DL3057: instance GHC.Classes.Ord Hadolint.Rule.DL3057.StageID
+ Hadolint.Rule.DL3057: instance GHC.Show.Show Hadolint.Rule.DL3057.Acc
+ Hadolint.Rule.DL3057: instance GHC.Show.Show Hadolint.Rule.DL3057.StageID
+ Hadolint.Rule.DL3057: rule :: Rule args
+ Hadolint.Rule.DL4000: rule :: Rule args
+ Hadolint.Rule.DL4001: rule :: Rule ParsedShell
+ Hadolint.Rule.DL4003: rule :: Rule args
+ Hadolint.Rule.DL4004: rule :: Rule args
+ Hadolint.Rule.DL4005: rule :: Rule ParsedShell
+ Hadolint.Rule.DL4006: rule :: Rule ParsedShell
+ Hadolint.Rule.Shellcheck: rule :: Rule ParsedShell
+ Hadolint.Shell: getFlagArg :: Text -> Command -> [Text]
+ Hadolint.Shell: isPipInstall :: Command -> Bool
+ Hadolint.Shell: usingProgram :: Text -> ParsedShell -> Bool
- Hadolint: Result :: !Seq (ParseErrorBundle s e) -> !Seq RuleCheck -> Result s e
+ Hadolint: Result :: Text -> Seq (ParseErrorBundle s e) -> Failures -> Result s e
- Hadolint: [checks] :: Result s e -> !Seq RuleCheck
+ Hadolint: [checks] :: Result s e -> Failures
- Hadolint: [errors] :: Result s e -> !Seq (ParseErrorBundle s e)
+ Hadolint: [errors] :: Result s e -> Seq (ParseErrorBundle s e)
- Hadolint.Config: ConfigFile :: Maybe OverrideConfig -> Maybe [IgnoreRule] -> Maybe [TrustedRegistry] -> ConfigFile
+ Hadolint.Config: ConfigFile :: Maybe OverrideConfig -> Maybe [IgnoreRule] -> Maybe [TrustedRegistry] -> Maybe LabelSchema -> Maybe Bool -> ConfigFile
- Hadolint.Formatter.Format: Result :: !Seq (ParseErrorBundle s e) -> !Seq RuleCheck -> Result s e
+ Hadolint.Formatter.Format: Result :: Text -> Seq (ParseErrorBundle s e) -> Failures -> Result s e
- Hadolint.Formatter.Format: [checks] :: Result s e -> !Seq RuleCheck
+ Hadolint.Formatter.Format: [checks] :: Result s e -> Failures
- Hadolint.Formatter.Format: [errors] :: Result s e -> !Seq (ParseErrorBundle s e)
+ Hadolint.Formatter.Format: [errors] :: Result s e -> Seq (ParseErrorBundle s e)
- Hadolint.Formatter.Format: severityText :: DLSeverity -> String
+ Hadolint.Formatter.Format: severityText :: DLSeverity -> Text
- Hadolint.Formatter.Format: toResult :: Either (ParseErrorBundle s e) [RuleCheck] -> Result s e
+ Hadolint.Formatter.Format: toResult :: Text -> Either (ParseErrorBundle s e) Failures -> Result s e
- Hadolint.Lint: lint :: LintOptions -> NonEmpty String -> IO (Result Text DockerfileError)
+ Hadolint.Lint: lint :: LintOptions -> [(Text, Either Error Dockerfile)] -> [Result Text DockerfileError]
- Hadolint.Lint: type IgnoreRule = Text
+ Hadolint.Lint: type IgnoreRule = RuleCode
- Hadolint.Shell: CmdPart :: !Text -> !Int -> CmdPart
+ Hadolint.Shell: CmdPart :: Text -> Int -> CmdPart
- Hadolint.Shell: Command :: !Text -> [CmdPart] -> [CmdPart] -> Command
+ Hadolint.Shell: Command :: Text -> [CmdPart] -> [CmdPart] -> Command
- Hadolint.Shell: ParsedShell :: !Text -> !ParseResult -> ![Command] -> ParsedShell
+ Hadolint.Shell: ParsedShell :: Text -> ParseResult -> [Command] -> ParsedShell
- Hadolint.Shell: [arg] :: CmdPart -> !Text
+ Hadolint.Shell: [arg] :: CmdPart -> Text
- Hadolint.Shell: [name] :: Command -> !Text
+ Hadolint.Shell: [name] :: Command -> Text
- Hadolint.Shell: [original] :: ParsedShell -> !Text
+ Hadolint.Shell: [original] :: ParsedShell -> Text
- Hadolint.Shell: [parsed] :: ParsedShell -> !ParseResult
+ Hadolint.Shell: [parsed] :: ParsedShell -> ParseResult
- Hadolint.Shell: [partId] :: CmdPart -> !Int
+ Hadolint.Shell: [partId] :: CmdPart -> Int
- Hadolint.Shell: [presentCommands] :: ParsedShell -> ![Command]
+ Hadolint.Shell: [presentCommands] :: ParsedShell -> [Command]

Files

README.md view
@@ -152,6 +152,62 @@  Inline ignores will only work if place directly above the instruction. +## Linting Labels++Hadolint has the ability to check that specific labels be present and conform+to a predefined label schema.+First a label schema must be defined either via commandline:+```bash+$ hadolint --require-label author:text --require-label version:semver Dockerfile+```+or via config file:++```yaml+label-schema:+  author: text+  created: rfc3339+  version: semver+  documentation: url+  git-revision: hash+  license: spdx+```+The value of a label can be either of `text`, `url`, `semver`, `hash` or+`rfc3339`:+| Schema  | Description                                        |+|:--------|:---------------------------------------------------|+| text    | Anything                                           |+| rfc3339 | A time, formatted according to [RFC 3339][rfc3339] |+| semver  | A [semantic version][semver]                       |+| url     | A URI as described in [RFC 3986][rfc3986]          |+| hash    | Either a short or a long [Git hash][githash]       |+| spdx    | An [SPDX license identifier][spdxid]               |++By default, Hadolint ignores any label not specified in the label schema. To+warn on such additional labels, turn on strict labels:+```bash+$ hadolint --strict-labels --require-label version:semver Dockerfile+```+or in the config file:+```yaml+strict-labels: true+```+When strict labels is enabled, but no label schema has been specified, Hadolint+will warn if any label is present.++### Note on dealing with variables in labels+It is a common pattern to fill the value of a label not statically, but rather+dynamically at build time by using a variable:+```dockerfile+FROM debian:buster+ARG VERSION="du-jour"+LABEL version="${VERSION}"+```+To allow this, the label schema must specify `text` as value for that label:+```yaml+label-schema:+  version: text+```+ ## Integrations  To get most of `hadolint` it is useful to integrate it as a check to your CI@@ -190,7 +246,7 @@ | [DL3009](https://github.com/hadolint/hadolint/wiki/DL3009)   | Delete the apt-get lists after installing something.                                                                                                | | [DL3010](https://github.com/hadolint/hadolint/wiki/DL3010)   | Use ADD for extracting archives into an image.                                                                                                      | | [DL3011](https://github.com/hadolint/hadolint/wiki/DL3011)   | Valid UNIX ports range from 0 to 65535.                                                                                                             |-| [DL3012](https://github.com/hadolint/hadolint/wiki/DL3012)   | Provide an email address or URL as maintainer.                                                                                                      |+| [DL3012](https://github.com/hadolint/hadolint/wiki/DL3012)   | Multiple `HEALTHCHECK` instructions.                                                                                                                | | [DL3013](https://github.com/hadolint/hadolint/wiki/DL3013)   | Pin versions in pip.                                                                                                                                | | [DL3014](https://github.com/hadolint/hadolint/wiki/DL3014)   | Use the `-y` switch.                                                                                                                                | | [DL3015](https://github.com/hadolint/hadolint/wiki/DL3015)   | Avoid additional packages by specifying --no-install-recommends.                                                                                    |@@ -223,6 +279,19 @@ | [DL3042](https://github.com/hadolint/hadolint/wiki/DL3042)   | Avoid cache directory with `pip install --no-cache-dir <package>`.                                                                                  | | [DL3043](https://github.com/hadolint/hadolint/wiki/DL3043)   | `ONBUILD`, `FROM` or `MAINTAINER` triggered from within `ONBUILD` instruction.                                                                      | | [DL3044](https://github.com/hadolint/hadolint/wiki/DL3044)   | Do not refer to an environment variable within the same `ENV` statement where it is defined.                                                        |+| [DL3045](https://github.com/hadolint/hadolint/wiki/DL3045)   | `COPY` to a relative destination without `WORKDIR` set.                                                                                             |+| [DL3046](https://github.com/hadolint/hadolint/wiki/DL3046)   | `useradd` without flag `-l` and high UID will result in excessively large Image.                                                                    |+| [DL3047](https://github.com/hadolint/hadolint/wiki/DL3047)   | `wget` without flag `--progress` will result in excessively bloated build logs when downloading larger files.                                       |+| [DL3048](https://github.com/hadolint/hadolint/wiki/DL3048)   | Invalid Label Key                                                                                                                                   |+| [DL3049](https://github.com/hadolint/hadolint/wiki/DL3049)   | Label `<label>` is missing.                                                                                                                         |+| [DL3050](https://github.com/hadolint/hadolint/wiki/DL3050)   | Superfluous label(s) present.                                                                                                                       |+| [DL3051](https://github.com/hadolint/hadolint/wiki/DL3051)   | Label `<label>` is empty.                                                                                                                           |+| [DL3052](https://github.com/hadolint/hadolint/wiki/DL3052)   | Label `<label>` is not a valid URL.                                                                                                                 |+| [DL3053](https://github.com/hadolint/hadolint/wiki/DL3053)   | Label `<label>` is not a valid time format - must be conform to RFC3339.                                                                            |+| [DL3054](https://github.com/hadolint/hadolint/wiki/DL3054)   | Label `<label>` is not a valid SPDX license identifier.                                                                                             |+| [DL3055](https://github.com/hadolint/hadolint/wiki/DL3055)   | Label `<label>` is not a valid git hash.                                                                                                            |+| [DL3056](https://github.com/hadolint/hadolint/wiki/DL3056)   | Label `<label>` does not conform to semantic versioning.                                                                                            |+| [DL3057](https://github.com/hadolint/hadolint/wiki/DL3057)   | `HEALTHCHECK` instruction missing.                                                                                                                  | | [DL4000](https://github.com/hadolint/hadolint/wiki/DL4000)   | MAINTAINER is deprecated.                                                                                                                           | | [DL4001](https://github.com/hadolint/hadolint/wiki/DL4001)   | Either use Wget or Curl but not both.                                                                                                               | | [DL4003](https://github.com/hadolint/hadolint/wiki/DL4003)   | Multiple `CMD` instructions found.                                                                                                                  |@@ -342,3 +411,8 @@ [create an issue]: https://github.com/hadolint/hadolint/issues/new [dockerfile reference]: http://docs.docker.com/engine/reference/builder/ [syntax.hs]: https://www.stackage.org/haddock/nightly-2018-01-07/language-docker-2.0.1/Language-Docker-Syntax.html+[rfc3339]: https://www.ietf.org/rfc/rfc3339.txt+[semver]: https://semver.org/+[rfc3986]: https://www.ietf.org/rfc/rfc3986.txt+[githash]: https://git-scm.com/book/en/v2/Git-Tools-Revision-Selection+[spdxid]: https://spdx.org/licenses/
app/Main.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE TemplateHaskell #-}@@ -5,19 +6,24 @@ module Main where  import Control.Applicative-import Data.String (IsString (fromString))+import qualified Data.Bifunctor as Bifunctor import qualified Data.List.NonEmpty as NonEmpty+import qualified Data.Map as Map+import Data.Maybe import qualified Data.Sequence as Seq import qualified Data.Set as Set+import Data.String (IsString (fromString))+import qualified Data.Text as Text import qualified Data.Version import qualified Development.GitRev import qualified Hadolint-import Data.Maybe+import qualified Hadolint.Rule as Rule import Options.Applicative   ( Parser,     action,     argument,     completeWith,+    eitherReader,     execParser,     fullDesc,     header,@@ -29,6 +35,7 @@     metavar,     option,     progDesc,+    ReadM,     short,     showDefaultWith,     str,@@ -85,6 +92,9 @@      nocolor = switch (long "no-color" <> help "Don't colorize output") +    strictlabels = switch (long "strict-labels"+        <> help "Do not permit labels other than specified in `label-schema`")+     configFile =       optional         ( strOption@@ -108,37 +118,37 @@     errorList =       many         ( strOption-          ( long "error"-              <> help "Make the rule `RULECODE` have the level `error`"-              <> metavar "RULECODE"-          )+            ( long "error"+                <> help "Make the rule `RULECODE` have the level `error`"+                <> metavar "RULECODE"+            )         )      warningList =       many         ( strOption-          ( long "warning"-              <> help "Make the rule `RULECODE` have the level `warning`"-              <> metavar "RULECODE"-          )+            ( long "warning"+                <> help "Make the rule `RULECODE` have the level `warning`"+                <> metavar "RULECODE"+            )         )      infoList =       many         ( strOption-          ( long "info"-              <> help "Make the rule `RULECODE` have the level `info`"-              <> metavar "RULECODE"-          )+            ( long "info"+                <> help "Make the rule `RULECODE` have the level `info`"+                <> metavar "RULECODE"+            )         )      styleList =       many         ( strOption-          ( long "style"-              <> help "Make the rule `RULECODE` have the level `style`"-              <> metavar "RULECODE"-          )+            ( long "style"+                <> help "Make the rule `RULECODE` have the level `style`"+                <> metavar "RULECODE"+            )         )      ignoreList =@@ -161,8 +171,22 @@         <*> ignoreList         <*> parseRulesConfig +    labels = Map.fromList+        <$> many+          ( option readSingleLabelSchema+              ( long "require-label"+                  <> help "The option --require-label=label:format makes Hadolint check that the label `label` conforms to format requirement `format`"+                  <> metavar "LABELSCHEMA (e.g. maintainer:text)"+              )+          )+     parseRulesConfig =-      Hadolint.RulesConfig . Set.fromList . fmap fromString+      Hadolint.RulesConfig+        <$> parseAllowedRegistries+        <*> labels+        <*> strictlabels++    parseAllowedRegistries = Set.fromList . fmap fromString         <$> many           ( strOption               ( long "trusted-registry"@@ -171,20 +195,31 @@               )           ) +type SingleLabelSchema = (Rule.LabelName, Rule.LabelType)++readSingleLabelSchema :: ReadM SingleLabelSchema+readSingleLabelSchema = eitherReader $ \s -> labelParser (Text.pack s)++labelParser :: Text.Text -> Either String (Rule.LabelName, Rule.LabelType)+labelParser l =+    case Bifunctor.second (Rule.read . Text.drop 1) $ Text.breakOn ":" l of+      (ln, Right lt) -> Right (ln, lt)+      (_, Left e) -> Left $ Text.unpack e+ noFailure :: Hadolint.Result s e -> Bool-noFailure (Hadolint.Result Seq.Empty Seq.Empty) = True+noFailure (Hadolint.Result _ Seq.Empty Seq.Empty) = True noFailure _ = False -exitProgram :: CommandOptions -> Hadolint.Result s e -> IO()+exitProgram :: Foldable f => CommandOptions -> f (Hadolint.Result s e) -> IO () exitProgram cmd res-  | noFail cmd                                 = exitSuccess+  | noFail cmd = exitSuccess   | Hadolint.shallSkipErrorStatus (format cmd) = exitSuccess-  | noFailure res                              = exitSuccess-  | otherwise                                  = exitFailure+  | all noFailure res = exitSuccess+  | otherwise = exitFailure -runLint :: CommandOptions -> Hadolint.LintOptions -> NonEmpty.NonEmpty String -> IO()+runLint :: CommandOptions -> Hadolint.LintOptions -> NonEmpty.NonEmpty String -> IO () runLint cmd conf files = do-  res <-  Hadolint.lint conf files+  res <- Hadolint.lintIO conf files   noColorEnv <- lookupEnv "NO_COLOR"   let noColor = nocolor cmd || isJust noColorEnv   Hadolint.printResults (format cmd) noColor res
hadolint.cabal view
@@ -4,10 +4,10 @@ -- -- see: https://github.com/sol/hpack ----- hash: 06df88cbceaf76da3599a8943568f912b17a63d1795839eb4da974832e1ae0d4+-- hash: 6dc5574db9b4edf093ae954fa31aa254d6a3b1a37aa6dc0bf793182d24daf375  name:           hadolint-version:        1.23.0+version:        2.0.0 synopsis:       Dockerfile Linter JavaScript API description:    A smarter Dockerfile linter that helps you build best practice Docker images. category:       Development@@ -40,8 +40,75 @@       Hadolint.Formatter.Format       Hadolint.Formatter.Json       Hadolint.Formatter.TTY+      Hadolint.Ignore       Hadolint.Lint-      Hadolint.Rules+      Hadolint.Process+      Hadolint.Rule+      Hadolint.Rule.DL3000+      Hadolint.Rule.DL3001+      Hadolint.Rule.DL3002+      Hadolint.Rule.DL3003+      Hadolint.Rule.DL3004+      Hadolint.Rule.DL3005+      Hadolint.Rule.DL3006+      Hadolint.Rule.DL3007+      Hadolint.Rule.DL3008+      Hadolint.Rule.DL3009+      Hadolint.Rule.DL3010+      Hadolint.Rule.DL3011+      Hadolint.Rule.DL3012+      Hadolint.Rule.DL3013+      Hadolint.Rule.DL3014+      Hadolint.Rule.DL3015+      Hadolint.Rule.DL3016+      Hadolint.Rule.DL3017+      Hadolint.Rule.DL3018+      Hadolint.Rule.DL3019+      Hadolint.Rule.DL3020+      Hadolint.Rule.DL3021+      Hadolint.Rule.DL3022+      Hadolint.Rule.DL3023+      Hadolint.Rule.DL3024+      Hadolint.Rule.DL3025+      Hadolint.Rule.DL3026+      Hadolint.Rule.DL3027+      Hadolint.Rule.DL3028+      Hadolint.Rule.DL3029+      Hadolint.Rule.DL3030+      Hadolint.Rule.DL3031+      Hadolint.Rule.DL3032+      Hadolint.Rule.DL3033+      Hadolint.Rule.DL3034+      Hadolint.Rule.DL3035+      Hadolint.Rule.DL3036+      Hadolint.Rule.DL3037+      Hadolint.Rule.DL3038+      Hadolint.Rule.DL3039+      Hadolint.Rule.DL3040+      Hadolint.Rule.DL3041+      Hadolint.Rule.DL3042+      Hadolint.Rule.DL3043+      Hadolint.Rule.DL3044+      Hadolint.Rule.DL3045+      Hadolint.Rule.DL3046+      Hadolint.Rule.DL3047+      Hadolint.Rule.DL3048+      Hadolint.Rule.DL3049+      Hadolint.Rule.DL3050+      Hadolint.Rule.DL3051+      Hadolint.Rule.DL3052+      Hadolint.Rule.DL3053+      Hadolint.Rule.DL3054+      Hadolint.Rule.DL3055+      Hadolint.Rule.DL3056+      Hadolint.Rule.DL3057+      Hadolint.Rule.DL4000+      Hadolint.Rule.DL4001+      Hadolint.Rule.DL4003+      Hadolint.Rule.DL4004+      Hadolint.Rule.DL4005+      Hadolint.Rule.DL4006+      Hadolint.Rule.Shellcheck       Hadolint.Shell   other-modules:       Paths_hadolint@@ -49,12 +116,13 @@       Paths_hadolint   hs-source-dirs:       src-  ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints -optP-Wno-nonportable-include-path+  default-extensions: OverloadedStrings NamedFieldPuns DeriveGeneric DeriveAnyClass RecordWildCards StrictData ScopedTypeVariables PatternSynonyms+  ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints -optP-Wno-nonportable-include-path -flate-dmd-anal   build-depends:-      HsYAML+      Cabal+    , HsYAML     , ShellCheck >=0.7.1     , aeson-    , async     , base >=4.8 && <5     , bytestring     , colourista@@ -63,13 +131,20 @@     , deepseq ==1.4.4.*     , directory >=1.3.0     , filepath+    , foldl     , ilist     , language-docker >=9.1.3 && <10     , megaparsec >=9.0.0     , mtl+    , network-uri     , parallel+    , parsec >=3.1.14+    , semver+    , spdx     , split >=0.2     , text+    , time+    , timerep >=2.0     , void   default-language: Haskell2010 @@ -79,7 +154,8 @@       Paths_hadolint   hs-source-dirs:       app-  ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints -optP-Wno-nonportable-include-path -O2 -threaded -rtsopts "-with-rtsopts=-N5 -A4m"+  default-extensions: OverloadedStrings NamedFieldPuns DeriveGeneric DeriveAnyClass RecordWildCards StrictData ScopedTypeVariables PatternSynonyms+  ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints -optP-Wno-nonportable-include-path -flate-dmd-anal -O2 -threaded -rtsopts "-with-rtsopts=-N5 -A4m"   build-depends:       base >=4.8 && <5     , containers@@ -98,10 +174,12 @@   main-is: Spec.hs   other-modules:       ConfigSpec+      ShellSpec       Paths_hadolint   hs-source-dirs:       test-  ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints -optP-Wno-nonportable-include-path+  default-extensions: OverloadedStrings NamedFieldPuns DeriveGeneric DeriveAnyClass RecordWildCards StrictData ScopedTypeVariables PatternSynonyms+  ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints -optP-Wno-nonportable-include-path -flate-dmd-anal   build-depends:       HUnit >=1.2     , HsYAML@@ -109,6 +187,8 @@     , aeson     , base >=4.8 && <5     , bytestring >=0.10+    , containers+    , foldl     , hadolint     , hspec     , language-docker >=9.1.3 && <10
src/Hadolint.hs view
@@ -1,12 +1,44 @@ module Hadolint   ( module Hadolint.Lint,-    module Hadolint.Rules,+    module Hadolint.Process,     module Hadolint.Config,-    Result (..)+    Result (..),+    OutputFormat (..),+    shallSkipErrorStatus,+    printResults,   ) where +import Data.Text (Text) import Hadolint.Config-import Hadolint.Lint-import Hadolint.Rules+import qualified Hadolint.Formatter.Checkstyle+import qualified Hadolint.Formatter.Codacy+import qualified Hadolint.Formatter.Codeclimate import Hadolint.Formatter.Format (Result (..))+import qualified Hadolint.Formatter.Json+import qualified Hadolint.Formatter.TTY+import Hadolint.Lint+import Hadolint.Process+import Language.Docker.Parser (DockerfileError)++data OutputFormat+  = Json+  | TTY+  | CodeclimateJson+  | GitlabCodeclimateJson+  | Checkstyle+  | Codacy+  deriving (Show, Eq)++shallSkipErrorStatus :: OutputFormat -> Bool+shallSkipErrorStatus format = format `elem` [CodeclimateJson, Codacy]++printResults :: Foldable f => OutputFormat -> Bool -> f (Result Text DockerfileError) -> IO ()+printResults format nocolor allResults =+  case format of+    TTY -> Hadolint.Formatter.TTY.printResults allResults nocolor+    Json -> Hadolint.Formatter.Json.printResults allResults+    Checkstyle -> Hadolint.Formatter.Checkstyle.printResults allResults+    CodeclimateJson -> Hadolint.Formatter.Codeclimate.printResults allResults+    GitlabCodeclimateJson -> Hadolint.Formatter.Codeclimate.printGitlabResults allResults+    Codacy -> Hadolint.Formatter.Codacy.printResults allResults
src/Hadolint/Config.hs view
@@ -1,9 +1,12 @@-{-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE OverloadedStrings #-}--module Hadolint.Config (applyConfig, ConfigFile (..), OverrideConfig (..)) where+module Hadolint.Config+  ( applyConfig,+    ConfigFile (..),+    OverrideConfig (..),+  )+where +import Control.Applicative ((<|>))+import qualified Control.Foldl.Text as Text import Control.Monad (filterM) import qualified Data.ByteString as Bytes import Data.Coerce (coerce)@@ -13,7 +16,8 @@ import qualified Data.YAML as Yaml import GHC.Generics (Generic) import qualified Hadolint.Lint as Lint-import qualified Hadolint.Rules as Rules+import qualified Hadolint.Process as Process+import qualified Hadolint.Rule as Rule import qualified Language.Docker as Docker import System.Directory   ( XdgDirectory (..),@@ -23,7 +27,6 @@   ) import System.FilePath ((</>)) - data OverrideConfig = OverrideConfig   { overrideErrorRules :: Maybe [Lint.ErrorRule],     overrideWarningRules :: Maybe [Lint.WarningRule],@@ -32,27 +35,39 @@   }   deriving (Show, Eq, Generic) +instance Semigroup OverrideConfig where+  OverrideConfig a1 a2 a3 a4 <> OverrideConfig b1 b2 b3 b4 =+    OverrideConfig (a1 <> b1) (a2 <> b2) (a3 <> b3) (a4 <> b4)++instance Monoid OverrideConfig where+  mempty = OverrideConfig Nothing Nothing Nothing Nothing+ data ConfigFile = ConfigFile   { overrideRules :: Maybe OverrideConfig,     ignoredRules :: Maybe [Lint.IgnoreRule],-    trustedRegistries :: Maybe [Lint.TrustedRegistry]+    trustedRegistries :: Maybe [Lint.TrustedRegistry],+    labelSchemaConfig :: Maybe Rule.LabelSchema,+    strictLabelSchema :: Maybe Bool   }   deriving (Show, Eq, Generic) -instance Yaml.FromYAML OverrideConfig-  where-    parseYAML = Yaml.withMap "OverrideConfig" $ \m -> OverrideConfig-        <$> m .:? "error"-        <*> m .:? "warning"-        <*> m .:? "info"-        <*> m .:? "style"+instance Yaml.FromYAML OverrideConfig where+  parseYAML = Yaml.withMap "OverrideConfig" $ \m ->+    OverrideConfig+      <$> m .:? "error"+      <*> m .:? "warning"+      <*> m .:? "info"+      <*> m .:? "style"  instance Yaml.FromYAML ConfigFile where-  parseYAML = Yaml.withMap "ConfigFile" $ \m ->-    ConfigFile-      <$> m .:? "override"-      <*> m .:? "ignored"-      <*> m .:? "trustedRegistries"+  parseYAML = Yaml.withMap "ConfigFile" $ \m -> do+    overrideRules <- m .:? "override"+    ignored <- m .:? "ignored"+    let ignoredRules = coerce (ignored :: Maybe [Text.Text])+    trustedRegistries <- m .:? "trustedRegistries"+    labelSchemaConfig <- m .:? "label-schema"+    strictLabelSchema <- m .:? "strict-labels"+    return ConfigFile {..}  -- | 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@@ -60,7 +75,7 @@ -- 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)+  | not (Prelude.null (Lint.ignoreRules o)) && Lint.rulesConfig o /= mempty = return (Right o)   | otherwise = do     theConfig <-       case maybeConfig of@@ -78,59 +93,45 @@     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 Nothing ignore trusted) ->-          return $-            Right (applyOverride Nothing Nothing Nothing Nothing ignore trusted)-        Right (ConfigFile (Just (OverrideConfig errors warnings infos styles)) ignore trusted) ->-          return $-            Right (applyOverride errors warnings infos styles ignore trusted)--    applyOverride errors warnings infos styles ignore trusted =-      applyTrusted trusted-        . applyIgnore ignore-        . applyStyles styles-        . applyInfos infos-        . applyWarnings warnings-        . applyErrors errors-        $ o--    applyErrors errors opts =-      case Lint.errorRules opts of-        [] -> opts {Lint.errorRules = fromMaybe [] errors}-        _ -> opts--    applyWarnings warnings opts =-      case Lint.warningRules opts of-        [] -> opts {Lint.warningRules = fromMaybe [] warnings}-        _ -> opts+      return $ case Yaml.decode1Strict contents of+        Left (_, err) -> Left (formatError err configFile)+        Right config -> Right $ fromMaybe o (applyOverride config) -    applyInfos infos opts =-      case Lint.infoRules opts of-        [] -> opts {Lint.infoRules = fromMaybe [] infos}-        _ -> opts+    applyOverride ConfigFile {..} =+      -- Maybe.do+      do+        OverrideConfig {..} <- overrideRules <|> Just mempty+        overrideError <- overrideErrorRules <|> Just mempty+        overrideWarning <- overrideWarningRules <|> Just mempty+        overrideInfo <- overrideInfoRules <|> Just mempty+        overrideStyle <- overrideStyleRules <|> Just mempty+        overrideIgnored <- ignoredRules <|> Just mempty -    applyStyles styles opts =-      case Lint.styleRules opts of-        [] -> opts {Lint.styleRules = fromMaybe [] styles}-        _ -> opts+        trusted <- Set.fromList . coerce <$> (trustedRegistries <|> Just mempty)+        schema <- labelSchemaConfig <|> Just mempty+        strictLabels <- strictLabelSchema <|> Just False -    applyIgnore ignore opts =-      case Lint.ignoreRules opts of-        [] -> opts {Lint.ignoreRules = fromMaybe [] ignore}-        _ -> opts+        let rulesConfig = Lint.rulesConfig o -    applyTrusted trusted opts-      | null (Rules.allowedRegistries (Lint.rulesConfig opts)) =-        opts {Lint.rulesConfig = toRules trusted <> Lint.rulesConfig opts}-      | otherwise = opts+        return $+          Lint.LintOptions+            { Lint.errorRules = Lint.errorRules o <|> overrideError,+              Lint.warningRules = Lint.warningRules o <|> overrideWarning,+              Lint.infoRules = Lint.infoRules o <|> overrideInfo,+              Lint.styleRules = Lint.styleRules o <|> overrideStyle,+              Lint.ignoreRules = Lint.ignoreRules o <|> overrideIgnored,+              Lint.rulesConfig =+                Process.RulesConfig+                  { Process.allowedRegistries = Process.allowedRegistries rulesConfig `ifNull` trusted,+                    Process.labelSchema = Process.labelSchema rulesConfig `ifNull` schema,+                    Process.strictLabels = Process.strictLabels rulesConfig || strictLabels+                  }+            } -    toRules (Just trusted) = Rules.RulesConfig (Set.fromList . coerce $ trusted)-    toRules _ = mempty+    ifNull value override = if null value then override else value      formatError err config =-      unlines+      Prelude.unlines         [ "Error parsing your config file in  '" ++ config ++ "':",           "It should contain one of the keys 'override', 'ignored'",           "or 'trustedRegistries'. For example:\n",
src/Hadolint/Formatter/Checkstyle.hs view
@@ -1,97 +1,111 @@-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RecordWildCards #-}- module Hadolint.Formatter.Checkstyle-  ( printResult,+  ( printResults,     formatResult,   ) where +import qualified Control.Foldl as Foldl 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.Char (isAsciiLower, isAsciiUpper, isDigit, ord) import qualified Data.Text as Text+import Data.Text.Encoding (encodeUtf8Builder) import Hadolint.Formatter.Format-import Hadolint.Rules (Metadata (..), RuleCheck (..), DLSeverity (..))+  ( Result (..),+    errorBundlePretty,+    errorPosition,+    severityText,+  )+import Hadolint.Rule (CheckFailure (..), DLSeverity (..), RuleCode (..))+import System.IO (stdout) import Text.Megaparsec (TraversableStream) import Text.Megaparsec.Error-import Text.Megaparsec.Pos (sourceColumn, sourceLine, sourceName, unPos)+  ( ParseErrorBundle,+    ShowErrorComponent,+  )+import Text.Megaparsec.Pos (sourceColumn, sourceLine, unPos) import Text.Megaparsec.Stream (VisualStream)  data CheckStyle = CheckStyle-  { file :: String,-    line :: Int,+  { line :: Int,     column :: Int,-    impact :: String,-    msg :: String,-    source :: String+    impact :: Text.Text,+    msg :: Text.Text,+    source :: Text.Text   }  errorToCheckStyle :: (VisualStream s, TraversableStream s, ShowErrorComponent e) => ParseErrorBundle s e -> CheckStyle errorToCheckStyle err =   CheckStyle-    { file = sourceName pos,-      line = unPos (sourceLine pos),+    { line = unPos (sourceLine pos),       column = unPos (sourceColumn pos),       impact = severityText DLErrorC,-      msg = errorBundlePretty err,+      msg = Text.pack (errorBundlePretty err),       source = "DL1000"     }   where     pos = errorPosition err -ruleToCheckStyle :: RuleCheck -> CheckStyle-ruleToCheckStyle RuleCheck {..} =+ruleToCheckStyle :: CheckFailure -> CheckStyle+ruleToCheckStyle CheckFailure {..} =   CheckStyle-    { file = Text.unpack filename,-      line = linenumber,+    { line = line,       column = 1,-      impact = severityText (severity metadata),-      msg = Text.unpack (message metadata),-      source = Text.unpack (code metadata)+      impact = severityText severity,+      msg = message,+      source = unRuleCode code     } -toXml :: [CheckStyle] -> Builder.Builder-toXml checks = wrap fileName (foldMap convert checks)-  where-    wrap name innerNode = "<file " <> attr "name" name <> ">" <> innerNode <> "</file>"-    convert CheckStyle {..} =-      "<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+toXml :: CheckStyle -> Builder.Builder+toXml CheckStyle {..} =+  "<error "+    <> attr "line" (Builder.intDec line)+    <> attr "column" (Builder.intDec column)+    <> attr "severity" (encode impact)+    <> attr "message" (encode msg)+    <> attr "source" (encode source)+    <> "/>" -attr :: String -> String -> Builder.Builder-attr name value = Builder.string8 name <> "='" <> Builder.string8 (escape value) <> "' "+encode :: Text.Text -> Builder.Builder+encode = encodeUtf8Builder . escape -escape :: String -> String-escape = concatMap doEscape+attr :: Text.Text -> Builder.Builder -> Builder.Builder+attr name value = encodeUtf8Builder name <> "='" <> value <> "' "++escape :: Text.Text -> Text.Text+escape = Text.concatMap doEscape   where     doEscape c =       if isOk c-        then [c]-        else "&#" ++ show (ord c) ++ ";"+        then Text.singleton c+        else "&#" <> Text.pack (show (ord c)) <> ";"     isOk x = any (\check -> check x) [isAsciiUpper, isAsciiLower, isDigit, (`elem` [' ', '.', '/'])]  formatResult :: (VisualStream s, TraversableStream 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>"+formatResult (Result filename errors checks) = header <> xmlBody <> footer   where-    xmlBody = mconcat xmlChunks-    xmlChunks = fmap toXml (groupBy sameFileName flatten)-    flatten = toList $ checkstyleErrors <> checkstyleChecks+    xmlBody = Foldl.fold (Foldl.premap toXml Foldl.mconcat) issues++    issues = checkstyleErrors <> checkstyleChecks     checkstyleErrors = fmap errorToCheckStyle errors     checkstyleChecks = fmap ruleToCheckStyle checks-    sameFileName CheckStyle {file = f1} CheckStyle {file = f2} = f1 == f2 -printResult :: (VisualStream s, TraversableStream s, ShowErrorComponent e) => Result s e -> IO ()-printResult result = B.putStr (Builder.toLazyByteString (formatResult result))+    isEmpty = null checks && null errors+    header =+      if isEmpty+        then ""+        else "<file " <> attr "name" (encode filename) <> ">"+    footer = if isEmpty then "" else "</file>"++printResults ::+  (Foldable f, VisualStream s, TraversableStream s, ShowErrorComponent e) =>+  f (Result s e) ->+  IO ()+printResults results = do+  B.putStr header+  mapM_ put results+  B.putStr footer+  where+    header = "<?xml version='1.0' encoding='UTF-8'?><checkstyle version='4.3'>"+    footer = "</checkstyle>"+    put result = Builder.hPutBuilder stdout (formatResult result)
src/Hadolint/Formatter/Codacy.hs view
@@ -1,27 +1,25 @@-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RecordWildCards #-}- module Hadolint.Formatter.Codacy-  ( printResult,+  ( printResults,     formatResult,   ) where +import qualified Control.Foldl as Foldl import Data.Aeson hiding (Result) import qualified Data.ByteString.Lazy.Char8 as B import Data.Sequence (Seq) import qualified Data.Text as Text import Hadolint.Formatter.Format (Result (..), errorPosition)-import Hadolint.Rules (Metadata (..), RuleCheck (..))+import Hadolint.Rule (CheckFailure (..), RuleCode (..)) import Text.Megaparsec (TraversableStream) import Text.Megaparsec.Error import Text.Megaparsec.Pos (sourceLine, sourceName, unPos) import Text.Megaparsec.Stream (VisualStream)  data Issue = Issue-  { filename :: String,-    msg :: String,-    patternId :: String,+  { filename :: Text.Text,+    msg :: Text.Text,+    patternId :: Text.Text,     line :: Int   } @@ -32,32 +30,36 @@ errorToIssue :: (VisualStream s, TraversableStream s, ShowErrorComponent e) => ParseErrorBundle s e -> Issue errorToIssue err =   Issue-    { filename = sourceName pos,+    { filename = Text.pack $ sourceName pos,       patternId = "DL1000",-      msg = errorBundlePretty err,+      msg = Text.pack $ errorBundlePretty err,       line = linenumber     }   where     pos = errorPosition err     linenumber = unPos (sourceLine pos) -checkToIssue :: RuleCheck -> Issue-checkToIssue RuleCheck {..} =+checkToIssue :: Text.Text -> CheckFailure -> Issue+checkToIssue filename CheckFailure {..} =   Issue-    { filename = Text.unpack filename,-      patternId = Text.unpack (code metadata),-      msg = Text.unpack (message metadata),-      line = linenumber+    { filename = filename,+      patternId = unRuleCode code,+      msg = message,+      line = line     }  formatResult :: (VisualStream s, TraversableStream s, ShowErrorComponent e) => Result s e -> Seq Issue-formatResult (Result errors checks) = allIssues+formatResult (Result filename errors checks) = allIssues   where     allIssues = errorMessages <> checkMessages     errorMessages = fmap errorToIssue errors-    checkMessages = fmap checkToIssue checks+    checkMessages = fmap (checkToIssue filename) checks -printResult :: (VisualStream s, TraversableStream s, ShowErrorComponent e) => Result s e -> IO ()-printResult result = mapM_ output (formatResult result)+printResults ::+  (Foldable f, VisualStream s, TraversableStream s, ShowErrorComponent e) =>+  f (Result s e) ->+  IO ()+printResults results = mapM_ output flattened   where+    flattened = Foldl.fold (Foldl.premap formatResult Foldl.mconcat) results     output value = B.putStrLn (encode value)
src/Hadolint/Formatter/Codeclimate.hs view
@@ -1,14 +1,12 @@-{-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RecordWildCards #-}- module Hadolint.Formatter.Codeclimate-  ( printResult,-    printGitlabResult,+  ( printResults,+    printGitlabResults,     formatResult,+    formatGitlabResult,   ) where +import qualified Control.Foldl as Foldl import Crypto.Hash (Digest, SHA1 (..), hash) import Data.Aeson hiding (Result) import qualified Data.ByteString.Lazy as B@@ -16,17 +14,17 @@ import qualified Data.Text as Text import GHC.Generics import Hadolint.Formatter.Format (Result (..), errorPosition)-import Hadolint.Rules (Metadata (..), RuleCheck (..), DLSeverity (..))+import Hadolint.Rule (CheckFailure (..), DLSeverity (..), RuleCode (..)) import Text.Megaparsec (TraversableStream) import Text.Megaparsec.Error import Text.Megaparsec.Pos (sourceColumn, sourceLine, sourceName, unPos) import Text.Megaparsec.Stream (VisualStream)  data Issue = Issue-  { checkName :: String,-    description :: String,+  { checkName :: Text.Text,+    description :: Text.Text,     location :: Location,-    impact :: String+    impact :: Text.Text   }  data FingerprintIssue = FingerprintIssue@@ -36,10 +34,10 @@  data Location   = LocLine-      String+      Text.Text       Int   | LocPos-      String+      Text.Text       Pos  instance ToJSON Location where@@ -58,10 +56,10 @@ instance ToJSON Issue where   toJSON Issue {..} =     object-      [ "type" .= ("issue" :: String),+      [ "type" .= ("issue" :: Text.Text),         "check_name" .= checkName,         "description" .= description,-        "categories" .= (["Bug Risk"] :: [String]),+        "categories" .= (["Bug Risk"] :: [Text.Text]),         "location" .= location,         "severity" .= impact       ]@@ -69,11 +67,11 @@ instance ToJSON FingerprintIssue where   toJSON FingerprintIssue {..} =     object-      [ "type" .= ("issue" :: String),+      [ "type" .= ("issue" :: Text.Text),         "fingerprint" .= show fingerprint,         "check_name" .= checkName issue,         "description" .= description issue,-        "categories" .= (["Bug Risk"] :: [String]),+        "categories" .= (["Bug Risk"] :: [Text.Text]),         "location" .= location issue,         "severity" .= impact issue       ]@@ -82,8 +80,8 @@ errorToIssue err =   Issue     { checkName = "DL1000",-      description = errorBundlePretty err,-      location = LocPos (sourceName pos) Pos {..},+      description = Text.pack $ errorBundlePretty err,+      location = LocPos (Text.pack $ sourceName pos) Pos {..},       impact = severityText DLErrorC     }   where@@ -91,16 +89,16 @@     line = unPos (sourceLine pos)     column = unPos (sourceColumn pos) -checkToIssue :: RuleCheck -> Issue-checkToIssue RuleCheck {..} =+checkToIssue :: Text.Text -> CheckFailure -> Issue+checkToIssue fileName CheckFailure {..} =   Issue-    { checkName = Text.unpack (code metadata),-      description = Text.unpack (message metadata),-      location = LocLine (Text.unpack filename) linenumber,-      impact = severityText (severity metadata)+    { checkName = unRuleCode code,+      description = message,+      location = LocLine fileName line,+      impact = severityText severity     } -severityText :: DLSeverity -> String+severityText :: DLSeverity -> Text.Text severityText severity =   case severity of     DLErrorC -> "blocker"@@ -120,7 +118,7 @@     }  formatResult :: (VisualStream s, TraversableStream s, ShowErrorComponent e) => Result s e -> Seq Issue-formatResult (Result errors checks) = (errorToIssue <$> errors) <> (checkToIssue <$> checks)+formatResult (Result filename errors checks) = (errorToIssue <$> errors) <> (checkToIssue filename <$> checks)  formatGitlabResult :: (VisualStream s, TraversableStream s, ShowErrorComponent e) => Result s e -> Seq FingerprintIssue formatGitlabResult result = issueToFingerprintIssue <$> formatResult result@@ -132,5 +130,10 @@       B.putStr (encode value)       B.putStr (B.singleton 0x00) -printGitlabResult :: (VisualStream s, TraversableStream s, ShowErrorComponent e) => Result s e -> IO ()-printGitlabResult = B.putStr . encode . formatGitlabResult+printResults :: (VisualStream s, TraversableStream s, ShowErrorComponent e, Foldable f) => f (Result s e) -> IO ()+printResults = mapM_ printResult++printGitlabResults :: (Foldable f, VisualStream s, TraversableStream s, ShowErrorComponent e) => f (Result s e) -> IO ()+printGitlabResults results = B.putStr . encode $ flattened+  where+    flattened = Foldl.fold (Foldl.premap formatGitlabResult Foldl.mconcat) results
src/Hadolint/Formatter/Format.hs view
@@ -10,41 +10,35 @@   ) where -import Data.List (sort) import qualified Data.List.NonEmpty as NE import Data.Sequence (Seq) import qualified Data.Sequence as Seq-import Hadolint.Rules+import qualified Data.Text as Text+import qualified Hadolint.Rule import Text.Megaparsec (TraversableStream (..), pstateSourcePos) import Text.Megaparsec.Error import Text.Megaparsec.Pos (SourcePos, sourcePosPretty) import Text.Megaparsec.Stream (VisualStream)  data Result s e = Result-  { errors :: !(Seq (ParseErrorBundle s e)),-    checks :: !(Seq RuleCheck)+  { fileName :: Text.Text,+    errors :: Seq (ParseErrorBundle s e),+    checks :: Hadolint.Rule.Failures   } -instance Semigroup (Result s e) where-  (Result e1 c1) <> (Result e2 c2) = Result (e1 <> e2) (c1 <> c2)--instance Monoid (Result s e) where-  mappend = (<>)-  mempty = Result mempty mempty--toResult :: Either (ParseErrorBundle s e) [RuleCheck] -> Result s e-toResult res =+toResult :: Text.Text -> Either (ParseErrorBundle s e) Hadolint.Rule.Failures -> Result s e+toResult file res =   case res of-    Left err -> Result (Seq.singleton err) mempty-    Right c -> Result mempty (Seq.fromList (sort c))+    Left err -> Result file (Seq.singleton err) mempty+    Right c -> Result file mempty (Seq.unstableSort c) -severityText :: DLSeverity -> String+severityText :: Hadolint.Rule.DLSeverity -> Text.Text severityText s =   case s of-    DLErrorC -> "error"-    DLWarningC -> "warning"-    DLInfoC -> "info"-    DLStyleC -> "style"+    Hadolint.Rule.DLErrorC -> "error"+    Hadolint.Rule.DLWarningC -> "warning"+    Hadolint.Rule.DLInfoC -> "info"+    Hadolint.Rule.DLStyleC -> "style"     _ -> ""  stripNewlines :: String -> String
src/Hadolint/Formatter/Json.hs view
@@ -1,34 +1,32 @@-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RecordWildCards #-}- module Hadolint.Formatter.Json-  ( printResult,+  ( printResults,     formatResult,   ) where  import Data.Aeson hiding (Result) import qualified Data.ByteString.Lazy.Char8 as B+import qualified Data.Text as Text import Hadolint.Formatter.Format (Result (..), errorPosition, severityText)-import Hadolint.Rules (Metadata (..), RuleCheck (..), DLSeverity (..))+import Hadolint.Rule (CheckFailure (..), DLSeverity (..), unRuleCode) import Text.Megaparsec (TraversableStream) import Text.Megaparsec.Error import Text.Megaparsec.Pos (sourceColumn, sourceLine, sourceName, unPos) import Text.Megaparsec.Stream (VisualStream)  data JsonFormat s e-  = JsonCheck RuleCheck+  = JsonCheck Text.Text CheckFailure   | JsonParseError (ParseErrorBundle s e)  instance (VisualStream s, TraversableStream s, ShowErrorComponent e) => ToJSON (JsonFormat s e) where-  toJSON (JsonCheck RuleCheck {..}) =+  toJSON (JsonCheck filename CheckFailure {..}) =     object       [ "file" .= filename,-        "line" .= linenumber,+        "line" .= line,         "column" .= (1 :: Int),-        "level" .= severityText (severity metadata),-        "code" .= code metadata,-        "message" .= message metadata+        "level" .= severityText severity,+        "code" .= unRuleCode code,+        "message" .= message       ]   toJSON (JsonParseError err) =     object@@ -36,18 +34,27 @@         "line" .= unPos (sourceLine pos),         "column" .= unPos (sourceColumn pos),         "level" .= severityText DLErrorC,-        "code" .= ("DL1000" :: String),+        "code" .= ("DL1000" :: Text.Text),         "message" .= errorBundlePretty err       ]     where       pos = errorPosition err +printResults ::+  (VisualStream s, TraversableStream s, ShowErrorComponent e, Foldable f) =>+  f (Result s e) ->+  IO ()+printResults = mapM_ printResult+ formatResult :: (VisualStream s, TraversableStream s, ShowErrorComponent e) => Result s e -> Value-formatResult (Result errors checks) = toJSON allMessages+formatResult (Result fileName errors checks) = toJSON allMessages   where     allMessages = errorMessages <> checkMessages     errorMessages = fmap JsonParseError errors-    checkMessages = fmap JsonCheck checks+    checkMessages = fmap (JsonCheck fileName) checks -printResult :: (VisualStream s, TraversableStream s, ShowErrorComponent e) => Result s e -> IO ()+printResult ::+  (VisualStream s, TraversableStream s, ShowErrorComponent e) =>+  Result s e ->+  IO () printResult result = B.putStrLn (encode (formatResult result))
src/Hadolint/Formatter/TTY.hs view
@@ -2,53 +2,56 @@ {-# LANGUAGE OverloadedStrings #-}  module Hadolint.Formatter.TTY-  ( printResult,+  ( printResults,+    formatCheck,     formatError,-    formatChecks,   ) where  import Colourista+import qualified Control.Foldl as Foldl import qualified Data.Text as Text import Hadolint.Formatter.Format-import Hadolint.Rules+import Hadolint.Rule (CheckFailure (..), DLSeverity (..), RuleCode (..)) import Language.Docker.Syntax import Text.Megaparsec (TraversableStream) import Text.Megaparsec.Error import Text.Megaparsec.Stream (VisualStream) -formatErrors :: (VisualStream s, TraversableStream s, ShowErrorComponent e, Functor f) => f (ParseErrorBundle s e) -> f String-formatErrors = fmap formatError- formatError :: (VisualStream s, TraversableStream s, ShowErrorComponent e) => ParseErrorBundle s e -> String formatError err = stripNewlines (errorMessageLine err) -formatChecks :: Functor f => f RuleCheck -> Bool -> f Text.Text-formatChecks rc nocolor = fmap formatCheck rc-  where-    formatCheck (RuleCheck meta source line _) =-      formatPos source line-          <> code meta-          <> " "-          <> (if nocolor then Text.pack (severityText (severity meta))-                         else colorizedSeverity (severity meta))-          <> ": "-          <> message meta+formatCheck :: Bool -> Text.Text -> CheckFailure -> Text.Text+formatCheck nocolor source CheckFailure {code, severity, line, message} =+  formatPos source line+    <> unRuleCode code+    <> " "+    <> ( if nocolor+           then severityText severity+           else colorizedSeverity severity+       )+    <> ": "+    <> message  formatPos :: Filename -> Linenumber -> Text.Text formatPos source line = source <> ":" <> Text.pack (show line) <> " " -printResult :: (VisualStream s, TraversableStream s, ShowErrorComponent e) => Result s e -> Bool -> IO ()-printResult Result {errors, checks} color = printErrors >> printChecks+printResults ::+  (VisualStream s, TraversableStream s, ShowErrorComponent e, Foldl.Foldable f) =>+  f (Result s e) ->+  Bool ->+  IO ()+printResults results color = mapM_ printResult results   where-    printErrors = mapM_ putStrLn (formatErrors errors)-    printChecks = mapM_ (putStrLn . Text.unpack) (formatChecks checks color)+    printResult Result {fileName, errors, checks} = printErrors errors >> printChecks fileName checks+    printErrors errors = mapM_ (putStrLn . formatError) errors+    printChecks fileName checks = mapM_ (putStrLn . Text.unpack . formatCheck color fileName) checks  colorizedSeverity :: DLSeverity -> Text.Text colorizedSeverity s =   case s of-    DLErrorC -> Text.pack $ formatWith [bold, red] $ severityText s-    DLWarningC -> Text.pack $ formatWith [bold, yellow] $ severityText s-    DLInfoC -> Text.pack $ formatWith [green] $ severityText s-    DLStyleC -> Text.pack $ formatWith [cyan] $ severityText s-    _ -> Text.pack $ severityText s+    DLErrorC -> formatWith [bold, red] $ severityText s+    DLWarningC -> formatWith [bold, yellow] $ severityText s+    DLInfoC -> formatWith [green] $ severityText s+    DLStyleC -> formatWith [cyan] $ severityText s+    _ -> severityText s
+ src/Hadolint/Ignore.hs view
@@ -0,0 +1,40 @@+module Hadolint.Ignore (ignored) where++import qualified Control.Foldl as Foldl+import qualified Data.IntMap.Strict as Map+import qualified Data.Set as Set+import qualified Data.Text as Text+import Data.Void (Void)+import Hadolint.Rule (RuleCode (RuleCode))+import Language.Docker.Syntax+import qualified Text.Megaparsec as Megaparsec+import qualified Text.Megaparsec.Char as Megaparsec++ignored :: Foldl.Fold (InstructionPos Text.Text) (Map.IntMap (Set.Set RuleCode))+ignored = Foldl.Fold parse mempty id+  where+    parse acc InstructionPos {instruction = Comment comment, lineNumber = line} =+      case parseComment comment of+        Just ignores@(_ : _) -> Map.insert (line + 1) (Set.fromList . fmap RuleCode $ ignores) acc+        _ -> acc+    parse acc _ = acc++    parseComment :: Text.Text -> Maybe [Text.Text]+    parseComment =+      Megaparsec.parseMaybe commentParser++    commentParser :: Megaparsec.Parsec Void Text.Text [Text.Text]+    commentParser =+      do+        spaces+        >> string "hadolint"+        >> spaces1+        >> string "ignore="+        >> spaces+        >> Megaparsec.sepBy1 ruleName (spaces >> string "," >> spaces)++    ruleName = Megaparsec.takeWhile1P Nothing (\c -> c `elem` Set.fromList "DLSC0123456789")+    string = Megaparsec.string+    spaces = Megaparsec.takeWhileP Nothing space+    spaces1 = Megaparsec.takeWhile1P Nothing space+    space c = c == ' ' || c == '\t'
src/Hadolint/Lint.hs view
@@ -1,30 +1,39 @@-{-# LANGUAGE NamedFieldPuns #-}--module Hadolint.Lint where+module Hadolint.Lint+  ( lintIO,+    lint,+    analyze,+    LintOptions (..),+    ErrorRule,+    WarningRule,+    InfoRule,+    StyleRule,+    IgnoreRule,+    TrustedRegistry,+  )+where -import qualified Control.Concurrent.Async as Async-import Control.Parallel.Strategies (parListChunk, rseq, using)+import qualified Control.Parallel.Strategies as Parallel import qualified Data.List.NonEmpty as NonEmpty+import qualified Data.Sequence as Seq import Data.Text (Text)-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+import qualified Data.Text as Text import qualified Hadolint.Formatter.Format as Format-import qualified Hadolint.Formatter.Json as Json-import qualified Hadolint.Formatter.TTY as TTY-import qualified Hadolint.Rules as Rules+import qualified Hadolint.Process+import qualified Hadolint.Rule import qualified Language.Docker as Docker import Language.Docker.Parser (DockerfileError, Error) import Language.Docker.Syntax (Dockerfile) - type ErrorRule = Text+ type WarningRule = Text+ type InfoRule = Text+ type StyleRule = Text-type IgnoreRule = Text +type IgnoreRule = Hadolint.Rule.RuleCode+ type TrustedRegistry = Text  data LintOptions = LintOptions@@ -33,85 +42,75 @@     infoRules :: [InfoRule],     styleRules :: [StyleRule],     ignoreRules :: [IgnoreRule],-    rulesConfig :: Rules.RulesConfig+    rulesConfig :: Hadolint.Process.RulesConfig   }   deriving (Show) -data OutputFormat-  = Json-  | TTY-  | CodeclimateJson-  | GitlabCodeclimateJson-  | Checkstyle-  | Codacy-  deriving (Show, Eq)--printResults :: OutputFormat -> Bool -> Format.Result Text DockerfileError -> IO ()-printResults format nocolor allResults =-  case format of-    TTY -> TTY.printResult allResults nocolor-    Json -> Json.printResult allResults-    Checkstyle -> Checkstyle.printResult allResults-    CodeclimateJson -> Codeclimate.printResult allResults-    GitlabCodeclimateJson -> Codeclimate.printGitlabResult allResults-    Codacy -> Codacy.printResult allResults+instance Semigroup LintOptions where+  LintOptions a1 a2 a3 a4 a5 a6 <> LintOptions b1 b2 b3 b4 b5 b6 =+    LintOptions+      (a1 <> b1)+      (a2 <> b2)+      (a3 <> b3)+      (a4 <> b4)+      (a5 <> b5)+      (a6 <> b6) -shallSkipErrorStatus:: OutputFormat -> Bool-shallSkipErrorStatus format  = format `elem` [CodeclimateJson, Codacy]+instance Monoid LintOptions where+  mempty = LintOptions mempty mempty mempty mempty mempty mempty  -- | 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-    { errorRules = errorList,-      warningRules = warningList,-      infoRules = infoList,-      styleRules = styleList,-      ignoreRules = ignoreList,-      rulesConfig-    }-  dFiles = do-    parsedFiles <- Async.mapConcurrently parseFile (NonEmpty.toList dFiles)-    let results = lintAll parsedFiles `using` parListChunk (div numCapabilities 2) rseq-    return $ mconcat results-    where-      parseFile :: String -> IO (Either Error Dockerfile)-      parseFile "-" = Docker.parseStdin-      parseFile s = Docker.parseFile s--      lintAll = fmap lintDockerfile+lintIO :: LintOptions -> NonEmpty.NonEmpty FilePath -> IO (NonEmpty.NonEmpty (Format.Result Text DockerfileError))+lintIO options dFiles = do+  parsedFiles <- mapM parseFile (NonEmpty.toList dFiles)+  return $ NonEmpty.fromList (lint options parsedFiles)+  where+    parseFile :: String -> IO (Text, Either Error Dockerfile)+    parseFile "-" = do+      res <- Docker.parseStdin+      return (Text.pack "-", res)+    parseFile s = do+      res <- Docker.parseFile s+      return (Text.pack s, res) -      lintDockerfile = processedFile-        where-          processedFile = Format.toResult . fmap processRules-          processRules fileLines =-            filter ignoredRules $-              map-                ( makeSeverity Rules.DLErrorC errorList-                . makeSeverity Rules.DLWarningC warningList-                . makeSeverity Rules.DLInfoC infoList-                . makeSeverity Rules.DLStyleC styleList-                )-                $ analyzeAll rulesConfig fileLines+lint ::+  LintOptions ->+  [(Text, Either Error Dockerfile)] ->+  [Format.Result Text DockerfileError]+lint options parsedFiles = gather results `Parallel.using` parallelRun+  where+    gather = fmap (uncurry Format.toResult)+    results =+      [ ( name,+          fmap (analyze options) parseResult+        )+        | (name, parseResult) <- parsedFiles+      ]+    parallelRun = Parallel.parList Parallel.rseq -          ignoredRules = ignoreFilter ignoreList+analyze :: LintOptions -> Dockerfile -> Seq.Seq Hadolint.Rule.CheckFailure+analyze options dockerfile = fixer process+  where+    fixer = fixSeverity options+    process = Hadolint.Process.run (rulesConfig options) dockerfile -          makeSeverity s rules rule@(Rules.RuleCheck (Rules.Metadata code _ message) filename linenumber success) =-            if code `elem` rules-              then Rules.RuleCheck (Rules.Metadata code s message) filename linenumber success-              else rule+fixSeverity :: LintOptions -> Seq.Seq Hadolint.Rule.CheckFailure -> Seq.Seq Hadolint.Rule.CheckFailure+fixSeverity LintOptions {..} = Seq.filter ignoredRules . Seq.mapWithIndex (const correctSeverity)+  where+    correctSeverity =+      makeSeverity Hadolint.Rule.DLErrorC (fmap Hadolint.Rule.RuleCode errorRules)+        . makeSeverity Hadolint.Rule.DLWarningC (fmap Hadolint.Rule.RuleCode warningRules)+        . makeSeverity Hadolint.Rule.DLInfoC (fmap Hadolint.Rule.RuleCode infoRules)+        . makeSeverity Hadolint.Rule.DLStyleC (fmap Hadolint.Rule.RuleCode styleRules) -          ignoreFilter :: [IgnoreRule] -> Rules.RuleCheck -> Bool-          ignoreFilter rules (Rules.RuleCheck (Rules.Metadata code severity _) _ _ _) =-            code `notElem` rules && severity /= Rules.DLIgnoreC+    ignoredRules = ignoreFilter ignoreRules --- | Returns the result of applying all the rules to the given dockerfile-analyzeAll :: Rules.RulesConfig -> Dockerfile -> [Rules.RuleCheck]-analyzeAll config = Rules.analyze (Rules.rules ++ Rules.optionalRules config)+    makeSeverity s rules rule@Hadolint.Rule.CheckFailure {code} =+      if code `elem` rules+        then rule {Hadolint.Rule.severity = s}+        else rule --- | Helper to analyze AST quickly in GHCI-analyzeEither :: Rules.RulesConfig -> Either t Dockerfile -> [Rules.RuleCheck]-analyzeEither _ (Left _) = []-analyzeEither config (Right dockerFile) = analyzeAll config dockerFile+    ignoreFilter :: [IgnoreRule] -> Hadolint.Rule.CheckFailure -> Bool+    ignoreFilter ignored Hadolint.Rule.CheckFailure {code, severity} =+      code `notElem` ignored && severity /= Hadolint.Rule.DLIgnoreC
+ src/Hadolint/Process.hs view
@@ -0,0 +1,203 @@+module Hadolint.Process (run, RulesConfig (..)) where++import qualified Control.Foldl as Foldl+import qualified Data.IntMap.Strict as SMap+import qualified Data.Sequence as Seq+import qualified Data.Set as Set+import qualified Data.Text as Text+import qualified Hadolint.Ignore+import Hadolint.Rule (CheckFailure (..), Failures, Rule, RuleCode)+import qualified Hadolint.Rule as Rule+import qualified Hadolint.Rule.DL3000+import qualified Hadolint.Rule.DL3001+import qualified Hadolint.Rule.DL3002+import qualified Hadolint.Rule.DL3003+import qualified Hadolint.Rule.DL3004+import qualified Hadolint.Rule.DL3005+import qualified Hadolint.Rule.DL3006+import qualified Hadolint.Rule.DL3007+import qualified Hadolint.Rule.DL3008+import qualified Hadolint.Rule.DL3009+import qualified Hadolint.Rule.DL3010+import qualified Hadolint.Rule.DL3011+import qualified Hadolint.Rule.DL3012+import qualified Hadolint.Rule.DL3013+import qualified Hadolint.Rule.DL3014+import qualified Hadolint.Rule.DL3015+import qualified Hadolint.Rule.DL3016+import qualified Hadolint.Rule.DL3017+import qualified Hadolint.Rule.DL3018+import qualified Hadolint.Rule.DL3019+import qualified Hadolint.Rule.DL3020+import qualified Hadolint.Rule.DL3021+import qualified Hadolint.Rule.DL3022+import qualified Hadolint.Rule.DL3023+import qualified Hadolint.Rule.DL3024+import qualified Hadolint.Rule.DL3025+import qualified Hadolint.Rule.DL3026+import qualified Hadolint.Rule.DL3027+import qualified Hadolint.Rule.DL3028+import qualified Hadolint.Rule.DL3029+import qualified Hadolint.Rule.DL3030+import qualified Hadolint.Rule.DL3031+import qualified Hadolint.Rule.DL3032+import qualified Hadolint.Rule.DL3033+import qualified Hadolint.Rule.DL3034+import qualified Hadolint.Rule.DL3035+import qualified Hadolint.Rule.DL3036+import qualified Hadolint.Rule.DL3037+import qualified Hadolint.Rule.DL3038+import qualified Hadolint.Rule.DL3039+import qualified Hadolint.Rule.DL3040+import qualified Hadolint.Rule.DL3041+import qualified Hadolint.Rule.DL3042+import qualified Hadolint.Rule.DL3043+import qualified Hadolint.Rule.DL3044+import qualified Hadolint.Rule.DL3045+import qualified Hadolint.Rule.DL3046+import qualified Hadolint.Rule.DL3047+import qualified Hadolint.Rule.DL3048+import qualified Hadolint.Rule.DL3049+import qualified Hadolint.Rule.DL3050+import qualified Hadolint.Rule.DL3051+import qualified Hadolint.Rule.DL3052+import qualified Hadolint.Rule.DL3053+import qualified Hadolint.Rule.DL3054+import qualified Hadolint.Rule.DL3055+import qualified Hadolint.Rule.DL3056+import qualified Hadolint.Rule.DL3057+import qualified Hadolint.Rule.DL4000+import qualified Hadolint.Rule.DL4001+import qualified Hadolint.Rule.DL4003+import qualified Hadolint.Rule.DL4004+import qualified Hadolint.Rule.DL4005+import qualified Hadolint.Rule.DL4006+import qualified Hadolint.Rule.Shellcheck+import qualified Hadolint.Shell as Shell+import Language.Docker.Syntax+++-- | Contains the required parameters for optional rules+data RulesConfig = RulesConfig+  { -- | The docker registries that are allowed in FROM+    allowedRegistries :: Set.Set Registry,+    labelSchema :: Rule.LabelSchema,+    strictLabels :: Bool+  }+  deriving (Show, Eq)++instance Semigroup RulesConfig where+  RulesConfig a1 a2 a3 <> RulesConfig b1 b2 b3 =+    RulesConfig+      (a1 <> b1)+      (a2 <> b2)+      (a3 || b3)++instance Monoid RulesConfig where+  mempty = RulesConfig mempty mempty False++data AnalisisResult = AnalisisResult+  { -- | The set of ignored rules per line+    ignored :: SMap.IntMap (Set.Set RuleCode),+    -- | A set of failures collected for reach rule+    failed :: Failures+  }++run :: RulesConfig -> [InstructionPos Text.Text] -> Failures+run config dockerfile = Seq.filter shouldKeep failed+  where+    AnalisisResult {..} = Foldl.fold (analyze config) dockerfile++    shouldKeep CheckFailure {line, code} =+      Just True /= do+        ignoreList <- SMap.lookup line ignored+        return $ code `Set.member` ignoreList++analyze :: RulesConfig -> Foldl.Fold (InstructionPos Text.Text) AnalisisResult+analyze config =+  AnalisisResult+    <$> Hadolint.Ignore.ignored+    <*> Foldl.premap parseShell (failures config <> onBuildFailures config)++parseShell :: InstructionPos Text.Text -> InstructionPos Shell.ParsedShell+parseShell = fmap Shell.parseShell++onBuildFailures :: RulesConfig -> Rule Shell.ParsedShell+onBuildFailures config =+  Foldl.prefilter+    isOnBuild+    (Foldl.premap unwrapOnbuild (failures config))+  where+    isOnBuild InstructionPos {instruction = OnBuild {}} = True+    isOnBuild _ = False++    unwrapOnbuild inst@InstructionPos {instruction = OnBuild i} = inst {instruction = i}+    unwrapOnbuild inst = inst++failures :: RulesConfig -> Rule Shell.ParsedShell+failures RulesConfig {allowedRegistries, labelSchema, strictLabels} =+  Hadolint.Rule.DL3000.rule+    <> Hadolint.Rule.DL3001.rule+    <> Hadolint.Rule.DL3002.rule+    <> Hadolint.Rule.DL3003.rule+    <> Hadolint.Rule.DL3004.rule+    <> Hadolint.Rule.DL3005.rule+    <> Hadolint.Rule.DL3006.rule+    <> Hadolint.Rule.DL3007.rule+    <> Hadolint.Rule.DL3008.rule+    <> Hadolint.Rule.DL3009.rule+    <> Hadolint.Rule.DL3010.rule+    <> Hadolint.Rule.DL3011.rule+    <> Hadolint.Rule.DL3012.rule+    <> Hadolint.Rule.DL3013.rule+    <> Hadolint.Rule.DL3014.rule+    <> Hadolint.Rule.DL3015.rule+    <> Hadolint.Rule.DL3016.rule+    <> Hadolint.Rule.DL3017.rule+    <> Hadolint.Rule.DL3018.rule+    <> Hadolint.Rule.DL3019.rule+    <> Hadolint.Rule.DL3020.rule+    <> Hadolint.Rule.DL3021.rule+    <> Hadolint.Rule.DL3022.rule+    <> Hadolint.Rule.DL3023.rule+    <> Hadolint.Rule.DL3024.rule+    <> Hadolint.Rule.DL3025.rule+    <> Hadolint.Rule.DL3026.rule allowedRegistries+    <> Hadolint.Rule.DL3027.rule+    <> Hadolint.Rule.DL3028.rule+    <> Hadolint.Rule.DL3029.rule+    <> Hadolint.Rule.DL3030.rule+    <> Hadolint.Rule.DL3031.rule+    <> Hadolint.Rule.DL3032.rule+    <> Hadolint.Rule.DL3033.rule+    <> Hadolint.Rule.DL3034.rule+    <> Hadolint.Rule.DL3035.rule+    <> Hadolint.Rule.DL3036.rule+    <> Hadolint.Rule.DL3037.rule+    <> Hadolint.Rule.DL3038.rule+    <> Hadolint.Rule.DL3039.rule+    <> Hadolint.Rule.DL3040.rule+    <> Hadolint.Rule.DL3041.rule+    <> Hadolint.Rule.DL3042.rule+    <> Hadolint.Rule.DL3043.rule+    <> Hadolint.Rule.DL3044.rule+    <> Hadolint.Rule.DL3045.rule+    <> Hadolint.Rule.DL3046.rule+    <> Hadolint.Rule.DL3047.rule+    <> Hadolint.Rule.DL3048.rule+    <> Hadolint.Rule.DL3049.rule labelSchema+    <> Hadolint.Rule.DL3050.rule labelSchema strictLabels+    <> Hadolint.Rule.DL3051.rule labelSchema+    <> Hadolint.Rule.DL3052.rule labelSchema+    <> Hadolint.Rule.DL3053.rule labelSchema+    <> Hadolint.Rule.DL3054.rule labelSchema+    <> Hadolint.Rule.DL3055.rule labelSchema+    <> Hadolint.Rule.DL3056.rule labelSchema+    <> Hadolint.Rule.DL3057.rule+    <> Hadolint.Rule.DL4000.rule+    <> Hadolint.Rule.DL4001.rule+    <> Hadolint.Rule.DL4003.rule+    <> Hadolint.Rule.DL4004.rule+    <> Hadolint.Rule.DL4005.rule+    <> Hadolint.Rule.DL4006.rule+    <> Hadolint.Rule.Shellcheck.rule
+ src/Hadolint/Rule.hs view
@@ -0,0 +1,172 @@+module Hadolint.Rule where++import Control.DeepSeq (NFData)+import qualified Control.Foldl as Foldl+import qualified Data.Map as Map+import qualified Data.Sequence as Seq+import Data.String (IsString (..))+import qualified Data.Text as Text+import qualified Data.YAML as Yaml+import GHC.Generics (Generic)+import Language.Docker.Syntax++infixl 0 |>++(|>) :: a -> (a -> b) -> b+x |> f = f x++data DLSeverity+  = DLErrorC+  | DLWarningC+  | DLInfoC+  | DLStyleC+  | DLIgnoreC+  deriving (Show, Eq, Ord, Generic, NFData)++newtype RuleCode = RuleCode {unRuleCode :: Text.Text}+  deriving (Show, Eq, Ord)++instance IsString RuleCode where+  fromString = RuleCode . Text.pack++data CheckFailure = CheckFailure+  { code :: RuleCode,+    severity :: DLSeverity,+    message :: Text.Text,+    line :: Linenumber+  }+  deriving (Show, Eq)++instance Ord CheckFailure where+  a `compare` b = line a `compare` line b++type Failures = Seq.Seq CheckFailure++data State a = State+  { failures :: Failures,+    state :: a+  }+  deriving (Show)++type LabelName = Text.Text++data LabelType+  = RawText+  | Url+  | Spdx+  | GitHash+  | Rfc3339+  | SemVer+  deriving (Eq, Read, Show)++read :: Text.Text -> Either Text.Text LabelType+read "url"     = Right Url+read "spdx"    = Right Spdx+read "hash"    = Right GitHash+read "rfc3339" = Right Rfc3339+read "semver"  = Right SemVer+read "text"    = Right RawText+read ""        = Right RawText+read t         = Left ("Invalid label type: " <> t)++instance Yaml.FromYAML LabelType where+  parseYAML = withLabelType pure++withLabelType :: (LabelType -> Yaml.Parser a) -> Yaml.Node Yaml.Pos -> Yaml.Parser a+withLabelType f v@(Yaml.Scalar _ (Yaml.SStr b)) =+    case Hadolint.Rule.read b of+      Right lt -> f lt+      Left _ -> Yaml.typeMismatch "labeltype" v+withLabelType _ v = Yaml.typeMismatch "labeltype" v++type LabelSchema = Map.Map LabelName LabelType+++withLineNumber ::+  (Linenumber -> t1 -> Instruction args -> t2) ->+  t1 ->+  InstructionPos args ->+  t2+withLineNumber f state InstructionPos {instruction, lineNumber} =+  f lineNumber state instruction++addFail :: CheckFailure -> State a -> State a+addFail failure state@(State fails _) =+  state+    { failures =+        fails+          Seq.|> failure+    }++emptyState :: a -> State a+emptyState = State Seq.empty++simpleState :: State ()+simpleState = State Seq.empty ()++modify :: (a -> a) -> State a -> State a+modify f s@(State _ st) = s {state = f st}++replaceWith :: a -> State a -> State a+replaceWith newState s = s {state = newState}++type Rule args = Foldl.Fold (InstructionPos args) Failures++-- | A simple rule that can be implemented in terms of returning True or False for each instruction+-- If you need to calculate some state to decide upon past information, use 'customRule'+simpleRule ::+  -- | rule code+  RuleCode ->+  -- | severity for the rule+  DLSeverity ->+  -- | failure message for the rule+  Text.Text ->+  -- | step calculation for the rule. Returns True or False for each line in the dockerfile depending on its validity.+  (Instruction args -> Bool) ->+  Rule args+simpleRule code severity message checker = customRule step simpleState+  where+    step line s instr+      | checker instr = s+      | otherwise = s |> addFail (CheckFailure code severity message line)++-- | A rule that accumulates a State a. The state contains the collection of failed lines and a custom data+-- type that can be used to track properties for the rule. Each step always returns the new State, which offers+-- the ability to both accumulate properties and mark failures for every given instruction.+customRule ::+  (Linenumber -> State a -> Instruction args -> State a) ->+  State a ->+  Rule args+customRule step initial = veryCustomRule step initial failures++-- | Similarly to 'customRule', it returns a State a for each step, but it has the ability to run a+-- done callback as the last step of the rule. The done callback can be used to transform the state+-- and mark failures for any arbitrary line in the input. This helper is meant for rules that need+-- to do lookahead. Instead of looking ahead, the state should store the facts and make a decision about+-- them once the input is finished.+veryCustomRule ::+  -- | step calculation for the rule. Called for each instruction in the docker file+  -- it must return the state after being modified by the rule+  (Linenumber -> State a -> Instruction args -> State a) ->+  -- | initial state+  State a ->+  -- | done callaback. It is passed the final accumulated state and it should return all failures+  -- found by the rule+  (State a -> Failures) ->+  Rule args+veryCustomRule step = Foldl.Fold (withLineNumber step)++foldArguments :: (a -> b) -> Arguments a -> b+foldArguments applyRule args =+  case args of+    ArgumentsText as -> applyRule as+    ArgumentsList as -> applyRule as++-- | 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
+ src/Hadolint/Rule/DL3000.hs view
@@ -0,0 +1,22 @@+module Hadolint.Rule.DL3000 (rule) where++import qualified Data.Text as Text+import Hadolint.Rule+import Language.Docker.Syntax (Instruction (..))++rule :: Rule args+rule = simpleRule code severity message check+  where+    code = "DL3000"+    severity = DLErrorC+    message = "Use absolute WORKDIR"+    check (Workdir loc)+      | "$" `Text.isPrefixOf` Text.dropAround dropQuotes loc = True+      | "/" `Text.isPrefixOf` Text.dropAround dropQuotes loc = True+      | otherwise = False+    check _ = True+    dropQuotes chr+      | chr == '\"' = True+      | chr == '\'' = True+      | otherwise = False+{-# INLINEABLE rule #-}
+ src/Hadolint/Rule/DL3001.hs view
@@ -0,0 +1,33 @@+module Hadolint.Rule.DL3001 (rule) where++import qualified Data.Set as Set+import Hadolint.Rule+import qualified Hadolint.Shell as Shell+import Language.Docker.Syntax (Instruction (..), RunArgs (..))++rule :: Rule Shell.ParsedShell+rule = simpleRule code severity message check+  where+    code = "DL3001"+    severity = DLInfoC+    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 (RunArgs args _)) = foldArguments hasInvalid args+    check _ = True++    hasInvalid args = null [arg | arg <- Shell.findCommandNames args, Set.member arg invalidCmds]+    invalidCmds =+      Set.fromList+        [ "free",+          "kill",+          "mount",+          "ps",+          "service",+          "shutdown",+          "ssh",+          "top",+          "vim"+        ]+{-# INLINEABLE rule #-}
+ src/Hadolint/Rule/DL3002.hs view
@@ -0,0 +1,49 @@+module Hadolint.Rule.DL3002 (rule) where++import qualified Data.IntMap.Strict as Map+import qualified Data.Sequence as Seq+import qualified Data.Text as Text+import Hadolint.Rule+import Language.Docker.Syntax (Instruction (..), Linenumber)++type StageLine = Linenumber++type UserLine = Linenumber++data Acc+  = Acc StageLine (Map.IntMap UserLine)+  | Empty+  deriving (Show)++rule :: Rule args+rule = veryCustomRule check (emptyState Empty) markFailures+  where+    code = "DL3002"+    severity = DLWarningC+    message = "Last USER should not be root"++    check line st (From _) = st |> modify (rememberStage line)+    check line st (User user)+      | not (isRoot user) = st |> modify forgetStage+      | otherwise = st |> modify (rememberLine line)+    check _ st _ = st++    isRoot user =+      Text.isPrefixOf "root:" user || Text.isPrefixOf "0:" user || user == "root" || user == "0"++    markFailures (State fails (Acc _ st)) = Map.foldl' (Seq.|>) fails (fmap makeFail st)+    markFailures st = failures st+    makeFail line = CheckFailure {..}+{-# INLINEABLE rule #-}++rememberStage :: StageLine -> Acc -> Acc+rememberStage from (Acc _ m) = Acc from m+rememberStage from Empty = Acc from Map.empty++forgetStage :: Acc -> Acc+forgetStage (Acc from m) = Acc from (m |> Map.delete from)+forgetStage Empty = Empty++rememberLine :: StageLine -> Acc -> Acc+rememberLine line (Acc from m) = Acc from (m |> Map.insert from line)+rememberLine _ Empty = Empty
+ src/Hadolint/Rule/DL3003.hs view
@@ -0,0 +1,15 @@+module Hadolint.Rule.DL3003 (rule) where++import Hadolint.Rule+import Hadolint.Shell (ParsedShell, usingProgram)+import Language.Docker.Syntax (Instruction (..), RunArgs (..))++rule :: Rule ParsedShell+rule = simpleRule code severity message check+  where+    code = "DL3003"+    severity = DLWarningC+    message = "Use WORKDIR to switch to a directory"+    check (Run (RunArgs args _)) = foldArguments (not . usingProgram "cd") args+    check _ = True+{-# INLINEABLE rule #-}
+ src/Hadolint/Rule/DL3004.hs view
@@ -0,0 +1,17 @@+module Hadolint.Rule.DL3004 (rule) where++import Hadolint.Rule+import Hadolint.Shell (ParsedShell, usingProgram)+import Language.Docker.Syntax (Instruction (..), RunArgs (..))++rule :: Rule ParsedShell+rule = simpleRule code severity message check+  where+    code = "DL3004"+    severity = DLErrorC+    message =+      "Do not use sudo as it leads to unpredictable behavior. Use a tool like gosu to enforce \+      \root"+    check (Run (RunArgs args _)) = foldArguments (not . usingProgram "sudo") args+    check _ = True+{-# INLINEABLE rule #-}
+ src/Hadolint/Rule/DL3005.hs view
@@ -0,0 +1,17 @@+module Hadolint.Rule.DL3005 (rule) where++import Hadolint.Rule+import Hadolint.Shell (ParsedShell)+import qualified Hadolint.Shell as Shell+import Language.Docker.Syntax (Instruction (..), RunArgs (..))++rule :: Rule ParsedShell+rule = simpleRule code severity message check+  where+    code = "DL3005"+    severity = DLErrorC+    message = "Do not use apt-get upgrade or dist-upgrade"+    check (Run (RunArgs args _)) =+      foldArguments (Shell.noCommands (Shell.cmdHasArgs "apt-get" ["upgrade", "dist-upgrade"])) args+    check _ = True+{-# INLINEABLE rule #-}
+ src/Hadolint/Rule/DL3006.hs view
@@ -0,0 +1,33 @@+module Hadolint.Rule.DL3006 (rule) where++import qualified Data.Set as Set+import Data.Text (Text)+import Hadolint.Rule+import Language.Docker.Syntax++rule :: Rule args+rule = customRule check (emptyState Set.empty)+  where+    code = "DL3006"+    severity = DLWarningC+    message = "Always tag the version of an image explicitly"++    check line st (From from) =+      let newState = st |> modify (insertFromAlias from)+       in case from of+            BaseImage {image = (Image _ "scratch")} -> newState+            BaseImage {digest = Just _} -> newState+            BaseImage {image = (Image _ i), tag = Nothing} ->+              -- When the image being used is a previously defined FROM alias,+              -- then we can safely ignore that the image is not tagged. Otherwise+              -- we marked it as a failure+              if Set.member i (state st)+                then newState+                else newState |> addFail (CheckFailure {..})+            _ -> newState+    check _ st _ = st+{-# INLINEABLE rule #-}++insertFromAlias :: BaseImage -> Set.Set Text -> Set.Set Text+insertFromAlias BaseImage {alias = Just a} st = st |> Set.insert (unImageAlias a)+insertFromAlias _ st = st
+ src/Hadolint/Rule/DL3007.hs view
@@ -0,0 +1,16 @@+module Hadolint.Rule.DL3007 (rule) where++import Hadolint.Rule+import Language.Docker.Syntax++rule :: Rule args+rule = simpleRule code severity message check+  where+    code = "DL3007"+    severity = DLWarningC+    message =+      "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+{-# INLINEABLE rule #-}
+ src/Hadolint/Rule/DL3008.hs view
@@ -0,0 +1,31 @@+module Hadolint.Rule.DL3008 (rule) where++import qualified Data.Text as Text+import Hadolint.Rule+import Hadolint.Shell (ParsedShell)+import qualified Hadolint.Shell as Shell+import Language.Docker.Syntax++rule :: Rule ParsedShell+rule = simpleRule code severity message check+  where+    code = "DL3008"+    severity = DLWarningC+    message =+      "Pin versions in apt get install. Instead of `apt-get install <package>` use `apt-get \+      \install <package>=<version>`"+    check (Run (RunArgs args _)) = foldArguments (all versionFixed . aptGetPackages) args+    check _ = True+    versionFixed package = "=" `Text.isInfixOf` package || ("/" `Text.isInfixOf` package || ".deb" `Text.isSuffixOf` package)+{-# INLINEABLE rule #-}++aptGetPackages :: ParsedShell -> [Text.Text]+aptGetPackages args =+  [ 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"]
+ src/Hadolint/Rule/DL3009.hs view
@@ -0,0 +1,65 @@+module Hadolint.Rule.DL3009 (rule) where++import qualified Data.Map.Strict as Map+import qualified Data.Text as Text+import Hadolint.Rule+import qualified Hadolint.Shell as Shell+import Language.Docker.Syntax++data Acc+  = Acc+      { lastFrom :: BaseImage,+        stages :: Map.Map Text.Text Linenumber,+        forgets :: Map.Map Linenumber BaseImage+      }+  | Empty+  deriving (Show)++rule :: Rule Shell.ParsedShell+rule = veryCustomRule check (emptyState Empty) markFailures+  where+    code = "DL3009"+    severity = DLInfoC+    message = "Delete the apt-get lists after installing something"++    check line st (From from) = st |> modify (rememberStage line from)+    check line st (Run (RunArgs args _))+      | foldArguments forgotToCleanup args = st |> modify (rememberLine line)+      | otherwise = st+    check _ st _ = st++    -- Convert the final state into failures+    markFailures (State fails Empty) = fails+    markFailures (State _ Acc {..}) = forgets |> Map.foldMapWithKey mapFail+      where+        mapFail line from+          | from == lastFrom = pure CheckFailure {..}+          | BaseImage {alias = Just (ImageAlias als)} <- from, -- Check if this stage is used later+            Just _ <- Map.lookup als stages =+            -- If the same alias is used in another stage, fail+            pure CheckFailure {..}+          | otherwise = mempty+{-# INLINEABLE rule #-}++rememberStage :: Linenumber -> BaseImage -> Acc -> Acc+rememberStage line from@BaseImage {image = Image _ als} (Acc _ stages o) = Acc from (Map.insert als line stages) o+rememberStage line from@BaseImage {image = Image _ als} Empty = Acc from (Map.singleton als line) Map.empty++rememberLine :: Linenumber -> Acc -> Acc+rememberLine line (Acc from stages o) = Acc from stages (Map.insert line from o)+rememberLine line Empty = Acc emptyImage mempty (Map.singleton line emptyImage)++forgotToCleanup :: Shell.ParsedShell -> Bool+forgotToCleanup args+  | hasUpdate && not hasCleanup = True+  | otherwise = False+  where+    hasCleanup =+      any (Shell.cmdHasArgs "rm" ["-rf", "/var/lib/apt/lists/*"]) (Shell.presentCommands args)++    hasUpdate = any (Shell.cmdHasArgs "apt-get" ["update"]) (Shell.presentCommands args)++-- | Even though dockerfiles without a FROM are not valid, we still want to provide some feedback for this rule+-- so we pretend there is a base image at the start of the file if there is none+emptyImage :: BaseImage+emptyImage = (BaseImage {image = "scratch", tag = Nothing, digest = Nothing, alias = Nothing, platform = Nothing})
+ src/Hadolint/Rule/DL3010.hs view
@@ -0,0 +1,38 @@+module Hadolint.Rule.DL3010 (rule) where++import Data.Foldable (toList)+import qualified Data.Text as Text+import Hadolint.Rule+import Language.Docker.Syntax++rule :: Rule args+rule = simpleRule code severity message check+  where+    code = "DL3010"+    severity = DLInfoC+    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+        ]+    check _ = True+    archiveFormats =+      [ ".tar",+        ".tar.bz2",+        ".tb2",+        ".tbz",+        ".tbz2",+        ".tar.gz",+        ".tgz",+        ".tpz",+        ".tar.lz",+        ".tar.lzma",+        ".tlz",+        ".tar.xz",+        ".txz",+        ".tar.Z",+        ".tZ"+      ]+{-# INLINEABLE rule #-}
+ src/Hadolint/Rule/DL3011.hs view
@@ -0,0 +1,16 @@+module Hadolint.Rule.DL3011 (rule) where++import Hadolint.Rule+import Language.Docker.Syntax++rule :: Rule args+rule = simpleRule code severity message check+  where+    code = "DL3011"+    severity = DLErrorC+    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]+    check _ = True+{-# INLINEABLE rule #-}
+ src/Hadolint/Rule/DL3012.hs view
@@ -0,0 +1,18 @@+module Hadolint.Rule.DL3012 (rule) where++import Hadolint.Rule+import Language.Docker.Syntax+++rule :: Rule args+rule = customRule check (emptyState False)+  where+    code = "DL3012"+    severity = DLErrorC+    message = "Multiple `HEALTHCHECK` instructions"+    check _ st From {} = st |> replaceWith False+    check line st Healthcheck {}+        | not (state st) = st |> replaceWith True+        | otherwise = st |> addFail CheckFailure {..}+    check _ st _ = st+{-# INLINEABLE rule #-}
+ src/Hadolint/Rule/DL3013.hs view
@@ -0,0 +1,83 @@+module Hadolint.Rule.DL3013 (rule) where++import Data.List (isInfixOf)+import qualified Data.Text as Text+import Hadolint.Rule+import Hadolint.Shell (ParsedShell)+import qualified Hadolint.Shell as Shell+import Language.Docker.Syntax++rule :: Rule ParsedShell+rule = simpleRule code severity message check+  where+    code = "DL3013"+    severity = DLWarningC+    message =+      "Pin versions in pip. Instead of `pip install <package>` use `pip install \+      \<package>==<version>` or `pip install --requirement <requirements file>`"+    check (Run (RunArgs args _)) = foldArguments (Shell.noCommands forgotToPinVersion) args+    check _ = True++    forgotToPinVersion 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 =+      ( Shell.isPipInstall cmd+          && not (hasBuildConstraint cmd)+          && not (all versionFixed (packages cmd))+      )+        && not (requirementInstall 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++    hasBuildConstraint cmd = Shell.hasFlag "constraint" cmd || Shell.hasFlag "c" cmd+    versionFixed package = hasVersionSymbol package || isVersionedGit package || isLocalPackage package+    isVersionedGit package = "git+http" `Text.isInfixOf` package && "@" `Text.isInfixOf` package+    versionSymbols = ["==", ">=", "<=", ">", "<", "!=", "~=", "==="]+    hasVersionSymbol package = or [s `Text.isInfixOf` package | s <- versionSymbols]+    localPackageFileExtensions = [".whl", ".tar.gz"]+    isLocalPackage package = or [s `Text.isSuffixOf` package | s <- localPackageFileExtensions]+{-# INLINEABLE rule #-}++packages :: Shell.Command -> [Text.Text]+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 :: [Text.Text] -> [Text.Text]+stripInstallPrefix cmd = dropWhile (== "install") (dropWhile (/= "install") cmd)
+ src/Hadolint/Rule/DL3014.hs view
@@ -0,0 +1,21 @@+module Hadolint.Rule.DL3014 (rule) where++import Hadolint.Rule+import Hadolint.Shell (ParsedShell)+import qualified Hadolint.Shell as Shell+import Language.Docker.Syntax++rule :: Rule ParsedShell+rule = simpleRule code severity message check+  where+    code = "DL3014"+    severity = DLWarningC+    message = "Use the `-y` switch to avoid manual input `apt-get -y install <package>`"++    check (Run (RunArgs args _)) = foldArguments (Shell.noCommands forgotAptYesOption) args+    check _ = True++    forgotAptYesOption cmd = isAptGetInstall cmd && not (hasYesOption cmd)+    isAptGetInstall = Shell.cmdHasArgs "apt-get" ["install"]+    hasYesOption = Shell.hasAnyFlag ["y", "yes", "q", "assume-yes"]+{-# INLINEABLE rule #-}
+ src/Hadolint/Rule/DL3015.hs view
@@ -0,0 +1,23 @@+module Hadolint.Rule.DL3015 (rule) where++import Hadolint.Rule+import Hadolint.Shell (ParsedShell)+import qualified Hadolint.Shell as Shell+import Language.Docker.Syntax++rule :: Rule ParsedShell+rule = simpleRule code severity message check+  where+    code = "DL3015"+    severity = DLInfoC+    message = "Avoid additional packages by specifying `--no-install-recommends`"++    check (Run (RunArgs args _)) = foldArguments (Shell.noCommands forgotNoInstallRecommends) args+    check _ = True++    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+{-# INLINEABLE rule #-}
+ src/Hadolint/Rule/DL3016.hs view
@@ -0,0 +1,61 @@+module Hadolint.Rule.DL3016 (rule) where++import Data.List (isPrefixOf)+import qualified Data.Text as Text+import Hadolint.Rule+import Hadolint.Shell (ParsedShell)+import qualified Hadolint.Shell as Shell+import Language.Docker.Syntax (Instruction (..), RunArgs (..))++-- | 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 :: Rule ParsedShell+rule = simpleRule code severity message check+  where+    code = "DL3016"+    severity = DLWarningC+    message =+      "Pin versions in npm. Instead of `npm install <package>` use `npm install \+      \<package>@<version>`"++    check (Run (RunArgs args _)) = foldArguments (Shell.noCommands forgotToPinVersion) args+    check _ = True++    forgotToPinVersion 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++    gitPrefixes = ["git://", "git+ssh://", "git+http://", "git+https://"]+    pathPrefixes = ["/", "./", "../", "~/"]+    tarballSuffixes = [".tar", ".tar.gz", ".tgz"]++    hasGitPrefix package = or [p `Text.isPrefixOf` package | p <- gitPrefixes]+    hasTarballSuffix package = or [p `Text.isSuffixOf` package | p <- tarballSuffixes]+    isFolder package = or [p `Text.isPrefixOf` package | p <- pathPrefixes]+    isVersionedGit package = "#" `Text.isInfixOf` package++    hasVersionSymbol package = "@" `Text.isInfixOf` dropScope package+      where+        dropScope pkg =+          if "@" `Text.isPrefixOf` pkg+            then Text.dropWhile ('/' <) pkg+            else pkg+{-# INLINEABLE rule #-}++stripInstallPrefix :: [Text.Text] -> [Text.Text]+stripInstallPrefix cmd = dropWhile (== "install") (dropWhile (/= "install") cmd)
+ src/Hadolint/Rule/DL3017.hs view
@@ -0,0 +1,16 @@+module Hadolint.Rule.DL3017 (rule) where++import Hadolint.Rule+import Hadolint.Shell (ParsedShell)+import qualified Hadolint.Shell as Shell+import Language.Docker.Syntax++rule :: Rule ParsedShell+rule = simpleRule code severity message check+  where+    code = "DL3017"+    severity = DLErrorC+    message = "Do not use apk upgrade"+    check (Run (RunArgs args _)) = foldArguments (Shell.noCommands (Shell.cmdHasArgs "apk" ["upgrade"])) args+    check _ = True+{-# INLINEABLE rule #-}
+ src/Hadolint/Rule/DL3018.hs view
@@ -0,0 +1,30 @@+module Hadolint.Rule.DL3018 (rule) where++import qualified Data.Text as Text+import Hadolint.Rule+import Hadolint.Shell (ParsedShell)+import qualified Hadolint.Shell as Shell+import Language.Docker.Syntax++rule :: Rule ParsedShell+rule = simpleRule code severity message check+  where+    code = "DL3018"+    severity = DLWarningC+    message =+      "Pin versions in apk add. Instead of `apk add <package>` use `apk add <package>=<version>`"+    check (Run (RunArgs args _)) = foldArguments (\as -> and [versionFixed p | p <- apkAddPackages as]) args+    check _ = True+    versionFixed package = "=" `Text.isInfixOf` package+{-# INLINEABLE rule #-}++apkAddPackages :: ParsedShell -> [Text.Text]+apkAddPackages args =+  [ 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"]
+ src/Hadolint/Rule/DL3019.hs view
@@ -0,0 +1,19 @@+module Hadolint.Rule.DL3019 (rule) where++import Hadolint.Rule+import Hadolint.Shell (ParsedShell)+import qualified Hadolint.Shell as Shell+import Language.Docker.Syntax++rule :: Rule ParsedShell+rule = simpleRule code severity message check+  where+    code = "DL3019"+    severity = DLInfoC+    message =+      "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 _)) = foldArguments (Shell.noCommands forgotCacheOption) args+    check _ = True+    forgotCacheOption cmd = Shell.cmdHasArgs "apk" ["add"] cmd && not (Shell.hasFlag "no-cache" cmd)+{-# INLINEABLE rule #-}
+ src/Hadolint/Rule/DL3020.hs view
@@ -0,0 +1,45 @@+module Hadolint.Rule.DL3020 (rule) where++import Data.Foldable (toList)+import qualified Data.Text as Text+import Hadolint.Rule+import Language.Docker.Syntax++rule :: Rule args+rule = simpleRule code severity message check+  where+    code = "DL3020"+    severity = DLErrorC+    message = "Use COPY instead of ADD for files and folders"++    check (Add (AddArgs srcs _ _)) =+      and [isArchive src || isUrl src | SourcePath src <- toList srcs]+    check _ = True+{-# INLINEABLE rule #-}++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"+            ]+      ]+    )++isUrl :: Text.Text -> Bool+isUrl path = or ([proto `Text.isPrefixOf` path | proto <- ["https://", "http://"]])
+ src/Hadolint/Rule/DL3021.hs view
@@ -0,0 +1,20 @@+module Hadolint.Rule.DL3021 (rule) where++import qualified Data.Text as Text+import Hadolint.Rule+import Language.Docker.Syntax++rule :: Rule args+rule = simpleRule code severity message check+  where+    code = "DL3021"+    severity = DLErrorC+    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+    check _ = True++    endsWithSlash (TargetPath t) = not (Text.null t) && Text.last t == '/'+{-# INLINEABLE rule #-}
+ src/Hadolint/Rule/DL3022.hs view
@@ -0,0 +1,19 @@+module Hadolint.Rule.DL3022 (rule) where++import qualified Data.Set as Set+import Hadolint.Rule+import Language.Docker.Syntax++rule :: Rule args+rule = customRule check (emptyState Set.empty)+  where+    code = "DL3022"+    severity = DLWarningC+    message = "COPY --from should reference a previously defined FROM alias"++    check _ st (From BaseImage {alias = Just (ImageAlias als)}) = st |> modify (Set.insert als)+    check line st (Copy (CopyArgs _ _ _ (CopySource s)))+      | Set.member s (state st) = st+      | otherwise = st |> addFail CheckFailure {..}+    check _ st _ = st+{-# INLINEABLE rule #-}
+ src/Hadolint/Rule/DL3023.hs view
@@ -0,0 +1,19 @@+module Hadolint.Rule.DL3023 (rule) where++import Hadolint.Rule+import Language.Docker.Syntax++rule :: Rule args+rule = customRule check (emptyState Nothing)+  where+    code = "DL3023"+    severity = DLErrorC+    message = "COPY --from should reference a previously defined FROM alias"++    check _ st f@(From _) = st |> replaceWith (Just f) -- Remember the last FROM instruction found+    check line st@(State _ (Just fromInstr)) (Copy (CopyArgs _ _ _ (CopySource stageName)))+      | aliasMustBe (/= stageName) fromInstr = st+      | otherwise = st |> addFail CheckFailure {..}+    -- cannot copy from the same stage!+    check _ st _ = st+{-# INLINEABLE rule #-}
+ src/Hadolint/Rule/DL3024.hs view
@@ -0,0 +1,20 @@+module Hadolint.Rule.DL3024 (rule) where++import qualified Data.Set as Set+import Hadolint.Rule+import Language.Docker.Syntax++rule :: Rule args+rule = customRule check (emptyState Set.empty)+  where+    code = "DL3024"+    severity = DLErrorC+    message = "FROM aliases (stage names) must be unique"++    check line st (From BaseImage {alias = Just (ImageAlias als)}) =+      let newState = st |> modify (Set.insert als)+       in if Set.member als (state st)+            then newState |> addFail CheckFailure {..}+            else newState+    check _ st _ = st+{-# INLINEABLE rule #-}
+ src/Hadolint/Rule/DL3025.hs view
@@ -0,0 +1,16 @@+module Hadolint.Rule.DL3025 (rule) where++import Hadolint.Rule+import Language.Docker.Syntax++rule :: Rule args+rule = simpleRule code severity message check+  where+    code = "DL3025"+    severity = DLWarningC+    message = "Use arguments JSON notation for CMD and ENTRYPOINT arguments"++    check (Cmd (ArgumentsText _)) = False+    check (Entrypoint (ArgumentsText _)) = False+    check _ = True+{-# INLINEABLE rule #-}
+ src/Hadolint/Rule/DL3026.hs view
@@ -0,0 +1,29 @@+module Hadolint.Rule.DL3026 (rule) where++import qualified Data.Set as Set+import Hadolint.Rule+import Language.Docker.Syntax++rule :: Set.Set Registry -> Rule args+rule allowed = customRule check (emptyState Set.empty)+  where+    code = "DL3026"+    severity = DLErrorC+    message = "Use only an allowed registry in the FROM image"++    check line st (From BaseImage {image, alias}) =+      let newState = st |> modify (Set.insert alias)+       in if doCheck (state st) image+            then newState+            else newState |> addFail CheckFailure {..}+    check _ st _ = st++    doCheck st img = Set.member (toImageAlias img) st || Set.null allowed || isAllowed img++    toImageAlias = Just . ImageAlias . imageName+    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+{-# INLINEABLE rule #-}
+ src/Hadolint/Rule/DL3027.hs view
@@ -0,0 +1,18 @@+module Hadolint.Rule.DL3027 (rule) where++import Hadolint.Rule+import Hadolint.Shell (ParsedShell)+import qualified Hadolint.Shell as Shell+import Language.Docker.Syntax++rule :: Rule ParsedShell+rule = simpleRule code severity message check+  where+    code = "DL3027"+    severity = DLWarningC+    message =+      "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 _)) = foldArguments (not . Shell.usingProgram "apt") args+    check _ = True+{-# INLINEABLE rule #-}
+ src/Hadolint/Rule/DL3028.hs view
@@ -0,0 +1,36 @@+module Hadolint.Rule.DL3028 (rule) where++import qualified Data.Text as Text+import Hadolint.Rule+import Hadolint.Shell (ParsedShell)+import qualified Hadolint.Shell as Shell+import Language.Docker.Syntax (Instruction (..), RunArgs (..))++rule :: Rule ParsedShell+rule = simpleRule code severity message check+  where+    code = "DL3028"+    severity = DLWarningC+    message =+      "Pin versions in gem install. Instead of `gem install <gem>` use `gem \+      \install <gem>:<version>`"++    check (Run (RunArgs args _)) = foldArguments (all versionFixed . gems) args+    check _ = True++    versionFixed package = ":" `Text.isInfixOf` package+{-# INLINEABLE rule #-}++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 /= "--"+  ]
+ src/Hadolint/Rule/DL3029.hs view
@@ -0,0 +1,15 @@+module Hadolint.Rule.DL3029 (rule) where++import Hadolint.Rule+import Language.Docker.Syntax++rule :: Rule args+rule = simpleRule code severity message check+  where+    code = "DL3029"+    severity = DLWarningC+    message = "Do not use --platform flag with FROM"++    check (From BaseImage {platform = Just p}) = p == ""+    check _ = True+{-# INLINEABLE rule #-}
+ src/Hadolint/Rule/DL3030.hs view
@@ -0,0 +1,20 @@+module Hadolint.Rule.DL3030 (rule) where++import Hadolint.Rule+import qualified Hadolint.Shell as Shell+import Language.Docker.Syntax++rule :: Rule Shell.ParsedShell+rule = simpleRule code severity message check+  where+    code = "DL3030"+    severity = DLWarningC+    message = "Use the -y switch to avoid manual input `yum install -y <package`"++    check (Run (RunArgs args _)) = foldArguments (Shell.noCommands forgotYumYesOption) args+    check _ = True++    forgotYumYesOption cmd = isYumInstall cmd && not (hasYesOption cmd)+    isYumInstall = Shell.cmdHasArgs "yum" ["install", "groupinstall", "localinstall"]+    hasYesOption = Shell.hasAnyFlag ["y", "assumeyes"]+{-# INLINEABLE rule #-}
+ src/Hadolint/Rule/DL3031.hs view
@@ -0,0 +1,27 @@+module Hadolint.Rule.DL3031 (rule) where++import Hadolint.Rule+import qualified Hadolint.Shell as Shell+import Language.Docker.Syntax++rule :: Rule Shell.ParsedShell+rule = simpleRule code severity message check+  where+    code = "DL3031"+    severity = DLErrorC+    message = "Do not use yum update."+    check (Run (RunArgs args _)) =+      foldArguments+        ( Shell.noCommands+            ( Shell.cmdHasArgs+                "yum"+                [ "update",+                  "update-to",+                  "upgrade",+                  "upgrade-to"+                ]+            )+        )+        args+    check _ = True+{-# INLINEABLE rule #-}
+ src/Hadolint/Rule/DL3032.hs view
@@ -0,0 +1,23 @@+module Hadolint.Rule.DL3032 (rule) where++import Hadolint.Rule+import qualified Hadolint.Shell as Shell+import Language.Docker.Syntax++rule :: Rule Shell.ParsedShell+rule = simpleRule code severity message check+  where+    code = "DL3032"+    severity = DLWarningC+    message = "`yum clean all` missing after yum command."++    check (Run (RunArgs args _)) =+      foldArguments (Shell.noCommands yumInstall) args+        || ( foldArguments (Shell.anyCommands yumInstall) args+               && foldArguments (Shell.anyCommands yumClean) args+           )+    check _ = True++    yumInstall = Shell.cmdHasArgs "yum" ["install"]+    yumClean = Shell.cmdHasArgs "yum" ["clean", "all"]+{-# INLINEABLE rule #-}
+ src/Hadolint/Rule/DL3033.hs view
@@ -0,0 +1,26 @@+module Hadolint.Rule.DL3033 (rule) where++import qualified Data.Text as Text+import Hadolint.Rule+import qualified Hadolint.Shell as Shell+import Language.Docker.Syntax++rule :: Rule Shell.ParsedShell+rule = simpleRule code severity message check+  where+    code = "DL3033"+    severity = DLWarningC+    message = "Specify version with `yum install -y <package>-<version>`."++    check (Run (RunArgs args _)) = foldArguments (all versionFixed . yumPackages) args+    check _ = True++    versionFixed package =+      "-" `Text.isInfixOf` package+        || ".rpm" `Text.isSuffixOf` package+{-# INLINEABLE rule #-}++yumPackages :: Shell.ParsedShell -> [Text.Text]+yumPackages args =+  [ arg | cmd <- Shell.presentCommands args, Shell.cmdHasArgs "yum" ["install"] cmd, arg <- Shell.getArgsNoFlags cmd, arg /= "install"+  ]
+ src/Hadolint/Rule/DL3034.hs view
@@ -0,0 +1,30 @@+module Hadolint.Rule.DL3034 (rule) where++import Hadolint.Rule+import qualified Hadolint.Shell as Shell+import Language.Docker.Syntax++rule :: Rule Shell.ParsedShell+rule = simpleRule code severity message check+  where+    code = "DL3034"+    severity = DLWarningC+    message = "Non-interactive switch missing from `zypper` command: `zypper install -y`"++    check (Run (RunArgs args _)) = foldArguments (Shell.noCommands forgotZypperYesOption) args+    check _ = True++    forgotZypperYesOption cmd = isZypperInstall cmd && not (hasYesOption cmd)+    isZypperInstall =+      Shell.cmdHasArgs+        "zypper"+        [ "install",+          "in",+          "remove",+          "rm",+          "source-install",+          "si",+          "patch"+        ]+    hasYesOption = Shell.hasAnyFlag ["no-confirm", "y"]+{-# INLINEABLE rule #-}
+ src/Hadolint/Rule/DL3035.hs view
@@ -0,0 +1,28 @@+module Hadolint.Rule.DL3035 (rule) where++import Hadolint.Rule+import qualified Hadolint.Shell as Shell+import Language.Docker.Syntax++rule :: Rule Shell.ParsedShell+rule = simpleRule code severity message check+  where+    code = "DL3035"+    severity = DLWarningC+    message = "Do not use `zypper update`."++    check (Run (RunArgs args _)) =+      foldArguments+        ( Shell.noCommands+            ( Shell.cmdHasArgs+                "zypper"+                [ "update",+                  "up",+                  "dist-upgrade",+                  "dup"+                ]+            )+        )+        args+    check _ = True+{-# INLINEABLE rule #-}
+ src/Hadolint/Rule/DL3036.hs view
@@ -0,0 +1,23 @@+module Hadolint.Rule.DL3036 (rule) where++import Hadolint.Rule+import qualified Hadolint.Shell as Shell+import Language.Docker.Syntax++rule :: Rule Shell.ParsedShell+rule = simpleRule code severity message check+  where+    code = "DL3036"+    severity = DLWarningC+    message = "`zypper clean` missing after zypper use."++    check (Run (RunArgs args _)) =+      foldArguments (Shell.noCommands zypperInstall) args+        || ( foldArguments (Shell.anyCommands zypperInstall) args+               && foldArguments (Shell.anyCommands zypperClean) args+           )+    check _ = True++    zypperInstall = Shell.cmdHasArgs "zypper" ["install", "in"]+    zypperClean = Shell.cmdHasArgs "zypper" ["clean", "cc"]+{-# INLINEABLE rule #-}
+ src/Hadolint/Rule/DL3037.hs view
@@ -0,0 +1,30 @@+module Hadolint.Rule.DL3037 (rule) where++import qualified Data.Text as Text+import Hadolint.Rule+import qualified Hadolint.Shell as Shell+import Language.Docker.Syntax++rule :: Rule Shell.ParsedShell+rule = simpleRule code severity message check+  where+    code = "DL3037"+    severity = DLWarningC+    message = "Specify version with `zypper install -y <package>=<version>`."++    check (Run (RunArgs args _)) = foldArguments (all versionFixed . zypperPackages) args+    check _ = True++    versionFixed package =+      "=" `Text.isInfixOf` package+        || ">=" `Text.isInfixOf` package+        || ">" `Text.isInfixOf` package+        || "<=" `Text.isInfixOf` package+        || "<" `Text.isInfixOf` package+        || ".rpm" `Text.isSuffixOf` package+{-# INLINEABLE rule #-}++zypperPackages :: Shell.ParsedShell -> [Text.Text]+zypperPackages args =+  [ arg | cmd <- Shell.presentCommands args, Shell.cmdHasArgs "zypper" ["install", "in"] cmd, arg <- Shell.getArgsNoFlags cmd, arg /= "install", arg /= "in"+  ]
+ src/Hadolint/Rule/DL3038.hs view
@@ -0,0 +1,20 @@+module Hadolint.Rule.DL3038 (rule) where++import Hadolint.Rule+import qualified Hadolint.Shell as Shell+import Language.Docker.Syntax++rule :: Rule Shell.ParsedShell+rule = simpleRule code severity message check+  where+    code = "DL3038"+    severity = DLWarningC+    message = "Use the -y switch to avoid manual input `dnf install -y <package`"++    check (Run (RunArgs args _)) = foldArguments (Shell.noCommands forgotDnfYesOption) args+    check _ = True++    forgotDnfYesOption cmd = isDnfInstall cmd && not (hasYesOption cmd)+    isDnfInstall = Shell.cmdHasArgs "dnf" ["install", "groupinstall", "localinstall"]+    hasYesOption = Shell.hasAnyFlag ["y", "assumeyes"]+{-# INLINEABLE rule #-}
+ src/Hadolint/Rule/DL3039.hs view
@@ -0,0 +1,26 @@+module Hadolint.Rule.DL3039 (rule) where++import Hadolint.Rule+import qualified Hadolint.Shell as Shell+import Language.Docker.Syntax++rule :: Rule Shell.ParsedShell+rule = simpleRule code severity message check+  where+    code = "DL3039"+    severity = DLErrorC+    message = "Do not use dnf update."++    check (Run (RunArgs args _)) =+      foldArguments+        ( Shell.noCommands+            ( Shell.cmdHasArgs+                "dnf"+                [ "upgrade",+                  "upgrade-minimal"+                ]+            )+        )+        args+    check _ = True+{-# INLINEABLE rule #-}
+ src/Hadolint/Rule/DL3040.hs view
@@ -0,0 +1,23 @@+module Hadolint.Rule.DL3040 (rule) where++import Hadolint.Rule+import qualified Hadolint.Shell as Shell+import Language.Docker.Syntax++rule :: Rule Shell.ParsedShell+rule = simpleRule code severity message check+  where+    code = "DL3040"+    severity = DLWarningC+    message = "`dnf clean all` missing after dnf command."++    check (Run (RunArgs args _)) =+      foldArguments (Shell.noCommands dnfInstall) args+        || ( foldArguments (Shell.anyCommands dnfInstall) args+               && foldArguments (Shell.anyCommands dnfClean) args+           )+    check _ = True++    dnfInstall = Shell.cmdHasArgs "dnf" ["install"]+    dnfClean = Shell.cmdHasArgs "dnf" ["clean", "all"]+{-# INLINEABLE rule #-}
+ src/Hadolint/Rule/DL3041.hs view
@@ -0,0 +1,26 @@+module Hadolint.Rule.DL3041 (rule) where++import qualified Data.Text as Text+import Hadolint.Rule+import qualified Hadolint.Shell as Shell+import Language.Docker.Syntax++rule :: Rule Shell.ParsedShell+rule = simpleRule code severity message check+  where+    code = "DL3041"+    severity = DLWarningC+    message = "Specify version with `dnf install -y <package>-<version>`."++    check (Run (RunArgs args _)) = foldArguments (all versionFixed . dnfPackages) args+    check _ = True++    versionFixed package =+      "-" `Text.isInfixOf` package+        || ".rpm" `Text.isSuffixOf` package+{-# INLINEABLE rule #-}++dnfPackages :: Shell.ParsedShell -> [Text.Text]+dnfPackages args =+  [ arg | cmd <- Shell.presentCommands args, Shell.cmdHasArgs "dnf" ["install"] cmd, arg <- Shell.getArgsNoFlags cmd, arg /= "install"+  ]
+ src/Hadolint/Rule/DL3042.hs view
@@ -0,0 +1,29 @@+module Hadolint.Rule.DL3042 (rule) where++import Data.List (isInfixOf)+import qualified Data.Text as Text+import Hadolint.Rule+import qualified Hadolint.Shell as Shell+import Language.Docker.Syntax++rule :: Rule Shell.ParsedShell+rule = simpleRule code severity message check+  where+    code = "DL3042"+    severity = DLWarningC+    message =+      "Avoid use of cache directory with pip. Use `pip install --no-cache-dir <package>`"+    check (Run (RunArgs args _)) = foldArguments (Shell.noCommands forgotNoCacheDir) args+    check _ = True+    forgotNoCacheDir cmd =+      Shell.isPipInstall cmd && not (usesNoCacheDir cmd) && not (isPipWrapper cmd)+    usesNoCacheDir cmd = "--no-cache-dir" `elem` Shell.getArgs cmd+{-# INLINEABLE rule #-}++isPipWrapper :: Shell.Command -> Bool+isPipWrapper cmd@(Shell.Command name _ _) = isWrapper "pipx" || isWrapper "pipenv"+  where+    isWrapper :: Text.Text -> Bool+    isWrapper w =+      w `Text.isInfixOf` name+        || ("python" `Text.isPrefixOf` name && ["-m", w] `isInfixOf` Shell.getArgs cmd)
+ src/Hadolint/Rule/DL3043.hs view
@@ -0,0 +1,17 @@+module Hadolint.Rule.DL3043 (rule) where++import Hadolint.Rule+import Language.Docker.Syntax++rule :: Rule args+rule = simpleRule code severity message check+  where+    code = "DL3043"+    severity = DLErrorC+    message = "`ONBUILD`, `FROM` or `MAINTAINER` triggered from within `ONBUILD` instruction."++    check (OnBuild (OnBuild _)) = False+    check (OnBuild (From _)) = False+    check (OnBuild (Maintainer _)) = False+    check _ = True+{-# INLINEABLE rule #-}
+ src/Hadolint/Rule/DL3044.hs view
@@ -0,0 +1,69 @@+module Hadolint.Rule.DL3044 (rule) where++import Data.List.Index (indexed)+import qualified Data.Set as Set+import qualified Data.Text as Text+import Hadolint.Rule+import Language.Docker.Syntax++rule :: Rule args+rule = customRule check (emptyState Set.empty)+  where+    code = "DL3044"+    severity = DLErrorC+    message = "Do not refer to an environment variable within the same `ENV` statement where it is defined."++    check line st (Env pairs) =+      let newState = st |> modify (Set.union (Set.fromList (map fst pairs)))+       in if null [env | env <- listOfReferences pairs, env `Set.notMember` state st]+            then newState+            else newState |> addFail CheckFailure {..}+    check _ st (Arg arg _) = st |> modify (Set.insert arg)+    check _ st _ = st+{-# INLINEABLE rule #-}++-- | generates a list of references to variable names referenced on the right+-- hand side of a variable definition, except when the variable is+-- referenced on its own right hand side.+listOfReferences :: Pairs -> [Text.Text]+listOfReferences prs =+  [ var+    | (idx, (var, _)) <- indexed prs,+      var `isSubstringOfAny` map (snd . snd) (filter ((/= idx) . fst) (indexed prs))+  ]++-- | is a reference of a variable substring of any text?+-- matches ${var_name} and $var_name, but not $var_nameblafoo+isSubstringOfAny :: Text.Text -> [Text.Text] -> Bool+isSubstringOfAny t l =+  not $+    null+      [ v+        | v <- l,+          (Text.pack "${" <> t <> Text.pack "}") `Text.isInfixOf` v+            || (t `bareVariableInText` v)+      ]++-- | we find a 'bare' variable with name v in a text, if+-- '$v' is in the text at any place and any text following after that+-- occurence would terminate a variable name. To determine that, the text t+-- is split at every occurence of var, check if '$v' is in the text and if+-- any part of the split text would terminate a variable name.+bareVariableInText :: Text.Text -> Text.Text -> Bool+bareVariableInText v t =+  let var = "$" <> v+      rest = drop 1 $ Text.splitOn var t+   in var `Text.isInfixOf` t && any terminatesVarName rest+  where+    -- x would terminate a variable name if it was appended directly to+    -- that name+    terminatesVarName :: Text.Text -> Bool+    terminatesVarName x = Text.null x || not (beginsWithAnyOf x varChar)++    -- txt begins with any character of String+    beginsWithAnyOf :: Text.Text -> Set.Set Char -> Bool+    beginsWithAnyOf txt str = any (`Text.isPrefixOf` txt) (Set.map Text.singleton str)++    -- all characters valid in the inner of a shell variable name+    varChar :: Set.Set Char+    varChar = Set.fromList (['0' .. '9'] ++ ['a' .. 'z'] ++ ['A' .. 'Z'] ++ ['_'])
+ src/Hadolint/Rule/DL3045.hs view
@@ -0,0 +1,83 @@+module Hadolint.Rule.DL3045 (rule) where++import Data.Map.Strict (Map)+import qualified Data.Map.Strict as Map+import Data.Maybe (fromMaybe)+import Data.Text (Text)+import qualified Data.Text as Text+import Hadolint.Rule+import Language.Docker.Syntax++-- This data encapsulates the name of a build stage. It may be None withing an+-- `ONBUILD` context.+data Stage+  = Stage {stage :: Text}+  | None+  deriving (Eq, Ord)++-- | The state here keeps the image name/alias of the current build stage+-- and a map from image names/aliases to a Bool, saving whether or+-- not a `WORKDIR` has been set in a build stage.+data Acc+  = Acc {current :: Stage, workdirSet :: Map Stage Bool}+  | Empty++rule :: Rule args+rule = customRule check (emptyState Empty)+  where+    code = "DL3045"+    severity = DLWarningC+    message = "`COPY` to a relative destination without `WORKDIR` set."++    check _ st (From from) = st |> modify (rememberStage from)+    check _ st (Workdir _) = st |> modify rememberWorkdir+    check line st (Copy (CopyArgs _ (TargetPath dest) _ _))+      | Acc s m <- state st, Just True <- Map.lookup s m = st -- workdir has been set+      | "/" `Text.isPrefixOf` Text.dropAround quotePredicate dest = st -- absolute dest. normal+      | ":\\" `Text.isPrefixOf` Text.drop 1 (Text.dropAround quotePredicate dest) = st -- absolute dest. windows+      | "$" `Text.isPrefixOf` Text.dropAround quotePredicate dest = st -- dest is a variable+      | otherwise = st |> addFail CheckFailure {..}+    check _ st _ = st+{-# INLINEABLE rule #-}++rememberStage :: BaseImage -> Acc -> Acc+rememberStage BaseImage {alias = Just als} Empty =+  Acc+    { current = Stage {stage = unImageAlias als},+      workdirSet = mempty+    }+rememberStage BaseImage {alias = Nothing, image} Empty =+  Acc+    { current = Stage {stage = imageName image},+      workdirSet = mempty+    }+rememberStage BaseImage {alias = Just als, image} Acc {..} =+  Acc+    { current = Stage {stage = unImageAlias als},+      workdirSet =+        let parentValue =+              Map.lookup (Stage { stage = imageName image}) workdirSet+                |> fromMaybe False+         in workdirSet+              |> Map.insert (Stage {stage = unImageAlias als}) parentValue+    }+rememberStage BaseImage {alias = Nothing, image} Acc {..} =+  Acc+    { current = Stage {stage = imageName image},+      workdirSet =+        let parentValue =+              Map.lookup (Stage {stage = imageName image}) workdirSet+                |> fromMaybe False+         in workdirSet+              |> Map.insert (Stage {stage = imageName image}) parentValue+    }++rememberWorkdir :: Acc -> Acc+rememberWorkdir Empty = Acc {current = None, workdirSet = Map.insert None True Map.empty}+rememberWorkdir Acc {..} = Acc {current, workdirSet = Map.insert current True workdirSet}++quotePredicate :: Char -> Bool+quotePredicate c+  | c == '"' = True+  | c == '\'' = True+  | otherwise = False
+ src/Hadolint/Rule/DL3046.hs view
@@ -0,0 +1,24 @@+module Hadolint.Rule.DL3046 (rule) where++import qualified Data.Text as Text+import Hadolint.Rule+import Hadolint.Shell (ParsedShell)+import qualified Hadolint.Shell as Shell+import Language.Docker.Syntax (Instruction (..), RunArgs (..))++rule :: Rule ParsedShell+rule = simpleRule code severity message check+  where+    code = "DL3046"+    severity = DLWarningC+    message = "`useradd` without flag `-l` and high UID will result in excessively large Image."++    check (Run (RunArgs args _)) = foldArguments (Shell.noCommands forgotFlagL) args+    check _ = True++    forgotFlagL cmd = isUseradd cmd && (not (hasLFlag cmd) && hasUFlag cmd && hasLongUID cmd)+    isUseradd (Shell.Command name _ _) = name == "useradd"+    hasLFlag = Shell.hasAnyFlag ["l", "no-log-init"]+    hasUFlag = Shell.hasAnyFlag ["u", "uid"]+    hasLongUID cmd = any ((> 5) . Text.length) (Shell.getFlagArg "u" cmd)+{-# INLINEABLE rule #-}
+ src/Hadolint/Rule/DL3047.hs view
@@ -0,0 +1,36 @@+module Hadolint.Rule.DL3047 (rule) where++import Hadolint.Rule+import Hadolint.Shell (ParsedShell)+import qualified Hadolint.Shell as Shell+import Language.Docker.Syntax++rule :: Rule ParsedShell+rule = simpleRule code severity message check+  where+    code = "DL3047"+    severity = DLWarningC+    message =+      "Avoid use of wget without progress bar. Use `wget --progress=dot:giga <url>`.\+      \Or consider using `-q` or `-nv` (shorthands for `--quiet` or `--no-verbose`)."++    check (Run (RunArgs args _)) = foldArguments (Shell.noCommands forgotProgress) args+    check _ = True++    forgotProgress cmd = isWget cmd && (not (hasProgressOption cmd) && not (hasSpecialFlags cmd))+    isWget (Shell.Command name _ _) = name == "wget"+    hasProgressOption cmd = Shell.hasFlag "progress" cmd++    hasSpecialFlags cmd =+      hasQuietFlag cmd+        || hasOutputFlag cmd+        || hasAppendOutputFlag cmd+        || hasNoVerboseFlag cmd++    hasQuietFlag cmd = Shell.hasAnyFlag ["q", "quiet"] cmd+    hasOutputFlag cmd = Shell.hasAnyFlag ["o", "output-file"] cmd+    hasAppendOutputFlag cmd = Shell.hasAnyFlag ["a", "append-output"] cmd+    hasNoVerboseFlag cmd =+      Shell.hasAnyFlag ["no-verbose"] cmd+        || Shell.cmdHasArgs "wget" ["-nv"] cmd+{-# INLINEABLE rule #-}
+ src/Hadolint/Rule/DL3048.hs view
@@ -0,0 +1,39 @@+module Hadolint.Rule.DL3048 (rule) where++import qualified Data.Text as Text+import Hadolint.Rule+import Language.Docker.Syntax+++rule :: Rule args+rule = simpleRule code severity message check+  where+    code = "DL3048"+    severity = DLStyleC+    message = "Invalid label key."+    check (Label pairs) = hasNoInvalidKey pairs+    check _ = True++    hasNoInvalidKey prs = null $ [(l, v) | (l, v) <- prs,+                                          Text.take 1 l `notElem`+                                              fmap Text.singleton ['a'..'z'] ||+                                          Text.takeEnd 1 l `notElem`+                                              fmap Text.singleton (['a'..'z'] ++ ['0'..'9']) ||+                                          containsIllegalChar l ||+                                          hasReservedNamespace l ||+                                          hasConsecutiveSeparators l]+{-# INLINEABLE rule #-}++containsIllegalChar :: Text.Text -> Bool+containsIllegalChar = Text.any (`notElem` validChars)++hasReservedNamespace :: Text.Text -> Bool+hasReservedNamespace l = "com.docker." `Text.isPrefixOf` l+  || "io.docker." `Text.isPrefixOf` l+  || "org.dockerproject." `Text.isPrefixOf` l++hasConsecutiveSeparators :: Text.Text -> Bool+hasConsecutiveSeparators l = ".." `Text.isInfixOf` l || "--" `Text.isInfixOf` l++validChars :: String+validChars = ['0'..'9'] ++ ['a' .. 'z'] ++ ['.', '-']
+ src/Hadolint/Rule/DL3049.hs view
@@ -0,0 +1,60 @@+ module Hadolint.Rule.DL3049 (rule) where++import qualified Data.Map as Map+import qualified Data.Set as Set+import qualified Data.Sequence as Seq+import qualified Data.Text as Text+import Hadolint.Rule+import Language.Docker.Syntax+++rule :: LabelSchema -> Rule args+rule labelschema = mconcat $ fmap missingLabelRule (Map.keys labelschema)+{-# INLINEABLE rule #-}++data StageID = StageID+  { name :: Text.Text,+    line :: Linenumber+  } deriving (Eq, Ord, Show)++data Acc+  = Acc StageID (Set.Set StageID) (Set.Set StageID)+  | Empty+  deriving (Show)++-- missingLabelRule+--+-- triggers on a `FROM` instruction when label `label` is not defined within+-- that stage. Tracks defined labels through multi stage builds+missingLabelRule :: LabelName -> Rule args+missingLabelRule label = veryCustomRule check (emptyState Empty) markFailure+  where+    code = "DL3049"+    severity = DLInfoC+    message = "Label `" <> label <> "` is missing."+    check line state (From BaseImage {image, alias = Just als}) =+        state |> modify (currentStage (imageName image) (StageID (unImageAlias als) line))+    check line state (From BaseImage {image, alias = Nothing}) =+        state |> modify (currentStage (imageName image) (StageID (imageName image) line))+    check _ state (Label pairs)+        | label `elem` fmap fst pairs = state |> modify goodStage+        | otherwise = state+    check _ state _ = state++    markFailure :: State Acc -> Failures+    markFailure (State fails (Acc _ _ b)) = Set.foldl' (Seq.|>) fails (Set.map markFail b)+    markFailure st = failures st++    markFail (StageID _ line) = CheckFailure {..}++currentStage :: Text.Text -> StageID -> Acc -> Acc+currentStage src stageid (Acc _ g b)+    | not $ Set.null (Set.filter (predicate src) g) = Acc stageid (g |> Set.insert stageid) b+    | otherwise = Acc stageid g (b |> Set.insert stageid)+  where+    predicate n0 StageID {name = n1} = n1 == n0+currentStage _ stageid Empty = Acc stageid Set.empty (Set.singleton stageid)++goodStage :: Acc -> Acc+goodStage (Acc stageid g b) = Acc stageid (g |> Set.insert stageid) (b |> Set.delete stageid)+goodStage Empty = Empty
+ src/Hadolint/Rule/DL3050.hs view
@@ -0,0 +1,18 @@+module Hadolint.Rule.DL3050 (rule) where++import qualified Data.Map as Map+import Hadolint.Rule+import Language.Docker.Syntax+++rule :: LabelSchema -> Bool -> Rule args+rule labelschema strictlabels = simpleRule code severity message check+  where+    code = "DL3050"+    severity = DLInfoC+    message = "Superfluous label(s) present."+    check (Label pairs)+        | strictlabels = all ((`elem` Map.keys labelschema) . fst) pairs+        | otherwise = True+    check _ = True+{-# INLINEABLE rule #-}
+ src/Hadolint/Rule/DL3051.hs view
@@ -0,0 +1,23 @@+module Hadolint.Rule.DL3051 (rule) where++import qualified Data.Map as Map+import qualified Data.Text as Text+import Hadolint.Rule+import Language.Docker.Syntax+++rule :: LabelSchema -> Rule args+rule labelschema = mconcat $ fmap labelIsNotEmptyRule (Map.keys labelschema)+{-# INLINEABLE rule #-}++labelIsNotEmptyRule :: LabelName -> Rule args+labelIsNotEmptyRule label = simpleRule code severity message check+  where+    code = "DL3051"+    severity = DLWarningC+    message = "label `" <> label <> "` is empty."+    check (Label pairs) = null $ getEmptyLabels label pairs+    check _ = True++getEmptyLabels :: LabelName -> Pairs -> Pairs+getEmptyLabels lbl prs = [(l, v) | (l, v) <- prs, l == lbl, Text.null v]
+ src/Hadolint/Rule/DL3052.hs view
@@ -0,0 +1,27 @@+module Hadolint.Rule.DL3052 (rule) where++import qualified Data.Map as Map+import Data.Maybe+import qualified Data.Text as Text+import Hadolint.Rule+import Language.Docker.Syntax+import Network.URI as Uri+++rule :: LabelSchema -> Rule args+rule labelschema = mconcat $+  fmap labelIsNotUrlRule (Map.keys (Map.filter (== Url) labelschema))+{-# INLINEABLE rule #-}++labelIsNotUrlRule :: LabelName -> Rule args+labelIsNotUrlRule label = simpleRule code severity message check+  where+    code = "DL3052"+    severity = DLWarningC+    message = "Label `" <> label <> "` is not a valid URL."+    check (Label ls) = null $ getBadURLLabels label ls+    check _ = True++getBadURLLabels :: LabelName -> Pairs -> Pairs+getBadURLLabels lbl prs = [(l, u) | (l, u) <- prs, l == lbl,+                                    (isNothing . Uri.parseURI) (Text.unpack u)]
+ src/Hadolint/Rule/DL3053.hs view
@@ -0,0 +1,28 @@+module Hadolint.Rule.DL3053 (rule) where++import qualified Data.Map as Map+import Data.Maybe+import Data.Time.RFC3339+import Hadolint.Rule+import Language.Docker.Syntax+++rule :: LabelSchema -> Rule args+rule labelschema = mconcat $+  fmap labelIsNotRFC3339Rule (Map.keys (Map.filter (== Rfc3339) labelschema))+{-# INLINEABLE rule #-}+++labelIsNotRFC3339Rule :: LabelName -> Rule args+labelIsNotRFC3339Rule label = simpleRule code severity message check+  where+    code = "DL3053"+    severity = DLWarningC+    message = "Label `" <> label <> "` is not a valid time format - must be conform to RFC3339."+    check (Label ls) = null $ getBadTimeformatLabels label ls+    check _ = True++getBadTimeformatLabels :: LabelName -> Pairs -> Pairs+getBadTimeformatLabels lbl pairs = [(l, v) | (l, v) <- pairs,+                                             l == lbl,+                                             isNothing $ parseTimeRFC3339 v]
+ src/Hadolint/Rule/DL3054.hs view
@@ -0,0 +1,32 @@+module Hadolint.Rule.DL3054 (rule) where++import Data.Either+import qualified Data.Map as Map+import qualified Data.Text as Text+import Distribution.Parsec+import Distribution.SPDX.Extra+import Hadolint.Rule+import Language.Docker.Syntax+++rule :: LabelSchema -> Rule args+rule labelschema = mconcat $+  fmap labelIsNotSPDXRule (Map.keys (Map.filter (== Spdx) labelschema))+{-# INLINEABLE rule #-}++labelIsNotSPDXRule :: LabelName -> Rule args+labelIsNotSPDXRule label = simpleRule code severity message check+  where+    code = "DL3054"+    severity = DLWarningC+    message = "Label `" <> label <> "` is not a valid SPDX identifier."+    check (Label ls) = null $ getBadLicenseLabels label ls+    check _ = True+++getBadLicenseLabels :: LabelName -> Pairs -> Pairs+getBadLicenseLabels lbl pairs =+    [ (l, v) | (l, v) <- pairs,+               l == lbl,+               isLeft (eitherParsec (Text.unpack v) :: Either String LicenseId)+    ]
+ src/Hadolint/Rule/DL3055.hs view
@@ -0,0 +1,31 @@+module Hadolint.Rule.DL3055 (rule) where++import qualified Data.Map as Map+import qualified Data.Text as Text+import Hadolint.Rule+import Language.Docker.Syntax+++rule :: LabelSchema -> Rule args+rule labelschema = mconcat $+  fmap labelIsNotGitHashRule (Map.keys (Map.filter (== GitHash) labelschema))+{-# INLINEABLE rule #-}++labelIsNotGitHashRule :: LabelName -> Rule args+labelIsNotGitHashRule label = simpleRule code severity message check+  where+    code = "DL3055"+    severity = DLWarningC+    message = "Label `" <> label <> "` is not a valid git hash."+    check (Label ls) = null $ getBadHashLabels label ls+    check _ = True++getBadHashLabels :: LabelName -> Pairs -> Pairs+getBadHashLabels lbl prs = [(l, v) | (l, v) <- prs, l == lbl, isBadHash v]++isBadHash :: Text.Text -> Bool+isBadHash h = Text.any (`notElem` validHash) h+            || (Text.length h /= 40 && Text.length h /= 7)++validHash :: String+validHash = ['0'..'9'] ++ ['a'..'f']
+ src/Hadolint/Rule/DL3056.hs view
@@ -0,0 +1,27 @@+module Hadolint.Rule.DL3056 (rule) where++import Data.Either+import qualified Data.Map as Map+import qualified Data.SemVer as SemVer+import qualified Data.Text as Text+import Hadolint.Rule+import Language.Docker.Syntax+++rule :: LabelSchema -> Rule args+rule labelschema = mconcat $+  fmap labelIsNotSemVerRule (Map.keys (Map.filter (== SemVer) labelschema))+{-# INLINEABLE rule #-}++labelIsNotSemVerRule :: LabelName -> Rule args+labelIsNotSemVerRule label = simpleRule code severity message check+  where+    code = "DL3056"+    severity = DLWarningC+    message = Text.pack "Label `" <> label <> Text.pack "` does not conform to semantic versioning."+    check (Label pairs) = hasNoBadVersioning label pairs+    check _ = True++hasNoBadVersioning :: LabelName -> Pairs -> Bool+hasNoBadVersioning lbl prs = null [(l, v) | (l, v) <- prs, l == lbl,+                                            isLeft $ SemVer.fromText v]
+ src/Hadolint/Rule/DL3057.hs view
@@ -0,0 +1,50 @@+module Hadolint.Rule.DL3057 (rule) where++import qualified Data.Sequence as Seq+import qualified Data.Set as Set+import qualified Data.Text as Text+import Hadolint.Rule+import Language.Docker.Syntax+++data StageID = StageID+  { name :: Text.Text,+    line :: Linenumber+  } deriving (Show, Eq, Ord)++data Acc+  = Acc StageID (Set.Set StageID) (Set.Set StageID)+  | Empty+  deriving (Show)++rule :: Rule args+rule = veryCustomRule check (emptyState Empty) markFailures+  where+    code = "DL3057"+    severity = DLIgnoreC+    message = "`HEALTHCHECK` instruction missing."++    check line state (From BaseImage {image, alias = Just als}) =+      state |> modify (currentStage (imageName image) (StageID (unImageAlias als) line))+    check line state (From BaseImage {image, alias = Nothing}) =+      state |> modify (currentStage (imageName image) (StageID (imageName image) line))+    check _ state (Healthcheck _) = state |> modify goodStage+    check _ state _ = state++    markFailures :: State Acc -> Failures+    markFailures (State fails (Acc _ _ b)) = Set.foldl' (Seq.|>) fails (Set.map makeFail b)+    markFailures st = failures st+    makeFail (StageID _ line) = CheckFailure {..}+{-# INLINEABLE rule #-}++currentStage :: Text.Text -> StageID -> Acc -> Acc+currentStage src stageid (Acc _ g b)+    | not $ Set.null (Set.filter (predicate src) g) = Acc stageid (g |> Set.insert stageid) b+    | otherwise = Acc stageid g (b |> Set.insert stageid)+  where+    predicate n0 StageID {name = n1} = n1 == n0+currentStage _ stageid Empty = Acc stageid Set.empty (Set.singleton stageid)++goodStage :: Acc -> Acc+goodStage (Acc stageid g b) = Acc stageid (g |> Set.insert stageid) (b |> Set.delete stageid)+goodStage Empty = Empty
+ src/Hadolint/Rule/DL4000.hs view
@@ -0,0 +1,14 @@+module Hadolint.Rule.DL4000 (rule) where++import Hadolint.Rule+import Language.Docker.Syntax (Instruction (..))++rule :: Rule args+rule = simpleRule code severity message check+  where+    code = "DL4000"+    severity = DLErrorC+    message = "MAINTAINER is deprecated"+    check (Maintainer _) = False+    check _ = True+{-# INLINEABLE rule #-}
+ src/Hadolint/Rule/DL4001.hs view
@@ -0,0 +1,32 @@+module Hadolint.Rule.DL4001 (rule) where++import qualified Data.IntSet as Set+import Hadolint.Rule+import qualified Hadolint.Shell as Shell+import Language.Docker.Syntax (Instruction (..), RunArgs (..))++rule :: Rule Shell.ParsedShell+rule = customRule check (emptyState Set.empty)+  where+    code = "DL4001"+    severity = DLWarningC+    message = "Either use Wget or Curl but not both"++    check line st (Run (RunArgs args _)) =+      let newArgs = foldArguments extractCommands args+          newState = st |> modify (Set.union newArgs)+       in if Set.size newArgs > 0 && Set.size (state newState) >= 2+            then newState |> addFail (CheckFailure {..})+            else newState+    -- Reset the state for each stage+    check _ st From {} = st |> replaceWith Set.empty+    check _ st _ = st+{-# INLINEABLE rule #-}++extractCommands :: Shell.ParsedShell -> Set.IntSet+extractCommands args =+  Set.fromList+    [ if w == "curl" then 0 else 1+      | w <- Shell.findCommandNames args,+        w == "curl" || w == "wget"+    ]
+ src/Hadolint/Rule/DL4003.hs view
@@ -0,0 +1,25 @@+module Hadolint.Rule.DL4003 (rule) where++import Hadolint.Rule+import Language.Docker.Syntax (Instruction (..))++data HasCmd = HasCmd | NoCmd++rule :: Rule args+rule = customRule check (emptyState NoCmd)+  where+    code = "DL4003"+    severity = DLWarningC+    message =+      "Multiple `CMD` instructions found. If you list more than one `CMD` then only the last \+      \`CMD` will take effect"++    -- Reset the state each time we find a FROM+    check _ st From {} = st |> replaceWith NoCmd+    -- Remember we found a CMD, fail if we found a CMD before+    check _ st@(State _ NoCmd) Cmd {} = st |> replaceWith HasCmd+    -- If we already saw a CMD, add an error for the line+    check line st@(State _ HasCmd) Cmd {} = st |> addFail (CheckFailure {..})+    -- otherwise return the accumulator+    check _ st _ = st+{-# INLINEABLE rule #-}
+ src/Hadolint/Rule/DL4004.hs view
@@ -0,0 +1,24 @@+module Hadolint.Rule.DL4004 (rule) where++import Hadolint.Rule+import Language.Docker.Syntax (Instruction (..))++data HasEntry = HasEntry | NoEntry++rule :: Rule args+rule = customRule check (emptyState NoEntry)+  where+    code = "DL4004"+    severity = DLErrorC+    message =+      "Multiple `ENTRYPOINT` instructions found. If you list more than one `ENTRYPOINT` then \+      \only the last `ENTRYPOINT` will take effect"++    -- Reset the state each time we find a FROM+    check _ st From {} = st |> replaceWith NoEntry+    -- Remember we found an ENTRYPOINT+    check _ st@(State _ NoEntry) Entrypoint {} = st |> replaceWith HasEntry+    -- Add a failure if we found another entrypoint in the same stage+    check line st@(State _ HasEntry) Entrypoint {} = st |> addFail (CheckFailure {..})+    check _ st _ = st+{-# INLINEABLE rule #-}
+ src/Hadolint/Rule/DL4005.hs view
@@ -0,0 +1,17 @@+module Hadolint.Rule.DL4005 (rule) where++import Hadolint.Rule+import Hadolint.Shell (ParsedShell)+import qualified Hadolint.Shell as Shell+import Language.Docker.Syntax++rule :: Rule ParsedShell+rule = simpleRule code severity message check+  where+    code = "DL4005"+    severity = DLWarningC+    message = "Use SHELL to change the default shell"++    check (Run (RunArgs args _)) = foldArguments (Shell.noCommands (Shell.cmdHasArgs "ln" ["/bin/sh"])) args+    check _ = True+{-# INLINEABLE rule #-}
+ src/Hadolint/Rule/DL4006.hs view
@@ -0,0 +1,41 @@+module Hadolint.Rule.DL4006 (rule) where++import qualified Data.Text as Text+import Hadolint.Rule+import Hadolint.Shell (ParsedShell)+import qualified Hadolint.Shell as Shell+import Language.Docker.Syntax++rule :: Rule ParsedShell+rule = customRule check (emptyState False)+  where+    code = "DL4006"+    severity = DLWarningC+    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 _ st From {} = st |> replaceWith False -- Reset the state each time we find a new FROM+    check _ st (Shell args)+      | foldArguments isPowerShell args = st |> replaceWith True+      | otherwise = st |> replaceWith (foldArguments hasPipefailOption args)+    check line st@(State _ False) (Run (RunArgs args _))+      | foldArguments hasPipes args = st |> addFail CheckFailure {..}+      | otherwise = st+    check _ st _ = st++    isPowerShell (Shell.ParsedShell orig _ _) = "pwsh" `Text.isPrefixOf` orig+    hasPipes script = Shell.hasPipes script+    hasPipefailOption script =+      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"+          ]+{-# INLINEABLE rule #-}
+ src/Hadolint/Rule/Shellcheck.hs view
@@ -0,0 +1,44 @@+module Hadolint.Rule.Shellcheck (rule) where++import qualified Data.Set as Set+import qualified Data.Text as Text+import Hadolint.Rule+import Hadolint.Shell (ParsedShell)+import qualified Hadolint.Shell as Shell+import Language.Docker.Syntax+import qualified ShellCheck.Interface++rule :: Rule ParsedShell+rule = customRule check (emptyState Shell.defaultShellOpts)+  where+    check _ st (From _) = st |> replaceWith Shell.defaultShellOpts -- Reset the state+    check _ st (Arg name _) = st |> modify (Shell.addVars [name])+    check _ st (Env pairs) = st |> modify (Shell.addVars (map fst pairs))+    check _ st (Shell args) = st |> modify (Shell.setShell (foldArguments Shell.original args))+    check line st (Run (RunArgs args _)) = getFailures |> foldr addFail st+      where+        getFailures = foldArguments (runShellCheck (state st)) args+        runShellCheck opts script = Set.fromList [toFailure line c | c <- Shell.shellcheck opts script]+    check _ st _ = st+{-# INLINEABLE rule #-}++-- | Converts ShellCheck errors into our own errors type+toFailure :: Linenumber -> ShellCheck.Interface.PositionedComment -> CheckFailure+toFailure line c =+  CheckFailure+    { code = RuleCode $ Text.pack ("SC" ++ show (code c)),+      severity = getDLSeverity $ severity c,+      message = Text.pack (message c),+      line = line+    }+  where+    getDLSeverity :: ShellCheck.Interface.Severity -> DLSeverity+    getDLSeverity s =+      case s of+        ShellCheck.Interface.WarningC -> DLWarningC+        ShellCheck.Interface.InfoC -> DLInfoC+        ShellCheck.Interface.StyleC -> DLStyleC+        _ -> DLErrorC+    severity pc = ShellCheck.Interface.cSeverity $ ShellCheck.Interface.pcComment pc+    code pc = ShellCheck.Interface.cCode $ ShellCheck.Interface.pcComment pc+    message pc = ShellCheck.Interface.cMessage $ ShellCheck.Interface.pcComment pc
− src/Hadolint/Rules.hs
@@ -1,1236 +0,0 @@-{-# LANGUAGE DeriveAnyClass #-}-{-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE NamedFieldPuns #-}-{-# LANGUAGE OverloadedStrings #-}--module Hadolint.Rules where--import Control.Arrow ((&&&))-import Control.DeepSeq (NFData)-import Data.List (foldl', isInfixOf, isPrefixOf, mapAccumL, nub)-import Data.List.NonEmpty (toList)-import Data.List.Index-import qualified Data.Map as Map-import qualified Data.Set as Set-import qualified Data.Text as Text-import Data.Void (Void)-import GHC.Generics (Generic)-import qualified Hadolint.Shell as Shell-import Language.Docker.Syntax-import ShellCheck.Interface (Severity (..))-import qualified ShellCheck.Interface-import qualified Text.Megaparsec as Megaparsec-import qualified Text.Megaparsec.Char as Megaparsec--data DLSeverity-  = DLErrorC-  | DLWarningC-  | DLInfoC-  | DLStyleC-  | DLIgnoreC-  deriving (Show, Eq, Ord, Generic, NFData)--data Metadata = Metadata-  { code :: Text.Text,-    severity :: DLSeverity,-    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--- position only records the linenumber at the moment to keep it easy--- 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, Show)---- | Contains the required parameters for optional rules-newtype RulesConfig = RulesConfig-  { -- | 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--instance Semigroup RulesConfig where-  RulesConfig a <> RulesConfig b = RulesConfig (a <> b)--instance Monoid RulesConfig where-  mempty = RulesConfig mempty-  mappend = (<>)--type IgnoreRuleParser = Megaparsec.Parsec Void Text.Text--type ParsedFile = [InstructionPos Shell.ParsedShell]---- | 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)---- | A function to check individual dockerfile instructions.--- It gets the current line number.--- It should return True if the check passes for the given instruction.-type SimpleCheckerWithLine = (Linenumber -> Instruction Shell.ParsedShell -> Bool)---- | 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])--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 with parsed shell 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 :: CheckerWithState state -> state -> Rule-mapInstructions f initialState dockerfile =-  let (_, results) = mapAccumL applyRule initialState dockerfile-   in concat results-  where-    applyRule state (InstructionPos onbuild@(OnBuild i) source linenumber) =-      -- All rules applying to instructions also apply to ONBUILD,-      -- so we unwrap the OnBuild constructor and check directly the inner-      -- instruction. Then we also check the instruction itself and append the-      -- result to avoid losing out on detecting problems with `ONBUILD`-      let (innerState, innerResults) = applyWithState state source linenumber i-          (finalState, outerResults) = applyWithState innerState source linenumber onbuild-       in (finalState, innerResults <> outerResults)-    applyRule state (InstructionPos i source linenumber) =-      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])--instructionRule ::-  Text.Text -> DLSeverity -> Text.Text -> (Instruction Shell.ParsedShell -> Bool) -> Rule-instructionRule code severity message check =-  instructionRuleLine code severity message (const check)--instructionRuleLine :: Text.Text -> DLSeverity -> Text.Text -> SimpleCheckerWithLine -> Rule-instructionRuleLine code severity message check =-  instructionRuleState code severity message checkAndDropState ()-  where-    checkAndDropState state line instr = (state, check line instr)--instructionRuleState ::-  Text.Text -> DLSeverity -> 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, [])--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---- 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-  ]-  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]-    allIgnores = ignored dockerfile-    parsedFile = map (fmap Shell.parseShell) dockerfile--ignored :: Dockerfile -> [(Linenumber, [Text.Text])]-ignored dockerfile =-  [(l + 1, ignores) | (l, Just ignores) <- map (lineNumber &&& extractIgnored) dockerfile]-  where-    extractIgnored = ignoreFromInstruction . instruction-    ignoreFromInstruction (Comment comment) = parseComment comment-    ignoreFromInstruction _ = Nothing-    parseComment :: Text.Text -> Maybe [Text.Text]-    parseComment = Megaparsec.parseMaybe commentParser-    commentParser :: IgnoreRuleParser [Text.Text]-    commentParser =-      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-    space c = c == ' ' || c == '\t'-    ruleName = Megaparsec.takeWhile1P Nothing (\c -> c `elem` ("DLSC0123456789" :: String))--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,-    zypperYes,-    noZypperUpdate,-    zypperCleanup,-    zypperVersionPinned,-    dnfYes,-    noDnfUpdate,-    dnfCleanup,-    dnfVersionPinned,-    pipNoCacheDir,-    noIllegalInstructionInOnbuild,-    noSelfreferencingEnv-  ]--optionalRules :: RulesConfig -> [Rule]-optionalRules RulesConfig {allowedRegistries} = [registryIsAllowed allowedRegistries]--allFromImages :: ParsedFile -> [(Linenumber, BaseImage)]-allFromImages dockerfile = [(l, f) | (l, From f) <- instr]-  where-    instr = fmap (lineNumber &&& instruction) dockerfile--allAliasedImages :: ParsedFile -> [(Linenumber, ImageAlias)]-allAliasedImages dockerfile =-  [(l, alias) | (l, Just alias) <- map extractAlias (allFromImages dockerfile)]-  where-    extractAlias (l, f) = (l, fromAlias f)--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 -> ParsedFile -> [Text.Text]-previouslyDefinedAliases line dockerfile =-  [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--fromName :: BaseImage -> Text.Text-fromName BaseImage {image = Image {imageName}} = imageName--fromAlias :: BaseImage -> Maybe ImageAlias-fromAlias BaseImage {alias} = alias------------------  RULES  -----------------shellcheck :: Rule-shellcheck = mapInstructions check Shell.defaultShellOpts-  where-    check :: CheckerWithState Shell.ShellOpts-    check _ _ (From _) = (Shell.defaultShellOpts, []) -- Reset the state-    check st _ (Arg name _) = (Shell.addVars [name] st, [])-    check st _ (Env pairs) = (Shell.addVars (map fst pairs) st, [])-    check st _ (Shell (ArgumentsList script)) = (Shell.setShell (Shell.original script) st, [])-    check st _ (Shell (ArgumentsText script)) = (Shell.setShell (Shell.original script) st, [])-    check st _ (Run (RunArgs (ArgumentsList script) _)) = (st, doCheck st script)-    check st _ (Run (RunArgs (ArgumentsText script) _)) = (st, doCheck st script)-    check st _ _ = (st, [])-    doCheck opts script = nub [commentMetadata c | c <- Shell.shellcheck opts script]---- | Converts ShellCheck errors into our own errors type-commentMetadata :: ShellCheck.Interface.PositionedComment -> Metadata-commentMetadata c =-  Metadata (Text.pack ("SC" ++ show (code c))) (getDLSeverity $ severity c) (Text.pack (message c))-  where-    getDLSeverity :: Severity -> DLSeverity-    getDLSeverity s =-      case s of-        WarningC -> DLWarningC-        InfoC -> DLInfoC-        StyleC -> DLStyleC-        _ -> DLErrorC-    severity pc = ShellCheck.Interface.cSeverity $ ShellCheck.Interface.pcComment pc-    code pc = ShellCheck.Interface.cCode $ ShellCheck.Interface.pcComment pc-    message pc = ShellCheck.Interface.cMessage $ ShellCheck.Interface.pcComment pc--absoluteWorkdir :: Rule-absoluteWorkdir = instructionRule code severity message check-  where-    code = "DL3000"-    severity = DLErrorC-    message = "Use absolute WORKDIR"-    check (Workdir loc)-      | "$" `Text.isPrefixOf` Text.dropAround dropQuotes loc = True-      | "/" `Text.isPrefixOf` Text.dropAround dropQuotes loc = True-      | otherwise = False-    check _ = True-    dropQuotes chr-      | chr == '\"' = True-      | chr == '\'' = True-      | otherwise = False--hasNoMaintainer :: Rule-hasNoMaintainer = instructionRule code severity message check-  where-    code = "DL4000"-    severity = DLErrorC-    message = "MAINTAINER is deprecated"-    check (Maintainer _) = False-    check _ = True---- Check if a command contains a program call in the Run instruction-usingProgram :: Text.Text -> Shell.ParsedShell -> Bool-usingProgram prog args = not $ null [cmd | cmd <- Shell.findCommandNames args, cmd == prog]--multipleCmds :: Rule-multipleCmds = instructionRuleState code severity message check False-  where-    code = "DL4003"-    severity = DLWarningC-    message =-      "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--multipleEntrypoints :: Rule-multipleEntrypoints = instructionRuleState code severity message check False-  where-    code = "DL4004"-    severity = DLErrorC-    message =-      "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-    check st _ _ = withState st True--wgetOrCurl :: Rule-wgetOrCurl = instructionRuleState code severity message check Set.empty-  where-    code = "DL4001"-    severity = DLWarningC-    message = "Either use Wget or Curl but not both"-    check state _ (Run (RunArgs args _)) = argumentsRule (detectDoubleUsage state) args-    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)-    extractCommands args =-      Set.fromList [w | w <- Shell.findCommandNames args, w == "curl" || w == "wget"]--invalidCmd :: Rule-invalidCmd = instructionRule code severity message check-  where-    code = "DL3001"-    severity = DLInfoC-    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 (RunArgs args _)) = argumentsRule detectInvalid args-    check _ = True-    detectInvalid args = null [arg | arg <- Shell.findCommandNames args, arg `elem` invalidCmds]-    invalidCmds = ["ssh", "vim", "shutdown", "service", "ps", "free", "top", "kill", "mount"]--noRootUser :: Rule-noRootUser dockerfile = instructionRuleState code severity message check Nothing dockerfile-  where-    code = "DL3002"-    severity = DLWarningC-    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-    check st _ _ = withState st True-    ---    ---    lastUserIsRoot from line = Map.lookup from rootStages == Just line-    ---    ---    rootStages :: Map.Map BaseImage Linenumber-    rootStages =-      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-    buildMap st _ = st-    ---    ---    isRoot user =-      Text.isPrefixOf "root:" user || Text.isPrefixOf "0:" user || user == "root" || user == "0"--noCd :: Rule-noCd = instructionRule code severity message check-  where-    code = "DL3003"-    severity = DLWarningC-    message = "Use WORKDIR to switch to a directory"-    check (Run (RunArgs args _)) = argumentsRule (not . usingProgram "cd") args-    check _ = True--noSudo :: Rule-noSudo = instructionRule code severity message check-  where-    code = "DL3004"-    severity = DLErrorC-    message =-      "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--noAptGetUpgrade :: Rule-noAptGetUpgrade = instructionRule code severity message check-  where-    code = "DL3005"-    severity = DLErrorC-    message = "Do not use apt-get upgrade or dist-upgrade"-    check (Run (RunArgs args _)) =-      argumentsRule (Shell.noCommands (Shell.cmdHasArgs "apt-get" ["upgrade", "dist-upgrade"])) args-    check _ = True--noUntagged :: Rule-noUntagged dockerfile = instructionRuleLine code severity message check dockerfile-  where-    code = "DL3006"-    severity = DLWarningC-    message = "Always tag the version of an image explicitly"-    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-    check _ _ = True--noLatestTag :: Rule-noLatestTag = instructionRule code severity message check-  where-    code = "DL3007"-    severity = DLWarningC-    message =-      "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--aptGetVersionPinned :: Rule-aptGetVersionPinned = instructionRule code severity message check-  where-    code = "DL3008"-    severity = DLWarningC-    message =-      "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"-  ]-  where-    dropTarget = Shell.dropFlagArg ["t", "target-release"]--aptGetCleanup :: Rule-aptGetCleanup dockerfile = instructionRuleState code severity message check Nothing dockerfile-  where-    code = "DL3009"-    severity = DLInfoC-    message = "Delete the apt-get lists after installing something"--    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)-    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-    hasCleanup 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-    imageIsUsedLater line baseimage =-      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-  where-    code = "DL3017"-    severity = DLErrorC-    message = "Do not use apk upgrade"-    check (Run (RunArgs args _)) = argumentsRule (Shell.noCommands (Shell.cmdHasArgs "apk" ["upgrade"])) args-    check _ = True--apkAddVersionPinned :: Rule-apkAddVersionPinned = instructionRule code severity message check-  where-    code = "DL3018"-    severity = DLWarningC-    message =-      "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"-  ]-  where-    dropTarget = Shell.dropFlagArg ["t", "virtual", "repository", "X"]--apkAddNoCache :: Rule-apkAddNoCache = instructionRule code severity message check-  where-    code = "DL3019"-    severity = DLInfoC-    message =-      "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)--useAdd :: Rule-useAdd = instructionRule code severity message check-  where-    code = "DL3010"-    severity = DLInfoC-    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-        ]-    check _ = True-    archiveFormats =-      [ ".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-  where-    code = "DL3011"-    severity = DLErrorC-    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]-    check _ = True--pipVersionPinned :: Rule-pipVersionPinned = instructionRule code severity message check-  where-    code = "DL3013"-    severity = DLWarningC-    message =-      "Pin versions in pip. Instead of `pip install <package>` use `pip install \-      \<package>==<version>` or `pip install --requirement <requirements file>`"-    check (Run (RunArgs args _)) = argumentsRule (Shell.noCommands forgotToPinVersion) args-    check _ = True-    forgotToPinVersion 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 =-      (isPipInstall cmd && not (hasBuildConstraint cmd) && not (all versionFixed (packages cmd))) && not (requirementInstall 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-    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-    versionFixed package = hasVersionSymbol package || isVersionedGit package || isLocalPackage package-    isVersionedGit package = "git+http" `Text.isInfixOf` package && "@" `Text.isInfixOf` package-    versionSymbols = ["==", ">=", "<=", ">", "<", "!=", "~=", "==="]-    hasVersionSymbol package = or [s `Text.isInfixOf` package | s <- versionSymbols]-    localPackageFileExtensions = [".whl", ".tar.gz"]-    isLocalPackage package = or [s `Text.isSuffixOf` package | s <- localPackageFileExtensions]--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>]-npmVersionPinned :: Rule-npmVersionPinned = instructionRule code severity message check-  where-    code = "DL3016"-    severity = DLWarningC-    message =-      "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 = 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-    gitPrefixes = ["git://", "git+ssh://", "git+http://", "git+https://"]-    hasGitPrefix package = or [p `Text.isPrefixOf` package | p <- gitPrefixes]-    tarballSuffixes = [".tar", ".tar.gz", ".tgz"]-    hasTarballSuffix package = or [p `Text.isSuffixOf` package | p <- tarballSuffixes]-    pathPrefixes = ["/", "./", "../", "~/"]-    isFolder package = or [p `Text.isPrefixOf` package | p <- pathPrefixes]-    isVersionedGit package = "#" `Text.isInfixOf` package-    hasVersionSymbol package = "@" `Text.isInfixOf` dropScope package-      where-        dropScope pkg =-          if "@" `Text.isPrefixOf` pkg-            then Text.dropWhile ('/' <) pkg-            else pkg--aptGetYes :: Rule-aptGetYes = instructionRule code severity message check-  where-    code = "DL3014"-    severity = DLWarningC-    message = "Use the `-y` switch to avoid manual input `apt-get -y install <package>`"-    check (Run (RunArgs args _)) = argumentsRule (Shell.noCommands forgotAptYesOption) args-    check _ = True-    forgotAptYesOption cmd = isAptGetInstall cmd && not (hasYesOption cmd)-    isAptGetInstall = Shell.cmdHasArgs "apt-get" ["install"]-    hasYesOption = Shell.hasAnyFlag ["y", "yes", "q", "assume-yes"]--aptGetNoRecommends :: Rule-aptGetNoRecommends = instructionRule code severity message check-  where-    code = "DL3015"-    severity = DLInfoC-    message = "Avoid additional packages by specifying `--no-install-recommends`"-    check (Run (RunArgs args _)) = argumentsRule (Shell.noCommands forgotNoInstallRecommends) args-    check _ = True-    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--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"-            ]-      ]-    )--isUrl :: Text.Text -> Bool-isUrl path = or ([proto `Text.isPrefixOf` path | proto <- ["https://", "http://"]])--copyInsteadAdd :: Rule-copyInsteadAdd = instructionRule code severity message check-  where-    code = "DL3020"-    severity = DLErrorC-    message = "Use COPY instead of ADD for files and folders"-    check (Add (AddArgs srcs _ _)) =-      and [isArchive src || isUrl src | SourcePath src <- toList srcs]-    check _ = True--copyEndingSlash :: Rule-copyEndingSlash = instructionRule code severity message check-  where-    code = "DL3021"-    severity = DLErrorC-    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-    check _ = True-    endsWithSlash (TargetPath t) = not (Text.null t) && Text.last t == '/'--copyFromExists :: Rule-copyFromExists dockerfile = instructionRuleLine code severity message check dockerfile-  where-    code = "DL3022"-    severity = DLWarningC-    message = "COPY --from should reference a previously defined FROM alias"-    check l (Copy (CopyArgs _ _ _ (CopySource s))) = s `elem` previouslyDefinedAliases l dockerfile-    check _ _ = True--copyFromAnother :: Rule-copyFromAnother = instructionRuleState code severity message check Nothing-  where-    code = "DL3023"-    severity = DLErrorC-    message = "COPY --from should reference a previously defined FROM alias"--    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!-    check state _ _ = withState state True--fromAliasUnique :: Rule-fromAliasUnique dockerfile = instructionRuleLine code severity message check dockerfile-  where-    code = "DL3024"-    severity = DLErrorC-    message = "FROM aliases (stage names) must be unique"-    check line = aliasMustBe (not . alreadyTaken line)-    alreadyTaken line alias = alias `elem` previouslyDefinedAliases line dockerfile--useShell :: Rule-useShell = instructionRule code severity message check-  where-    code = "DL4005"-    severity = DLWarningC-    message = "Use SHELL to change the default shell"-    check (Run (RunArgs args _)) = argumentsRule (Shell.noCommands (Shell.cmdHasArgs "ln" ["/bin/sh"])) args-    check _ = True--useJsonArgs :: Rule-useJsonArgs = instructionRule code severity message check-  where-    code = "DL3025"-    severity = DLWarningC-    message = "Use arguments JSON notation for CMD and ENTRYPOINT arguments"-    check (Cmd (ArgumentsText _)) = False-    check (Entrypoint (ArgumentsText _)) = False-    check _ = True--noApt :: Rule-noApt = instructionRule code severity message check-  where-    code = "DL3027"-    severity = DLWarningC-    message =-      "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--usePipefail :: Rule-usePipefail = instructionRuleState code severity message check False-  where-    code = "DL4006"-    severity = DLWarningC-    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)-    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 $-        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"-          ]--registryIsAllowed :: Set.Set Registry -> Rule-registryIsAllowed allowed = instructionRuleState code severity message check Set.empty-  where-    code = "DL3026"-    severity = DLErrorC-    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)-    toImageAlias = Just . ImageAlias . imageName--    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--gemVersionPinned :: Rule-gemVersionPinned = instructionRule code severity message check-  where-    code = "DL3028"-    severity = DLWarningC-    message =-      "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--noPlatformFlag :: Rule-noPlatformFlag = instructionRule code severity message check-  where-    code = "DL3029"-    severity = DLWarningC-    message = "Do not use --platform flag with FROM"-    check (From BaseImage {platform = Just p}) = p == ""-    check _ = True--yumYes :: Rule-yumYes = instructionRule code severity message check-  where-    code = "DL3030"-    severity = DLWarningC-    message = "Use the -y switch to avoid manual input `yum install -y <package`"-    check (Run (RunArgs args _)) = argumentsRule (Shell.noCommands forgotYumYesOption) args-    check _ = True-    forgotYumYesOption cmd = isYumInstall cmd && not (hasYesOption cmd)-    isYumInstall = Shell.cmdHasArgs "yum" ["install", "groupinstall", "localinstall"]-    hasYesOption = Shell.hasAnyFlag ["y", "assumeyes"]--noYumUpdate :: Rule-noYumUpdate = instructionRule code severity message check-  where-    code = "DL3031"-    severity = DLErrorC-    message = "Do not use yum update."-    check (Run (RunArgs args _)) =-      argumentsRule-        ( Shell.noCommands-            ( Shell.cmdHasArgs-                "yum"-                [ "update",-                  "update-to",-                  "upgrade",-                  "upgrade-to"-                ]-            )-        )-        args-    check _ = True--yumCleanup :: Rule-yumCleanup = instructionRule code severity message check-  where-    code = "DL3032"-    severity = DLWarningC-    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 _ = True-    yumInstall = Shell.cmdHasArgs "yum" ["install"]-    yumClean = Shell.cmdHasArgs "yum" ["clean", "all"]--yumVersionPinned :: Rule-yumVersionPinned = instructionRule code severity message check-  where-    code = "DL3033"-    severity = DLWarningC-    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--yumPackages :: Shell.ParsedShell -> [Text.Text]-yumPackages args =-  [ arg | cmd <- Shell.presentCommands args, Shell.cmdHasArgs "yum" ["install"] cmd, arg <- Shell.getArgsNoFlags cmd, arg /= "install"-  ]--zypperYes :: Rule-zypperYes = instructionRule code severity message check-  where-    code = "DL3034"-    severity = DLWarningC-    message = "Non-interactive switch missing from `zypper` command: `zypper install -y`"-    check (Run (RunArgs args _)) = argumentsRule (Shell.noCommands forgotZypperYesOption) args-    check _ = True-    forgotZypperYesOption cmd = isZypperInstall cmd && not (hasYesOption cmd)-    isZypperInstall =-      Shell.cmdHasArgs-        "zypper"-        [ "install",-          "in",-          "remove",-          "rm",-          "source-install",-          "si",-          "patch"-        ]-    hasYesOption = Shell.hasAnyFlag ["no-confirm", "y"]--noZypperUpdate :: Rule-noZypperUpdate = instructionRule code severity message check-  where-    code = "DL3035"-    severity = DLWarningC-    message = "Do not use `zypper update`."-    check (Run (RunArgs args _)) =-      argumentsRule-        ( Shell.noCommands-            ( Shell.cmdHasArgs-                "zypper"-                [ "update",-                  "up",-                  "dist-upgrade",-                  "dup"-                ]-            )-        )-        args-    check _ = True--zypperCleanup :: Rule-zypperCleanup = instructionRule code severity message check-  where-    code = "DL3036"-    severity = DLWarningC-    message = "`zypper clean` missing after zypper use."-    check (Run (RunArgs args _)) =-      argumentsRule (Shell.noCommands zypperInstall) args-        || ( argumentsRule (Shell.anyCommands zypperInstall) args-               && argumentsRule (Shell.anyCommands zypperClean) args-           )-    check _ = True-    zypperInstall = Shell.cmdHasArgs "zypper" ["install", "in"]-    zypperClean = Shell.cmdHasArgs "zypper" ["clean", "cc"]--zypperVersionPinned :: Rule-zypperVersionPinned = instructionRule code severity message check-  where-    code = "DL3037"-    severity = DLWarningC-    message = "Specify version with `zypper install -y <package>=<version>`."-    check (Run (RunArgs args _)) = argumentsRule (all versionFixed . zypperPackages) args-    check _ = True-    versionFixed package =-      "=" `Text.isInfixOf` package-        || ">=" `Text.isInfixOf` package-        || ">" `Text.isInfixOf` package-        || "<=" `Text.isInfixOf` package-        || "<" `Text.isInfixOf` package-        || ".rpm" `Text.isSuffixOf` package--zypperPackages :: Shell.ParsedShell -> [Text.Text]-zypperPackages args =-  [ arg | cmd <- Shell.presentCommands args, Shell.cmdHasArgs "zypper" ["install", "in"] cmd, arg <- Shell.getArgsNoFlags cmd, arg /= "install", arg /= "in"-  ]--dnfYes :: Rule-dnfYes = instructionRule code severity message check-  where-    code = "DL3038"-    severity = DLWarningC-    message = "Use the -y switch to avoid manual input `dnf install -y <package`"-    check (Run (RunArgs args _)) = argumentsRule (Shell.noCommands forgotDnfYesOption) args-    check _ = True-    forgotDnfYesOption cmd = isDnfInstall cmd && not (hasYesOption cmd)-    isDnfInstall = Shell.cmdHasArgs "dnf" ["install", "groupinstall", "localinstall"]-    hasYesOption = Shell.hasAnyFlag ["y", "assumeyes"]--noDnfUpdate :: Rule-noDnfUpdate = instructionRule code severity message check-  where-    code = "DL3039"-    severity = DLErrorC-    message = "Do not use dnf update."-    check (Run (RunArgs args _)) =-      argumentsRule-        ( Shell.noCommands-            ( Shell.cmdHasArgs-                "dnf"-                [ "upgrade",-                  "upgrade-minimal"-                ]-            )-        )-        args-    check _ = True--dnfCleanup :: Rule-dnfCleanup = instructionRule code severity message check-  where-    code = "DL3040"-    severity = DLWarningC-    message = "`dnf clean all` missing after dnf command."-    check (Run (RunArgs args _)) =-      argumentsRule (Shell.noCommands dnfInstall) args-        || ( argumentsRule (Shell.anyCommands dnfInstall) args-               && argumentsRule (Shell.anyCommands dnfClean) args-           )-    check _ = True-    dnfInstall = Shell.cmdHasArgs "dnf" ["install"]-    dnfClean = Shell.cmdHasArgs "dnf" ["clean", "all"]--dnfVersionPinned :: Rule-dnfVersionPinned = instructionRule code severity message check-  where-    code = "DL3041"-    severity = DLWarningC-    message = "Specify version with `dnf install -y <package>-<version>`."-    check (Run (RunArgs args _)) = argumentsRule (all versionFixed . dnfPackages) args-    check _ = True-    versionFixed package =-      "-" `Text.isInfixOf` package-        || ".rpm" `Text.isSuffixOf` package--dnfPackages :: Shell.ParsedShell -> [Text.Text]-dnfPackages args =-  [ arg | cmd <- Shell.presentCommands args, Shell.cmdHasArgs "dnf" ["install"] cmd, arg <- Shell.getArgsNoFlags cmd, arg /= "install"-  ]--pipNoCacheDir :: Rule-pipNoCacheDir = instructionRule code severity message check-  where-    code = "DL3042"-    severity = DLWarningC-    message =-      "Avoid use of cache directory with pip. Use `pip install --no-cache-dir <package>`"-    check (Run (RunArgs args _)) = argumentsRule (Shell.noCommands forgotNoCacheDir) args-    check _ = True-    forgotNoCacheDir cmd =-      isPipInstall cmd && not (usesNoCacheDir cmd) && not (isPipWrapper cmd)-    usesNoCacheDir cmd = "--no-cache-dir" `elem` Shell.getArgs cmd--isPipInstall :: Shell.Command -> Bool-isPipInstall cmd@(Shell.Command name _ _) = isStdPipInstall || isPythonPipInstall-  where-    isStdPipInstall =-      "pip" `Text.isPrefixOf` name-        && ["install"] `isInfixOf` Shell.getArgs cmd-    isPythonPipInstall =-      "python" `Text.isPrefixOf` name-        && ["-m", "pip", "install"] `isInfixOf` Shell.getArgs cmd--isPipWrapper :: Shell.Command -> Bool-isPipWrapper cmd@(Shell.Command name _ _) = isWrapper "pipx" || isWrapper "pipenv"-  where-    isWrapper :: Text.Text -> Bool-    isWrapper w =-      w `Text.isInfixOf` name-        || ("python" `Text.isPrefixOf` name && ["-m", w] `isInfixOf` Shell.getArgs cmd)--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 /= "--"-  ]--noIllegalInstructionInOnbuild :: Rule-noIllegalInstructionInOnbuild = instructionRule code severity message check-  where-    code = "DL3043"-    severity = DLErrorC-    message = "`ONBUILD`, `FROM` or `MAINTAINER` triggered from within `ONBUILD` instruction."-    check (OnBuild (OnBuild _)) = False-    check (OnBuild (From _)) = False-    check (OnBuild (Maintainer _)) = False-    check _ = True--noSelfreferencingEnv :: Rule-noSelfreferencingEnv = instructionRuleState code severity message check Set.empty-  where-    code = "DL3044"-    severity = DLErrorC-    message = "Do not refer to an environment variable within the same `ENV` statement where it is defined."-    check st _ (Env pairs) = withState (Set.union st (Set.fromList (map fst pairs))) $-        null [ env | env <- listOfReferences pairs, env `Set.notMember` st ]-    check st _ (Arg arg _) = withState (Set.insert arg st) True-    check st _ _ = withState st True--    -- generates a list of references to variable names referenced on the right-    -- hand side of a variable definition, except when the variable is-    -- referenced on its own right hand side.-    listOfReferences :: Pairs -> [Text.Text]-    listOfReferences prs = [ var | (idx, (var, _)) <- indexed prs,-                                   var `isSubstringOfAny` map (snd . snd) (filter ((/= idx) . fst) (indexed prs))]-    -- is a reference of a variable substring of any text?-    -- matches ${var_name} and $var_name, but not $var_nameblafoo-    isSubstringOfAny :: Text.Text -> [Text.Text] -> Bool-    isSubstringOfAny t l = not $ null [ v | v <- l,-        (Text.pack "${" <> t <> Text.pack "}") `Text.isInfixOf` v-        || (t `bareVariableInText` v)]--    -- we find a 'bare' variable with name v in a text, if-    -- '$v' is in the text at any place and any text following after that-    -- occurence would terminate a variable name. To determine that, the text t-    -- is split at every occurence of var, check if '$v' is in the text and if-    -- any part of the split text would terminate a variable name.-    bareVariableInText :: Text.Text -> Text.Text -> Bool-    bareVariableInText v t =-      let var = Text.pack "$" <> v-          rest = Text.splitOn var t-       in var `Text.isInfixOf` t && any terminatesVarName rest-      where-        -- x would terminate a variable name if it was appended directly to-        -- that name-        terminatesVarName :: Text.Text -> Bool-        terminatesVarName x = not $ beginsWithAnyOf x varChar--        -- txt begins with any character of String-        beginsWithAnyOf :: Text.Text -> String -> Bool-        beginsWithAnyOf txt str = Text.null txt || (Text.head txt `elem` str)--        -- all characters valid in the inner of a shell variable name-        varChar :: String-        varChar = ['0'..'9'] ++ ['a'..'z'] ++ ['A'..'Z'] ++ ['_']
src/Hadolint/Shell.hs view
@@ -1,13 +1,9 @@-{-# LANGUAGE NamedFieldPuns #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE PatternSynonyms #-}-{-# LANGUAGE ScopedTypeVariables #-}- module Hadolint.Shell where  import Control.Monad.Writer (Writer, execWriter, tell) import Data.Functor.Identity (runIdentity)-import Data.Maybe (listToMaybe, mapMaybe)+import Data.List (isInfixOf)+import Data.Maybe (fromMaybe, listToMaybe, mapMaybe) import qualified Data.Set as Set import Data.Text (Text) import qualified Data.Text as Text@@ -19,22 +15,22 @@ import qualified ShellCheck.Parser  data CmdPart = CmdPart-  { arg :: !Text,-    partId :: !Int+  { arg :: Text,+    partId :: Int   }   deriving (Show)  data Command = Command-  { name :: !Text.Text,+  { 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@@ -80,15 +76,14 @@           csShellTypeOverride = Nothing,           csMinSeverity = StyleC         }-    script = "#!" ++ extractShell sh ++ "\n" ++ printVars ++ Text.unpack txt+    script = Text.unpack $ "#!" <> extractShell sh <> "\n" <> printVars <> 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       ] -    extractShell s =-      maybe "" Text.unpack (listToMaybe . Text.words $ s)-    printVars = Text.unpack . Text.unlines . Set.toList $ Set.map (\v -> "export " <> v <> "=1") env+    extractShell s = fromMaybe "" (listToMaybe . Text.words $ s)+    printVars = Text.unlines . Set.toList $ Set.map (\v -> "export " <> v <> "=1") env  parseShell :: Text.Text -> ParsedShell parseShell txt = ParsedShell {original = txt, parsed = parsedResult, presentCommands = commands}@@ -99,7 +94,7 @@           (mockedSystemInterface [("", "")])           newParseSpec             { psFilename = "", -- There is no filename-              psScript = "#!/bin/bash\n" ++ Text.unpack txt,+              psScript = Text.unpack $ "#!/bin/bash\n" <> txt,               psCheckSourced = False             } @@ -164,7 +159,7 @@   where     mkPart token =       CmdPart-        (Text.pack . concat $ ShellCheck.ASTLib.oversimplify token)+        (Text.concat . fmap Text.pack $ ShellCheck.ASTLib.oversimplify token)         (mkId (ShellCheck.AST.getId token))     mkId (Id i) = i extractAllArgs _ = []@@ -201,5 +196,28 @@     idsToDrop = Set.fromList [getValueId fId arguments | CmdPart f fId <- flags, f `elem` flagsToDrop]     filterdArgs = [arg | arg@(CmdPart _ aId) <- arguments, not (aId `Set.member` idsToDrop)] +-- | given a flag and a command, return list of arguments for that particulat+-- flag. E.g., if the command is `useradd -u 12345 luser` and this function is+-- called for the command `u`, it returns ["12345"].+getFlagArg :: Text.Text -> Command -> [Text.Text]+getFlagArg flag Command {arguments, flags} = extractArgs+  where+    idsToGet = [getValueId fId arguments | CmdPart f fId <- flags, f == flag]+    extractArgs = [arg | (CmdPart arg aId) <- arguments, aId `elem` idsToGet]+ getValueId :: Int -> [CmdPart] -> Int getValueId fId flags = foldl min (maxBound :: Int) $ filter (> fId) $ map partId flags++-- | Check if a command contains a program call in the Run instruction+usingProgram :: Text.Text -> ParsedShell -> Bool+usingProgram prog args = not $ null [cmd | cmd <- findCommandNames args, cmd == prog]++isPipInstall :: Command -> Bool+isPipInstall cmd@(Command name _ _) = isStdPipInstall || isPythonPipInstall+  where+    isStdPipInstall =+      "pip" `Text.isPrefixOf` name+        && ["install"] `isInfixOf` getArgs cmd+    isPythonPipInstall =+      "python" `Text.isPrefixOf` name+        && ["-m", "pip", "install"] `isInfixOf` getArgs cmd
test/ConfigSpec.hs view
@@ -4,8 +4,10 @@  import Control.Monad (unless) import qualified Data.ByteString.Char8 as Bytes+import Data.Map import qualified Data.YAML as Yaml import Hadolint.Config+import Hadolint.Rule as Rule import Test.HUnit import Test.Hspec @@ -20,7 +22,7 @@               "    - SC1010"             ]           override = Just (OverrideConfig (Just ["DL3000", "SC1010"]) Nothing Nothing Nothing)-          expected = ConfigFile override Nothing Nothing+          expected = ConfigFile override Nothing Nothing Nothing Nothing        in assertConfig expected (Bytes.unlines configFile)      it "Parses config with only warning severities" $@@ -31,7 +33,7 @@               "    - SC1010"             ]           override = Just (OverrideConfig Nothing (Just ["DL3000", "SC1010"]) Nothing Nothing)-          expected = ConfigFile override Nothing Nothing+          expected = ConfigFile override Nothing Nothing Nothing Nothing        in assertConfig expected (Bytes.unlines configFile)      it "Parses config with only info severities" $@@ -42,7 +44,7 @@               "    - SC1010"             ]           override = Just (OverrideConfig Nothing Nothing (Just ["DL3000", "SC1010"]) Nothing)-          expected = ConfigFile override Nothing Nothing+          expected = ConfigFile override Nothing Nothing Nothing Nothing        in assertConfig expected (Bytes.unlines configFile)      it "Parses config with only style severities" $@@ -53,7 +55,7 @@               "    - SC1010"             ]           override = Just (OverrideConfig Nothing Nothing Nothing (Just ["DL3000", "SC1010"]))-          expected = ConfigFile override Nothing Nothing+          expected = ConfigFile override Nothing Nothing Nothing Nothing        in assertConfig expected (Bytes.unlines configFile)      it "Parses config with only ignores" $@@ -62,7 +64,7 @@               "- DL3000",               "- SC1010"             ]-          expected = ConfigFile Nothing (Just ["DL3000", "SC1010"]) Nothing+          expected = ConfigFile Nothing (Just ["DL3000", "SC1010"]) Nothing Nothing Nothing        in assertConfig expected (Bytes.unlines configFile)      it "Parses config with only trustedRegistries" $@@ -71,9 +73,23 @@               "- hub.docker.com",               "- my.shady.xyz"             ]-          expected = ConfigFile Nothing Nothing (Just ["hub.docker.com", "my.shady.xyz"])+          expected = ConfigFile Nothing Nothing (Just ["hub.docker.com", "my.shady.xyz"]) Nothing Nothing        in assertConfig expected (Bytes.unlines configFile) +    it "Parses config with only label-schema" $+      let configFile =+            [ "label-schema:",+              "  author: text",+              "  url: url"+            ]+          expected = ConfigFile Nothing Nothing Nothing (Just (fromList [("author", Rule.RawText), ("url", Rule.Url)])) Nothing+       in assertConfig expected (Bytes.unlines configFile)++    it "Parses config with only label-schema" $+      let configFile = [ "strict-labels: true" ]+          expected = ConfigFile Nothing Nothing Nothing Nothing (Just True)+       in assertConfig expected (Bytes.unlines configFile)+     it "Parses full file" $       let configFile =             [ "override:",@@ -90,10 +106,16 @@               "- hub.docker.com",               "",               "ignored:",-              "- DL3000"+              "- DL3000",+              "",+              "strict-labels: false",+              "label-schema:",+              "  author: text",+              "  url: url"             ]           override = Just (OverrideConfig (Just ["DL3001"]) (Just ["DL3003"]) (Just ["DL3002"]) (Just ["DL3004"]))-          expected = ConfigFile override (Just ["DL3000"]) (Just ["hub.docker.com"])+          labelschema = Just (fromList [("author", Rule.RawText), ("url", Rule.Url)])+          expected = ConfigFile override (Just ["DL3000"]) (Just ["hub.docker.com"]) labelschema (Just False)        in assertConfig expected (Bytes.unlines configFile)  assertConfig :: HasCallStack => ConfigFile -> Bytes.ByteString -> Assertion
+ test/ShellSpec.hs view
@@ -0,0 +1,42 @@+{-# LANGUAGE OverloadedStrings #-}++module ShellSpec where++import Data.Text as Text+import Hadolint.Shell+import Test.Hspec++tests :: SpecWith ()+tests =+  describe "Shell unit tests" $ do+  --+    it "getFlagArgs" $ do+      let cmd = Command (Text.pack "useradd")+                        [ CmdPart (Text.pack "12345") 2,+                          CmdPart (Text.pack "67890") 4,+                          CmdPart (Text.pack "luser") 5 ]+                        [ CmdPart (Text.pack "u") 1,+                          CmdPart (Text.pack "u") 3 ]+       in getFlagArg (Text.pack "u") cmd `shouldBe` [ Text.pack "12345",+                                                      Text.pack "67890" ]+      let cmd = Command (Text.pack "useradd")+                        [ CmdPart (Text.pack "12345") 2,+                          CmdPart (Text.pack "luser") 3 ]+                        [ CmdPart (Text.pack "u") 1 ]+       in getFlagArg (Text.pack "u") cmd `shouldBe` [ Text.pack "12345" ]++      let cmd = Command (Text.pack "useradd")+                        [ CmdPart (Text.pack "12345") 2,+                          CmdPart (Text.pack "luser") 3 ]+                        [ CmdPart (Text.pack "u") 1 ]+       in getFlagArg (Text.pack "f") cmd `shouldBe` []+  --+    it "hasFlag" $ do+      let cmd = Command (Text.pack "useradd")+                        [ CmdPart (Text.pack "luser") 1 ]+                        [ CmdPart (Text.pack "l") 0 ]+       in hasFlag (Text.pack "l") cmd `shouldBe` True+      let cmd = Command (Text.pack "useradd")+                        [ CmdPart (Text.pack "luser") 1 ]+                        [ CmdPart (Text.pack "l") 0 ]+       in hasFlag (Text.pack "f") cmd `shouldBe` False
test/Spec.hs view
@@ -1,1533 +1,1998 @@-{-# LANGUAGE OverloadedLists #-}-{-# LANGUAGE OverloadedStrings #-}--import qualified ConfigSpec-import Control.Monad (unless, when)-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"-            ruleCatchesNot noYumUpdate "RUN yum install -y httpd-2.4.42 && yum clean all"-            ruleCatchesNot noYumUpdate "RUN bash -c `# not even a yum command`"-            onBuildRuleCatches noYumUpdate "RUN yum update"-            onBuildRuleCatchesNot noYumUpdate "RUN yum install -y httpd-2.4.42 && yum clean all"-            onBuildRuleCatchesNot noYumUpdate "RUN bash -c `# not even a yum command`"-        it "yum version pinning" $ do-            ruleCatches yumVersionPinned "RUN yum install -y tomcat && yum clean all"-            ruleCatchesNot yumVersionPinned "RUN yum install -y tomcat-9.2 && yum clean all"-            ruleCatchesNot yumVersionPinned "RUN bash -c `# not even a yum command`"-            onBuildRuleCatches yumVersionPinned "RUN yum install -y tomcat && yum clean all"-            onBuildRuleCatchesNot yumVersionPinned "RUN yum install -y tomcat-9.2 && yum clean all"-            onBuildRuleCatchesNot yumVersionPinned "RUN bash -c `# not even a yum command`"-        it "yum no clean all" $ do-            ruleCatches yumCleanup "RUN yum install -y mariadb-10.4"-            ruleCatchesNot yumCleanup "RUN yum install -y mariadb-10.4 && yum clean all"-            ruleCatchesNot yumCleanup "RUN bash -c `# not even a yum command`"-            onBuildRuleCatches yumCleanup "RUN yum install -y mariadb-10.4"-            onBuildRuleCatchesNot yumCleanup "RUN yum install -y mariadb-10.4 && yum clean all"-            onBuildRuleCatchesNot yumCleanup "RUN bash -c `# not even a yum command`"-        it "yum non-interactive" $ do-            ruleCatches yumYes "RUN yum install httpd-2.4.24 && yum clean all"-            ruleCatchesNot yumYes "RUN yum install -y httpd-2.4.24 && yum clean all"-            ruleCatchesNot yumYes "RUN bash -c `# not even a yum command`"-            onBuildRuleCatches yumYes "RUN yum install httpd-2.4.24 && yum clean all"-            onBuildRuleCatchesNot yumYes "RUN yum install -y httpd-2.4.24 && yum clean all"-            onBuildRuleCatchesNot yumYes "RUN bash -c `# not even a yum command`"-    ---    describe "zypper rules" $ do-        it "zupper update" $ do-            ruleCatches noZypperUpdate "RUN zypper update"-            ruleCatches noZypperUpdate "RUN zypper up"-            ruleCatches noZypperUpdate "RUN zypper dist-upgrade"-            ruleCatches noZypperUpdate "RUN zypper dup"-            onBuildRuleCatches noZypperUpdate "RUN zypper update"-            onBuildRuleCatches noZypperUpdate "RUN zypper up"-            onBuildRuleCatches noZypperUpdate "RUN zypper dist-upgrade"-            onBuildRuleCatches noZypperUpdate "RUN zypper dup"-        it "zypper version pinning" $ do--- NOTE: In Haskell strings, '\' has to be escaped. And in shell commands, '>'--- and '<' have to be escaped. Hence the double escaping.-            ruleCatches zypperVersionPinned "RUN zypper install -y tomcat && zypper clean"-            ruleCatchesNot zypperVersionPinned "RUN zypper install -y tomcat=9.0.39 && zypper clean"-            ruleCatchesNot zypperVersionPinned "RUN zypper install -y tomcat\\>=9.0 && zypper clean"-            ruleCatchesNot zypperVersionPinned "RUN zypper install -y tomcat\\>9.0 && zypper clean"-            ruleCatchesNot zypperVersionPinned "RUN zypper install -y tomcat\\<=9.0 && zypper clean"-            ruleCatchesNot zypperVersionPinned "RUN zypper install -y tomcat\\<9.0 && zypper clean"-            ruleCatchesNot zypperVersionPinned "RUN zypper install -y tomcat-9.0.39-1.rpm && zypper clean"-            onBuildRuleCatches zypperVersionPinned "RUN zypper install -y tomcat && zypper clean"-            onBuildRuleCatchesNot zypperVersionPinned "RUN zypper install -y tomcat=9.0.39 && zypper clean"-            onBuildRuleCatchesNot zypperVersionPinned "RUN zypper install -y tomcat\\>=9.0 && zypper clean"-            onBuildRuleCatchesNot zypperVersionPinned "RUN zypper install -y tomcat\\>9.0 && zypper clean"-            onBuildRuleCatchesNot zypperVersionPinned "RUN zypper install -y tomcat\\<=9.0 && zypper clean"-            onBuildRuleCatchesNot zypperVersionPinned "RUN zypper install -y tomcat\\<9.0 && zypper clean"-            onBuildRuleCatchesNot zypperVersionPinned "RUN zypper install -y tomcat-9.0.39-1.rpm && zypper clean"-        it "zypper no clean all" $ do-            ruleCatches zypperCleanup "RUN zypper install -y mariadb=10.4"-            ruleCatchesNot zypperCleanup "RUN zypper install -y mariadb=10.4 && zypper clean"-            ruleCatchesNot zypperCleanup "RUN zypper install -y mariadb=10.4 && zypper cc"-            onBuildRuleCatches zypperCleanup "RUN zypper install -y mariadb=10.4"-            onBuildRuleCatchesNot zypperCleanup "RUN zypper install -y mariadb=10.4 && zypper clean"-            onBuildRuleCatchesNot zypperCleanup "RUN zypper install -y mariadb=10.4 && zypper cc"-        it "zypper non-interactive" $ do-            ruleCatches zypperYes "RUN zypper install httpd=2.4.24 && zypper clean"-            ruleCatchesNot zypperYes "RUN zypper install -y httpd=2.4.24 && zypper clean"-            ruleCatchesNot zypperYes "RUN zypper install --no-confirm httpd=2.4.24 && zypper clean"-            onBuildRuleCatches zypperYes "RUN zypper install httpd=2.4.24 && zypper clean"-            onBuildRuleCatchesNot zypperYes "RUN zypper install -y httpd=2.4.24 && zypper clean"-            onBuildRuleCatchesNot zypperYes "RUN zypper install --no-confirm httpd=2.4.24 && zypper clean"-    ---    describe "dnf rules" $ do-      it "dnf update" $ do-        ruleCatches noDnfUpdate "RUN dnf upgrade"-        onBuildRuleCatches noDnfUpdate "RUN dnf upgrade"-      it "dnf version pinning" $ do-        ruleCatches dnfVersionPinned "RUN dnf install -y tomcat && dnf clean all"-        ruleCatchesNot dnfVersionPinned "RUN dnf install -y tomcat-9.0.1 && dnf clean all"-        onBuildRuleCatches dnfVersionPinned "RUN dnf install -y tomcat && dnf clean all"-        onBuildRuleCatchesNot dnfVersionPinned "RUN dnf install -y tomcat-9.0.1 && dnf clean all"-      it "dnf no clean all" $ do-        ruleCatches dnfCleanup "RUN dnf install -y mariadb-10.4"-        ruleCatchesNot dnfCleanup "RUN dnf install -y mariadb-10.4 && dnf clean all"-        onBuildRuleCatches dnfCleanup "RUN dnf install -y mariadb-10.4"-        onBuildRuleCatchesNot dnfCleanup "RUN dnf install -y mariadb-10.4 && dnf clean all"-      it "dnf non-interactive" $ do-        ruleCatches dnfYes "RUN dnf install httpd-2.4.24 && dnf clean all"-        ruleCatchesNot dnfYes "RUN dnf install -y httpd-2.4.24 && dnf clean all"-        onBuildRuleCatches dnfYes "RUN dnf install httpd-2.4.24 && dnf clean all"-        onBuildRuleCatchesNot dnfYes "RUN dnf install -y httpd-2.4.24 && dnf clean all"-    ---    describe "dnf rules" $ do-      it "dnf update" $ do-        ruleCatches noDnfUpdate "RUN dnf upgrade"-        ruleCatches noDnfUpdate "RUN dnf upgrade-minimal"-        ruleCatchesNot noDnfUpdate "RUN notdnf upgrade"-        onBuildRuleCatches noDnfUpdate "RUN dnf upgrade"-        onBuildRuleCatches noDnfUpdate "RUN dnf upgrade-minimal"-        onBuildRuleCatchesNot noDnfUpdate "RUN notdnf upgrade"-      it "dnf version pinning" $ do-        ruleCatches dnfVersionPinned "RUN dnf install -y tomcat && dnf clean all"-        ruleCatchesNot dnfVersionPinned "RUN dnf install -y tomcat-9.0.1 && dnf clean all"-        ruleCatchesNot dnfVersionPinned "RUN notdnf install tomcat"-        onBuildRuleCatches dnfVersionPinned "RUN dnf install -y tomcat && dnf clean all"-        onBuildRuleCatchesNot dnfVersionPinned "RUN dnf install -y tomcat-9.0.1 && dnf clean all"-        onBuildRuleCatchesNot dnfVersionPinned "RUN notdnf install tomcat"-      it "dnf no clean all" $ do-        ruleCatches dnfCleanup "RUN dnf install -y mariadb-10.4"-        ruleCatchesNot dnfCleanup "RUN dnf install -y mariadb-10.4 && dnf clean all"-        ruleCatchesNot dnfCleanup "RUN notdnf install mariadb"-        onBuildRuleCatches dnfCleanup "RUN dnf install -y mariadb-10.4"-        onBuildRuleCatchesNot dnfCleanup "RUN dnf install -y mariadb-10.4 && dnf clean all"-        onBuildRuleCatchesNot dnfCleanup "RUN notdnf install mariadb"-      it "dnf non-interactive" $ do-        ruleCatches dnfYes "RUN dnf install httpd-2.4.24 && dnf clean all"-        ruleCatchesNot dnfYes "RUN dnf install -y httpd-2.4.24 && dnf clean all"-        ruleCatchesNot dnfYes "RUN notdnf install httpd"-        onBuildRuleCatches dnfYes "RUN dnf install httpd-2.4.24 && dnf clean all"-        onBuildRuleCatchesNot dnfYes "RUN dnf install -y httpd-2.4.24 && dnf clean all"-        onBuildRuleCatchesNot dnfYes "RUN notdnf install httpd"-    ---    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 dist-upgrade" $ do-        ruleCatches noAptGetUpgrade "RUN apt-get update && apt-get dist-upgrade"-        onBuildRuleCatches noAptGetUpgrade "RUN apt-get update && apt-get dist-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 "pip3 install from local package" $ do-        ruleCatchesNot pipVersionPinned "RUN pip3 install mypkg.whl"-        ruleCatchesNot pipVersionPinned "RUN pip3 install mypkg.tar.gz"-        onBuildRuleCatchesNot pipVersionPinned "RUN pip3 install mypkg.whl"-        onBuildRuleCatchesNot pipVersionPinned "RUN pip3 install mypkg.tar.gz"-      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 "pip cache dir" $ do-      it "pip2 --no-cache-dir not used" $ do-        ruleCatches pipNoCacheDir           "RUN pip2 install MySQL_python"-        onBuildRuleCatches pipNoCacheDir    "RUN pip2 install MySQL_python"-      it "pip3 --no-cache-dir not used" $ do-        ruleCatches pipNoCacheDir           "RUN pip3 install MySQL_python"-        onBuildRuleCatches pipNoCacheDir    "RUN pip3 install MySQL_python"-      it "pip --no-cache-dir not used" $ do-        ruleCatches pipNoCacheDir           "RUN pip install MySQL_python"-        onBuildRuleCatches pipNoCacheDir    "RUN pip install MySQL_python"-      it "pip2 --no-cache-dir used" $ do-        ruleCatchesNot pipNoCacheDir        "RUN pip2 install MySQL_python --no-cache-dir"-        onBuildRuleCatchesNot pipNoCacheDir "RUN pip2 install MySQL_python --no-cache-dir"-      it "pip3 --no-cache-dir used" $ do-        ruleCatchesNot pipNoCacheDir        "RUN pip3 install --no-cache-dir MySQL_python"-        onBuildRuleCatchesNot pipNoCacheDir "RUN pip3 install --no-cache-dir MySQL_python"-      it "pip --no-cache-dir used" $ do-        ruleCatchesNot pipNoCacheDir        "RUN pip install MySQL_python --no-cache-dir"-        onBuildRuleCatchesNot pipNoCacheDir "RUN pip install MySQL_python --no-cache-dir"-      it "don't match on pipx" $ do-        ruleCatchesNot pipNoCacheDir        "RUN pipx install software"-        onBuildRuleCatchesNot pipNoCacheDir "Run pipx install software"-      it "don't match on pipenv" $ do-        ruleCatchesNot pipNoCacheDir        "RUN pipenv install library"-        onBuildRuleCatchesNot pipNoCacheDir "RUN pipenv install library"-    ---    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 relative" $ ruleCatches absoluteWorkdir "WORKDIR relative/dir"-      it "workdir absolute" $ ruleCatchesNot absoluteWorkdir "WORKDIR /usr/local"-      it "workdir variable" $ ruleCatchesNot absoluteWorkdir "WORKDIR ${work}"-      it "workdir relative single quotes" $ ruleCatches absoluteWorkdir "WORKDIR \'relative/dir\'"-      it "workdir absolute single quotes" $ ruleCatchesNot absoluteWorkdir "WORKDIR \'/usr/local\'"-      -- no test for variable/single quotes since the variable would not expand.-      it "workdir relative double quotes" $ ruleCatches absoluteWorkdir "WORKDIR \"relative/dir\""-      it "workdir absolute double quotes" $ ruleCatchesNot absoluteWorkdir "WORKDIR \"/usr/local\""-      it "workdir variable double quotes" $ ruleCatchesNot absoluteWorkdir "WORKDIR \"${dir}\""-      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 "ONBUILD" $ do-      it "error when using `ONBUILD` within `ONBUILD`" $-        let dockerFile =-              [ "ONBUILD ONBUILD RUN anything"-              ]-        in ruleCatches noIllegalInstructionInOnbuild $ Text.unlines dockerFile-      it "error when using `FROM` within `ONBUILD`" $-        let dockerFile =-              [ "ONBUILD FROM debian:buster"-              ]-        in ruleCatches noIllegalInstructionInOnbuild $ Text.unlines dockerFile-      it "error when using `MAINTAINER` within `ONBUILD`" $-        let dockerFile =-              [ "ONBUILD MAINTAINER \"BoJack Horseman\""-              ]-        in ruleCatches noIllegalInstructionInOnbuild $ Text.unlines dockerFile-      it "ok with `ADD`" $ ruleCatchesNot noIllegalInstructionInOnbuild "ONBUILD ADD anything anywhere"-      it "ok with `USER`" $ ruleCatchesNot noIllegalInstructionInOnbuild "ONBUILD USER anything"-      it "ok with `LABEL`" $ ruleCatchesNot noIllegalInstructionInOnbuild "ONBUILD LABEL bla=\"blubb\""-      it "ok with `STOPSIGNAL`" $ ruleCatchesNot noIllegalInstructionInOnbuild "ONBUILD STOPSIGNAL anything"-      it "ok with `COPY`" $ ruleCatchesNot noIllegalInstructionInOnbuild "ONBUILD COPY anything anywhere"-      it "ok with `RUN`" $ ruleCatchesNot noIllegalInstructionInOnbuild "ONBUILD RUN anything"-      it "ok with `CMD`" $ ruleCatchesNot noIllegalInstructionInOnbuild "ONBUILD CMD anything"-      it "ok with `SHELL`" $ ruleCatchesNot noIllegalInstructionInOnbuild "ONBUILD SHELL anything"-      it "ok with `WORKDIR`" $ ruleCatchesNot noIllegalInstructionInOnbuild "ONBUILD WORKDIR anything"-      it "ok with `EXPOSE`" $ ruleCatchesNot noIllegalInstructionInOnbuild "ONBUILD EXPOSE 69"-      it "ok with `VOLUME`" $ ruleCatchesNot noIllegalInstructionInOnbuild "ONBUILD VOLUME anything"-      it "ok with `ENTRYPOINT`" $ ruleCatchesNot noIllegalInstructionInOnbuild "ONBUILD ENTRYPOINT anything"-      it "ok with `ENV`" $ ruleCatchesNot noIllegalInstructionInOnbuild "ONBUILD ENV MYVAR=\"bla\""-      it "ok with `ARG`" $ ruleCatchesNot noIllegalInstructionInOnbuild "ONBUILD ARG anything"-      it "ok with `HEALTHCHECK`" $ ruleCatchesNot noIllegalInstructionInOnbuild "ONBUILD HEALTHCHECK NONE"-      it "ok with `FROM` outside of `ONBUILD`" $ ruleCatchesNot noIllegalInstructionInOnbuild "FROM debian:buster"-      it "ok with `MAINTAINER` outside of `ONBUILD`" $ ruleCatchesNot noIllegalInstructionInOnbuild "MAINTAINER \"Some Guy\""-    ---    describe "Selfreferencing `ENV`s" $ do-      it "ok with normal ENV" $-        ruleCatchesNot noSelfreferencingEnv "ENV BLA=\"blubb\"\nENV BLUBB=\"${BLA}/blubb\""-      it "ok with partial match 1" $-        ruleCatchesNot noSelfreferencingEnv "ENV BLA=\"blubb\" BLUBB=\"${FOOBLA}/blubb\""-      it "ok with partial match 2" $-        ruleCatchesNot noSelfreferencingEnv "ENV BLA=\"blubb\" BLUBB=\"${BLAFOO}/blubb\""-      it "ok with partial match 3" $-        ruleCatchesNot noSelfreferencingEnv "ENV BLA=\"blubb\" BLUBB=\"$FOOBLA/blubb\""-      it "ok with partial match 4" $-        ruleCatchesNot noSelfreferencingEnv "ENV BLA=\"blubb\" BLUBB=\"$BLAFOO/blubb\""-      it "fail with partial match 5" $-        ruleCatches noSelfreferencingEnv "ENV BLA=\"blubb\" BLUBB=\"$BLA/$BLAFOO/blubb\""-      it "ok when previously defined in `ARG`" $-        ruleCatchesNot noSelfreferencingEnv "ARG BLA\nENV BLA=${BLA}"-      it "ok when previously defined in `ENV`" $-        ruleCatchesNot noSelfreferencingEnv "ENV BLA blubb\nENV BLA=${BLA}"-      it "ok with referencing a variable on its own right hand side" $-        ruleCatchesNot noSelfreferencingEnv "ENV PATH=/bla:${PATH}"-      it "ok with referencing a variable on its own right side twice in different `ENV`s" $-        ruleCatchesNot noSelfreferencingEnv "ENV PATH=/bla:${PATH}\nENV PATH=/blubb:${PATH}"-      it "fail when referencing a variable on its own right side twice within the same `ENV`" $-        ruleCatches noSelfreferencingEnv "ENV PATH=/bla:${PATH} PATH=/blubb:${PATH}"-      it "fail with selfreferencing with curly braces ENV" $-        ruleCatches noSelfreferencingEnv "ENV BLA=\"blubb\" BLUBB=\"${BLA}/blubb\""-      it "fail with selfreferencing without curly braces ENV" $-        ruleCatches noSelfreferencingEnv "ENV BLA=\"blubb\" BLUBB=\"$BLA/blubb\""-    ---    describe "Regression Tests" $ do-      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-      it "`ARG` can correctly unset variables" $-        let dockerFile =-              [ "ARG A_WITHOUT_EQ",-                "ARG A_WITH_EQ=",-                "RUN echo bla"-              ]-         in assertChecks-              shellcheck-              (Text.unlines dockerFile)-              (assertBool "No Warnings or Errors should be triggered" . null)--    -- 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 . formatChecksNoColor $ 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 . formatChecksNoColor $ 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 . formatChecksNoColor $ checks)--formatChecksNoColor :: Functor f => f RuleCheck -> f Text.Text-formatChecksNoColor checks = formatChecks checks False+{-# LANGUAGE ImplicitParams #-}+{-# LANGUAGE OverloadedLists #-}+{-# LANGUAGE OverloadedStrings #-}++import qualified ConfigSpec+import qualified Control.Foldl as Foldl+import Control.Monad (unless, when)+import qualified Data.Sequence as Seq+import qualified Data.Map as Map+import qualified Data.Text as Text+import Hadolint.Formatter.TTY (formatCheck, formatError)+import qualified Hadolint.Process+import Hadolint.Rule (CheckFailure (..), Failures, RuleCode (..))+import Hadolint.Rule as Rule+import Language.Docker.Parser+import Language.Docker.Syntax+import qualified ShellSpec+import Test.HUnit hiding (Label)+import Test.Hspec++main :: IO ()+main =+  hspec $ do+    let ?rulesConfig = mempty -- default implicit parameter running the checkers+    describe "FROM rules" $ do+      it "no untagged" $ ruleCatches "DL3006" "FROM debian"+      it "no untagged with name" $ ruleCatches "DL3006" "FROM debian AS builder"+      it "explicit latest" $ ruleCatches "DL3007" "FROM debian:latest"+      it "explicit latest with name" $ ruleCatches "DL3007" "FROM debian:latest AS builder"+      it "explicit tagged" $ ruleCatchesNot "DL3007" "FROM debian:jessie"+      it "explicit platform flag" $ ruleCatches "DL3029" "FROM --platform=linux debian:jessie"+      it "no platform flag" $ ruleCatchesNot "DL3029" "FROM debian:jessie"+      it "explicit SHA" $+        ruleCatchesNot+          "DL3007"+          "FROM hub.docker.io/debian@sha256:\+          \7959ed6f7e35f8b1aaa06d1d8259d4ee25aa85a086d5c125480c333183f9deeb"+      it "explicit tagged with name" $+        ruleCatchesNot "DL3007" "FROM debian:jessie AS builder"+      it "untagged digest is not an error" $+        ruleCatchesNot "DL3006" "FROM ruby@sha256:f1dbca0f5dbc9"+      it "untagged digest is not an error" $+        ruleCatchesNot "DL3006" "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 "DL3006" $ Text.unlines dockerFile+              onBuildRuleCatchesNot "DL3006" $ 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 "DL3006" $ Text.unlines dockerFile+              onBuildRuleCatches "DL3006" $ Text.unlines dockerFile+    --+    describe "no root or sudo rules" $ do+      it "sudo" $ do+        ruleCatches "DL3004" "RUN sudo apt-get update"+        onBuildRuleCatches "DL3004" "RUN sudo apt-get update"++      it "last user should not be root" $+        let dockerFile =+              [ "FROM scratch",+                "USER root"+              ]+         in ruleCatches "DL3002" $ Text.unlines dockerFile++      it "no root" $+        let dockerFile =+              [ "FROM scratch",+                "USER foo"+              ]+         in ruleCatchesNot "DL3002" $ Text.unlines dockerFile++      it "no root UID" $+        let dockerFile =+              [ "FROM scratch",+                "USER 0"+              ]+         in ruleCatches "DL3002" $ Text.unlines dockerFile++      it "no root:root" $+        let dockerFile =+              [ "FROM scratch",+                "USER root:root"+              ]+         in ruleCatches "DL3002" $ Text.unlines dockerFile++      it "no UID:GID" $+        let dockerFile =+              [ "FROM scratch",+                "USER 0:0"+              ]+         in ruleCatches "DL3002" $ Text.unlines dockerFile++      it "can switch back to non root" $+        let dockerFile =+              [ "FROM scratch",+                "USER root",+                "RUN something",+                "USER foo"+              ]+         in ruleCatchesNot "DL3002" $ 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 "DL3002" $ 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 "DL3002" $ 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 "DL3002" $ Text.unlines dockerFile++      it "install sudo" $ do+        ruleCatchesNot "DL3004" "RUN apt-get install sudo"+        onBuildRuleCatchesNot "DL3004" "RUN apt-get install sudo"+      it "sudo chained programs" $ do+        ruleCatches "DL3004" "RUN apt-get update && sudo apt-get install"+        onBuildRuleCatches "DL3004" "RUN apt-get update && sudo apt-get install"+    --+    describe "invalid CMD rules" $ do+      it "invalid cmd" $ do+        ruleCatches "DL3001" "RUN top"+        onBuildRuleCatches "DL3001" "RUN top"+      it "install ssh" $ do+        ruleCatchesNot "DL3001" "RUN apt-get install ssh"+        onBuildRuleCatchesNot "DL3001" "RUN apt-get install ssh"+    --+    describe "gem" $+      describe "version pinning" $ do+        describe "i" $ do+          it "unpinned" $ do+            ruleCatches "DL3028" "RUN gem i bundler"+            onBuildRuleCatches "DL3028" "RUN gem i bundler"+          it "pinned" $ do+            ruleCatchesNot "DL3028" "RUN gem i bundler:1"+            onBuildRuleCatchesNot "DL3028" "RUN gem i bundler:1"+          it "multi" $ do+            ruleCatches "DL3028" "RUN gem i bunlder:1 nokogiri"+            onBuildRuleCatches "DL3028" "RUN gem i bunlder:1 nokogiri"+            ruleCatchesNot "DL3028" "RUN gem i bunlder:1 nokogirii:1"+            onBuildRuleCatchesNot "DL3028" "RUN gem i bunlder:1 nokogiri:1"+        describe "install" $ do+          it "unpinned" $ do+            ruleCatches "DL3028" "RUN gem install bundler"+            onBuildRuleCatches "DL3028" "RUN gem install bundler"+          it "pinned" $ do+            ruleCatchesNot "DL3028" "RUN gem install bundler:1"+            onBuildRuleCatchesNot "DL3028" "RUN gem install bundler:1"+          it "does not warn on -v" $ do+            ruleCatchesNot "DL3028" "RUN gem install bundler -v '2.0.1'"+            onBuildRuleCatchesNot "DL3028" "RUN gem install bundler -v '2.0.1'"+          it "does not warn on --version without =" $ do+            ruleCatchesNot "DL3028" "RUN gem install bundler --version '2.0.1'"+            onBuildRuleCatchesNot "DL3028" "RUN gem install bundler --version '2.0.1'"+          it "does not warn on --version with =" $ do+            ruleCatchesNot "DL3028" "RUN gem install bundler --version='2.0.1'"+            onBuildRuleCatchesNot "DL3028" "RUN gem install bundler --version='2.0.1'"+          it "does not warn on extra flags" $ do+            ruleCatchesNot "DL3028" "RUN gem install bundler:2.0.1 -- --use-system-libraries=true"+            onBuildRuleCatchesNot "DL3028" "RUN gem install bundler:2.0.1 -- --use-system-libraries=true"+    --+    describe "yum rules" $ do+      it "yum update" $ do+        ruleCatches "DL3031" "RUN yum update"+        ruleCatchesNot "DL3031" "RUN yum install -y httpd-2.4.42 && yum clean all"+        ruleCatchesNot "DL3031" "RUN bash -c `# not even a yum command`"+        onBuildRuleCatches "DL3031" "RUN yum update"+        onBuildRuleCatchesNot "DL3031" "RUN yum install -y httpd-2.4.42 && yum clean all"+        onBuildRuleCatchesNot "DL3031" "RUN bash -c `# not even a yum command`"+      it "yum version pinning" $ do+        ruleCatches "DL3033" "RUN yum install -y tomcat && yum clean all"+        ruleCatchesNot "DL3033" "RUN yum install -y tomcat-9.2 && yum clean all"+        ruleCatchesNot "DL3033" "RUN bash -c `# not even a yum command`"+        onBuildRuleCatches "DL3033" "RUN yum install -y tomcat && yum clean all"+        onBuildRuleCatchesNot "DL3033" "RUN yum install -y tomcat-9.2 && yum clean all"+        onBuildRuleCatchesNot "DL3033" "RUN bash -c `# not even a yum command`"+      it "yum no clean all" $ do+        ruleCatches "DL3032" "RUN yum install -y mariadb-10.4"+        ruleCatchesNot "DL3032" "RUN yum install -y mariadb-10.4 && yum clean all"+        ruleCatchesNot "DL3032" "RUN bash -c `# not even a yum command`"+        onBuildRuleCatches "DL3032" "RUN yum install -y mariadb-10.4"+        onBuildRuleCatchesNot "DL3032" "RUN yum install -y mariadb-10.4 && yum clean all"+        onBuildRuleCatchesNot "DL3032" "RUN bash -c `# not even a yum command`"+      it "yum non-interactive" $ do+        ruleCatches "DL3030" "RUN yum install httpd-2.4.24 && yum clean all"+        ruleCatchesNot "DL3030" "RUN yum install -y httpd-2.4.24 && yum clean all"+        ruleCatchesNot "DL3030" "RUN bash -c `# not even a yum command`"+        onBuildRuleCatches "DL3030" "RUN yum install httpd-2.4.24 && yum clean all"+        onBuildRuleCatchesNot "DL3030" "RUN yum install -y httpd-2.4.24 && yum clean all"+        onBuildRuleCatchesNot "DL3030" "RUN bash -c `# not even a yum command`"+    --+    describe "zypper rules" $ do+      it "zupper update" $ do+        ruleCatches "DL3035" "RUN zypper update"+        ruleCatches "DL3035" "RUN zypper up"+        ruleCatches "DL3035" "RUN zypper dist-upgrade"+        ruleCatches "DL3035" "RUN zypper dup"+        onBuildRuleCatches "DL3035" "RUN zypper update"+        onBuildRuleCatches "DL3035" "RUN zypper up"+        onBuildRuleCatches "DL3035" "RUN zypper dist-upgrade"+        onBuildRuleCatches "DL3035" "RUN zypper dup"+      it "zypper version pinning" $ do+        -- NOTE: In Haskell strings, '\' has to be escaped. And in shell commands, '>'+        -- and '<' have to be escaped. Hence the double escaping.+        ruleCatches "DL3037" "RUN zypper install -y tomcat && zypper clean"+        ruleCatchesNot "DL3037" "RUN zypper install -y tomcat=9.0.39 && zypper clean"+        ruleCatchesNot "DL3037" "RUN zypper install -y tomcat\\>=9.0 && zypper clean"+        ruleCatchesNot "DL3037" "RUN zypper install -y tomcat\\>9.0 && zypper clean"+        ruleCatchesNot "DL3037" "RUN zypper install -y tomcat\\<=9.0 && zypper clean"+        ruleCatchesNot "DL3037" "RUN zypper install -y tomcat\\<9.0 && zypper clean"+        ruleCatchesNot "DL3037" "RUN zypper install -y tomcat-9.0.39-1.rpm && zypper clean"+        onBuildRuleCatches "DL3037" "RUN zypper install -y tomcat && zypper clean"+        onBuildRuleCatchesNot "DL3037" "RUN zypper install -y tomcat=9.0.39 && zypper clean"+        onBuildRuleCatchesNot "DL3037" "RUN zypper install -y tomcat\\>=9.0 && zypper clean"+        onBuildRuleCatchesNot "DL3037" "RUN zypper install -y tomcat\\>9.0 && zypper clean"+        onBuildRuleCatchesNot "DL3037" "RUN zypper install -y tomcat\\<=9.0 && zypper clean"+        onBuildRuleCatchesNot "DL3037" "RUN zypper install -y tomcat\\<9.0 && zypper clean"+        onBuildRuleCatchesNot "DL3037" "RUN zypper install -y tomcat-9.0.39-1.rpm && zypper clean"+      it "zypper no clean all" $ do+        ruleCatches "DL3036" "RUN zypper install -y mariadb=10.4"+        ruleCatchesNot "DL3036" "RUN zypper install -y mariadb=10.4 && zypper clean"+        ruleCatchesNot "DL3036" "RUN zypper install -y mariadb=10.4 && zypper cc"+        onBuildRuleCatches "DL3036" "RUN zypper install -y mariadb=10.4"+        onBuildRuleCatchesNot "DL3036" "RUN zypper install -y mariadb=10.4 && zypper clean"+        onBuildRuleCatchesNot "DL3036" "RUN zypper install -y mariadb=10.4 && zypper cc"+      it "zypper non-interactive" $ do+        ruleCatches "DL3034" "RUN zypper install httpd=2.4.24 && zypper clean"+        ruleCatchesNot "DL3034" "RUN zypper install -y httpd=2.4.24 && zypper clean"+        ruleCatchesNot "DL3034" "RUN zypper install --no-confirm httpd=2.4.24 && zypper clean"+        onBuildRuleCatches "DL3034" "RUN zypper install httpd=2.4.24 && zypper clean"+        onBuildRuleCatchesNot "DL3034" "RUN zypper install -y httpd=2.4.24 && zypper clean"+        onBuildRuleCatchesNot "DL3034" "RUN zypper install --no-confirm httpd=2.4.24 && zypper clean"+    --+    describe "dnf rules" $ do+      it "dnf update" $ do+        ruleCatches "DL3039" "RUN dnf upgrade"+        onBuildRuleCatches "DL3039" "RUN dnf upgrade"+      it "dnf version pinning" $ do+        ruleCatches "DL3041" "RUN dnf install -y tomcat && dnf clean all"+        ruleCatchesNot "DL3041" "RUN dnf install -y tomcat-9.0.1 && dnf clean all"+        onBuildRuleCatches "DL3041" "RUN dnf install -y tomcat && dnf clean all"+        onBuildRuleCatchesNot "DL3041" "RUN dnf install -y tomcat-9.0.1 && dnf clean all"+      it "dnf no clean all" $ do+        ruleCatches "DL3040" "RUN dnf install -y mariadb-10.4"+        ruleCatchesNot "DL3040" "RUN dnf install -y mariadb-10.4 && dnf clean all"+        onBuildRuleCatches "DL3040" "RUN dnf install -y mariadb-10.4"+        onBuildRuleCatchesNot "DL3040" "RUN dnf install -y mariadb-10.4 && dnf clean all"+      it "dnf non-interactive" $ do+        ruleCatches "DL3038" "RUN dnf install httpd-2.4.24 && dnf clean all"+        ruleCatchesNot "DL3038" "RUN dnf install -y httpd-2.4.24 && dnf clean all"+        onBuildRuleCatches "DL3038" "RUN dnf install httpd-2.4.24 && dnf clean all"+        onBuildRuleCatchesNot "DL3038" "RUN dnf install -y httpd-2.4.24 && dnf clean all"+    --+    describe "dnf rules" $ do+      it "dnf update" $ do+        ruleCatches "DL3039" "RUN dnf upgrade"+        ruleCatches "DL3039" "RUN dnf upgrade-minimal"+        ruleCatchesNot "DL3039" "RUN notdnf upgrade"+        onBuildRuleCatches "DL3039" "RUN dnf upgrade"+        onBuildRuleCatches "DL3039" "RUN dnf upgrade-minimal"+        onBuildRuleCatchesNot "DL3039" "RUN notdnf upgrade"+      it "dnf version pinning" $ do+        ruleCatches "DL3041" "RUN dnf install -y tomcat && dnf clean all"+        ruleCatchesNot "DL3041" "RUN dnf install -y tomcat-9.0.1 && dnf clean all"+        ruleCatchesNot "DL3041" "RUN notdnf install tomcat"+        onBuildRuleCatches "DL3041" "RUN dnf install -y tomcat && dnf clean all"+        onBuildRuleCatchesNot "DL3041" "RUN dnf install -y tomcat-9.0.1 && dnf clean all"+        onBuildRuleCatchesNot "DL3041" "RUN notdnf install tomcat"+      it "dnf no clean all" $ do+        ruleCatches "DL3040" "RUN dnf install -y mariadb-10.4"+        ruleCatchesNot "DL3040" "RUN dnf install -y mariadb-10.4 && dnf clean all"+        ruleCatchesNot "DL3040" "RUN notdnf install mariadb"+        onBuildRuleCatches "DL3040" "RUN dnf install -y mariadb-10.4"+        onBuildRuleCatchesNot "DL3040" "RUN dnf install -y mariadb-10.4 && dnf clean all"+        onBuildRuleCatchesNot "DL3040" "RUN notdnf install mariadb"+      it "dnf non-interactive" $ do+        ruleCatches "DL3038" "RUN dnf install httpd-2.4.24 && dnf clean all"+        ruleCatchesNot "DL3038" "RUN dnf install -y httpd-2.4.24 && dnf clean all"+        ruleCatchesNot "DL3038" "RUN notdnf install httpd"+        onBuildRuleCatches "DL3038" "RUN dnf install httpd-2.4.24 && dnf clean all"+        onBuildRuleCatchesNot "DL3038" "RUN dnf install -y httpd-2.4.24 && dnf clean all"+        onBuildRuleCatchesNot "DL3038" "RUN notdnf install httpd"+    --+    describe "apt-get rules" $ do+      it "apt" $+        let dockerFile =+              [ "FROM ubuntu",+                "RUN apt install python"+              ]+         in do+              ruleCatches "DL3027" $ Text.unlines dockerFile+              onBuildRuleCatches "DL3027" $ Text.unlines dockerFile+      it "apt-get upgrade" $ do+        ruleCatches "DL3005" "RUN apt-get update && apt-get upgrade"+        onBuildRuleCatches "DL3005" "RUN apt-get update && apt-get upgrade"+      it "apt-get dist-upgrade" $ do+        ruleCatches "DL3005" "RUN apt-get update && apt-get dist-upgrade"+        onBuildRuleCatches "DL3005" "RUN apt-get update && apt-get dist-upgrade"+      it "apt-get version pinning" $ do+        ruleCatches "DL3008" "RUN apt-get update && apt-get install python"+        onBuildRuleCatches "DL3008" "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 "DL3009" $ Text.unlines dockerFile+              onBuildRuleCatches "DL3009" $ 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 "DL3009" $ Text.unlines dockerFile+              onBuildRuleCatches "DL3009" $ 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 "DL3009" $ Text.unlines dockerFile+              onBuildRuleCatches "DL3009" $ 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 "DL3009" $ Text.unlines dockerFile+              onBuildRuleCatches "DL3009" $ 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 "DL3009" $ Text.unlines dockerFile+              onBuildRuleCatchesNot "DL3009" $ 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 "DL3009" $ Text.unlines dockerFile+              onBuildRuleCatches "DL3009" $ 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 "DL3009" $ Text.unlines dockerFile+              onBuildRuleCatchesNot "DL3009" $ 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 "DL3008" $ Text.unlines dockerFile+              onBuildRuleCatchesNot "DL3008" $ 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 "DL3008" $ Text.unlines dockerFile+              onBuildRuleCatchesNot "DL3008" $ Text.unlines dockerFile++      it "has deprecated maintainer" $+        ruleCatches "DL4000" "FROM busybox\nMAINTAINER hudu@mail.com"+    --+    describe "apk add rules" $ do+      it "apk upgrade" $ do+        ruleCatches "DL3017" "RUN apk update && apk upgrade"+        onBuildRuleCatches "DL3017" "RUN apk update && apk upgrade"+      it "apk add version pinning single" $ do+        ruleCatches "DL3018" "RUN apk add flex"+        onBuildRuleCatches "DL3018" "RUN apk add flex"+      it "apk add no version pinning single" $ do+        ruleCatchesNot "DL3018" "RUN apk add flex=2.6.4-r1"+        onBuildRuleCatchesNot "DL3018" "RUN apk add flex=2.6.4-r1"+      it "apk add version pinned chained" $+        let dockerFile =+              [ "RUN apk add --no-cache flex=2.6.4-r1 \\",+                " && pip install -r requirements.txt"+              ]+         in do+              ruleCatchesNot "DL3018" $ Text.unlines dockerFile+              onBuildRuleCatchesNot "DL3018" $ Text.unlines dockerFile+      it "apk add version pinned regression" $+        let dockerFile =+              [ "RUN apk add --no-cache \\",+                "flex=2.6.4-r1 \\",+                "libffi=3.2.1-r3 \\",+                "python2=2.7.13-r1 \\",+                "libbz2=1.0.6-r5"+              ]+         in do+              ruleCatchesNot "DL3018" $ Text.unlines dockerFile+              onBuildRuleCatchesNot "DL3018" $ 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 "DL3018" $ Text.unlines dockerFile+              onBuildRuleCatches "DL3018" $ Text.unlines dockerFile+      it "apk add with --no-cache" $ do+        ruleCatches "DL3019" "RUN apk add flex=2.6.4-r1"+        onBuildRuleCatches "DL3019" "RUN apk add flex=2.6.4-r1"+      it "apk add without --no-cache" $ do+        ruleCatchesNot "DL3019" "RUN apk add --no-cache flex=2.6.4-r1"+        onBuildRuleCatchesNot "DL3019" "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 "DL3018" $ Text.unlines dockerFile+              onBuildRuleCatchesNot "DL3018" $ 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 "DL3018" $ Text.unlines dockerFile+              onBuildRuleCatchesNot "DL3018" $ 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 "DL3018" $ Text.unlines dockerFile+              onBuildRuleCatchesNot "DL3018" $ Text.unlines dockerFile+      it "apk add with repository (-X) without equal sign" $+        let dockerFile =+              [ "RUN apk add --no-cache \\",+                "-X https://nl.alpinelinux.org/alpine/edge/testing \\",+                "flow=0.78.0-r0"+              ]+         in do+              ruleCatchesNot "DL3018" $ Text.unlines dockerFile+              onBuildRuleCatchesNot "DL3018" $ Text.unlines dockerFile+    --+    describe "EXPOSE rules" $ do+      it "invalid port" $ ruleCatches "DL3011" "EXPOSE 80000"+      it "valid port" $ ruleCatchesNot "DL3011" "EXPOSE 60000"+    --+    describe "pip pinning" $ do+      it "pip2 version not pinned" $ do+        ruleCatches "DL3013" "RUN pip2 install MySQL_python"+        onBuildRuleCatches "DL3013" "RUN pip2 install MySQL_python"+      it "pip3 version not pinned" $ do+        ruleCatches "DL3013" "RUN pip3 install MySQL_python"+        onBuildRuleCatches "DL3013" "RUN pip2 install MySQL_python"+      it "pip3 version pinned" $ do+        ruleCatchesNot "DL3013" "RUN pip3 install MySQL_python==1.2.2"+        onBuildRuleCatchesNot "DL3013" "RUN pip3 install MySQL_python==1.2.2"+      it "pip3 install from local package" $ do+        ruleCatchesNot "DL3013" "RUN pip3 install mypkg.whl"+        ruleCatchesNot "DL3013" "RUN pip3 install mypkg.tar.gz"+        onBuildRuleCatchesNot "DL3013" "RUN pip3 install mypkg.whl"+        onBuildRuleCatchesNot "DL3013" "RUN pip3 install mypkg.tar.gz"+      it "pip install requirements" $ do+        ruleCatchesNot "DL3013" "RUN pip install -r requirements.txt"+        onBuildRuleCatchesNot "DL3013" "RUN pip install -r requirements.txt"+      it "pip install requirements with long flag" $ do+        ruleCatchesNot "DL3013" "RUN pip install --requirement requirements.txt"+        onBuildRuleCatchesNot "DL3013" "RUN pip install --requirement requirements.txt"+      it "pip install use setup.py" $ do+        ruleCatchesNot "DL3013" "RUN pip install ."+        onBuildRuleCatchesNot "DL3013" "RUN pip install ."+      it "pip version not pinned" $ do+        ruleCatches "DL3013" "RUN pip install MySQL_python"+        onBuildRuleCatches "DL3013" "RUN pip install MySQL_python"+      it "pip version pinned" $ do+        ruleCatchesNot "DL3013" "RUN pip install MySQL_python==1.2.2"+        onBuildRuleCatchesNot "DL3013" "RUN pip install MySQL_python==1.2.2"+      it "pip version pinned with ~= operator" $ do+        ruleCatchesNot "DL3013" "RUN pip install MySQL_python~=1.2.2"+        onBuildRuleCatchesNot "DL3013" "RUN pip install MySQL_python~=1.2.2"+      it "pip version pinned with === operator" $ do+        ruleCatchesNot "DL3013" "RUN pip install MySQL_python===1.2.2"+        onBuildRuleCatchesNot "DL3013" "RUN pip install MySQL_python===1.2.2"+      it "pip version pinned with flag --ignore-installed" $ do+        ruleCatchesNot "DL3013" "RUN pip install --ignore-installed MySQL_python==1.2.2"+        onBuildRuleCatchesNot "DL3013" "RUN pip install --ignore-installed MySQL_python==1.2.2"+      it "pip version pinned with flag --build" $ do+        ruleCatchesNot "DL3013" "RUN pip3 install --build /opt/yamllint yamllint==1.20.0"+        onBuildRuleCatchesNot "DL3013" "RUN pip3 install --build /opt/yamllint yamllint==1.20.0"+      it "pip version pinned with flag --prefix" $ do+        ruleCatchesNot "DL3013" "RUN pip3 install --prefix /opt/yamllint yamllint==1.20.0"+        onBuildRuleCatchesNot "DL3013" "RUN pip3 install --prefix /opt/yamllint yamllint==1.20.0"+      it "pip version pinned with flag --root" $ do+        ruleCatchesNot "DL3013" "RUN pip3 install --root /opt/yamllint yamllint==1.20.0"+        onBuildRuleCatchesNot "DL3013" "RUN pip3 install --root /opt/yamllint yamllint==1.20.0"+      it "pip version pinned with flag --target" $ do+        ruleCatchesNot "DL3013" "RUN pip3 install --target /opt/yamllint yamllint==1.20.0"+        onBuildRuleCatchesNot "DL3013" "RUN pip3 install --target /opt/yamllint yamllint==1.20.0"+      it "pip version pinned with flag --trusted-host" $ do+        ruleCatchesNot "DL3013" "RUN pip3 install --trusted-host host example==1.2.2"+        onBuildRuleCatchesNot "DL3013" "RUN pip3 install --trusted-host host example==1.2.2"+      it "pip version pinned with python -m" $ do+        ruleCatchesNot "DL3013" "RUN python -m pip install example==1.2.2"+        onBuildRuleCatchesNot "DL3013" "RUN python -m pip install example==1.2.2"+      it "pip version not pinned with python -m" $ do+        ruleCatches "DL3013" "RUN python -m pip install example"+        onBuildRuleCatches "DL3013" "RUN python -m pip install --index-url url example"+      it "pip install git" $ do+        ruleCatchesNot+          "DL3013"+          "RUN pip install git+https://github.com/rtfd/r-ext.git@0.6-alpha#egg=r-ext"+        onBuildRuleCatchesNot+          "DL3013"+          "RUN pip install git+https://github.com/rtfd/r-ext.git@0.6-alpha#egg=r-ext"+      it "pip install unversioned git" $ do+        ruleCatches+          "DL3013"+          "RUN pip install git+https://github.com/rtfd/read-ext.git#egg=read-ext"+        onBuildRuleCatches+          "DL3013"+          "RUN pip install git+https://github.com/rtfd/read-ext.git#egg=read-ext"+      it "pip install upper bound" $ do+        ruleCatchesNot "DL3013" "RUN pip install 'alabaster>=0.7'"+        onBuildRuleCatchesNot "DL3013" "RUN pip install 'alabaster>=0.7'"+      it "pip install lower bound" $ do+        ruleCatchesNot "DL3013" "RUN pip install 'alabaster<0.7'"+        onBuildRuleCatchesNot "DL3013" "RUN pip install 'alabaster<0.7'"+      it "pip install excluded version" $ do+        ruleCatchesNot "DL3013" "RUN pip install 'alabaster!=0.7'"+        onBuildRuleCatchesNot "DL3013" "RUN pip install 'alabaster!=0.7'"+      it "pip install user directory" $ do+        ruleCatchesNot "DL3013" "RUN pip install MySQL_python==1.2.2 --user"+        onBuildRuleCatchesNot "DL3013" "RUN pip install MySQL_python==1.2.2 --user"+      it "pip install no pip version check" $ do+        ruleCatchesNot+          "DL3013"+          "RUN pip install MySQL_python==1.2.2 --disable-pip-version-check"+        onBuildRuleCatchesNot+          "DL3013"+          "RUN pip install MySQL_python==1.2.2 --disable-pip-version-check"+      it "pip install --index-url" $ do+        ruleCatchesNot+          "DL3013"+          "RUN pip install --index-url https://eg.com/foo foobar==1.0.0"+        onBuildRuleCatchesNot+          "DL3013"+          "RUN pip install --index-url https://eg.com/foo foobar==1.0.0"+      it "pip install index-url with -i flag" $ do+        ruleCatchesNot+          "DL3013"+          "RUN pip install --index-url https://eg.com/foo foobar==1.0.0"+        onBuildRuleCatchesNot+          "DL3013"+          "RUN pip install --index-url https://eg.com/foo foobar==1.0.0"+      it "pip install --index-url with --extra-index-url" $ do+        ruleCatchesNot+          "DL3013"+          "RUN pip install --index-url https://eg.com/foo --extra-index-url https://ex-eg.io/foo foobar==1.0.0"+        onBuildRuleCatchesNot+          "DL3013"+          "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 "DL3013" "RUN pip install MySQL_python==1.2.2 --no-cache-dir"+        onBuildRuleCatchesNot "DL3013" "RUN pip install MySQL_python==1.2.2 --no-cache-dir"+      it "pip install constraints file - long version argument" $ do+        ruleCatchesNot "DL3013" "RUN pip install pykafka --constraint http://foo.bar.baz"+        onBuildRuleCatchesNot "DL3013" "RUN pip install pykafka --constraint http://foo.bar.baz"+      it "pip install constraints file - short version argument" $ do+        ruleCatchesNot "DL3013" "RUN pip install pykafka -c http://foo.bar.baz"+        onBuildRuleCatchesNot "DL3013" "RUN pip install pykafka -c http://foo.bar.baz"+      it "pip install --index-url with --extra-index-url with basic auth" $ do+        ruleCatchesNot+          "DL3013"+          "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+          "DL3013"+          "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 "pip cache dir" $ do+      it "pip2 --no-cache-dir not used" $ do+        ruleCatches "DL3042" "RUN pip2 install MySQL_python"+        onBuildRuleCatches "DL3042" "RUN pip2 install MySQL_python"+      it "pip3 --no-cache-dir not used" $ do+        ruleCatches "DL3042" "RUN pip3 install MySQL_python"+        onBuildRuleCatches "DL3042" "RUN pip3 install MySQL_python"+      it "pip --no-cache-dir not used" $ do+        ruleCatches "DL3042" "RUN pip install MySQL_python"+        onBuildRuleCatches "DL3042" "RUN pip install MySQL_python"+      it "pip2 --no-cache-dir used" $ do+        ruleCatchesNot "DL3042" "RUN pip2 install MySQL_python --no-cache-dir"+        onBuildRuleCatchesNot "DL3042" "RUN pip2 install MySQL_python --no-cache-dir"+      it "pip3 --no-cache-dir used" $ do+        ruleCatchesNot "DL3042" "RUN pip3 install --no-cache-dir MySQL_python"+        onBuildRuleCatchesNot "DL3042" "RUN pip3 install --no-cache-dir MySQL_python"+      it "pip --no-cache-dir used" $ do+        ruleCatchesNot "DL3042" "RUN pip install MySQL_python --no-cache-dir"+        onBuildRuleCatchesNot "DL3042" "RUN pip install MySQL_python --no-cache-dir"+      it "don't match on pipx" $ do+        ruleCatchesNot "DL3042" "RUN pipx install software"+        onBuildRuleCatchesNot "DL3042" "Run pipx install software"+      it "don't match on pipenv" $ do+        ruleCatchesNot "DL3042" "RUN pipenv install library"+        onBuildRuleCatchesNot "DL3042" "RUN pipenv install library"+    --+    describe "npm pinning" $ do+      it "version pinned in package.json" $ do+        ruleCatchesNot "DL3016" "RUN npm install"+        onBuildRuleCatchesNot "DL3016" "RUN npm install"+      it "version pinned in package.json with arguments" $ do+        ruleCatchesNot "DL3016" "RUN npm install --progress=false"+        onBuildRuleCatchesNot "DL3016" "RUN npm install --progress=false"+      it "version pinned" $ do+        ruleCatchesNot "DL3016" "RUN npm install express@4.1.1"+        onBuildRuleCatchesNot "DL3016" "RUN npm install express@4.1.1"+      it "version pinned with scope" $ do+        ruleCatchesNot "DL3016" "RUN npm install @myorg/privatepackage@\">=0.1.0\""+        onBuildRuleCatchesNot "DL3016" "RUN npm install @myorg/privatepackage@\">=0.1.0\""+      it "version pinned multiple packages" $ do+        ruleCatchesNot "DL3016" "RUN npm install express@\"4.1.1\" sax@0.1.1"+        onBuildRuleCatchesNot "DL3016" "RUN npm install express@\"4.1.1\" sax@0.1.1"+      it "version pinned with --global" $ do+        ruleCatchesNot "DL3016" "RUN npm install --global express@\"4.1.1\""+        onBuildRuleCatchesNot "DL3016" "RUN npm install --global express@\"4.1.1\""+      it "version pinned with -g" $ do+        ruleCatchesNot "DL3016" "RUN npm install -g express@\"4.1.1\""+        onBuildRuleCatchesNot "DL3016" "RUN npm install -g express@\"4.1.1\""+      it "version does not have to be pinned for tarball suffix .tar" $ do+        ruleCatchesNot "DL3016" "RUN npm install package-v1.2.3.tar"+        onBuildRuleCatchesNot "DL3016" "RUN npm install package-v1.2.3.tar"+      it "version does not have to be pinned for tarball suffix .tar.gz" $ do+        ruleCatchesNot "DL3016" "RUN npm install package-v1.2.3.tar.gz"+        onBuildRuleCatchesNot "DL3016" "RUN npm install package-v1.2.3.tar.gz"+      it "version does not have to be pinned for tarball suffix .tgz" $ do+        ruleCatchesNot "DL3016" "RUN npm install package-v1.2.3.tgz"+        onBuildRuleCatchesNot "DL3016" "RUN npm install package-v1.2.3.tgz"+      it "version does not have to be pinned for folder - absolute path" $ do+        ruleCatchesNot "DL3016" "RUN npm install /folder"+        onBuildRuleCatchesNot "DL3016" "RUN npm install /folder"+      it "version does not have to be pinned for folder - relative path from current folder" $ do+        ruleCatchesNot "DL3016" "RUN npm install ./folder"+        onBuildRuleCatchesNot "DL3016" "RUN npm install ./folder"+      it "version does not have to be pinned for folder - relative path to parent folder" $ do+        ruleCatchesNot "DL3016" "RUN npm install ../folder"+        onBuildRuleCatchesNot "DL3016" "RUN npm install ../folder"+      it "version does not have to be pinned for folder - relative path from home" $ do+        ruleCatchesNot "DL3016" "RUN npm install ~/folder"+        onBuildRuleCatchesNot "DL3016" "RUN npm install ~/folder"+      it "commit pinned for git+ssh" $ do+        ruleCatchesNot+          "DL3016"+          "RUN npm install git+ssh://git@github.com:npm/npm.git#v1.0.27"+        onBuildRuleCatchesNot+          "DL3016"+          "RUN npm install git+ssh://git@github.com:npm/npm.git#v1.0.27"+      it "commit pinned for git+http" $ do+        ruleCatchesNot+          "DL3016"+          "RUN npm install git+http://isaacs@github.com/npm/npm#semver:^5.0"+        onBuildRuleCatchesNot+          "DL3016"+          "RUN npm install git+http://isaacs@github.com/npm/npm#semver:^5.0"+      it "commit pinned for git+https" $ do+        ruleCatchesNot+          "DL3016"+          "RUN npm install git+https://isaacs@github.com/npm/npm.git#v1.0.27"+        onBuildRuleCatchesNot+          "DL3016"+          "RUN npm install git+https://isaacs@github.com/npm/npm.git#v1.0.27"+      it "commit pinned for git" $ do+        ruleCatchesNot+          "DL3016"+          "RUN npm install git://github.com/npm/npm.git#v1.0.27"+        onBuildRuleCatchesNot+          "DL3016"+          "RUN npm install git://github.com/npm/npm.git#v1.0.27"+      it "npm run install is fine" $ do+        ruleCatchesNot+          "DL3016"+          "RUN npm run --crazy install"+        onBuildRuleCatchesNot+          "DL3016"+          "RUN npm run --crazy install"++      --version range is not supported+      it "version pinned with scope" $ do+        ruleCatchesNot "DL3016" "RUN npm install @myorg/privatepackage@\">=0.1.0 <0.2.0\""+        onBuildRuleCatchesNot "DL3016" "RUN npm install @myorg/privatepackage@\">=0.1.0 <0.2.0\""+      it "version not pinned" $ do+        ruleCatches "DL3016" "RUN npm install express"+        onBuildRuleCatches "DL3016" "RUN npm install express"+      it "version not pinned with scope" $ do+        ruleCatches "DL3016" "RUN npm install @myorg/privatepackage"+        onBuildRuleCatches "DL3016" "RUN npm install @myorg/privatepackage"+      it "version not pinned multiple packages" $ do+        ruleCatches "DL3016" "RUN npm install express sax@0.1.1"+        onBuildRuleCatches "DL3016" "RUN npm install express sax@0.1.1"+      it "version not pinned with --global" $ do+        ruleCatches "DL3016" "RUN npm install --global express"+        onBuildRuleCatches "DL3016" "RUN npm install --global express"+      it "commit not pinned for git+ssh" $ do+        ruleCatches "DL3016" "RUN npm install git+ssh://git@github.com:npm/npm.git"+        onBuildRuleCatches "DL3016" "RUN npm install git+ssh://git@github.com:npm/npm.git"+      it "commit not pinned for git+http" $ do+        ruleCatches "DL3016" "RUN npm install git+http://isaacs@github.com/npm/npm"+        onBuildRuleCatches "DL3016" "RUN npm install git+http://isaacs@github.com/npm/npm"+      it "commit not pinned for git+https" $ do+        ruleCatches+          "DL3016"+          "RUN npm install git+https://isaacs@github.com/npm/npm.git"+        onBuildRuleCatches+          "DL3016"+          "RUN npm install git+https://isaacs@github.com/npm/npm.git"+      it "commit not pinned for git" $ do+        ruleCatches "DL3016" "RUN npm install git://github.com/npm/npm.git"+        onBuildRuleCatches "DL3016" "RUN npm install git://github.com/npm/npm.git"+    --+    describe "use SHELL" $ do+      it "RUN ln" $ do+        ruleCatches "DL4005" "RUN ln -sfv /bin/bash /bin/sh"+        onBuildRuleCatches "DL4005" "RUN ln -sfv /bin/bash /bin/sh"+      it "RUN ln with unrelated symlinks" $ do+        ruleCatchesNot "DL4005" "RUN ln -sf /bin/true /sbin/initctl"+        onBuildRuleCatchesNot "DL4005" "RUN ln -sf /bin/true /sbin/initctl"+      it "RUN ln with multiple acceptable commands" $ do+        ruleCatchesNot "DL4005" "RUN ln -s foo bar && unrelated && something_with /bin/sh"+        onBuildRuleCatchesNot "DL4005" "RUN ln -s foo bar && unrelated && something_with /bin/sh"+    --+    --+    describe "Shellcheck" $ do+      it "runs shellchek on RUN instructions" $ do+        assertChecks "RUN echo $MISSING_QUOTES" failsShellcheck+        assertOnBuildChecks "RUN echo $MISSING_QUOTES" failsShellcheck+      it "not warns on valid scripts" $ do+        assertChecks "RUN echo foo" passesShellcheck+        assertOnBuildChecks "RUN echo foo" passesShellcheck++      it "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+              assertChecks dockerFile passesShellcheck+              assertOnBuildChecks dockerFile passesShellcheck++      it "Complain on missing env vars" $+        let dockerFile =+              Text.unlines+                [ "RUN echo \"$RTTP_PROXY\""+                ]+         in do+              assertChecks dockerFile failsShellcheck+              assertOnBuildChecks dockerFile failsShellcheck++      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+              assertChecks dockerFile passesShellcheck+              assertOnBuildChecks dockerFile failsShellcheck++      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+              assertChecks dockerFile failsShellcheck+              assertOnBuildChecks dockerFile failsShellcheck++      it "Defaults the shell to sh" $+        let dockerFile =+              Text.unlines+                [ "RUN echo $RANDOM"+                ]+         in do+              assertChecks dockerFile failsShellcheck+              assertOnBuildChecks dockerFile failsShellcheck++      it "Can change the shell check to bash" $+        let dockerFile =+              Text.unlines+                [ "SHELL [\"/bin/bash\", \"-eo\", \"pipefail\", \"-c\"]",+                  "RUN echo $RANDOM"+                ]+         in do+              assertChecks dockerFile passesShellcheck+              -- This is debatable, as it should actaully pass, but detecting it correctly+              -- is quite difficult+              assertOnBuildChecks dockerFile failsShellcheck+      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+              assertChecks dockerFile failsShellcheck+              assertOnBuildChecks dockerFile failsShellcheck++      it "Does not complain on ash shell" $+        let dockerFile =+              Text.unlines+                [ "SHELL [\"/bin/ash\", \"-o\", \"pipefail\", \"-c\"]",+                  "RUN echo hello"+                ]+         in do+              assertChecks dockerFile passesShellcheck+              assertOnBuildChecks dockerFile passesShellcheck++      it "Does not complain on powershell" $+        let dockerFile =+              Text.unlines+                [ "SHELL [\"pwsh\", \"-c\"]",+                  "RUN Get-Variable PSVersionTable | Select-Object -ExpandProperty Value"+                ]+         in do+              assertChecks dockerFile passesShellcheck+              assertOnBuildChecks dockerFile passesShellcheck+    --+    --+    describe "COPY rules" $ do+      it "use add" $ ruleCatches "DL3010" "COPY packaged-app.tar /usr/src/app"+      it "use not add" $ ruleCatchesNot "DL3010" "COPY package.json /usr/src/app"+    --+    describe "`COPY` without `WORKDIR` set" $ do+      it "ok: `COPY` with absolute destination and no `WORKDIR` set" $ do+        ruleCatchesNot "DL3045" "COPY bla.sh /usr/local/bin/blubb.sh"+        onBuildRuleCatchesNot "DL3045" "COPY bla.sh /usr/local/bin/blubb.sh"+      it "ok: `COPY` with absolute destination and no `WORKDIR` set with quotes" $ do+        ruleCatchesNot "DL3045" "COPY bla.sh \"/usr/local/bin/blubb.s\""+        onBuildRuleCatchesNot "DL3045" "COPY bla.sh \"/usr/local/bin/blubb.sh\""+      it "ok: `COPY` with absolute destination and no `WORKDIR` set - windows" $ do+        ruleCatchesNot "DL3045" "COPY bla.sh c:\\system32\\blubb.sh"+        onBuildRuleCatchesNot "DL3045" "COPY bla.sh d:\\mypath\\blubb.sh"+      it "ok: `COPY` with absolute destination and no `WORKDIR` set - windows with quotes" $ do+        ruleCatchesNot "DL3045" "COPY bla.sh \"c:\\system32\\blubb.sh\""+        onBuildRuleCatchesNot "DL3045" "COPY bla.sh \"d:\\mypath\\blubb.sh\""+      it "ok: `COPY` with relative destination and `WORKDIR` set" $ do+        ruleCatchesNot "DL3045" "FROM scratch\nWORKDIR /usr\nCOPY bla.sh blubb.sh"+        onBuildRuleCatchesNot "FROM scratch\nDL3045" "WORKDIR /usr\nCOPY bla.sh blubb.sh"+      it "ok: `COPY` with relative destination and `WORKDIR` set - windows" $ do+        ruleCatchesNot "DL3045" "FROM scratch\nWORKDIR c:\\system32\nCOPY bla.sh blubb.sh"+        onBuildRuleCatchesNot "DL3045" "FROM scratch\nWORKDIR c:\\system32\nCOPY bla.sh blubb.sh"+      it "ok: `COPY` with destination being an environment variable 1" $ do+        ruleCatchesNot "DL3045" "COPY src.sh ${SRC_BASE_ENV}"+        onBuildRuleCatchesNot "DL3045" "COPY src.sh ${SRC_BASE_ENV}"+      it "ok: `COPY` with destination being an environment variable 2" $ do+        ruleCatchesNot "DL3045" "COPY src.sh $SRC_BASE_ENV"+        onBuildRuleCatchesNot "DL3045" "COPY src.sh $SRC_BASE_ENV"+      it "ok: `COPY` with destination being an environment variable 3" $ do+        ruleCatchesNot "DL3045" "COPY src.sh \"${SRC_BASE_ENV}\""+        onBuildRuleCatchesNot "DL3045" "COPY src.sh \"${SRC_BASE_ENV}\""+      it "ok: `COPY` with destination being an environment variable 4" $ do+        ruleCatchesNot "DL3045" "COPY src.sh \"$SRC_BASE_ENV\""+        onBuildRuleCatchesNot "DL3045" "COPY src.sh \"$SRC_BASE_ENV\""+      it "not ok: `COPY` with relative destination and no `WORKDIR` set" $ do+        ruleCatches "DL3045" "COPY bla.sh blubb.sh"+        onBuildRuleCatches "DL3045" "COPY bla.sh blubb.sh"+      it "not ok: `COPY` with relative destination and no `WORKDIR` set with quotes" $ do+        ruleCatches "DL3045" "COPY bla.sh \"blubb.sh\""+        onBuildRuleCatches "DL3045" "COPY bla.sh \"blubb.sh\""+      it "not ok: `COPY` to relative destination if `WORKDIR` is set in a previous stage but not inherited" $+        let dockerFile =+              Text.unlines+                [ "FROM debian:buster as stage1",+                  "WORKDIR /usr",+                  "FROM debian:buster",+                  "COPY foo bar"+                ]+         in do+              ruleCatches "DL3045" dockerFile+              onBuildRuleCatches "DL3045" dockerFile+      it "not ok: `COPY` to relative destination if `WORKDIR` is set in a previous stage but not inherited - windows" $+        let dockerFile =+              Text.unlines+                [ "FROM microsoft/windowsservercore as stage1",+                  "WORKDIR c:\\system32",+                  "FROM microsoft/windowsservercore",+                  "COPY foo bar"+                ]+         in do+              ruleCatches "DL3045" dockerFile+              onBuildRuleCatches "DL3045" dockerFile+      it "ok: `COPY` to relative destination if `WORKDIR` has been set in base image" $+        let dockerFile =+              Text.unlines+                [ "FROM debian:buster as base",+                  "WORKDIR /usr",+                  "FROM debian:buster as stage-inbetween",+                  "RUN foo",+                  "FROM base",+                  "COPY foo bar"+                ]+         in do+              ruleCatchesNot "DL3045" dockerFile+              onBuildRuleCatchesNot "DL3045" dockerFile+      it "ok: `COPY` to relative destination if `WORKDIR` has been set in base image - windows" $+        let dockerFile =+              Text.unlines+                [ "FROM microsoft/windowsservercore as base",+                  "WORKDIR c:\\system32",+                  "FROM microsoft/windowsservercore as stage-inbetween",+                  "RUN foo",+                  "FROM base",+                  "COPY foo bar"+                ]+         in do+              ruleCatchesNot "DL3045" dockerFile+              onBuildRuleCatchesNot "DL3045" dockerFile+      it "ok: `COPY` to relative destination if `WORKDIR` has been set in previous stage, deep case" $+        let dockerFile =+              Text.unlines+                [ "FROM debian:buster as base1",+                  "WORKDIR /usr",+                  "FROM base1 as base2",+                  "RUN foo",+                  "FROM base2",+                  "COPY foo bar"+                ]+         in do+              ruleCatchesNot "DL3045" dockerFile+              onBuildRuleCatchesNot "DL3045" dockerFile+      it "ok: `COPY` to relative destination if `WORKDIR` has been set in previous stage, deep case - windows" $+        let dockerFile =+              Text.unlines+                [ "FROM microsoft/windowsservercore as base1",+                  "WORKDIR c:\\system32",+                  "FROM base1 as base2",+                  "RUN foo",+                  "FROM base2",+                  "COPY foo bar"+                ]+         in do+              ruleCatchesNot "DL3045" dockerFile+              onBuildRuleCatchesNot "DL3045" dockerFile+      it "ok: `COPY` to relative destination if `WORKDIR` has been set, both within an `ONBUILD` context" $+        let dockerFile =+              Text.unlines+                [ "FROM debian:buster",+                  "ONBUILD WORKDIR /usr/local/lib",+                  "ONBUILD COPY foo bar"+                ]+         in do+              ruleCatchesNot "DL3045" dockerFile+              onBuildRuleCatchesNot "DL3045" dockerFile+    --+    describe "other rules" $ do+      it "apt-get auto yes" $ do+        ruleCatches "DL3014" "RUN apt-get install python"+        onBuildRuleCatches "DL3014" "RUN apt-get install python"+      it "apt-get yes shortflag" $ do+        ruleCatchesNot "DL3014" "RUN apt-get install -yq python"+        onBuildRuleCatchesNot "DL3014" "RUN apt-get install -yq python"+      it "apt-get yes quiet level 2 implies -y" $ do+        ruleCatchesNot "DL3014" "RUN apt-get install -qq python"+        onBuildRuleCatchesNot "DL3014" "RUN apt-get install -qq python"+      it "apt-get yes different pos" $ do+        ruleCatchesNot "DL3014" "RUN apt-get install -y python"+        onBuildRuleCatchesNot "DL3014" "RUN apt-get install -y python"+      it "apt-get with auto yes" $ do+        ruleCatchesNot "DL3014" "RUN apt-get -y install python"+        onBuildRuleCatchesNot "DL3014" "RUN apt-get -y install python"+      it "apt-get with auto expanded yes" $ do+        ruleCatchesNot "DL3014" "RUN apt-get --yes install python"+        onBuildRuleCatchesNot "DL3014" "RUN apt-get --yes install python"+      it "apt-get with assume-yes" $ do+        ruleCatchesNot "DL3014" "RUN apt-get --assume-yes install python"+        onBuildRuleCatchesNot "DL3014" "RUN apt-get --assume-yes install python"+      it "apt-get install recommends" $ do+        ruleCatchesNot+          "DL3015"+          "RUN apt-get install --no-install-recommends python"+        onBuildRuleCatchesNot+          "DL3015"+          "RUN apt-get install --no-install-recommends python"+      it "apt-get no install recommends" $ do+        ruleCatches "DL3015" "RUN apt-get install python"+        onBuildRuleCatches "DL3015" "RUN apt-get install python"+      it "apt-get no install recommends" $ do+        ruleCatches "DL3015" "RUN apt-get -y install python"+        onBuildRuleCatches "DL3015" "RUN apt-get -y install python"+      it "apt-get no install recommends via option" $ do+        ruleCatchesNot "DL3015" "RUN apt-get -o APT::Install-Recommends=false install python"+        onBuildRuleCatchesNot "DL3015" "RUN apt-get -o APT::Install-Recommends=false install python"+      it "apt-get version" $ do+        ruleCatchesNot "DL3008" "RUN apt-get install -y python=1.2.2"+        onBuildRuleCatchesNot "DL3008" "RUN apt-get install -y python=1.2.2"+      it "apt-get version" $ do+        ruleCatchesNot "DL3008" "RUN apt-get install ./wkhtmltox_0.12.5-1.bionic_amd64.deb"+        onBuildRuleCatchesNot "DL3008" "RUN apt-get install ./wkhtmltox_0.12.5-1.bionic_amd64.deb"+      it "apt-get pinned" $ do+        ruleCatchesNot+          "DL3008"+          "RUN apt-get -y --no-install-recommends install nodejs=0.10"+        onBuildRuleCatchesNot+          "DL3008"+          "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 "DL3008" $ Text.unlines dockerFile+              onBuildRuleCatchesNot "DL3008" $ Text.unlines dockerFile++      it "has maintainer" $ ruleCatches "DL4000" "FROM debian\nMAINTAINER Lukas"+      it "has maintainer first" $ ruleCatches "DL4000" "MAINTAINER Lukas\nFROM DEBIAN"+      it "has no maintainer" $ ruleCatchesNot "DL4000" "FROM debian"+      it "using add" $ ruleCatches "DL3020" "ADD file /usr/src/app/"++      it "many cmds" $+        let dockerFile =+              [ "FROM debian",+                "CMD bash",+                "RUN foo",+                "CMD another"+              ]+         in ruleCatches "DL4003" $ 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 "DL4003" $ 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 "DL4003" $ Text.unlines dockerFile++      it "single cmd" $ ruleCatchesNot "DL4003" "CMD /bin/true"+      it "no cmd" $ ruleCatchesNot "DL4004" "FROM busybox"++      it "many entrypoints" $+        let dockerFile =+              [ "FROM debian",+                "ENTRYPOINT bash",+                "RUN foo",+                "ENTRYPOINT another"+              ]+         in ruleCatches "DL4004" $ 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 "DL4004" $ 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 "DL4004" $ Text.unlines dockerFile++      it "single entry" $ ruleCatchesNot "DL4004" "ENTRYPOINT /bin/true"+      it "no entry" $ ruleCatchesNot "DL4004" "FROM busybox"+      it "workdir relative" $ ruleCatches "DL3000" "WORKDIR relative/dir"+      it "workdir absolute" $ ruleCatchesNot "DL3000" "WORKDIR /usr/local"+      it "workdir variable" $ ruleCatchesNot "DL3000" "WORKDIR ${work}"+      it "workdir relative single quotes" $ ruleCatches "DL3000" "WORKDIR \'relative/dir\'"+      it "workdir absolute single quotes" $ ruleCatchesNot "DL3000" "WORKDIR \'/usr/local\'"+      -- no test for variable/single quotes since the variable would not expand.+      it "workdir relative double quotes" $ ruleCatches "DL3000" "WORKDIR \"relative/dir\""+      it "workdir absolute double quotes" $ ruleCatchesNot "DL3000" "WORKDIR \"/usr/local\""+      it "workdir variable double quotes" $ ruleCatchesNot "DL3000" "WORKDIR \"${dir}\""+      it "scratch" $ ruleCatchesNot "DL3006" "FROM scratch"+    --+    describe "add files and archives" $ do+      it "add for tar" $ ruleCatchesNot "DL3020" "ADD file.tar /usr/src/app/"+      it "add for zip" $ ruleCatchesNot "DL3020" "ADD file.zip /usr/src/app/"+      it "add for gzip" $ ruleCatchesNot "DL3020" "ADD file.gz /usr/src/app/"+      it "add for bz2" $ ruleCatchesNot "DL3020" "ADD file.bz2 /usr/src/app/"+      it "add for xz" $ ruleCatchesNot "DL3020" "ADD file.xz /usr/src/app/"+      it "add for tgz" $ ruleCatchesNot "DL3020" "ADD file.tgz /usr/src/app/"+      it "add for url" $ ruleCatchesNot "DL3020" "ADD http://file.com /usr/src/app/"+    --+    describe "copy last argument" $ do+      it "no warn on 2 args" $ ruleCatchesNot "DL3021" "COPY foo bar"+      it "warn on 3 args" $ ruleCatches "DL3021" "COPY foo bar baz"+      it "no warn on 3 args" $ ruleCatchesNot "DL3021" "COPY foo bar baz/"+    --+    describe "copy from existing alias" $ do+      it "warn on missing alias" $ ruleCatches "DL3022" "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 "DL3022" $ 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 "DL3022" $ 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 "DL3023" $ Text.unlines dockerFile+      it "don't warn on copying from other sources" $+        let dockerFile =+              [ "FROM scratch as build",+                "RUN foo",+                "FROM node as run",+                "COPY --from=build foo .",+                "RUN baz"+              ]+         in ruleCatchesNot "DL3023" $ 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 "DL3024" $ 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 "DL3024" $ 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 "DL3002" $ Text.unlines dockerFile+      it "ignores only the given rule" $+        let dockerFile =+              [ "FROM scratch",+                "# hadolint ignore=DL3001",+                "USER root"+              ]+         in ruleCatches "DL3002" $ Text.unlines dockerFile+      it "ignores only the given rule, when multiple passed" $+        let dockerFile =+              [ "FROM scratch",+                "# hadolint ignore=DL3001,DL3002",+                "USER root"+              ]+         in ruleCatchesNot "DL3002" $ 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 "DL3002" $ 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 "DL3002" $ 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 "DL3023" $ Text.unlines dockerFile+              ruleCatchesNot "DL3021" $ 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 "DL3025" $ Text.unlines dockerFile+      it "don't warn on ENTRYPOINT json notation" $+        let dockerFile =+              [ "FROM scratch as build",+                "ENTRYPOINT [\"foo\", \"bar\"]"+              ]+         in ruleCatchesNot "DL3025" $ Text.unlines dockerFile+      it "warn on CMD" $+        let dockerFile =+              [ "FROM node as foo",+                "CMD something"+              ]+         in ruleCatches "DL3025" $ Text.unlines dockerFile+      it "don't warn on CMD json notation" $+        let dockerFile =+              [ "FROM scratch as build",+                "CMD [\"foo\", \"bar\"]",+                "CMD [ \"foo\", \"bar\" ]"+              ]+         in ruleCatchesNot "DL3025" $ 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 "DL4006" $ 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 "DL4006" $ 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 "DL4006" $ 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 "DL4006" $ 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 "DL4006" $ 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 "DL4006" $ 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 "DL4006" $ 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 "DL4006" $ 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 "DL4006" $ 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 "DL4006" $ Text.unlines dockerFile+    --+    describe "Allowed docker registries" $ do+      it "does not warn on empty allowed registries" $ do+        let dockerFile =+              [ "FROM random.com/debian"+              ]+        ruleCatchesNot "DL3026" $ Text.unlines dockerFile++      it "warn on non-allowed registry" $ do+        let dockerFile =+              [ "FROM random.com/debian"+              ]+        let ?rulesConfig = Hadolint.Process.RulesConfig ["docker.io"] Map.empty False+        ruleCatches "DL3026" $ Text.unlines dockerFile++      it "does not warn on allowed registries" $ do+        let dockerFile =+              [ "FROM random.com/debian"+              ]+        let ?rulesConfig = Hadolint.Process.RulesConfig ["x.com", "random.com"] Map.empty False+        ruleCatchesNot "DL3026" $ Text.unlines dockerFile++      it "doesn't warn on scratch image" $ do+        let dockerFile =+              [ "FROM scratch"+              ]+        let ?rulesConfig = Hadolint.Process.RulesConfig ["x.com", "random.com"] Map.empty False+        ruleCatchesNot "DL3026" $ Text.unlines dockerFile++      it "allows boths all forms of docker.io" $ do+        let dockerFile =+              [ "FROM ubuntu:18.04 AS builder1",+                "FROM zemanlx/ubuntu:18.04 AS builder2",+                "FROM docker.io/zemanlx/ubuntu:18.04 AS builder3"+              ]+        let ?rulesConfig = Hadolint.Process.RulesConfig ["docker.io"] Map.empty False+        ruleCatchesNot "DL3026" $ Text.unlines dockerFile++      it "allows using previous stages" $ do+        let dockerFile =+              [ "FROM random.com/foo AS builder1",+                "FROM builder1 AS builder2"+              ]+        let ?rulesConfig = Hadolint.Process.RulesConfig ["random.com"] Map.empty False+        ruleCatchesNot "DL3026" $ 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 "DL4001" $ 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 "DL4001" $ Text.unlines dockerFile+      it "does not warn when using only wget" $+        let dockerFile =+              [ "FROM node as foo",+                "RUN wget my.xyz"+              ]+         in ruleCatchesNot "DL4001" $ 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 "DL4001" $ 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 "DL4001" $ 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+              (Text.unlines dockerFile)+              (failsWith 1 "DL4001")++      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+              (Text.unlines dockerFile)+              (failsWith 2 "DL4001")+    --+    describe "Wget no progress" $ do+      it "warns when using wget without --progress option" $+        let dockerFile =+              [ "FROM node as foo",+                "RUN wget my.xyz"+              ]+         in ruleCatches "DL3047" $ Text.unlines dockerFile+      it "does not warn when running with --progress option" $+        let dockerFile =+              [ "FROM node as foo",+                "RUN wget --progress=dot:giga my.xyz"+              ]+         in ruleCatchesNot "DL3047" $ Text.unlines dockerFile+      it "does not warn when running with -q (quiet short option) and without --progress option" $+        let dockerFile =+              [ "FROM node as foo",+                "RUN wget -q my.xyz"+              ]+         in ruleCatchesNot "DL3047" $ Text.unlines dockerFile+      it "does not warn when running with --quiet (quiet long option) and without --progress option" $+        let dockerFile =+              [ "FROM node as foo",+                "RUN wget --quiet my.xyz"+              ]+         in ruleCatchesNot "DL3047" $ Text.unlines dockerFile+      it "does not warn when running with -nv (no-verbose short option) and without --progress option" $+        let dockerFile =+              [ "FROM node as foo",+                "RUN wget -nv my.xyz"+              ]+         in ruleCatchesNot "DL3047" $ Text.unlines dockerFile+      it "does not warn when running with --no-verbose (no-verbose long option) and without --progress option" $+        let dockerFile =+              [ "FROM node as foo",+                "RUN wget --no-verbose my.xyz"+              ]+         in ruleCatchesNot "DL3047" $ Text.unlines dockerFile+      it "does not warn when running with --output-file (output-file long option) and without --progress option" $+        let dockerFile =+              [ "FROM node as foo",+                "RUN wget --output-file=/tmp/wget.log my.xyz"+              ]+         in ruleCatchesNot "DL3047" $ Text.unlines dockerFile+      it "does not warn when running with -o (output-file long option) and without --progress option" $+        let dockerFile =+              [ "FROM node as foo",+                "RUN wget -o /tmp/wget.log my.xyz"+              ]+         in ruleCatchesNot "DL3047" $ Text.unlines dockerFile+      it "does not warn when running with --append-output (append-output long option) and without --progress option" $+        let dockerFile =+              [ "FROM node as foo",+                "RUN wget --append-output=/tmp/wget.log my.xyz"+              ]+         in ruleCatchesNot "DL3047" $ Text.unlines dockerFile+      it "does not warn when running with -a (append-output long option) and without --progress option" $+        let dockerFile =+              [ "FROM node as foo",+                "RUN wget -a /tmp/wget.log my.xyz"+              ]+         in ruleCatchesNot "DL3047" $ Text.unlines dockerFile+    --+    describe "DL3012 - Multiple `HEALTHCHECK` instructions" $ do+      it "ok with no HEALTHCHECK instruction" $+        ruleCatchesNot "DL3012" "FROM scratch"+      it "ok with one HEALTHCHECK instruction" $+        ruleCatchesNot "DL3012" "FROM scratch\nHEALTHCHECK CMD /bin/bla"+      it "ok with two HEALTHCHECK instructions in two stages" $+        ruleCatchesNot "DL3012" "FROM scratch\nHEALTHCHECK CMD /bin/bla1\nFROM scratch\nHEALTHCHECK CMD /bin/bla2"+      it "warn with two HEALTHCHECK instructions" $+        ruleCatches "DL3012" "FROM scratch\nHEALTHCHECK CMD /bin/bla1\nHEALTHCHECK CMD /bin/bla2"+    --+    describe "DL3057 - `HEALTHCHECK instruction missing" $ do+      it "warn with no HEALTHCHECK instructions" $+        ruleCatches "DL3057" "FROM scratch"+      it "ok with one HEALTHCHECK instruction" $+        ruleCatchesNot "DL3057" "FROM scratch\nHEALTHCHECK CMD /bin/bla"+      it "ok with inheriting HEALTHCHECK instruction" $+        ruleCatchesNot "DL3057" "FROM scratch AS base\nHEALTHCHECK CMD /bin/bla\nFROM base"+      it "warn when not inheriting with no HEALTHCHECK instruction" $+        ruleCatches "DL3057" "FROM scratch AS base\nHEALTHCHECK CMD /bin/bla\nFROM scratch"+    --+    describe "ONBUILD" $ do+      it "error when using `ONBUILD` within `ONBUILD`" $+        let dockerFile =+              [ "ONBUILD ONBUILD RUN anything"+              ]+         in ruleCatches "DL3043" $ Text.unlines dockerFile+      it "error when using `FROM` within `ONBUILD`" $+        let dockerFile =+              [ "ONBUILD FROM debian:buster"+              ]+         in ruleCatches "DL3043" $ Text.unlines dockerFile+      it "error when using `MAINTAINER` within `ONBUILD`" $+        let dockerFile =+              [ "ONBUILD MAINTAINER \"BoJack Horseman\""+              ]+         in ruleCatches "DL3043" $ Text.unlines dockerFile+      it "ok with `ADD`" $ ruleCatchesNot "DL3043" "ONBUILD ADD anything anywhere"+      it "ok with `USER`" $ ruleCatchesNot "DL3043" "ONBUILD USER anything"+      it "ok with `LABEL`" $ ruleCatchesNot "DL3043" "ONBUILD LABEL bla=\"blubb\""+      it "ok with `STOPSIGNAL`" $ ruleCatchesNot "DL3043" "ONBUILD STOPSIGNAL anything"+      it "ok with `COPY`" $ ruleCatchesNot "DL3043" "ONBUILD COPY anything anywhere"+      it "ok with `RUN`" $ ruleCatchesNot "DL3043" "ONBUILD RUN anything"+      it "ok with `CMD`" $ ruleCatchesNot "DL3043" "ONBUILD CMD anything"+      it "ok with `SHELL`" $ ruleCatchesNot "DL3043" "ONBUILD SHELL anything"+      it "ok with `WORKDIR`" $ ruleCatchesNot "DL3043" "ONBUILD WORKDIR anything"+      it "ok with `EXPOSE`" $ ruleCatchesNot "DL3043" "ONBUILD EXPOSE 69"+      it "ok with `VOLUME`" $ ruleCatchesNot "DL3043" "ONBUILD VOLUME anything"+      it "ok with `ENTRYPOINT`" $ ruleCatchesNot "DL3043" "ONBUILD ENTRYPOINT anything"+      it "ok with `ENV`" $ ruleCatchesNot "DL3043" "ONBUILD ENV MYVAR=\"bla\""+      it "ok with `ARG`" $ ruleCatchesNot "DL3043" "ONBUILD ARG anything"+      it "ok with `HEALTHCHECK`" $ ruleCatchesNot "DL3043" "ONBUILD HEALTHCHECK NONE"+      it "ok with `FROM` outside of `ONBUILD`" $ ruleCatchesNot "DL3043" "FROM debian:buster"+      it "ok with `MAINTAINER` outside of `ONBUILD`" $ ruleCatchesNot "DL3043" "MAINTAINER \"Some Guy\""+    --+    describe "Selfreferencing `ENV`s" $ do+      it "ok with normal ENV" $+        ruleCatchesNot "DL3044" "ENV BLA=\"blubb\"\nENV BLUBB=\"${BLA}/blubb\""+      it "ok with partial match 1" $+        ruleCatchesNot "DL3044" "ENV BLA=\"blubb\" BLUBB=\"${FOOBLA}/blubb\""+      it "ok with partial match 2" $+        ruleCatchesNot "DL3044" "ENV BLA=\"blubb\" BLUBB=\"${BLAFOO}/blubb\""+      it "ok with partial match 3" $+        ruleCatchesNot "DL3044" "ENV BLA=\"blubb\" BLUBB=\"$FOOBLA/blubb\""+      it "ok with partial match 4" $+        ruleCatchesNot "DL3044" "ENV BLA=\"blubb\" BLUBB=\"$BLAFOO/blubb\""+      it "fail with partial match 5" $+        ruleCatches "DL3044" "ENV BLA=\"blubb\" BLUBB=\"$BLA/$BLAFOO/blubb\""+      it "ok with parial match 6" $+        ruleCatchesNot "DL3044" "ENV BLA=\"blubb\" BLUBB=\"BLA/$BLAFOO/BLA\""+      it "ok when previously defined in `ARG`" $+        ruleCatchesNot "DL3044" "ARG BLA\nENV BLA=${BLA}"+      it "ok when previously defined in `ENV`" $+        ruleCatchesNot "DL3044" "ENV BLA blubb\nENV BLA=${BLA}"+      it "ok with referencing a variable on its own right hand side" $+        ruleCatchesNot "DL3044" "ENV PATH=/bla:${PATH}"+      it "ok with referencing a variable on its own right side twice in different `ENV`s" $+        ruleCatchesNot "DL3044" "ENV PATH=/bla:${PATH}\nENV PATH=/blubb:${PATH}"+      it "fail when referencing a variable on its own right side twice within the same `ENV`" $+        ruleCatches "DL3044" "ENV PATH=/bla:${PATH} PATH=/blubb:${PATH}"+      it "fail with selfreferencing with curly braces ENV" $+        ruleCatches "DL3044" "ENV BLA=\"blubb\" BLUBB=\"${BLA}/blubb\""+      it "fail with selfreferencing without curly braces ENV" $+        ruleCatches "DL3044" "ENV BLA=\"blubb\" BLUBB=\"$BLA/blubb\""+      it "fail with full match 1" $+        ruleCatches "DL3044" "ENV BLA=\"blubb\" BLUBB=\"$BLA\""+      it "fail with full match 2" $+        ruleCatches "DL3044" "ENV BLA=\"blubb\" BLUBB=\"${BLA}\""+    --+    describe "warn when using `useradd` with long UID and without `-l`" $ do+      it "ok with `useradd` alone" $ ruleCatchesNot "DL3046" "RUN useradd luser"+      it "ok with `useradd` short uid" $ ruleCatchesNot "DL3046" "RUN useradd -u 12345 luser"+      it "ok with `useradd` long uid and flag `-l`" $ ruleCatchesNot "DL3046" "RUN useradd -l -u 123456 luser"+      it "ok with `useradd` and just flag `-l`" $ ruleCatchesNot "DL3046" "RUN useradd -l luser"+      it "warn when `useradd` and long uid without flag `-l`" $ ruleCatches "DL3046" "RUN useradd -u 123456 luser"+    --+    describe "Missing label rule tests" $+      let ?rulesConfig = Hadolint.Process.RulesConfig [] (Map.fromList [("foo", Rule.RawText)]) False+       in do+    -- single stage tests+      it "not ok: single stage, no label" $ do+        ruleCatches "DL3049" "FROM baseimage"+        onBuildRuleCatches "DL3049" "FROM baseimage"+      it "not ok: single stage, wrong label" $ do+        ruleCatches "DL3049" "FROM baseimage\nLABEL bar=\"baz\""+        onBuildRuleCatches "DL3049" "FROM baseimage\nLABEL bar=\"baz\""+      it "ok: single stage, label present" $ do+        ruleCatchesNot "DL3049" "FROM baseimage\nLABEL foo=\"bar\""+        onBuildRuleCatchesNot "DL3049" "FROM baseimage\nLABEL foo=\"bar\""+    -- multi stage tests+      it "warn twice: two stages, no labels" $+        let dockerFile =+              [ "FROM stage1",+                "FROM stage2"+              ]+         in assertChecks+              (Text.unlines dockerFile)+              (failsWith 2 "DL3049")+      it "warn twice: two stages, wrong labels only" $+        let dockerFile =+              [ "FROM stage1",+                "LABEL bar=\"baz\"",+                "FROM stage2",+                "LABEL buzz=\"fuzz\""+              ]+         in assertChecks+              (Text.unlines dockerFile)+              (failsWith 2 "DL3049")+      it "warn once: two stages, label present in second only" $+        let dockerFile =+              [ "FROM baseimage",+                "FROM newimage",+                "LABEL foo=\"bar\""+              ]+         in assertChecks+              (Text.unlines dockerFile)+              (failsWith 1 "DL3049")+      it "warn once: two stages, no inheritance, wrong label in one" $+        let dockerFile =+              [ "FROM baseimage",+                "LABEL baz=\"bar\"",+                "FROM newimage",+                "LABEL foo=\"bar\""+              ]+         in assertChecks+              (Text.unlines dockerFile)+              (failsWith 1 "DL3049")+      it "warn once: two stages, inheritance, label only defined in second stage" $+        let dockerFile =+              [ "FROM baseimage as base",+                "FROM base",+                "LABEL foo=\"bar\""+              ]+         in assertChecks+              (Text.unlines dockerFile)+              (failsWith 1 "DL3049")+      it "don't warn: two stages, inheritance" $+        let dockerFile =+              [ "FROM baseimage as base",+                "LABEL foo=\"bar\"",+                "FROM base"+              ]+         in assertChecks+              (Text.unlines dockerFile)+              (failsWith 0 "DL3049")+    describe "Strict Labels" $+      let ?rulesConfig = Hadolint.Process.RulesConfig [] (Map.fromList [("required", Rule.RawText)]) True+       in do+      it "ok with no label" $ do+        ruleCatchesNot "DL3050" ""+        onBuildRuleCatchesNot "DL3050" ""+      it "ok with required label" $ do+        ruleCatchesNot "DL3050" "LABEL required=\"foo\""+        onBuildRuleCatchesNot "DL3050" "LABEL required=\"bar\""+      it "not ok with just other label" $ do+        ruleCatches "DL3050" "LABEL other=\"bar\""+        onBuildRuleCatches "DL3050" "LABEL other=\"bar\""+      it "not ok with other label and required label" $ do+        ruleCatches "DL3050" "LABEL required=\"foo\" other=\"bar\""+        onBuildRuleCatches "DL3050" "LABEL required=\"foo\" other=\"bar\""+    describe "Label is not empty rule" $+      let ?rulesConfig = Hadolint.Process.RulesConfig [] (Map.fromList [("emptylabel", Rule.RawText)]) False+       in do+      it "not ok with label empty" $ do+        ruleCatches "DL3051" "LABEL emptylabel=\"\""+        onBuildRuleCatches "DL3051" "LABEL emptylabel=\"\""+      it "ok with label not empty" $ do+        ruleCatchesNot "DL3051" "LABEL emptylabel=\"foo\""+        onBuildRuleCatchesNot "DL3051" "LABEL emptylabel=\"bar\""+      it "ok with other label empty" $ do+        ruleCatchesNot "DL3051" "LABEL other=\"\""+        onBuildRuleCatchesNot "DL3051" "LABEL other=\"\""+    describe "Label is not URL rule" $+      let ?rulesConfig = Hadolint.Process.RulesConfig [] (Map.fromList [("urllabel", Rule.Url)]) False+       in do+      it "not ok with label not containing URL" $ do+        ruleCatches "DL3052" "LABEL urllabel=\"not-url\""+        onBuildRuleCatches "DL3052" "LABEL urllabel=\"not-url\""+      it "ok with label containing URL" $ do+        ruleCatchesNot "DL3052" "LABEL urllabel=\"http://example.com\""+        onBuildRuleCatchesNot "DL3052" "LABEL urllabel=\"http://example.com\""+      it "ok with other label not containing URL" $ do+        ruleCatchesNot "DL3052" "LABEL other=\"foo\""+        onBuildRuleCatchesNot "DL3052" "LABEL other=\"bar\""+    describe "Label is not RFC3339 date rule" $+      let ?rulesConfig = Hadolint.Process.RulesConfig [] (Map.fromList [("datelabel", Rule.Rfc3339)]) False+       in do+      it "not ok with label not containing RFC3339 date" $ do+        ruleCatches "DL3053" "LABEL datelabel=\"not-date\""+        onBuildRuleCatches "DL3053" "LABEL datelabel=\"not-date\""+      it "ok with label containing RFC3339 date" $ do+        ruleCatchesNot "DL3053" "LABEL datelabel=\"2021-03-10T10:26:33.564595127+01:00\""+        onBuildRuleCatchesNot "DL3053" "LABEL datelabel=\"2021-03-10T10:26:33.564595127+01:00\""+      it "ok with other label not containing RFC3339 date" $ do+        ruleCatchesNot "DL3053" "LABEL other=\"doo\""+        onBuildRuleCatchesNot "DL3053" "LABEL other=\"bar\""+    describe "Label is not SPDX license identifier rule" $+      let ?rulesConfig = Hadolint.Process.RulesConfig [] (Map.fromList [("spdxlabel", Rule.Spdx)]) False+       in do+      it "not ok with label not containing SPDX identifier" $ do+        ruleCatches "DL3054" "LABEL spdxlabel=\"not-spdx\""+        onBuildRuleCatches "DL3054" "LABEL spdxlabel=\"not-spdx\""+      it "ok with label containing SPDX identifier" $ do+        ruleCatchesNot "DL3054" "LABEL spdxlabel=\"BSD-3-Clause\""+        onBuildRuleCatchesNot "DL3054" "LABEL spdxlabel=\"MIT\""+      it "ok with other label not containing SPDX identifier" $ do+        ruleCatchesNot "DL3054" "LABEL other=\"fooo\""+        onBuildRuleCatchesNot "DL3054" "LABEL other=\"bar\""+    describe "Label is not git hash rule" $+      let ?rulesConfig = Hadolint.Process.RulesConfig [] (Map.fromList [("githash", Rule.GitHash)]) False+       in do+      it "not ok with label not containing git hash" $ do+        ruleCatches "DL3055" "LABEL githash=\"not-git-hash\""+        onBuildRuleCatches "DL3055" "LABEL githash=\"not-git-hash\""+      it "ok with label containing short git hash" $ do+        ruleCatchesNot "DL3055" "LABEL githash=\"2dbfae9\""+        onBuildRuleCatchesNot "DL3055" "LABEL githash=\"2dbfae9\""+      it "ok with label containing long git hash" $ do+        ruleCatchesNot "DL3055" "LABEL githash=\"43c572f1272b6b3171dd1db9e41b7027128ce080\""+        onBuildRuleCatchesNot "DL3055" "LABEL githash=\"43c572f1272b6b3171dd1db9e41b7027128ce080\""+      it "ok with other label not containing git hash" $ do+        ruleCatchesNot "DL3055" "LABEL other=\"foo\""+        onBuildRuleCatchesNot "DL3055" "LABEL other=\"bar\""+    describe "Label is not semantic version rule" $+      let ?rulesConfig = Hadolint.Process.RulesConfig [] (Map.fromList [("semver", Rule.SemVer)]) False+       in do+      it "not ok with label not containing semantic version" $ do+        ruleCatches "DL3056" "LABEL semver=\"not-sem-ver\""+        onBuildRuleCatches "DL3056" "LABEL semver=\"not-sem-ver\""+      it "ok with label containing semantic version" $ do+        ruleCatchesNot "DL3056" "LABEL semver=\"1.0.0\""+        onBuildRuleCatchesNot "DL3056" "LABEL semver=\"2.0.1-rc1\""+      it "ok with other label not containing semantic version" $ do+        ruleCatchesNot "DL3056" "LABEL other=\"foo\""+        onBuildRuleCatchesNot "DL3056" "LABEL other=\"bar\""+    --+    describe "Invalid Label Key Rule" $ do+      it "not ok with reserved namespace" $ do+        ruleCatches "DL3048" "LABEL com.docker.label=\"foo\""+        ruleCatches "DL3048" "LABEL io.docker.label=\"foo\""+        ruleCatches "DL3048" "LABEL org.dockerproject.label=\"foo\""+        onBuildRuleCatches "DL3048" "LABEL com.docker.label=\"foo\""+        onBuildRuleCatches "DL3048" "LABEL io.docker.label=\"foo\""+        onBuildRuleCatches "DL3048" "LABEL org.dockerproject.label=\"foo\""+      it "not ok with invalid character" $ do+        ruleCatches "DL3048" "LABEL invalid$character=\"foo\""+        onBuildRuleCatches "DL3048" "LABEL invalid$character=\"foo\""+      it "not ok with invalid start and end characters" $ do+        ruleCatches "DL3048" "LABEL .invalid =\"foo\""+        ruleCatches "DL3048" "LABEL -invalid =\"foo\""+        ruleCatches "DL3048" "LABEL 1invalid =\"foo\""+        onBuildRuleCatches "DL3048" "LABEL .invalid=\"foo\""+        onBuildRuleCatches "DL3048" "LABEL -invalid=\"foo\""+        onBuildRuleCatches "DL3048" "LABEL 1invalid=\"foo\""+      it "not ok with consecutive dividers" $ do+        ruleCatches "DL3048" "LABEL invalid..character=\"foo\""+        ruleCatches "DL3048" "LABEL invalid--character=\"foo\""+        onBuildRuleCatches "DL3048" "LABEL invalid..character=\"foo\""+        onBuildRuleCatches "DL3048" "LABEL invalid--character=\"foo\""+      it "ok with valid labels" $ do+        ruleCatchesNot "DL3048" "LABEL org.valid-key.label3=\"foo\""+        ruleCatchesNot "DL3048" "LABEL validlabel=\"foo\""+        onBuildRuleCatchesNot "DL3048" "LABEL org.valid-key.label3=\"foo\""+        onBuildRuleCatchesNot "DL3048" "LABEL validlabel=\"foo\""+    --+    describe "Regression Tests" $ do+      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 "DL4006" $ Text.unlines dockerFile+      it "`ARG` can correctly unset variables" $+        let dockerFile =+              [ "ARG A_WITHOUT_EQ",+                "ARG A_WITH_EQ=",+                "RUN echo bla"+              ]+         in assertChecks+              (Text.unlines dockerFile)+              (assertBool "No Warnings or Errors should be triggered" . null)++    -- Run tests for the Config module+    ConfigSpec.tests+    -- Run tests for the Shell module+    ShellSpec.tests++assertChecks ::+  (HasCallStack, ?rulesConfig :: Hadolint.Process.RulesConfig) =>+  Text.Text ->+  (Failures -> IO a) ->+  IO a+assertChecks dockerfile makeAssertions =+  case parseText (dockerfile <> "\n") of+    Left err -> assertFailure $ show err+    Right dockerFile -> makeAssertions $ Hadolint.Process.run ?rulesConfig dockerFile++assertOnBuildChecks ::+  (HasCallStack, ?rulesConfig :: Hadolint.Process.RulesConfig) =>+  Text.Text ->+  (Failures -> IO a) ->+  IO a+assertOnBuildChecks dockerfile makeAssertions =+  case parseText (dockerfile <> "\n") of+    Left err -> assertFailure $ show err+    Right dockerFile -> checkOnBuild dockerFile+  where+    checkOnBuild dockerFile = makeAssertions $ Hadolint.Process.run ?rulesConfig (fmap wrapInOnBuild dockerFile)+    wrapInOnBuild (InstructionPos (Run args) so li) = InstructionPos (OnBuild (Run args)) so li+    wrapInOnBuild i = i++hasInvalidLines :: Failures -> Bool+hasInvalidLines =+  Foldl.fold+    (Foldl.any (\CheckFailure {line} -> line <= 0))++-- Assert a failed check exists for rule+ruleCatches ::+  (HasCallStack, ?rulesConfig :: Hadolint.Process.RulesConfig) =>+  RuleCode ->+  Text.Text ->+  Assertion+ruleCatches expectedCode dockerfile = assertChecks dockerfile f+  where+    f checks = do+      failsWithSome expectedCode checks+      assertBool "Incorrect line number for result" $ not $ hasInvalidLines checks++onBuildRuleCatches ::+  (HasCallStack, ?rulesConfig :: Hadolint.Process.RulesConfig) =>+  RuleCode ->+  Text.Text ->+  Assertion+onBuildRuleCatches ruleCode dockerfile = assertOnBuildChecks dockerfile f+  where+    f checks = do+      failsWith 1 ruleCode checks+      assertBool "Incorrect line number for result" $ not $ hasInvalidLines checks++ruleCatchesNot ::+  (HasCallStack, ?rulesConfig :: Hadolint.Process.RulesConfig) =>+  RuleCode ->+  Text.Text ->+  Assertion+ruleCatchesNot ruleCode dockerfile = assertChecks dockerfile f+  where+    f = failsWith 0 ruleCode++onBuildRuleCatchesNot ::+  (HasCallStack, ?rulesConfig :: Hadolint.Process.RulesConfig) =>+  RuleCode ->+  Text.Text ->+  Assertion+onBuildRuleCatchesNot ruleCode dockerfile = assertOnBuildChecks dockerfile f+  where+    f = failsWith 0 ruleCode++formatChecksNoColor :: Foldable f => f CheckFailure -> Text.Text+formatChecksNoColor = Foldl.fold (Foldl.premap (\c -> formatCheck True "line" c <> "\n") Foldl.mconcat)++failsWithSome :: HasCallStack => RuleCode -> Failures -> Assertion+failsWithSome expectedCode failures =+  when (null matched) $+    assertFailure $ "I was expecting to catch at least one error for " <> show (unRuleCode expectedCode)+  where+    matched = Seq.filter (\CheckFailure {code} -> expectedCode == code) failures++failsWith :: HasCallStack => Int -> RuleCode -> Failures -> Assertion+failsWith times expectedCode failures =+  when (length matched /= times) $+    assertFailure $+      "I was expecting to catch exactly " <> show times <> " error(s) for " <> show (unRuleCode expectedCode) <> ". Found: \n"+        <> (Text.unpack . formatChecksNoColor $ matched)+  where+    matched = Seq.filter (\CheckFailure {code} -> expectedCode == code) failures++failsShellcheck :: HasCallStack => Failures -> Assertion+failsShellcheck checks =+  when (null matched) $+    assertFailure+      "I was expecting to catch at least one error with shellcheck"+  where+    matched = Seq.filter (\CheckFailure {code = RuleCode rc} -> "SC" `Text.isPrefixOf` rc) checks++passesShellcheck :: HasCallStack => Failures -> Assertion+passesShellcheck checks =+  unless (null matched) $+    assertFailure $+      "I was expecting to catch no errors with shellcheck. Found: \n"+        <> (Text.unpack . formatChecksNoColor $ matched)+  where+    matched = Seq.filter (\CheckFailure {code = RuleCode rc} -> "SC" `Text.isPrefixOf` rc) checks