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 +75/−1
- app/Main.hs +61/−26
- hadolint.cabal +88/−8
- src/Hadolint.hs +36/−4
- src/Hadolint/Config.hs +68/−67
- src/Hadolint/Formatter/Checkstyle.hs +68/−54
- src/Hadolint/Formatter/Codacy.hs +22/−20
- src/Hadolint/Formatter/Codeclimate.hs +31/−28
- src/Hadolint/Formatter/Format.hs +14/−20
- src/Hadolint/Formatter/Json.hs +22/−15
- src/Hadolint/Formatter/TTY.hs +29/−26
- src/Hadolint/Ignore.hs +40/−0
- src/Hadolint/Lint.hs +82/−83
- src/Hadolint/Process.hs +203/−0
- src/Hadolint/Rule.hs +172/−0
- src/Hadolint/Rule/DL3000.hs +22/−0
- src/Hadolint/Rule/DL3001.hs +33/−0
- src/Hadolint/Rule/DL3002.hs +49/−0
- src/Hadolint/Rule/DL3003.hs +15/−0
- src/Hadolint/Rule/DL3004.hs +17/−0
- src/Hadolint/Rule/DL3005.hs +17/−0
- src/Hadolint/Rule/DL3006.hs +33/−0
- src/Hadolint/Rule/DL3007.hs +16/−0
- src/Hadolint/Rule/DL3008.hs +31/−0
- src/Hadolint/Rule/DL3009.hs +65/−0
- src/Hadolint/Rule/DL3010.hs +38/−0
- src/Hadolint/Rule/DL3011.hs +16/−0
- src/Hadolint/Rule/DL3012.hs +18/−0
- src/Hadolint/Rule/DL3013.hs +83/−0
- src/Hadolint/Rule/DL3014.hs +21/−0
- src/Hadolint/Rule/DL3015.hs +23/−0
- src/Hadolint/Rule/DL3016.hs +61/−0
- src/Hadolint/Rule/DL3017.hs +16/−0
- src/Hadolint/Rule/DL3018.hs +30/−0
- src/Hadolint/Rule/DL3019.hs +19/−0
- src/Hadolint/Rule/DL3020.hs +45/−0
- src/Hadolint/Rule/DL3021.hs +20/−0
- src/Hadolint/Rule/DL3022.hs +19/−0
- src/Hadolint/Rule/DL3023.hs +19/−0
- src/Hadolint/Rule/DL3024.hs +20/−0
- src/Hadolint/Rule/DL3025.hs +16/−0
- src/Hadolint/Rule/DL3026.hs +29/−0
- src/Hadolint/Rule/DL3027.hs +18/−0
- src/Hadolint/Rule/DL3028.hs +36/−0
- src/Hadolint/Rule/DL3029.hs +15/−0
- src/Hadolint/Rule/DL3030.hs +20/−0
- src/Hadolint/Rule/DL3031.hs +27/−0
- src/Hadolint/Rule/DL3032.hs +23/−0
- src/Hadolint/Rule/DL3033.hs +26/−0
- src/Hadolint/Rule/DL3034.hs +30/−0
- src/Hadolint/Rule/DL3035.hs +28/−0
- src/Hadolint/Rule/DL3036.hs +23/−0
- src/Hadolint/Rule/DL3037.hs +30/−0
- src/Hadolint/Rule/DL3038.hs +20/−0
- src/Hadolint/Rule/DL3039.hs +26/−0
- src/Hadolint/Rule/DL3040.hs +23/−0
- src/Hadolint/Rule/DL3041.hs +26/−0
- src/Hadolint/Rule/DL3042.hs +29/−0
- src/Hadolint/Rule/DL3043.hs +17/−0
- src/Hadolint/Rule/DL3044.hs +69/−0
- src/Hadolint/Rule/DL3045.hs +83/−0
- src/Hadolint/Rule/DL3046.hs +24/−0
- src/Hadolint/Rule/DL3047.hs +36/−0
- src/Hadolint/Rule/DL3048.hs +39/−0
- src/Hadolint/Rule/DL3049.hs +60/−0
- src/Hadolint/Rule/DL3050.hs +18/−0
- src/Hadolint/Rule/DL3051.hs +23/−0
- src/Hadolint/Rule/DL3052.hs +27/−0
- src/Hadolint/Rule/DL3053.hs +28/−0
- src/Hadolint/Rule/DL3054.hs +32/−0
- src/Hadolint/Rule/DL3055.hs +31/−0
- src/Hadolint/Rule/DL3056.hs +27/−0
- src/Hadolint/Rule/DL3057.hs +50/−0
- src/Hadolint/Rule/DL4000.hs +14/−0
- src/Hadolint/Rule/DL4001.hs +32/−0
- src/Hadolint/Rule/DL4003.hs +25/−0
- src/Hadolint/Rule/DL4004.hs +24/−0
- src/Hadolint/Rule/DL4005.hs +17/−0
- src/Hadolint/Rule/DL4006.hs +41/−0
- src/Hadolint/Rule/Shellcheck.hs +44/−0
- src/Hadolint/Rules.hs +0/−1236
- src/Hadolint/Shell.hs +36/−18
- test/ConfigSpec.hs +30/−8
- test/ShellSpec.hs +42/−0
- test/Spec.hs +1998/−1533
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