packages feed

hadolint 2.7.0 → 2.8.0

raw patch · 174 files changed

+6089/−3909 lines, 174 filesdep +data-defaultdep +prettyprinterdep +silentlydep ~hspecdep ~language-dockerPVP ok

version bump matches the API change (PVP)

Dependencies added: data-default, prettyprinter, silently

Dependency ranges changed: hspec, language-docker

API changes (from Hackage documentation)

- Hadolint: Checkstyle :: OutputFormat
- Hadolint: Codacy :: OutputFormat
- Hadolint: CodeclimateJson :: OutputFormat
- Hadolint: GitlabCodeclimateJson :: OutputFormat
- Hadolint: Json :: OutputFormat
- Hadolint: Result :: Text -> Seq (ParseErrorBundle s e) -> Failures -> Result s e
- Hadolint: SonarQube :: OutputFormat
- Hadolint: TTY :: OutputFormat
- Hadolint: [checks] :: Result s e -> Failures
- Hadolint: [errors] :: Result s e -> Seq (ParseErrorBundle s e)
- Hadolint: [fileName] :: Result s e -> Text
- Hadolint: data OutputFormat
- Hadolint: data Result s e
- Hadolint: instance GHC.Classes.Eq Hadolint.OutputFormat
- Hadolint: instance GHC.Show.Show Hadolint.OutputFormat
- Hadolint: printResults :: Foldable f => OutputFormat -> Bool -> Maybe FilePath -> f (Result Text DockerfileError) -> IO ()
- Hadolint: shallSkipErrorStatus :: OutputFormat -> Bool
- Hadolint.Config: ConfigFile :: Maybe OverrideConfig -> Maybe [IgnoreRule] -> Maybe [TrustedRegistry] -> Maybe LabelSchema -> Maybe Bool -> Maybe DLSeverity -> ConfigFile
- Hadolint.Config: OverrideConfig :: Maybe [ErrorRule] -> Maybe [WarningRule] -> Maybe [InfoRule] -> Maybe [StyleRule] -> OverrideConfig
- Hadolint.Config: [failureThreshold] :: ConfigFile -> Maybe DLSeverity
- Hadolint.Config: [ignoredRules] :: ConfigFile -> Maybe [IgnoreRule]
- Hadolint.Config: [labelSchemaConfig] :: ConfigFile -> Maybe LabelSchema
- Hadolint.Config: [overrideErrorRules] :: OverrideConfig -> Maybe [ErrorRule]
- Hadolint.Config: [overrideInfoRules] :: OverrideConfig -> Maybe [InfoRule]
- Hadolint.Config: [overrideRules] :: ConfigFile -> Maybe OverrideConfig
- Hadolint.Config: [overrideStyleRules] :: OverrideConfig -> Maybe [StyleRule]
- Hadolint.Config: [overrideWarningRules] :: OverrideConfig -> Maybe [WarningRule]
- Hadolint.Config: [strictLabelSchema] :: ConfigFile -> Maybe Bool
- Hadolint.Config: [trustedRegistries] :: ConfigFile -> Maybe [TrustedRegistry]
- Hadolint.Config: applyConfig :: Maybe FilePath -> LintOptions -> IO (Either String LintOptions)
- Hadolint.Config: data ConfigFile
- Hadolint.Config: data OverrideConfig
- Hadolint.Config: getConfig :: Maybe FilePath -> IO (Maybe FilePath)
- Hadolint.Config: instance Data.YAML.FromYAML Hadolint.Config.ConfigFile
- Hadolint.Config: instance Data.YAML.FromYAML Hadolint.Config.OverrideConfig
- Hadolint.Config: instance GHC.Base.Monoid Hadolint.Config.OverrideConfig
- Hadolint.Config: instance GHC.Base.Semigroup Hadolint.Config.OverrideConfig
- Hadolint.Config: instance GHC.Classes.Eq Hadolint.Config.ConfigFile
- Hadolint.Config: instance GHC.Classes.Eq Hadolint.Config.OverrideConfig
- Hadolint.Config: instance GHC.Generics.Generic Hadolint.Config.ConfigFile
- Hadolint.Config: instance GHC.Generics.Generic Hadolint.Config.OverrideConfig
- Hadolint.Config: instance GHC.Show.Show Hadolint.Config.ConfigFile
- Hadolint.Config: instance GHC.Show.Show Hadolint.Config.OverrideConfig
- Hadolint.Ignore: ignored :: Fold (InstructionPos Text) (IntMap (Set RuleCode))
- Hadolint.Lint: LintOptions :: [ErrorRule] -> [WarningRule] -> [InfoRule] -> [StyleRule] -> [IgnoreRule] -> RulesConfig -> DLSeverity -> LintOptions
- Hadolint.Lint: [errorRules] :: LintOptions -> [ErrorRule]
- Hadolint.Lint: [failThreshold] :: LintOptions -> DLSeverity
- Hadolint.Lint: [ignoreRules] :: LintOptions -> [IgnoreRule]
- Hadolint.Lint: [infoRules] :: LintOptions -> [InfoRule]
- Hadolint.Lint: [rulesConfig] :: LintOptions -> RulesConfig
- Hadolint.Lint: [styleRules] :: LintOptions -> [StyleRule]
- Hadolint.Lint: [warningRules] :: LintOptions -> [WarningRule]
- Hadolint.Lint: data LintOptions
- Hadolint.Lint: instance GHC.Base.Monoid Hadolint.Lint.LintOptions
- Hadolint.Lint: instance GHC.Base.Semigroup Hadolint.Lint.LintOptions
- Hadolint.Lint: instance GHC.Show.Show Hadolint.Lint.LintOptions
- Hadolint.Lint: type ErrorRule = RuleCode
- Hadolint.Lint: type IgnoreRule = RuleCode
- Hadolint.Lint: type InfoRule = RuleCode
- Hadolint.Lint: type StyleRule = RuleCode
- Hadolint.Lint: type WarningRule = RuleCode
- 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.Rule: instance GHC.Read.Read Hadolint.Rule.DLSeverity
- Hadolint.Rule: instance GHC.Read.Read Hadolint.Rule.LabelType
- Hadolint.Rule: read :: Text -> Either Text LabelType
- Hadolint.Rule: readSeverity :: Text -> Either Text DLSeverity
- Hadolint.Rule.DL3059: instance GHC.Show.Show Hadolint.Rule.DL3059.Acc
+ Hadolint: CheckFailure :: RuleCode -> DLSeverity -> Text -> Linenumber -> CheckFailure
+ Hadolint: DLErrorC :: DLSeverity
+ Hadolint: DLIgnoreC :: DLSeverity
+ Hadolint: DLInfoC :: DLSeverity
+ Hadolint: DLStyleC :: DLSeverity
+ Hadolint: DLWarningC :: DLSeverity
+ Hadolint: Email :: LabelType
+ Hadolint: GitHash :: LabelType
+ Hadolint: RawText :: LabelType
+ Hadolint: Rfc3339 :: LabelType
+ Hadolint: SemVer :: LabelType
+ Hadolint: Spdx :: LabelType
+ Hadolint: Url :: LabelType
+ Hadolint: [code] :: CheckFailure -> RuleCode
+ Hadolint: [line] :: CheckFailure -> Linenumber
+ Hadolint: [message] :: CheckFailure -> Text
+ Hadolint: [severity] :: CheckFailure -> DLSeverity
+ Hadolint: data CheckFailure
+ Hadolint: data DLSeverity
+ Hadolint: data LabelType
+ Hadolint: type LabelName = Text
+ Hadolint: type LabelSchema = Map LabelName LabelType
+ Hadolint.Config: CommandlineConfig :: Bool -> Maybe FilePath -> [String] -> Maybe FilePath -> PartialConfiguration -> CommandlineConfig
+ Hadolint.Config: [configFile] :: CommandlineConfig -> Maybe FilePath
+ Hadolint.Config: [configuration] :: CommandlineConfig -> PartialConfiguration
+ Hadolint.Config: [dockerfiles] :: CommandlineConfig -> [String]
+ Hadolint.Config: [filePathInReportOption] :: CommandlineConfig -> Maybe FilePath
+ Hadolint.Config: [showVersion] :: CommandlineConfig -> Bool
+ Hadolint.Config: data CommandlineConfig
+ Hadolint.Config: getConfigFromEnvironment :: IO PartialConfiguration
+ Hadolint.Config: getConfigFromFile :: Maybe FilePath -> Bool -> IO (Either String PartialConfiguration)
+ Hadolint.Config: parseCommandline :: Parser CommandlineConfig
+ Hadolint.Config.Commandline: CommandlineConfig :: Bool -> Maybe FilePath -> [String] -> Maybe FilePath -> PartialConfiguration -> CommandlineConfig
+ Hadolint.Config.Commandline: [configFile] :: CommandlineConfig -> Maybe FilePath
+ Hadolint.Config.Commandline: [configuration] :: CommandlineConfig -> PartialConfiguration
+ Hadolint.Config.Commandline: [dockerfiles] :: CommandlineConfig -> [String]
+ Hadolint.Config.Commandline: [filePathInReportOption] :: CommandlineConfig -> Maybe FilePath
+ Hadolint.Config.Commandline: [showVersion] :: CommandlineConfig -> Bool
+ Hadolint.Config.Commandline: data CommandlineConfig
+ Hadolint.Config.Commandline: instance GHC.Classes.Eq Hadolint.Config.Commandline.CommandlineConfig
+ Hadolint.Config.Commandline: instance GHC.Show.Show Hadolint.Config.Commandline.CommandlineConfig
+ Hadolint.Config.Commandline: parseCommandline :: Parser CommandlineConfig
+ Hadolint.Config.Configfile: getConfigFromFile :: Maybe FilePath -> Bool -> IO (Either String PartialConfiguration)
+ Hadolint.Config.Configuration: Configuration :: Bool -> Bool -> Bool -> OutputFormat -> [RuleCode] -> [RuleCode] -> [RuleCode] -> [RuleCode] -> [RuleCode] -> Set Registry -> LabelSchema -> Bool -> DLSeverity -> Configuration
+ Hadolint.Config.Configuration: PartialConfiguration :: Maybe Bool -> Maybe Bool -> Maybe Bool -> Maybe OutputFormat -> [RuleCode] -> [RuleCode] -> [RuleCode] -> [RuleCode] -> [RuleCode] -> Set Registry -> LabelSchema -> Maybe Bool -> Maybe DLSeverity -> PartialConfiguration
+ Hadolint.Config.Configuration: [allowedRegistries] :: Configuration -> Set Registry
+ Hadolint.Config.Configuration: [errorRules] :: Configuration -> [RuleCode]
+ Hadolint.Config.Configuration: [failureThreshold] :: Configuration -> DLSeverity
+ Hadolint.Config.Configuration: [format] :: Configuration -> OutputFormat
+ Hadolint.Config.Configuration: [ignoreRules] :: Configuration -> [RuleCode]
+ Hadolint.Config.Configuration: [infoRules] :: Configuration -> [RuleCode]
+ Hadolint.Config.Configuration: [labelSchema] :: Configuration -> LabelSchema
+ Hadolint.Config.Configuration: [noColor] :: Configuration -> Bool
+ Hadolint.Config.Configuration: [noFail] :: Configuration -> Bool
+ Hadolint.Config.Configuration: [partialAllowedRegistries] :: PartialConfiguration -> Set Registry
+ Hadolint.Config.Configuration: [partialErrorRules] :: PartialConfiguration -> [RuleCode]
+ Hadolint.Config.Configuration: [partialFailureThreshold] :: PartialConfiguration -> Maybe DLSeverity
+ Hadolint.Config.Configuration: [partialFormat] :: PartialConfiguration -> Maybe OutputFormat
+ Hadolint.Config.Configuration: [partialIgnoreRules] :: PartialConfiguration -> [RuleCode]
+ Hadolint.Config.Configuration: [partialInfoRules] :: PartialConfiguration -> [RuleCode]
+ Hadolint.Config.Configuration: [partialLabelSchema] :: PartialConfiguration -> LabelSchema
+ Hadolint.Config.Configuration: [partialNoColor] :: PartialConfiguration -> Maybe Bool
+ Hadolint.Config.Configuration: [partialNoFail] :: PartialConfiguration -> Maybe Bool
+ Hadolint.Config.Configuration: [partialStrictLabels] :: PartialConfiguration -> Maybe Bool
+ Hadolint.Config.Configuration: [partialStyleRules] :: PartialConfiguration -> [RuleCode]
+ Hadolint.Config.Configuration: [partialVerbose] :: PartialConfiguration -> Maybe Bool
+ Hadolint.Config.Configuration: [partialWarningRules] :: PartialConfiguration -> [RuleCode]
+ Hadolint.Config.Configuration: [strictLabels] :: Configuration -> Bool
+ Hadolint.Config.Configuration: [styleRules] :: Configuration -> [RuleCode]
+ Hadolint.Config.Configuration: [verbose] :: Configuration -> Bool
+ Hadolint.Config.Configuration: [warningRules] :: Configuration -> [RuleCode]
+ Hadolint.Config.Configuration: applyPartialConfiguration :: Configuration -> PartialConfiguration -> Configuration
+ Hadolint.Config.Configuration: data Configuration
+ Hadolint.Config.Configuration: data PartialConfiguration
+ Hadolint.Config.Configuration: instance Data.Default.Class.Default Hadolint.Config.Configuration.Configuration
+ Hadolint.Config.Configuration: instance Data.Default.Class.Default Hadolint.Config.Configuration.PartialConfiguration
+ Hadolint.Config.Configuration: instance Data.YAML.FromYAML Hadolint.Config.Configuration.OverrideConfig
+ Hadolint.Config.Configuration: instance Data.YAML.FromYAML Hadolint.Config.Configuration.PartialConfiguration
+ Hadolint.Config.Configuration: instance GHC.Base.Monoid Hadolint.Config.Configuration.OverrideConfig
+ Hadolint.Config.Configuration: instance GHC.Base.Monoid Hadolint.Config.Configuration.PartialConfiguration
+ Hadolint.Config.Configuration: instance GHC.Base.Semigroup Hadolint.Config.Configuration.OverrideConfig
+ Hadolint.Config.Configuration: instance GHC.Base.Semigroup Hadolint.Config.Configuration.PartialConfiguration
+ Hadolint.Config.Configuration: instance GHC.Classes.Eq Hadolint.Config.Configuration.Configuration
+ Hadolint.Config.Configuration: instance GHC.Classes.Eq Hadolint.Config.Configuration.OverrideConfig
+ Hadolint.Config.Configuration: instance GHC.Classes.Eq Hadolint.Config.Configuration.PartialConfiguration
+ Hadolint.Config.Configuration: instance GHC.Generics.Generic Hadolint.Config.Configuration.OverrideConfig
+ Hadolint.Config.Configuration: instance GHC.Show.Show Hadolint.Config.Configuration.Configuration
+ Hadolint.Config.Configuration: instance GHC.Show.Show Hadolint.Config.Configuration.OverrideConfig
+ Hadolint.Config.Configuration: instance GHC.Show.Show Hadolint.Config.Configuration.PartialConfiguration
+ Hadolint.Config.Configuration: instance Prettyprinter.Internal.Pretty Hadolint.Config.Configuration.Configuration
+ Hadolint.Config.Environment: getConfigFromEnvironment :: IO PartialConfiguration
+ Hadolint.Formatter: Checkstyle :: OutputFormat
+ Hadolint.Formatter: Codacy :: OutputFormat
+ Hadolint.Formatter: CodeclimateJson :: OutputFormat
+ Hadolint.Formatter: GitlabCodeclimateJson :: OutputFormat
+ Hadolint.Formatter: Json :: OutputFormat
+ Hadolint.Formatter: Result :: Text -> Seq (ParseErrorBundle s e) -> Failures -> Result s e
+ Hadolint.Formatter: Sarif :: OutputFormat
+ Hadolint.Formatter: SonarQube :: OutputFormat
+ Hadolint.Formatter: TTY :: OutputFormat
+ Hadolint.Formatter: [checks] :: Result s e -> Failures
+ Hadolint.Formatter: [errors] :: Result s e -> Seq (ParseErrorBundle s e)
+ Hadolint.Formatter: [fileName] :: Result s e -> Text
+ Hadolint.Formatter: data OutputFormat
+ Hadolint.Formatter: data Result s e
+ Hadolint.Formatter: printResults :: Foldable f => OutputFormat -> Bool -> Maybe FilePath -> f (Result Text DockerfileError) -> IO ()
+ Hadolint.Formatter: readMaybeOutputFormat :: Text -> Maybe OutputFormat
+ Hadolint.Formatter.Format: Checkstyle :: OutputFormat
+ Hadolint.Formatter.Format: Codacy :: OutputFormat
+ Hadolint.Formatter.Format: CodeclimateJson :: OutputFormat
+ Hadolint.Formatter.Format: GitlabCodeclimateJson :: OutputFormat
+ Hadolint.Formatter.Format: Json :: OutputFormat
+ Hadolint.Formatter.Format: Sarif :: OutputFormat
+ Hadolint.Formatter.Format: SonarQube :: OutputFormat
+ Hadolint.Formatter.Format: TTY :: OutputFormat
+ Hadolint.Formatter.Format: data OutputFormat
+ Hadolint.Formatter.Format: instance Data.Default.Class.Default Hadolint.Formatter.Format.OutputFormat
+ Hadolint.Formatter.Format: instance Data.YAML.FromYAML Hadolint.Formatter.Format.OutputFormat
+ Hadolint.Formatter.Format: instance GHC.Base.Monoid Hadolint.Formatter.Format.OutputFormat
+ Hadolint.Formatter.Format: instance GHC.Base.Semigroup Hadolint.Formatter.Format.OutputFormat
+ Hadolint.Formatter.Format: instance GHC.Classes.Eq Hadolint.Formatter.Format.OutputFormat
+ Hadolint.Formatter.Format: instance GHC.Show.Show Hadolint.Formatter.Format.OutputFormat
+ Hadolint.Formatter.Format: instance Prettyprinter.Internal.Pretty Hadolint.Formatter.Format.OutputFormat
+ Hadolint.Formatter.Format: readMaybeOutputFormat :: Text -> Maybe OutputFormat
+ Hadolint.Formatter.Sarif: formatResult :: Result s e -> Seq (SarifFormat s e)
+ Hadolint.Formatter.Sarif: instance (Text.Megaparsec.Stream.VisualStream s, Text.Megaparsec.Stream.TraversableStream s, Text.Megaparsec.Error.ShowErrorComponent e) => Data.Aeson.Types.ToJSON.ToJSON (Hadolint.Formatter.Sarif.SarifFormat s e)
+ Hadolint.Formatter.Sarif: printResults :: (VisualStream s, TraversableStream s, ShowErrorComponent e, Foldable f) => f (Result s e) -> IO ()
+ Hadolint.Meta: getShortVersion :: String
+ Hadolint.Meta: getVersion :: String
+ Hadolint.Pragma: ignored :: Fold (InstructionPos Text) (IntMap (Set RuleCode))
+ Hadolint.Pragma: parseShell :: Text -> Maybe Text
+ Hadolint.Rule: dropQuotes :: Text -> Text
+ Hadolint.Rule: instance Data.Default.Class.Default Hadolint.Rule.DLSeverity
+ Hadolint.Rule: instance Prettyprinter.Internal.Pretty Hadolint.Rule.DLSeverity
+ Hadolint.Rule: instance Prettyprinter.Internal.Pretty Hadolint.Rule.LabelType
+ Hadolint.Rule: instance Prettyprinter.Internal.Pretty Hadolint.Rule.RuleCode
+ Hadolint.Rule: readEitherLabelType :: Text -> Either Text LabelType
+ Hadolint.Rule: readEitherSeverity :: Text -> Either String DLSeverity
+ Hadolint.Rule: readMaybeSeverity :: Text -> Maybe DLSeverity
+ Hadolint.Rule.DL3009: instance GHC.Classes.Eq Hadolint.Rule.DL3009.Acc
- Hadolint.Lint: analyze :: LintOptions -> Dockerfile -> Seq CheckFailure
+ Hadolint.Lint: analyze :: Configuration -> Dockerfile -> Seq CheckFailure
- Hadolint.Lint: lint :: LintOptions -> [(Text, Either Error Dockerfile)] -> [Result Text DockerfileError]
+ Hadolint.Lint: lint :: Configuration -> [(Text, Either Error Dockerfile)] -> [Result Text DockerfileError]
- Hadolint.Lint: lintIO :: LintOptions -> NonEmpty FilePath -> IO (NonEmpty (Result Text DockerfileError))
+ Hadolint.Lint: lintIO :: Configuration -> NonEmpty FilePath -> IO (NonEmpty (Result Text DockerfileError))
- Hadolint.Process: run :: RulesConfig -> [InstructionPos Text] -> Failures
+ Hadolint.Process: run :: Configuration -> [InstructionPos Text] -> Failures
- Hadolint.Rule.DL3010: rule :: Rule args
+ Hadolint.Rule.DL3010: rule :: Rule ParsedShell
- Hadolint.Rule.DL3059: rule :: Rule args
+ Hadolint.Rule.DL3059: rule :: Rule ParsedShell

Files

README.md view
@@ -8,12 +8,12 @@ src="https://hadolint.github.io/hadolint/img/cat_container.png" />  A smarter Dockerfile linter that helps you build [best practice][] Docker-images. The linter is parsing the Dockerfile into an AST and performs rules on-top of the AST. It is standing on the shoulders of [ShellCheck][] to lint+images. The linter parses the Dockerfile into an AST and performs rules on+top of the AST. It stands on the shoulders of [ShellCheck][] to lint the Bash code inside `RUN` instructions.  [:globe_with_meridians: **Check the online version on- hadolint.github.io/hadolint**](https://hadolint.github.io/hadolint)+hadolint.github.io/hadolint**](https://hadolint.github.io/hadolint) [![Screenshot](screenshot.png)](https://hadolint.github.io/hadolint)  ## How to use@@ -26,55 +26,67 @@ hadolint --trusted-registry my-company.com:500 <Dockerfile> # Warn when using untrusted FROM images ``` -Docker comes to the rescue to provide an easy way how to run `hadolint` on most+Docker comes to the rescue, providing an easy way how to run `hadolint` on most platforms. Just pipe your `Dockerfile` to `docker run`:  ```bash-$ docker run --rm -i hadolint/hadolint < Dockerfile+docker run --rm -i hadolint/hadolint < Dockerfile # OR-$ docker run --rm -i ghcr.io/hadolint/hadolint < Dockerfile+docker run --rm -i ghcr.io/hadolint/hadolint < Dockerfile ``` -or if you are using Windows PowerShell:+or using [Podman](https://podman.io/): +```bash+podman run --rm -i docker.io/hadolint/hadolint < Dockerfile+# OR+podman run --rm -i ghcr.io/hadolint/hadolint < Dockerfile+```++or using Windows PowerShell:+ ```powershell-> cat .\Dockerfile | docker run --rm -i hadolint/hadolint+cat .\Dockerfile | docker run --rm -i hadolint/hadolint ```  ## Install  You can download prebuilt binaries for OSX, Windows and Linux from the latest-[release page][]. However, if it doesn't work for you, please fall back to-Docker, `brew` or source installation.+[release page][]. However, if this does not work for you, please fall back to+container (Docker), `brew` or source installation. -If you are on OSX you can use [brew](https://brew.sh/) to install `hadolint`.+On OSX, you can use [brew](https://brew.sh/) to install `hadolint`.  ```bash brew install hadolint ``` -On Windows you can use [scoop](https://github.com/lukesampson/scoop) to+On Windows, you can use [scoop](https://github.com/lukesampson/scoop) to install `hadolint`.  ```batch scoop install hadolint ``` -As shown before, `hadolint` is available as a Docker container:+On distributions that have `nix` installed, you can use the `hadolint`+package to run ad-hoc shells or permanently install `hadolint` into+your environment. +As mentioned earlier, `hadolint` is available as a container image:+ ```bash docker pull hadolint/hadolint # OR docker pull ghcr.io/hadolint/hadolint ``` -If you need a Docker container with shell access, use the Debian or Alpine-variants of the Docker image:+If you need a container with shell access, use the Debian or Alpine+variants:  ```bash docker pull hadolint/hadolint:latest-debian-# OR +# OR docker pull hadolint/hadolint:latest-alpine # OR docker pull ghcr.io/hadolint/hadolint:latest-debian@@ -91,6 +103,18 @@ && stack install ``` +If you want the+[VS Code Hadolint](https://github.com/michaellzc/vscode-hadolint)+extension to use Hadolint in a container, you can use the following+[wrapper script](https://github.com/hadolint/hadolint/issues/691#issuecomment-932116329):++```bash+#!/bin/bash+dockerfile="$1"+shift+docker run --rm -i hadolint/hadolint hadolint "$@" - < "$dockerfile"+```+ ## CLI  ```bash@@ -100,13 +124,13 @@ ```text hadolint - Dockerfile Linter written in Haskell -Usage: hadolint [-v|--version] [--no-fail] [--no-color] [-c|--config FILENAME] -                [-V|--verbose] [-f|--format ARG] [DOCKERFILE...] -                [--error RULECODE] [--warning RULECODE] [--info RULECODE] -                [--style RULECODE] [--ignore RULECODE] -                [--trusted-registry REGISTRY (e.g. docker.io)] -                [--require-label LABELSCHEMA (e.g. maintainer:text)] -                [--strict-labels] [-t|--failure-theshold THRESHOLD]+Usage: hadolint [-v|--version] [--no-fail] [--no-color] [-c|--config FILENAME]+                [-V|--verbose] [-f|--format ARG] [DOCKERFILE...]+                [--error RULECODE] [--warning RULECODE] [--info RULECODE]+                [--style RULECODE] [--ignore RULECODE]+                [--trusted-registry REGISTRY (e.g. docker.io)]+                [--require-label LABELSCHEMA (e.g. maintainer:text)]+                [--strict-labels] [-t|--failure-threshold THRESHOLD]   Lint Dockerfile for errors and best practices  Available options:@@ -136,7 +160,7 @@                            format requirement `format`   --strict-labels          Do not permit labels other than specified in                            `label-schema`-  -t,--failure-theshold THRESHOLD+  -t,--failure-threshold THRESHOLD                            Exit with failure code only when rules with a                            severity above THRESHOLD are violated. Accepted                            values: [error | warning | info | style | ignore |@@ -145,14 +169,22 @@  ## Configure -Configuration files can be used globally or per project. By default,-`hadolint` will look for a configuration file in the current directory-with the name `.hadolint.yaml` or `.hadolint.yml`+Configuration files can be used globally or per project.+Hadolint looks for configuration files in the following locations or their+platform specific equivalents in this order and uses the first one exclusively:+- `$PWD/.hadolint.yaml`+- `$XDG_CONFIG_HOME/hadolint.yaml`+- `$HOME/.config/hadolint.yaml`+- `$HOME/.hadolint/hadolint.yaml or $HOME/hadolint/config.yaml`+- `$HOME/.hadolint.yaml` +In windows, the `%LOCALAPPDATA%` environment variable is used instead of +`XDG_CONFIG_HOME`. Config files can have either `yaml` or `yml` extensions.+ `hadolint` full `yaml` config file schema  ```yaml-failure-threshold: string               # name of threshold level (error | warning | info | style | ignore | none)                +failure-threshold: string               # name of threshold level (error | warning | info | style | ignore | none) format: string                          # Output format (tty | json | checkstyle | codeclimate | gitlab_codeclimate | codacy) ignored: [string]                       # list of rules label-schema:                           # See Linting Labels below for specific label-schema details@@ -186,7 +218,7 @@  Additionally, `hadolint` can warn you when images from untrusted repositories are being used in Dockerfiles, you can append the-`trustedRegistries` keys to the configuration file as shown below:+`trustedRegistries` keys to the configuration file, as shown below:  ```yaml ignored:@@ -226,18 +258,6 @@     - DL3032 ``` -The global configuration file should be placed in the folder-specified by `XDG_CONFIG_HOME`,-with the name `hadolint.yaml` or `hadolint.yml`. In summary, the-following locations are valid for the configuration file, in order-or preference:--- `$PWD/.hadolint.yaml`-- `$XDG_CONFIG_HOME/hadolint.yaml`-- `~/.config/hadolint.yaml`--In windows, the `%LOCALAPPDATA%` environment variable is used instead of `XDG_CONFIG_HOME`- Additionally, you can pass a custom configuration file in the command line with the `--config` option @@ -245,7 +265,7 @@ hadolint --config /path/to/config.yaml Dockerfile ``` -To pass a custom configuration file (using relative or absolute path) to +To pass a custom configuration file (using relative or absolute path) to a container, use the following command:  ```bash@@ -254,11 +274,40 @@ docker run --rm -i -v /your/path/to/hadolint.yaml:/.config/hadolint.yaml ghcr.io/hadolint/hadolint < Dockerfile ``` +In addition to config files, Hadolint can be configured with environment+variables.+```bash+NO_COLOR=1                               # Truthy value e.g. 1, true or yes+HADOLINT_NOFAIL=1                        # Truthy value e.g. 1, true or yes+HADOLINT_VERBOSE=1                       # Truthy value e.g. 1, true or yes+HADOLINT_FORMAT=json                     # Output format (tty | json | checkstyle | codeclimate | gitlab_codeclimate | codacy | sarif )+HADOLINT_FAILURE_THRESHOLD=info          # threshold level (error | warning | info | style | ignore | none)+HADOLINT_OVERRIDE_ERROR=DL3010,DL3020    # comma separated list of rule codes+HADOLINT_OVERRIDE_WARNING=DL3010,DL3020  # comma separated list of rule codes+HADOLINT_OVERRIDE_INFO=DL3010,DL3020     # comma separated list of rule codes+HADOLINT_OVERRIDE_STYLE=DL3010,DL3020    # comma separated list of rule codes+HADOLINT_IGNORE=DL3010,DL3020            # comma separated list of rule codes+HADOLINT_STRICT_LABELS=1                 # Truthy value e.g. 1, true or yes+HADOLINT_TRUSTED_REGISTRIES              # comma separated list of registry urls+```++## Non-Posix Shells++When using base images with non-posix shells as default (e.g. Windows based+images) a special pragma `hadolint shell` can specify which shell the base image+uses, so that Hadolint can automatically ignore all shell-specific rules.++```Dockerfile+FROM mcr.microsoft.com/windows/servercore:ltsc2022+# hadolint shell=powershell+RUN Get-Process notepad | Stop-Process+```+ ## Inline ignores -It is also possible to ignore rules by using a special comment directly-above the Dockerfile instruction you want to make an exception for.-Ignore rule comments look like+It is also possible to ignore rules by adding a special comment directly+above the Dockerfile statement for which you want to make an exception for.+Such comments look like `# hadolint ignore=DL3001,SC1081`. For example:  ```dockerfile@@ -269,19 +318,19 @@ RUN cd /tmp && echo "hello!" ``` -Inline ignores will only work if place directly above the instruction.+The comment "inline ignores" applies only to the statement following it.  ## Linting Labels -Hadolint has the ability to check that specific labels be present and conform+Hadolint is able to check if specific labels are present and conform to a predefined label schema.-First a label schema must be defined either via commandline:+First, a label schema must be defined either via the command line:  ```bash hadolint --require-label author:text --require-label version:semver Dockerfile ``` -or via config file:+or via the config file:  ```yaml label-schema:@@ -306,20 +355,20 @@ | spdx    | An [SPDX license identifier][spdxid]               | | email   | An email address conforming to [RFC 5322][rfc5322] | -By default, Hadolint ignores any label not specified in the label schema. To-warn on such additional labels, turn on strict labels:+By default, Hadolint ignores any label that is not specified in the label schema. To+warn against such additional labels, turn on strict labels, using the command line:  ```bash hadolint --strict-labels --require-label version:semver Dockerfile ``` -or in the config file:+or the config file:  ```yaml strict-labels: true ``` -When strict labels is enabled, but no label schema has been specified, Hadolint+When strict labels is enabled, but no label schema is specified, `hadolint` will warn if any label is present.  ### Note on dealing with variables in labels@@ -342,8 +391,8 @@  ## Integrations -To get most of `hadolint` it is useful to integrate it as a check to your CI-or to your editor, or as a pre-commit hook, to lint your `Dockerfile` as you+To get most of `hadolint`, it is useful to integrate it as a check in your CI+or into your editor, or as a pre-commit hook, to lint your `Dockerfile` as you write it. See our [Integration][] docs.  - [Code Review Platform Integrations][]@@ -356,16 +405,18 @@ An incomplete list of implemented rules. Click on the error code to get more detailed information. --   Rules with the prefix `DL` originate from `hadolint`. Take a look at-`Rules.hs` to find the implementation of the rules.+-   Rules with the prefix `DL` are from `hadolint`. Have a look at+    `Rules.hs` to find the implementation of the rules. --   Rules with the `SC` prefix originate from **ShellCheck** (Only the most-common rules are listed, there are dozens more)+-   Rules with the `SC` prefix are from **ShellCheck** (only the most+    common rules are listed, there are dozens more).  Please [create an issue][] if you have an idea for a good rule.+ <!--lint disable maximum-line-length-->+ | Rule                                                         | Default Severity | Description                                                                                                                                         |-|:-------------------------------------------------------------|:-----------------|:----------------------------------------------------------------------------------------------------------------------------------------------------|+| :----------------------------------------------------------- | :--------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------- | | [DL3000](https://github.com/hadolint/hadolint/wiki/DL3000)   | Error            | Use absolute WORKDIR.                                                                                                                               | | [DL3001](https://github.com/hadolint/hadolint/wiki/DL3001)   | Info             | 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. | | [DL3002](https://github.com/hadolint/hadolint/wiki/DL3002)   | Warning          | Last user should not be root.                                                                                                                       |@@ -399,7 +450,7 @@ | [DL3032](https://github.com/hadolint/hadolint/wiki/DL3032)   | Warning          | `yum clean all` missing after yum command.                                                                                                          | | [DL3033](https://github.com/hadolint/hadolint/wiki/DL3033)   | Warning          | Specify version with `yum install -y <package>-<version>`                                                                                           | | [DL3034](https://github.com/hadolint/hadolint/wiki/DL3034)   | Warning          | Non-interactive switch missing from `zypper` command: `zypper install -y`                                                                           |-| [DL3035](https://github.com/hadolint/hadolint/wiki/DL3035)   | Warning          | Do not use `zypper dist-upgrade`.                                                                                                                  	|+| [DL3035](https://github.com/hadolint/hadolint/wiki/DL3035)   | Warning          | Do not use `zypper dist-upgrade`.                                                                                                                   | | [DL3036](https://github.com/hadolint/hadolint/wiki/DL3036)   | Warning          | `zypper clean` missing after zypper use.                                                                                                            | | [DL3037](https://github.com/hadolint/hadolint/wiki/DL3037)   | Warning          | Specify version with `zypper install -y <package>[=]<version>`.                                                                                     | | [DL3038](https://github.com/hadolint/hadolint/wiki/DL3038)   | Warning          | Use the `-y` switch to avoid manual input `dnf install -y <package>`                                                                                |@@ -431,7 +482,7 @@ | [DL4005](https://github.com/hadolint/hadolint/wiki/DL4005)   | Warning          | Use `SHELL` to change the default shell.                                                                                                            | | [DL4006](https://github.com/hadolint/hadolint/wiki/DL4006)   | Warning          | Set the `SHELL` option -o pipefail before `RUN` with a pipe in it                                                                                   | | [SC1000](https://github.com/koalaman/shellcheck/wiki/SC1000) |                  | `$` is not used specially and should therefore be escaped.                                                                                          |-| [SC1001](https://github.com/koalaman/shellcheck/wiki/SC1001) |                  | This `\c` will be a regular `'c'`  in this context.                                                                                                 |+| [SC1001](https://github.com/koalaman/shellcheck/wiki/SC1001) |                  | This `\c` will be a regular `'c'` in this context.                                                                                                  | | [SC1007](https://github.com/koalaman/shellcheck/wiki/SC1007) |                  | Remove space after `=` if trying to assign a value (or for empty string, use `var='' ...`).                                                         | | [SC1010](https://github.com/koalaman/shellcheck/wiki/SC1010) |                  | Use semicolon or linefeed before `done` (or quote to make it literal).                                                                              | | [SC1018](https://github.com/koalaman/shellcheck/wiki/SC1018) |                  | This is a unicode non-breaking space. Delete it and retype as space.                                                                                |@@ -463,11 +514,12 @@ | [SC2154](https://github.com/koalaman/shellcheck/wiki/SC2154) |                  | var is referenced but not assigned.                                                                                                                 | | [SC2155](https://github.com/koalaman/shellcheck/wiki/SC2155) |                  | Declare and assign separately to avoid masking return values.                                                                                       | | [SC2164](https://github.com/koalaman/shellcheck/wiki/SC2164) |                  | Use <code>cd ... &#124;&#124; exit</code> in case `cd` fails.                                                                                       |+ <!--lint enable maximum-line-length-->  ## Develop -If you are an experienced Haskeller we would be really thankful if you would+If you are an experienced Haskeller, we would be very grateful if you would tear our code apart in a review.  ### Setup@@ -501,13 +553,13 @@  ### Tests -Run unit tests.+Run unit tests:  ```bash stack test ``` -Run integration tests.+Run integration tests:  ```bash ./integration_test.sh@@ -521,10 +573,15 @@  ## Alternatives -- RedCoolBeans/[dockerlint](https://github.com/RedCoolBeans/dockerlint/)-- projectatomic/[dockerfile_lint](https://github.com/projectatomic/dockerfile_lint/)+-   replicatedhq/[dockerfilelint](https://github.com/replicatedhq/dockerfilelint),+    the other linter used by the [super-linter](https://github.com/github/super-linter/blob/main/README.md#supported-linters) +-   RedCoolBeans/[dockerlint](https://github.com/RedCoolBeans/dockerlint/)++-   projectatomic/[dockerfile_lint](https://github.com/projectatomic/dockerfile_lint/)+ <!-- References -->+ [github-actions-img]: https://github.com/hadolint/hadolint/workflows/Haskell%20Tests/badge.svg?branch=master [github-actions]: https://travis-ci.org/hadolint/hadolint/actions [license-img]: https://img.shields.io/badge/license-GPL--3-blue.svg
app/Main.hs view
@@ -1,262 +1,28 @@-{-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE NamedFieldPuns #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE TemplateHaskell #-}- module Main where -import Control.Applicative import Control.Monad (when)-import qualified Data.Bifunctor as Bifunctor+import Data.Default+import Hadolint (OutputFormat (..), printResults, DLSeverity (..))+import Hadolint.Config+import Prettyprinter 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 qualified Hadolint.Formatter.Format as Format import qualified Hadolint.Rule as Rule import Options.Applicative-  ( Parser,-    ReadM,-    action,-    argument,-    completeWith,-    eitherReader,-    execParser,+  ( execParser,     fullDesc,     header,-    help,     helper,     info,-    long,-    maybeReader,-    metavar,-    option,-    progDesc,-    short,-    showDefaultWith,-    str,-    strOption,-    switch,-    value,+    progDesc   ) -- version from hadolint.cabal file-import qualified Paths_hadolint as Meta-import System.Environment import System.Exit (exitFailure, exitSuccess)-import System.IO (hPutStrLn, stderr)+import System.IO (hPrint, stderr) -data CommandOptions = CommandOptions-  { showVersion :: Bool,-    noFail :: Bool,-    nocolor :: Bool,-    configFile :: Maybe FilePath,-    isVerbose :: Bool,-    format :: Hadolint.OutputFormat,-    dockerfiles :: [String],-    lintingOptions :: Hadolint.LintOptions,-    filePathInReportOption :: Maybe FilePath-  } -toOutputFormat :: String -> Maybe Hadolint.OutputFormat-toOutputFormat "json" = Just Hadolint.Json-toOutputFormat "sonarqube" = Just Hadolint.SonarQube-toOutputFormat "tty" = Just Hadolint.TTY-toOutputFormat "codeclimate" = Just Hadolint.CodeclimateJson-toOutputFormat "gitlab_codeclimate" = Just Hadolint.GitlabCodeclimateJson-toOutputFormat "checkstyle" = Just Hadolint.Checkstyle-toOutputFormat "codacy" = Just Hadolint.Codacy-toOutputFormat _ = Nothing--showFormat :: Hadolint.OutputFormat -> String-showFormat Hadolint.Json = "json"-showFormat Hadolint.SonarQube = "sonarqube"-showFormat Hadolint.TTY = "tty"-showFormat Hadolint.CodeclimateJson = "codeclimate"-showFormat Hadolint.GitlabCodeclimateJson = "gitlab_codeclimate"-showFormat Hadolint.Checkstyle = "checkstyle"-showFormat Hadolint.Codacy = "codacy"--toNofailSeverity :: String -> Maybe Rule.DLSeverity-toNofailSeverity "error" = Just Rule.DLErrorC-toNofailSeverity "warning" = Just Rule.DLWarningC-toNofailSeverity "info" = Just Rule.DLInfoC-toNofailSeverity "style" = Just Rule.DLStyleC-toNofailSeverity "ignore" = Just Rule.DLIgnoreC-toNofailSeverity "none" = Just Rule.DLIgnoreC-toNofailSeverity _ = Nothing--parseOptions :: Parser CommandOptions-parseOptions =-  CommandOptions-    <$> version -- CLI options parser definition-    <*> noFail-    <*> nocolor-    <*> configFile-    <*> isVerbose-    <*> outputFormat-    <*> files-    <*> lintOptions-    <*> filePathInReportOption-  where-    version = switch (long "version" <> short 'v' <> help "Show version")--    noFail = switch (long "no-fail" <> help "Don't exit with a failure status code when any rule is violated")--    noFailCutoff =-      option-        (maybeReader toNofailSeverity)-        ( short 't'-            <> long "failure-threshold"-            <> help-              "Exit with failure code only when rules with a severity \-              \equal to or above THRESHOLD are violated. Accepted values: \-              \[error | warning | info | style | ignore | none]"-            <> value Rule.DLInfoC-            <> metavar "THRESHOLD"-            <> showDefaultWith (Text.unpack . Format.severityText)-            <> completeWith ["error", "warning", "info", "style", "ignore", "none"]-        )--    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-            ( long "config" <> short 'c' <> metavar "FILENAME"-                <> help "Path to the configuration file"-            )-        )--    isVerbose = switch (long "verbose" <> short 'V' <> help "Enables verbose logging of hadolint's output to stderr")--    outputFormat =-      option-        (maybeReader toOutputFormat)-        ( long "format"-            <> short 'f' -- options for the output format-            <> help-              "The output format for the results [tty | json | checkstyle | codeclimate | gitlab_codeclimate | codacy | sonarqube]"-            <> value Hadolint.TTY-            <> showDefaultWith showFormat -- The default value-            <> completeWith ["tty", "json", "checkstyle", "codeclimate", "gitlab_codeclimate", "codacy", "sonarqube"]-        )--    errorList =-      many-        ( strOption-            ( 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"-            )-        )--    infoList =-      many-        ( strOption-            ( 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"-            )-        )--    ignoreList =-      many-        ( strOption-            ( long "ignore"-                <> help "A rule to ignore. If present, the ignore list in the config file is ignored"-                <> metavar "RULECODE"-            )-        )--    files = many (argument str (metavar "DOCKERFILE..." <> action "file"))--    lintOptions =-      Hadolint.LintOptions-        <$> errorList-        <*> warningList-        <*> infoList-        <*> styleList-        <*> ignoreList-        <*> parseRulesConfig-        <*> noFailCutoff--    filePathInReportOption =-      optional-        ( strOption-            ( long "file-path-in-report" <> metavar "FILEPATHINREPORT"-                <> help "The file path referenced in the generated report. This only applies for the 'checkstyle' format and is useful when running Hadolint with Docker to set the correct file path."-            )-        )--    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-        <$> parseAllowedRegistries-        <*> labels-        <*> strictlabels--    parseAllowedRegistries =-      Set.fromList . fmap fromString-        <$> many-          ( strOption-              ( long "trusted-registry"-                  <> help "A docker registry to allow to appear in FROM instructions"-                  <> metavar "REGISTRY (e.g. docker.io)"-              )-          )--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 -> Rule.DLSeverity -> Bool+noFailure :: Hadolint.Result s e -> DLSeverity -> Bool noFailure (Hadolint.Result _ Seq.Empty Seq.Empty) _ = True noFailure (Hadolint.Result _ Seq.Empty fails) cutoff =   Seq.null (Seq.filter (\f -> Rule.severity f <= cutoff) fails)@@ -264,56 +30,64 @@  exitProgram ::   Foldable f =>-  CommandOptions ->-  Hadolint.LintOptions ->+  Configuration ->   f (Hadolint.Result s e) ->   IO ()-exitProgram cmd conf res-  | noFail cmd = exitSuccess-  | Hadolint.shallSkipErrorStatus (format cmd) = exitSuccess-  | all (`noFailure` Hadolint.failThreshold conf) res = exitSuccess+exitProgram conf res+  | noFail conf = exitSuccess+  | CodeclimateJson == format conf = exitSuccess+  | Codacy == format conf = exitSuccess+  | all (`noFailure` failureThreshold conf) res = exitSuccess   | otherwise = exitFailure -runLint :: CommandOptions -> Hadolint.LintOptions -> NonEmpty.NonEmpty String -> IO ()-runLint cmd conf files = do+runLint ::+  CommandlineConfig ->+  Configuration ->+  IO ()+runLint cmd conf = do+  let files = NonEmpty.fromList $ dockerfiles cmd+      filePathInReport = filePathInReportOption cmd   res <- Hadolint.lintIO conf files-  noColorEnv <- lookupEnv "NO_COLOR"-  let noColor = nocolor cmd || isJust noColorEnv-  let filePathInReport = filePathInReportOption cmd-  Hadolint.printResults (format cmd) noColor filePathInReport res-  exitProgram cmd conf res+  printResults (format conf) (noColor conf) filePathInReport res+  exitProgram conf res +execute :: CommandlineConfig -> Configuration -> IO ()+execute CommandlineConfig {showVersion = True} _ =+  putStrLn Hadolint.getVersion >> exitSuccess+execute CommandlineConfig {dockerfiles = []} _ =+  putStrLn "Please provide a Dockerfile" >> exitFailure+execute cmd config = runLint cmd config++ main :: IO () main = do-  cmd <- execParser opts-  execute cmd+  invokedWith <- execParser opts+  fromEnvironment <- getConfigFromEnvironment+  let fromCommandline = configuration invokedWith+  eitherFromConfigfile <- getConfigFromFile+    (configFile invokedWith) (Just True == partialVerbose fromCommandline)+  fromConfigfile <- getConfigFromEither eitherFromConfigfile++  let runningConfig =+        applyPartialConfiguration+          def+          ( fromEnvironment <> fromConfigfile <> fromCommandline )++  when (verbose runningConfig) (hPrint stderr (pretty runningConfig))+  execute invokedWith runningConfig   where-    execute CommandOptions {showVersion = True} = putStrLn getVersion >> exitSuccess-    execute CommandOptions {dockerfiles = []} =-      putStrLn "Please provide a Dockerfile" >> exitFailure-    execute cmd = do-      maybeConfig <- Hadolint.getConfig (configFile cmd)-      when (isVerbose cmd) (hPutStrLn stderr $ getFilePathDescription maybeConfig)-      lintConfig <- Hadolint.applyConfig maybeConfig (lintingOptions cmd)-      let files = NonEmpty.fromList (dockerfiles cmd)-      case lintConfig of-        Left err -> error err-        Right conf -> runLint cmd conf files     opts =       info-        (helper <*> parseOptions)-        ( fullDesc <> progDesc "Lint Dockerfile for errors and best practices"+        ( helper <*> parseCommandline )+        ( fullDesc             <> header "hadolint - Dockerfile Linter written in Haskell"+            <> progDesc "Lint Dockerfile for errors and best practices"         ) -getVersion :: String-getVersion-  | version == "UNKNOWN" =-    "Haskell Dockerfile Linter " ++ Data.Version.showVersion Meta.version ++ "-no-git"-  | otherwise = "Haskell Dockerfile Linter " ++ version-  where-    version = $(Development.GitRev.gitDescribe)--getFilePathDescription :: Maybe FilePath -> String-getFilePathDescription Nothing = "No configuration was specified. Using default configuration"-getFilePathDescription (Just filepath) = "Configuration file used: " ++ filepath+    -- Either return the config or print the error message+    getConfigFromEither ei =+      case ei of+        Left err -> do+          hPrint stderr err+          return mempty+        Right conf -> return conf
hadolint.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack  name:           hadolint-version:        2.7.0+version:        2.8.0 synopsis:       Dockerfile Linter JavaScript API description:    A smarter Dockerfile linter that helps you build best practice Docker images. category:       Development@@ -32,15 +32,22 @@   exposed-modules:       Hadolint       Hadolint.Config+      Hadolint.Config.Commandline+      Hadolint.Config.Configfile+      Hadolint.Config.Configuration+      Hadolint.Config.Environment+      Hadolint.Formatter       Hadolint.Formatter.Checkstyle       Hadolint.Formatter.Codacy       Hadolint.Formatter.Codeclimate       Hadolint.Formatter.Format       Hadolint.Formatter.Json+      Hadolint.Formatter.Sarif       Hadolint.Formatter.SonarQube       Hadolint.Formatter.TTY-      Hadolint.Ignore       Hadolint.Lint+      Hadolint.Meta+      Hadolint.Pragma       Hadolint.Process       Hadolint.Rule       Hadolint.Rule.DL3000@@ -116,13 +123,16 @@   hs-source-dirs:       src   default-extensions:-      OverloadedStrings-      NamedFieldPuns-      DeriveGeneric       DeriveAnyClass+      DeriveGeneric+      NamedFieldPuns+      OverloadedStrings+      PatternSynonyms       RecordWildCards-      StrictData       ScopedTypeVariables+      StrictData+      TemplateHaskell+      TupleSections       PatternSynonyms   ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints -optP-Wno-nonportable-include-path -flate-dmd-anal   build-depends:@@ -135,18 +145,22 @@     , colourista     , containers     , cryptonite+    , data-default     , deepseq ==1.4.4.*     , directory >=1.3.0     , email-validate     , filepath     , foldl+    , gitrev >=1.3.1     , ilist-    , language-docker >=10.1.0 && <11+    , language-docker >=10.3.0 && <11     , megaparsec >=9.0.0     , mtl     , network-uri+    , optparse-applicative >=0.14.0     , parallel     , parsec >=3.1.14+    , prettyprinter >=1.7.0     , semver     , spdx     , split >=0.2@@ -163,24 +177,27 @@   hs-source-dirs:       app   default-extensions:-      OverloadedStrings-      NamedFieldPuns-      DeriveGeneric       DeriveAnyClass+      DeriveGeneric+      NamedFieldPuns+      OverloadedStrings+      PatternSynonyms       RecordWildCards-      StrictData       ScopedTypeVariables-      PatternSynonyms+      StrictData       TemplateHaskell+      TupleSections+      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-    , gitrev >=1.3.1+    , data-default     , hadolint-    , language-docker >=10.1.0 && <11+    , language-docker >=10.3.0 && <11     , megaparsec >=9.0.0     , optparse-applicative >=0.14.0+    , prettyprinter >=1.7.0     , text   if flag(static) && !(os(osx))     ld-options: -static -pthread@@ -190,85 +207,97 @@   type: exitcode-stdio-1.0   main-is: Spec.hs   other-modules:-      ConfigSpec-      DL3000-      DL3001-      DL3002-      DL3003-      DL3004-      DL3005-      DL3006-      DL3007-      DL3008-      DL3009-      DL3010-      DL3011-      DL3012-      DL3013-      DL3014-      DL3015-      DL3016-      DL3018-      DL3019-      DL3020-      DL3021-      DL3022-      DL3023-      DL3024-      DL3025-      DL3026-      DL3027-      DL3028-      DL3029-      DL3030-      DL3032-      DL3033-      DL3034-      DL3035-      DL3036-      DL3037-      DL3038-      DL3040-      DL3041-      DL3042-      DL3043-      DL3044-      DL3045-      DL3046-      DL3047-      DL3048-      DL3049-      DL3050-      DL3051-      DL3052-      DL3053-      DL3054-      DL3055-      DL3056-      DL3057-      DL3058-      DL3059-      DL3060-      DL4000-      DL4001-      DL4003-      DL4004-      DL4005-      DL4006+      Hadolint.Config.CommandlineSpec+      Hadolint.Config.ConfigfileSpec+      Hadolint.Config.ConfigurationSpec+      Hadolint.Config.EnvironmentSpec+      Hadolint.Config.SpecHook+      Hadolint.Formatter.ParseErrorSpec+      Hadolint.Formatter.SarifSpec+      Hadolint.Formatter.TTYSpec+      Hadolint.PragmaSpec+      Hadolint.Rule.DL3000Spec+      Hadolint.Rule.DL3001Spec+      Hadolint.Rule.DL3002Spec+      Hadolint.Rule.DL3003Spec+      Hadolint.Rule.DL3004Spec+      Hadolint.Rule.DL3005Spec+      Hadolint.Rule.DL3006Spec+      Hadolint.Rule.DL3007Spec+      Hadolint.Rule.DL3008Spec+      Hadolint.Rule.DL3009Spec+      Hadolint.Rule.DL3010Spec+      Hadolint.Rule.DL3011Spec+      Hadolint.Rule.DL3012Spec+      Hadolint.Rule.DL3013Spec+      Hadolint.Rule.DL3014Spec+      Hadolint.Rule.DL3015Spec+      Hadolint.Rule.DL3016Spec+      Hadolint.Rule.DL3018Spec+      Hadolint.Rule.DL3019Spec+      Hadolint.Rule.DL3020Spec+      Hadolint.Rule.DL3021Spec+      Hadolint.Rule.DL3022Spec+      Hadolint.Rule.DL3023Spec+      Hadolint.Rule.DL3024Spec+      Hadolint.Rule.DL3025Spec+      Hadolint.Rule.DL3026Spec+      Hadolint.Rule.DL3027Spec+      Hadolint.Rule.DL3028Spec+      Hadolint.Rule.DL3029Spec+      Hadolint.Rule.DL3030Spec+      Hadolint.Rule.DL3032Spec+      Hadolint.Rule.DL3033Spec+      Hadolint.Rule.DL3034Spec+      Hadolint.Rule.DL3035Spec+      Hadolint.Rule.DL3036Spec+      Hadolint.Rule.DL3037Spec+      Hadolint.Rule.DL3038Spec+      Hadolint.Rule.DL3040Spec+      Hadolint.Rule.DL3041Spec+      Hadolint.Rule.DL3042Spec+      Hadolint.Rule.DL3043Spec+      Hadolint.Rule.DL3044Spec+      Hadolint.Rule.DL3045Spec+      Hadolint.Rule.DL3046Spec+      Hadolint.Rule.DL3047Spec+      Hadolint.Rule.DL3048Spec+      Hadolint.Rule.DL3049Spec+      Hadolint.Rule.DL3050Spec+      Hadolint.Rule.DL3051Spec+      Hadolint.Rule.DL3052Spec+      Hadolint.Rule.DL3053Spec+      Hadolint.Rule.DL3054Spec+      Hadolint.Rule.DL3055Spec+      Hadolint.Rule.DL3056Spec+      Hadolint.Rule.DL3057Spec+      Hadolint.Rule.DL3058Spec+      Hadolint.Rule.DL3059Spec+      Hadolint.Rule.DL3060Spec+      Hadolint.Rule.DL4000Spec+      Hadolint.Rule.DL4001Spec+      Hadolint.Rule.DL4003Spec+      Hadolint.Rule.DL4004Spec+      Hadolint.Rule.DL4005Spec+      Hadolint.Rule.DL4006Spec+      Hadolint.Rule.ShellcheckSpec+      Hadolint.ShellSpec       Helpers-      Shellcheck-      ShellSpec+      RegressionSpec       Paths_hadolint   hs-source-dirs:       test   default-extensions:-      OverloadedStrings-      NamedFieldPuns-      DeriveGeneric       DeriveAnyClass+      DeriveGeneric+      NamedFieldPuns+      OverloadedStrings+      PatternSynonyms       RecordWildCards-      StrictData       ScopedTypeVariables+      StrictData+      TemplateHaskell+      TupleSections       PatternSynonyms       ImplicitParams       OverloadedLists@@ -281,11 +310,14 @@     , base >=4.8 && <5     , bytestring >=0.10     , containers+    , data-default     , foldl     , hadolint-    , hspec-    , language-docker >=10.1.0 && <11+    , hspec >=2.8.3+    , language-docker >=10.3.0 && <11     , megaparsec >=9.0.0+    , optparse-applicative >=0.14.0+    , silently     , split >=0.2     , text   default-language: Haskell2010
src/Hadolint.hs view
@@ -1,47 +1,28 @@ module Hadolint-  ( module Hadolint.Lint,-    module Hadolint.Process,+  (     module Hadolint.Config,-    Result (..),-    OutputFormat (..),-    shallSkipErrorStatus,-    printResults,+    module Hadolint.Formatter,+    module Hadolint.Lint,+    module Hadolint.Meta,+    module Hadolint.Process,++    CheckFailure (..),+    DLSeverity (..),+    LabelSchema,+    LabelName,+    LabelType(..)   ) where -import Data.Text (Text) import Hadolint.Config-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.SonarQube-import qualified Hadolint.Formatter.TTY+import Hadolint.Formatter import Hadolint.Lint+import Hadolint.Meta import Hadolint.Process-import Language.Docker.Parser (DockerfileError)--data OutputFormat-  = Json-  | SonarQube-  | TTY-  | CodeclimateJson-  | GitlabCodeclimateJson-  | Checkstyle-  | Codacy-  deriving (Show, Eq)--shallSkipErrorStatus :: OutputFormat -> Bool-shallSkipErrorStatus format = format `elem` [CodeclimateJson, Codacy]--printResults :: Foldable f => OutputFormat -> Bool -> Maybe FilePath -> f (Result Text DockerfileError) -> IO ()-printResults format nocolor filePathInReport allResults =-  case format of-    TTY -> Hadolint.Formatter.TTY.printResults allResults nocolor-    SonarQube -> Hadolint.Formatter.SonarQube.printResults allResults-    Json -> Hadolint.Formatter.Json.printResults allResults-    Checkstyle -> Hadolint.Formatter.Checkstyle.printResults allResults filePathInReport-    CodeclimateJson -> Hadolint.Formatter.Codeclimate.printResults allResults-    GitlabCodeclimateJson -> Hadolint.Formatter.Codeclimate.printGitlabResults allResults-    Codacy -> Hadolint.Formatter.Codacy.printResults allResults+import Hadolint.Rule+  ( CheckFailure (..),+    DLSeverity (..),+    LabelName,+    LabelSchema,+    LabelType (..),+  )
src/Hadolint/Config.hs view
@@ -1,170 +1,14 @@ module Hadolint.Config-  ( applyConfig,-    getConfig,-    ConfigFile (..),-    OverrideConfig (..),-  )-where+  ( module Hadolint.Config.Configuration, -import Control.Applicative ((<|>))-import qualified Control.Foldl.Text as Text-import Control.Monad (filterM)-import qualified Data.ByteString as Bytes-import Data.Coerce (coerce)-import Data.Maybe (fromMaybe, listToMaybe)-import qualified Data.Set as Set-import Data.YAML ((.:?))-import qualified Data.YAML as Yaml-import GHC.Generics (Generic)-import qualified Hadolint.Lint as Lint-import qualified Hadolint.Process as Process-import qualified Hadolint.Rule as Rule-import qualified Language.Docker as Docker-import System.Directory-  ( XdgDirectory (..),-    doesFileExist,-    getCurrentDirectory,-    getXdgDirectory,+    CommandlineConfig (..),+    getConfigFromEnvironment,+    getConfigFromFile,+    parseCommandline   )-import System.FilePath ((</>))--data OverrideConfig = OverrideConfig-  { overrideErrorRules :: Maybe [Lint.ErrorRule],-    overrideWarningRules :: Maybe [Lint.WarningRule],-    overrideInfoRules :: Maybe [Lint.InfoRule],-    overrideStyleRules :: Maybe [Lint.StyleRule]-  }-  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],-    labelSchemaConfig :: Maybe Rule.LabelSchema,-    strictLabelSchema :: Maybe Bool,-    failureThreshold :: Maybe Rule.DLSeverity-  }-  deriving (Show, Eq, Generic)--instance Yaml.FromYAML OverrideConfig where-  parseYAML = Yaml.withMap "OverrideConfig" $ \m -> do-    err <- m .:? "error"-    wrn <- m .:? "warning"-    inf <- m .:? "info"-    sty <- m .:? "style"-    let overrideErrorRules = coerce (err :: Maybe [Text.Text])-        overrideWarningRules = coerce (wrn :: Maybe [Text.Text])-        overrideInfoRules = coerce (inf :: Maybe [Text.Text])-        overrideStyleRules = coerce (sty:: Maybe [Text.Text])-    return OverrideConfig {..}--instance Yaml.FromYAML ConfigFile where-  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"-    failureThreshold <- m .:? "failure-threshold"-    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--- file. If there is an error parsing the default config file, this function will--- return the error string.-applyConfig :: Maybe FilePath -> Lint.LintOptions -> IO (Either String Lint.LintOptions)-applyConfig maybeConfig o-  | not (Prelude.null (Lint.ignoreRules o)) && Lint.rulesConfig o /= mempty = return (Right o)-  | otherwise = do-    case maybeConfig of-      Nothing -> return (Right o)-      Just config -> parseAndApply config-  where-    parseAndApply :: FilePath -> IO (Either String Lint.LintOptions)-    parseAndApply configFile = do-      contents <- Bytes.readFile configFile-      return $ case Yaml.decode1Strict contents of-        Left (_, err) -> Left (formatError err configFile)-        Right config -> Right $ fromMaybe o (applyOverride config)--    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-        overrideThreshold <- failureThreshold <|> Just mempty--        trusted <- Set.fromList . coerce <$> (trustedRegistries <|> Just mempty)-        schema <- labelSchemaConfig <|> Just mempty-        strictLabels <- strictLabelSchema <|> Just False--        let rulesConfig = Lint.rulesConfig o--        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-                  },-            Lint.failThreshold = Lint.failThreshold o <> overrideThreshold-            }--    ifNull value override = if null value then override else value--    formatError err config =-      Prelude.unlines-        [ "Error parsing your config file in  '" ++ config ++ "':",-          "It should contain one of the keys 'override', 'ignored'",-          "or 'trustedRegistries'. For example:\n",-          "ignored:",-          "\t- DL3000",-          "\t- SC1099\n\n",-          "The key 'override' should contain only lists with names 'error',",-          "'warning', 'info' or 'style', which each name rules to override the",-          "severity on. For example:\n",-          "override:",-          "\terror:",-          "\t\t- DL3008\n\n",-          "The key 'trustedRegistries' should contain the names of the allowed docker registries:\n",-          "trustedRegistries:",-          "\t- docker.io",-          "\t- my-company.com",-          "",-          err-        ]+where --- | Gets the configuration file which Hadolint uses-getConfig :: Maybe FilePath -> IO (Maybe FilePath)-getConfig maybeConfig =-  case maybeConfig of-    Nothing -> findConfig-    _ -> return maybeConfig-  where-    findConfig :: IO (Maybe FilePath)-    findConfig = do-      localConfigFiles <- traverse-                            (\filePath -> (</> filePath) <$> getCurrentDirectory)-                            (fmap ("."++) acceptedConfigs)-      configFiles <- traverse (getXdgDirectory XdgConfig) acceptedConfigs-      listToMaybe <$> filterM doesFileExist (localConfigFiles ++ configFiles)-      where-        acceptedConfigs = ["hadolint.yaml", "hadolint.yml"]+import Hadolint.Config.Commandline+import Hadolint.Config.Configuration+import Hadolint.Config.Environment+import Hadolint.Config.Configfile
+ src/Hadolint/Config/Commandline.hs view
@@ -0,0 +1,263 @@+module Hadolint.Config.Commandline+  ( CommandlineConfig (..),+    parseCommandline+  )+where++import Control.Applicative+import Data.Bifunctor (second)+import qualified Data.Map as Map+import qualified Data.Set as Set+import Data.String (IsString (fromString))+import Data.Text (Text, pack, unpack, breakOn, drop)+import Hadolint.Formatter.Format (readMaybeOutputFormat)+import Hadolint.Config.Configuration+import Hadolint.Rule+  ( LabelName,+    LabelType,+    readEitherLabelType,+    readEitherSeverity+  )+import Options.Applicative+  ( Parser,+    ReadM,+    action,+    argument,+    completeWith,+    eitherReader,+    flag',+    help,+    long,+    maybeReader,+    metavar,+    option,+    short,+    str,+    strOption,+    switch,+  )+++data CommandlineConfig =+  CommandlineConfig+    { showVersion :: Bool,+      configFile :: Maybe FilePath,+      dockerfiles :: [String],+      filePathInReportOption :: Maybe FilePath,+      configuration :: PartialConfiguration+    }+  deriving (Eq, Show)++parseCommandline :: Parser CommandlineConfig+parseCommandline =+  CommandlineConfig+    <$> parseVersion+    <*> parseConfigFile+    <*> parseFiles+    <*> parseFilePathInReportOption+    <*> parseConfiguration+  where+    parseVersion = switch (long "version" <> short 'v' <> help "Show version")++    parseConfigFile =+      optional+        ( strOption+            ( long "config" <> short 'c' <> metavar "FILENAME"+                <> help "Path to the configuration file"+            )+        )++    parseFiles = many (argument str (metavar "DOCKERFILE..." <> action "file"))++    parseFilePathInReportOption =+      optional+        ( strOption+            ( long "file-path-in-report"+                <> metavar "FILEPATHINREPORT"+                <> help "The file path referenced in the generated report.\+                        \ This only applies for the 'checkstyle' format and is\+                        \ useful when running Hadolint with Docker to set the\+                        \ correct file path."+            )+        )++    parseConfiguration =+      PartialConfiguration+        <$> parseNoFail+        <*> parseNoColor+        <*> parseVerbose+        <*> parseOutputFormat+        <*> parseErrorList+        <*> parseWarningList+        <*> parseInfoList+        <*> parseStyleList+        <*> parseIgnoreList+        <*> parseAllowedRegistries+        <*> parseLabelSchema+        <*> parseStrictlabels+        <*> parseFailureThreshold++    -- All optional flags with boolean value must not have a default value. The+    -- optional parser then converts it to Nothing.+    -- This is to ensure if they are not set, this is correctly mirrored in the+    -- parsed Configuration and then their behaviour with respect to+    -- configurations from environment variables or config files is correct.+    parseNoFail =+      optional+        ( flag' True+            ( long "no-fail"+                <> help "Don't exit with a failure status code when any rule is\+                        \ violated"+            )+        )++    parseNoColor =+      optional+        ( flag' True+            ( long "no-color"+              <> help "Don't colorize output"+            )+        )++    parseVerbose =+      optional+        ( flag' True+            ( long "verbose"+                <> short 'V'+                <> help "Enables verbose logging of hadolint's output to stderr"+            )+        )++    parseOutputFormat =+      optional $+        option+          ( maybeReader (readMaybeOutputFormat . pack) )+          ( long "format"+              <> short 'f' -- options for the output format+              <> help+                "The output format for the results [tty | json | checkstyle |\+                \ codeclimate | gitlab_codeclimate | codacy | sonarqube |\+                \ sarif] (default: tty)"+              <> completeWith+                  [ "tty",+                    "json",+                    "checkstyle",+                    "codeclimate",+                    "gitlab_codeclimate",+                    "codacy",+                    "sonarqube",+                    "sarif"+                  ]+          )++    parseErrorList =+      many+        ( strOption+            ( long "error"+                <> help "Make the rule `RULECODE` have the level `error`"+                <> metavar "RULECODE"+            )+        )++    parseWarningList =+      many+        ( strOption+            ( long "warning"+                <> help "Make the rule `RULECODE` have the level `warning`"+                <> metavar "RULECODE"+            )+        )++    parseInfoList =+      many+        ( strOption+            ( long "info"+                <> help "Make the rule `RULECODE` have the level `info`"+                <> metavar "RULECODE"+            )+        )++    parseStyleList =+      many+        ( strOption+            ( long "style"+                <> help "Make the rule `RULECODE` have the level `style`"+                <> metavar "RULECODE"+            )+        )++    parseIgnoreList =+      many+        ( strOption+            ( long "ignore"+                <> metavar "RULECODE"+                <> help "A rule to ignore. If present, the ignore list in the\+                        \ config file is ignored"+            )+        )++    parseAllowedRegistries =+      Set.fromList . fmap fromString+        <$> many+          ( strOption+              ( long "trusted-registry"+                  <> metavar "REGISTRY (e.g. docker.io)"+                  <> help "A docker registry to allow to appear in FROM \+                          \instructions"+              )+          )++    parseLabelSchema =+      Map.fromList+        <$> many+          ( option+              readSingleLabelSchema+              ( long "require-label"+                  <> metavar "LABELSCHEMA (e.g. maintainer:text)"+                  <> help "The option --require-label=label:format makes\+                          \ Hadolint check that the label `label` conforms to\+                          \ format requirement `format`"+              )+          )++    parseStrictlabels =+      optional+        ( flag' True+            ( long "strict-labels"+                <> help "Do not permit labels other than specified in\+                        \ `label-schema`"+            )+        )++    parseFailureThreshold =+      optional $+        option+          ( eitherReader (readEitherSeverity . pack) )+          ( short 't'+              <> long "failure-threshold"+              <> help+                "Exit with failure code only when rules with a severity equal\+                \ to or above THRESHOLD are violated. Accepted values: [error\+                \ | warning | info | style | ignore | none] (default: info)"+              <> metavar "THRESHOLD"+              <> completeWith+                  [ "error",+                    "warning",+                    "info",+                    "style",+                    "ignore",+                    "none"+                  ]+          )+++type SingleLabelSchema = (LabelName, LabelType)++readSingleLabelSchema :: ReadM SingleLabelSchema+readSingleLabelSchema = eitherReader $ \s -> labelParser (pack s)++labelParser :: Text -> Either String (LabelName, LabelType)+labelParser l =+  case second (readEitherLabelType . Data.Text.drop 1) $ breakOn ":" l of+    (ln, Right lt) -> Right (ln, lt)+    (_, Left e) -> Left (unpack e)
+ src/Hadolint/Config/Configfile.hs view
@@ -0,0 +1,101 @@+module Hadolint.Config.Configfile+  ( getConfigFromFile+  )+where++import Control.Monad (when, filterM)+import Data.Maybe (listToMaybe)+import Data.YAML as Yaml+import qualified Data.ByteString as Bytes+import Hadolint.Config.Configuration (PartialConfiguration (..))+import System.Directory+  ( XdgDirectory (..),+    doesFileExist,+    getCurrentDirectory,+    getAppUserDataDirectory,+    getUserDocumentsDirectory,+    getXdgDirectory,+  )+import System.FilePath ((</>))+import System.IO (hPrint, stderr)+++getConfigFromFile ::+  Maybe FilePath -> Bool -> IO (Either String PartialConfiguration)+getConfigFromFile maybeExplicitPath verbose = do+  maybePath <- getConfig maybeExplicitPath+  when verbose $ hPrint stderr $ getFilePathDescription maybePath+  case maybePath of+    Nothing -> return $ Right mempty+    Just path -> readConfig path++readConfig :: FilePath -> IO (Either String PartialConfiguration)+readConfig path = do+  contents <- Bytes.readFile path+  return $ case Yaml.decode1Strict contents of+    Left (_, err) -> Left (formatError err path)+    Right config -> Right config++getFilePathDescription :: Maybe FilePath -> String+getFilePathDescription Nothing =+  "No configuration was specified. Using default configuration"+getFilePathDescription (Just filepath) = "Configuration file used: " ++ filepath++formatError :: String -> String -> String+formatError err config =+  Prelude.unlines+    [ "Error parsing your config file in  '" ++ config ++ "':",+      "It should contain one of the keys 'override', 'ignored'",+      "or 'trustedRegistries'. For example:\n",+      "ignored:",+      "\t- DL3000",+      "\t- SC1099\n\n",+      "The key 'override' should contain only lists with names 'error',",+      "'warning', 'info' or 'style', which each name rules to override the",+      "severity on. For example:\n",+      "override:",+      "\terror:",+      "\t\t- DL3008\n\n",+      "The key 'trustedRegistries' should contain the names of the allowed",+      "docker registries:\n",+      "trustedRegistries:",+      "\t- docker.io",+      "\t- my-company.com",+      "",+      err+    ]++-- | Gets the configuration file which Hadolint uses+getConfig :: Maybe FilePath -> IO (Maybe FilePath)+getConfig maybeConfig =+  case maybeConfig of+    Nothing -> findConfig+    _ -> return maybeConfig++-- | If no configuration file path was given on the command line, Hadolint+-- searches these locations or their equivalents on MacOS/Windows:+--  - $(pwd)/.hadolint.{yaml|yml}+--  - $HOME/.config/hadolint.{yaml|yml}+--  - $HOME/.hadolint/{hadolint|config}.{yaml|yml}+--  - $HOME/.hadolint.{yaml|yml}+-- The first file found is used, all other are ignored.+findConfig :: IO (Maybe FilePath)+findConfig = do+  filesInCWD <- traverse+                  (\filePath -> (</> filePath) <$> getCurrentDirectory)+                  hiddenConfigs+  filesInXdgConfig <- traverse (getXdgDirectory XdgConfig) visibleConfigs+  filesInAppData <- traverse+                      (\fp -> (</> fp) <$> getAppUserDataDirectory "hadolint")+                      (visibleConfigs <> moreConfigs)+  filesInHome <- traverse+                   (\fp -> (</> fp) <$> getUserDocumentsDirectory)+                   hiddenConfigs+  listToMaybe+    <$> filterM+          doesFileExist+          (filesInCWD <> filesInXdgConfig <> filesInAppData <> filesInHome)+  where+    hiddenConfigs = [".hadolint.yaml", ".hadolint.yml"]+    visibleConfigs = ["hadolint.yaml", "hadolint.yml"]+    moreConfigs = ["config.yaml", "config.yml"]
+ src/Hadolint/Config/Configuration.hs view
@@ -0,0 +1,229 @@+module Hadolint.Config.Configuration+  ( Configuration (..),+    PartialConfiguration (..),++    applyPartialConfiguration,+  )+where++import Data.Maybe (fromMaybe)+import Control.Applicative+import Data.Coerce (coerce)+import Data.Default+import Data.Text (Text)+import Data.YAML ((.:?), (.!=))+import GHC.Generics (Generic)+import Hadolint.Formatter.Format (OutputFormat (..))+import Hadolint.Rule (RuleCode (..), DLSeverity (..), LabelSchema)+import Language.Docker+import Prettyprinter+import qualified Data.Map as Map+import qualified Data.Set as Set+import qualified Data.YAML as Yaml+++data Configuration =+  Configuration+    { noFail :: Bool,+      noColor :: Bool,+      verbose :: Bool,+      format :: OutputFormat,+      errorRules :: [RuleCode],+      warningRules :: [RuleCode],+      infoRules :: [RuleCode],+      styleRules :: [RuleCode],+      ignoreRules :: [RuleCode],+      allowedRegistries :: Set.Set Registry,+      labelSchema :: LabelSchema,+      strictLabels :: Bool,+      failureThreshold :: DLSeverity+    }+  deriving (Eq, Show)++instance Default Configuration where+  def =+    Configuration+      False+      False+      False+      def+      mempty+      mempty+      mempty+      mempty+      mempty+      mempty+      mempty+      False+      def++applyPartialConfiguration ::+  Configuration -> PartialConfiguration -> Configuration+applyPartialConfiguration config partial =+  Configuration+    (fromMaybe (noFail config) (partialNoFail partial))+    (fromMaybe (noColor config) (partialNoColor partial))+    (fromMaybe (verbose config) (partialVerbose partial))+    (fromMaybe (format config) (partialFormat partial))+    (errorRules config <> partialErrorRules partial)+    (warningRules config <> partialWarningRules partial)+    (infoRules config <> partialInfoRules partial)+    (styleRules config <> partialStyleRules partial)+    (ignoreRules config <> partialIgnoreRules partial)+    (allowedRegistries config <> partialAllowedRegistries partial)+    (labelSchema config <> partialLabelSchema partial)+    (fromMaybe (strictLabels config) (partialStrictLabels partial))+    (fromMaybe (failureThreshold config) (partialFailureThreshold partial))++instance Pretty Configuration where+  pretty c =+    nest 2+      ( vsep+          [ "Configuration:",+            "no fail:" <+> pretty (noFail c),+            "no color:" <+> pretty (noColor c),+            "output format:" <+> pretty (format c),+            "failure threshold:" <+> pretty (failureThreshold c),+            prettyPrintRulelist "error" (errorRules c),+            prettyPrintRulelist "warning" (warningRules c),+            prettyPrintRulelist "info" (infoRules c),+            prettyPrintRulelist "style" (styleRules c),+            prettyPrintRulelist "ignore" (ignoreRules c),+            "strict labels:" <+> pretty (strictLabels c),+            prettyPrintLabelSchema (labelSchema c),+            prettyPrintRegistries (allowedRegistries c)+          ]+      )++prettyPrintRulelist :: String -> [RuleCode] -> Doc ann+prettyPrintRulelist name lst =+  nest 2 (("override" <+> pretty name <> ":\n") <> prettyPrintList pretty lst)++-- | This function needs to convert the set to a list because Doc ann is not+-- ordered.+prettyPrintRegistries :: Set.Set Registry -> Doc ann+prettyPrintRegistries regs =+  nest 2 ( "allowed registries:\n"+             <> prettyPrintList+                 (\r -> "-" <+> pretty (unRegistry r))+                 (Set.toList regs)+         )++prettyPrintLabelSchema :: LabelSchema -> Doc ann+prettyPrintLabelSchema ls =+  nest 2 ( "label schema:\n"+             <> prettyPrintList+                  (\(n, t) -> pretty n <> ":" <+> pretty t)+                  (Map.toList ls)+         )++-- | pretty print a list with a custom pretty printing function for each element+prettyPrintList :: (a -> Doc ann) -> [a] -> Doc ann+prettyPrintList _ [] = "none"+prettyPrintList prnt lst = vsep (fmap (\i -> "-" <+> prnt i) lst)+++data PartialConfiguration =+  PartialConfiguration+    { partialNoFail :: Maybe Bool,+      partialNoColor :: Maybe Bool,+      partialVerbose :: Maybe Bool,+      partialFormat :: Maybe OutputFormat,+      partialErrorRules :: [RuleCode],+      partialWarningRules :: [RuleCode],+      partialInfoRules :: [RuleCode],+      partialStyleRules :: [RuleCode],+      partialIgnoreRules :: [RuleCode],+      partialAllowedRegistries :: Set.Set Registry,+      partialLabelSchema :: LabelSchema,+      partialStrictLabels :: Maybe Bool,+      partialFailureThreshold :: Maybe DLSeverity+    }+  deriving (Eq, Show)+++instance Semigroup PartialConfiguration where+  PartialConfiguration a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13+    <> PartialConfiguration b1 b2 b3 b4 b5 b6 b7 b8 b9 b10 b11 b12 b13 =+      PartialConfiguration+        (b1 <|> a1)+        (b2 <|> a2)+        (b3 <|> a3)+        (a4 <> b4)+        (a5 <> b5)+        (a6 <> b6)+        (a7 <> b7)+        (a8 <> b8)+        (a9 <> b9)+        (a10 <> b10)+        (a11 <> b11)+        (b12 <|> a12)+        (a13 <> b13)++instance Monoid PartialConfiguration where+  mempty =+    PartialConfiguration+      Nothing+      Nothing+      Nothing+      mempty+      mempty+      mempty+      mempty+      mempty+      mempty+      mempty+      mempty+      Nothing+      mempty++instance Default PartialConfiguration where+  def = mempty++instance Yaml.FromYAML PartialConfiguration where+  parseYAML = Yaml.withMap "Configuration" $ \m -> do+    partialNoFail <- m .:? "no-fail" .!= Nothing+    partialNoColor <- m .:? "no-color" .!= Nothing+    partialVerbose <- m .:? "verbose" .!= Nothing+    partialFormat <- m .:? "output-format"+    override <- m .:? "override" .!= mempty+    ignored <- m .:? "ignored" .!= mempty+    trusted <- m .:? "trusted-registries" .!= mempty+    partialLabelSchema <- m .:? "label-schema" .!= mempty+    partialStrictLabels <- m .:? "strict-labels" .!= Nothing+    let partialIgnoreRules = coerce (ignored :: [Text])+        partialErrorRules = overrideErrorRules override+        partialWarningRules = overrideWarningRules override+        partialInfoRules = overrideInfoRules override+        partialStyleRules = overrideStyleRules override+        partialAllowedRegistries = Set.fromList (coerce (trusted :: [Text]))+    partialFailureThreshold <- m .:? "failure-threshold"+    return PartialConfiguration {..}+++data OverrideConfig = OverrideConfig+  { overrideErrorRules :: [RuleCode],+    overrideWarningRules :: [RuleCode],+    overrideInfoRules :: [RuleCode],+    overrideStyleRules :: [RuleCode]+  }+  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 mempty mempty mempty mempty++instance Yaml.FromYAML OverrideConfig where+  parseYAML = Yaml.withMap "OverrideConfig" $ \m -> do+    err <- m .:? "error" .!= mempty+    wrn <- m .:? "warning" .!= mempty+    inf <- m .:? "info" .!= mempty+    sty <- m .:? "style" .!= mempty+    let overrideErrorRules = coerce (err :: [Text])+        overrideWarningRules = coerce (wrn :: [Text])+        overrideInfoRules = coerce (inf :: [Text])+        overrideStyleRules = coerce (sty:: [Text])+    return OverrideConfig {..}
+ src/Hadolint/Config/Environment.hs view
@@ -0,0 +1,103 @@+module Hadolint.Config.Environment+  ( getConfigFromEnvironment+  )+where++import Data.Char (toLower)+import Data.Coerce (coerce)+import Data.Map (empty, fromList)+import Data.Set (Set, empty, fromList)+import Data.Text (Text, pack, unpack, drop, splitOn, breakOn)+import Hadolint.Formatter.Format (OutputFormat (..), readMaybeOutputFormat)+import Hadolint.Config.Configuration+import Hadolint.Rule+import Language.Docker.Syntax+import System.Environment+++getConfigFromEnvironment :: IO PartialConfiguration+getConfigFromEnvironment =+  PartialConfiguration+    <$> maybeTruthy "HADOLINT_NOFAIL"+    <*> maybeTruthy "NO_COLOR"+    <*> maybeTruthy "HADOLINT_VERBOSE"+    <*> getFormat+    <*> getOverrideList "HADOLINT_OVERRIDE_ERROR"+    <*> getOverrideList "HADOLINT_OVERRIDE_WARNING"+    <*> getOverrideList "HADOLINT_OVERRIDE_INFO"+    <*> getOverrideList "HADOLINT_OVERRIDE_STYLE"+    <*> getOverrideList "HADOLINT_IGNORE"+    <*> getAllowedSet "HADOLINT_ALLOWED_REGISTRIES"+    <*> getLabelSchema "HADOLINT_REQUIRE_LABELS"+    <*> maybeTruthy "HADOLINT_STRICT_LABELS"+    <*> getFailureThreshold+++maybeTruthy :: String -> IO (Maybe Bool)+maybeTruthy name = do+  e <- lookupEnv name+  case e of+    Just v ->+      if truthy v+      then return $ Just True+      else return $ Just False+    Nothing -> return Nothing++truthy :: String -> Bool+truthy s = map toLower s `elem` ["1", "y", "on", "true", "yes"]++getFormat :: IO (Maybe OutputFormat)+getFormat = do+  fmt <- lookupEnv "HADOLINT_FORMAT"+  return $ (readMaybeOutputFormat . pack) =<< fmt++getOverrideList :: String -> IO [RuleCode]+getOverrideList env = do+  maybeString <- lookupEnv env+  case maybeString of+    Just s -> return $ getRulecodes (pack s)+    Nothing -> return []++getRulecodes :: Text -> [RuleCode]+getRulecodes s = do+  list <- splitOn "," s+  let rules = coerce (list :: Text)+  return rules++getAllowedSet :: String -> IO (Set Registry)+getAllowedSet env = do+  maybeString <- lookupEnv env+  case maybeString of+    Just s -> return $ Data.Set.fromList (getAllowed (pack s))+    Nothing -> return Data.Set.empty++getAllowed :: Text -> [Registry]+getAllowed s = do+  list <- splitOn "," s+  let regs = coerce (list :: Text)+  return regs++getLabelSchema :: String -> IO LabelSchema+getLabelSchema env = do+  maybeString <- lookupEnv env+  case maybeString of+    Just s -> return $ Data.Map.fromList (labelSchemaFromText (pack s))+    Nothing -> return Data.Map.empty++labelSchemaFromText :: Text -> [(LabelName, LabelType)]+labelSchemaFromText txt =+  [ (ln, lt) | Right (ln, lt) <- map convertToLabelSchema (convertToPairs txt) ]++convertToPairs :: Text -> [(Text, Text)]+convertToPairs txt = map (breakOn ":") (splitOn "," txt)++convertToLabelSchema :: (Text, Text) -> Either String (LabelName, LabelType)+convertToLabelSchema (tln, tlt) =+  case (readEitherLabelType . Data.Text.drop 1) tlt of+    Right lt -> Right (coerce tln :: Text, lt)+    Left e -> Left (unpack e)++getFailureThreshold :: IO (Maybe DLSeverity)+getFailureThreshold = do+  ft <- lookupEnv "HADOLINT_FAILURE_THRESHOLD"+  return $ (readMaybeSeverity . pack) =<< ft
+ src/Hadolint/Formatter.hs view
@@ -0,0 +1,37 @@+module Hadolint.Formatter+  ( OutputFormat (..),+    Result (..),+    printResults,+    readMaybeOutputFormat,+  )+where++import Data.Text (Text)+import Hadolint.Formatter.Format+import Language.Docker.Parser (DockerfileError)+import qualified Hadolint.Formatter.Checkstyle as FormatCheckstyle+import qualified Hadolint.Formatter.Codacy as FormatCodacy+import qualified Hadolint.Formatter.Codeclimate as FormatCodeclimate+import qualified Hadolint.Formatter.Json as FormatJson+import qualified Hadolint.Formatter.Sarif as FormatSarif+import qualified Hadolint.Formatter.SonarQube as FormatSonarQube+import qualified Hadolint.Formatter.TTY as FormatTTY+++printResults ::+  Foldable f =>+  OutputFormat ->+  Bool ->+  Maybe FilePath ->+  f (Result Text DockerfileError) ->+  IO ()+printResults format nocolor filePathInReport allResults =+  case format of+    Checkstyle -> FormatCheckstyle.printResults allResults filePathInReport+    Codacy -> FormatCodacy.printResults allResults+    CodeclimateJson -> FormatCodeclimate.printResults allResults+    GitlabCodeclimateJson -> FormatCodeclimate.printGitlabResults allResults+    Json -> FormatJson.printResults allResults+    Sarif -> FormatSarif.printResults allResults+    SonarQube -> FormatSonarQube.printResults allResults+    TTY -> FormatTTY.printResults allResults nocolor
src/Hadolint/Formatter/Format.hs view
@@ -1,25 +1,86 @@ module Hadolint.Formatter.Format-  ( severityText,-    stripNewlines,+  ( OutputFormat (..),+    Result (..),+    Text.Megaparsec.Error.errorBundlePretty,     errorMessage,     errorMessageLine,     errorPosition,     errorPositionPretty,-    Text.Megaparsec.Error.errorBundlePretty,-    Result (..),+    severityText,+    stripNewlines,+    readMaybeOutputFormat,     toResult,   ) where -import qualified Data.List.NonEmpty as NE+import Data.Default import Data.Sequence (Seq)-import qualified Data.Sequence as Seq-import qualified Data.Text as Text-import qualified Hadolint.Rule+import Data.Text (Text)+import Data.Text.Prettyprint.Doc (Pretty, pretty)+import Data.YAML import Text.Megaparsec (TraversableStream (..), pstateSourcePos) import Text.Megaparsec.Error import Text.Megaparsec.Pos (SourcePos, sourcePosPretty) import Text.Megaparsec.Stream (VisualStream)+import qualified Data.List.NonEmpty as NE+import qualified Data.Sequence as Seq+import qualified Data.Text as Text+import qualified Hadolint.Rule++data OutputFormat+  = Json+  | SonarQube+  | TTY+  | CodeclimateJson+  | GitlabCodeclimateJson+  | Checkstyle+  | Codacy+  | Sarif+  deriving (Eq, Show)++instance Pretty OutputFormat where+  pretty Json = "json"+  pretty SonarQube = "sonarqube"+  pretty TTY = "tty"+  pretty CodeclimateJson = "codeclimate"+  pretty GitlabCodeclimateJson = "gitlab_codeclimate"+  pretty Checkstyle = "checkstyle"+  pretty Codacy = "codacy"+  pretty Sarif = "sarif"++instance Semigroup OutputFormat where+  _ <> f = f++instance Monoid OutputFormat where+  mempty = TTY++instance FromYAML OutputFormat where+  parseYAML = withOutputFormat pure++withOutputFormat ::+  (OutputFormat -> Parser a) ->+  Node Pos ->+  Parser a+withOutputFormat f v@(Scalar _ (SStr b)) =+  case readMaybeOutputFormat b of+    Just out -> f out+    Nothing -> typeMismatch "output format" v+withOutputFormat _ v = typeMismatch "output format" v++instance Default OutputFormat where+  def = TTY++readMaybeOutputFormat :: Text -> Maybe OutputFormat+readMaybeOutputFormat "json" = Just Json+readMaybeOutputFormat "sonarqube" = Just SonarQube+readMaybeOutputFormat "tty" = Just TTY+readMaybeOutputFormat "codeclimate" = Just CodeclimateJson+readMaybeOutputFormat "gitlab_codeclimate" = Just GitlabCodeclimateJson+readMaybeOutputFormat "checkstyle" = Just Checkstyle+readMaybeOutputFormat "codacy" = Just Codacy+readMaybeOutputFormat "sarif" = Just Sarif+readMaybeOutputFormat _ = Nothing+  data Result s e = Result   { fileName :: Text.Text,
+ src/Hadolint/Formatter/Sarif.hs view
@@ -0,0 +1,165 @@+module Hadolint.Formatter.Sarif+  ( 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 as Seq+import qualified Data.Text as Text+import Hadolint.Formatter.Format+  ( Result (..),+    errorMessage,+    errorPosition,+  )+import Hadolint.Meta+  ( getShortVersion,+  )+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 SarifFormat s e+  = SarifCheck Text.Text CheckFailure+  | SarifError (ParseErrorBundle s e)++instance+  ( VisualStream s,+    TraversableStream s,+    ShowErrorComponent e+  ) =>+  ToJSON (SarifFormat s e)+  where+  toJSON (SarifCheck filename CheckFailure {..}) =+    object+      [ "ruleId" .= unRuleCode code,+        "level" .= toSeverity severity,+        "message"+          .= object+            [ "text" .= message+            ],+        "locations"+          .= [ object+                 [ "physicalLocation"+                     .= object+                       [ "artifactLocation"+                           .= object+                             [ "uri" .= filename+                             ],+                         "region"+                           .= object+                             [ "startLine" .= line,+                               "endLine" .= line,+                               "startColumn" .= (1 :: Int),+                               "endColumn" .= (1 :: Int),+                               "sourceLanguage" .= Text.pack language+                             ]+                       ]+                 ]+             ]+      ]+    where+      language = if "DL" `Text.isPrefixOf` unRuleCode code+                    then "dockerfile"+                    else "sh"+  toJSON (SarifError err) =+    object+      [ "ruleId" .= Text.pack "DL1000",+        "level" .= Text.pack "error",+        "message"+          .= object+            [ "text" .= errorMessage err+            ],+        "locations"+          .= [ object+                 [ "physicalLocation"+                     .= object+                       [ "artifactLocation"+                           .= object+                             [ "uri" .= Text.pack (sourceName pos)+                             ],+                         "region"+                           .= object+                             [ "startLine" .= linenumber,+                               "endLine" .= linenumber,+                               "startColumn" .= column,+                               "endColumn" .= column,+                               "sourceLanguage" .= Text.pack "dockerfile"+                             ]+                       ]+                 ]+             ]+      ]+    where+      pos = errorPosition err+      linenumber = unPos $ sourceLine pos+      column = unPos $ sourceColumn pos++formatResult :: Result s e -> Seq (SarifFormat s e)+formatResult (Result filename errors checks) = allMessages+  where+    allMessages = errorMessages <> checkMessages+    checkMessages = fmap (SarifCheck filename) checks+    errorMessages = fmap SarifError errors++printResults ::+  ( VisualStream s,+    TraversableStream s,+    ShowErrorComponent e,+    Foldable f+  ) =>+  f (Result s e) ->+  IO ()+printResults results =+  B.putStr . encode $+    object+      [ ("version", "2.1.0"),+        "$schema"+          .= Text.pack "http://json.schemastore.org/sarif-2.1.0",+        "runs"+          .= [ object+                 [ "tool"+                     .= object+                       [ "driver"+                           .= object+                             [ ("name", "Hadolint"),+                               ("fullName", "Haskell Dockerfile Linter"),+                               ("downloadUri",+                                  "https://github.com/hadolint/hadolint"),+                               "version"+                                 .= Text.pack Hadolint.Meta.getShortVersion,+                               "shortDescription"+                                 .= object+                                   [ ("text",+  "Dockerfile linter, validate inline bash, written in Haskell")+                                   ]+                             ]+                       ],+                   "results" .= flattened,+                   "defaultSourceLanguage" .= Text.pack "dockerfile"+                 ]+             ]+      ]+  where+    flattened = Foldl.fold (Foldl.premap formatResult Foldl.mconcat) results++-- | SARIF only specifies three severities "error", "warning" and "note"+-- We pack our "info" and "style" severities together into the "note" severity+-- here.+toSeverity :: DLSeverity -> Text.Text+toSeverity DLErrorC = "error"+toSeverity DLWarningC = "warning"+toSeverity _ = "note"
− src/Hadolint/Ignore.hs
@@ -1,40 +0,0 @@-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
@@ -2,71 +2,37 @@   ( lintIO,     lint,     analyze,-    LintOptions (..),-    ErrorRule,-    WarningRule,-    InfoRule,-    StyleRule,-    IgnoreRule,     TrustedRegistry,   ) where +import Data.Sequence (Seq)+import Data.Text (Text)+import Hadolint.Config.Configuration (Configuration (..))+import Hadolint.Rule (RuleCode, DLSeverity (..), CheckFailure (..))+import Language.Docker.Parser (DockerfileError, Error)+import Language.Docker.Syntax (Dockerfile) 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 qualified Data.Text as Text import qualified Hadolint.Formatter.Format as Format 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 = Hadolint.Rule.RuleCode -type WarningRule = Hadolint.Rule.RuleCode--type InfoRule = Hadolint.Rule.RuleCode--type StyleRule = Hadolint.Rule.RuleCode--type IgnoreRule = Hadolint.Rule.RuleCode- type TrustedRegistry = Text -data LintOptions = LintOptions-  { errorRules :: [ErrorRule],-    warningRules :: [WarningRule],-    infoRules :: [InfoRule],-    styleRules :: [StyleRule],-    ignoreRules :: [IgnoreRule],-    rulesConfig :: Hadolint.Process.RulesConfig,-    failThreshold :: Hadolint.Rule.DLSeverity-  }-  deriving (Show) -instance Semigroup LintOptions where-  LintOptions a1 a2 a3 a4 a5 a6 a7 <> LintOptions b1 b2 b3 b4 b5 b6 b7 =-    LintOptions-      (a1 <> b1)-      (a2 <> b2)-      (a3 <> b3)-      (a4 <> b4)-      (a5 <> b5)-      (a6 <> b6)-      (a7 <> b7)--instance Monoid LintOptions where-  mempty = LintOptions mempty 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.-lintIO :: LintOptions -> NonEmpty.NonEmpty FilePath -> IO (NonEmpty.NonEmpty (Format.Result Text DockerfileError))-lintIO options dFiles = do+-- | Performs the process of parsing the dockerfile and analyzing it with all+-- the applicable rules, depending on the list of ignored rules.+lintIO ::+  Configuration ->+  NonEmpty.NonEmpty FilePath ->+  IO (NonEmpty.NonEmpty (Format.Result Text DockerfileError))+lintIO config dFiles = do   parsedFiles <- mapM parseFile (NonEmpty.toList dFiles)-  return $ NonEmpty.fromList (lint options parsedFiles)+  return $ NonEmpty.fromList (lint config parsedFiles)   where     parseFile :: String -> IO (Text, Either Error Dockerfile)     parseFile "-" = do@@ -77,42 +43,46 @@       return (Text.pack s, res)  lint ::-  LintOptions ->+  Configuration ->   [(Text, Either Error Dockerfile)] ->   [Format.Result Text DockerfileError]-lint options parsedFiles = gather results `Parallel.using` parallelRun+lint config parsedFiles = gather results `Parallel.using` parallelRun   where     gather = fmap (uncurry Format.toResult)     results =       [ ( name,-          fmap (analyze options) parseResult+          fmap (analyze config) parseResult         )         | (name, parseResult) <- parsedFiles       ]     parallelRun = Parallel.parList Parallel.rseq -analyze :: LintOptions -> Dockerfile -> Seq.Seq Hadolint.Rule.CheckFailure-analyze options dockerfile = fixer process+analyze :: Configuration -> Dockerfile -> Seq Hadolint.Rule.CheckFailure+analyze config dockerfile = fixer process   where-    fixer = fixSeverity options-    process = Hadolint.Process.run (rulesConfig options) dockerfile+    fixer = fixSeverity config+    process = Hadolint.Process.run config dockerfile -fixSeverity :: LintOptions -> Seq.Seq Hadolint.Rule.CheckFailure -> Seq.Seq Hadolint.Rule.CheckFailure-fixSeverity LintOptions {..} = Seq.filter ignoredRules . Seq.mapWithIndex (const correctSeverity)+fixSeverity ::+  Configuration ->+  Seq CheckFailure ->+  Seq CheckFailure+fixSeverity Configuration {..} =+  Seq.filter ignoredRules . Seq.mapWithIndex (const correctSeverity)   where     correctSeverity =-      makeSeverity Hadolint.Rule.DLErrorC errorRules-        . makeSeverity Hadolint.Rule.DLWarningC warningRules-        . makeSeverity Hadolint.Rule.DLInfoC infoRules-        . makeSeverity Hadolint.Rule.DLStyleC styleRules+      makeSeverity DLErrorC errorRules+        . makeSeverity DLWarningC warningRules+        . makeSeverity DLInfoC infoRules+        . makeSeverity DLStyleC styleRules      ignoredRules = ignoreFilter ignoreRules -    makeSeverity s rules rule@Hadolint.Rule.CheckFailure {code} =+    makeSeverity s rules rule@CheckFailure {code} =       if code `elem` rules-        then rule {Hadolint.Rule.severity = s}+        then rule {severity = s}         else rule -    ignoreFilter :: [IgnoreRule] -> Hadolint.Rule.CheckFailure -> Bool-    ignoreFilter ignored Hadolint.Rule.CheckFailure {code, severity} =-      code `notElem` ignored && severity /= Hadolint.Rule.DLIgnoreC+    ignoreFilter :: [RuleCode] -> CheckFailure -> Bool+    ignoreFilter ignored CheckFailure {code, severity} =+      code `notElem` ignored && severity /= DLIgnoreC
+ src/Hadolint/Meta.hs view
@@ -0,0 +1,21 @@+module Hadolint.Meta+  ( getVersion,+    getShortVersion,+  )+where++import qualified Development.GitRev+++getVersion :: String+getVersion = "Haskell Dockerfile Linter " ++ getShortVersion++getShortVersion :: String+getShortVersion = v ++ d+  where+    version = $(Development.GitRev.gitDescribe)+    dirty = $(Development.GitRev.gitDirty)+    v = case version of+      "UNKONWN" -> "-no-git"+      _ -> version+    d = if dirty then "-dirty" else ""
+ src/Hadolint/Pragma.hs view
@@ -0,0 +1,76 @@+module Hadolint.Pragma+  ( ignored,+    parseShell+  )+  where++import Data.Functor.Identity (Identity)+import Data.Text (Text)+import Data.Void (Void)+import Hadolint.Rule (RuleCode (RuleCode))+import Language.Docker.Syntax+import qualified Control.Foldl as Foldl+import qualified Data.IntMap.Strict as Map+import qualified Data.Set as Set+import qualified Text.Megaparsec as Megaparsec+import qualified Text.Megaparsec.Char as Megaparsec+++ignored :: Foldl.Fold (InstructionPos 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 -> Maybe [Text]+parseComment =+  Megaparsec.parseMaybe commentParser++commentParser :: Megaparsec.Parsec Void Text [Text]+commentParser =+  do+    spaces+    >> string "hadolint"+    >> spaces1+    >> string "ignore="+    >> spaces+    >> Megaparsec.sepBy1 ruleName (spaces >> string "," >> spaces)++ruleName :: Megaparsec.ParsecT Void Text Identity (Megaparsec.Tokens Text)+ruleName = Megaparsec.takeWhile1P Nothing (\c -> c `elem` Set.fromList "DLSC0123456789")+++parseShell :: Text -> Maybe Text+parseShell = Megaparsec.parseMaybe shellParser++shellParser :: Megaparsec.Parsec Void Text Text+shellParser =+  do+    spaces+    >> string "hadolint"+    >> spaces1+    >> string "shell"+    >> spaces+    >> string "="+    >> spaces+    >> shellName++shellName :: Megaparsec.ParsecT Void Text Identity (Megaparsec.Tokens Text)+shellName = Megaparsec.takeWhile1P Nothing (/= '\n')+++string :: Megaparsec.Tokens Text+  -> Megaparsec.ParsecT Void Text Identity (Megaparsec.Tokens Text)+string = Megaparsec.string++spaces :: Megaparsec.ParsecT Void Text Identity (Megaparsec.Tokens Text)+spaces = Megaparsec.takeWhileP Nothing space++spaces1 :: Megaparsec.ParsecT Void Text Identity (Megaparsec.Tokens Text)+spaces1 = Megaparsec.takeWhile1P Nothing space++space :: Char -> Bool+space c = c == ' ' || c == '\t'
src/Hadolint/Process.hs view
@@ -1,13 +1,14 @@-module Hadolint.Process (run, RulesConfig (..)) where+module Hadolint.Process (run) where +import Hadolint.Config.Configuration (Configuration (..))+import Hadolint.Rule (CheckFailure (..), Failures, Rule, RuleCode)+import Language.Docker.Syntax 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.Pragma import qualified Hadolint.Rule.DL3000 import qualified Hadolint.Rule.DL3001 import qualified Hadolint.Rule.DL3002@@ -74,28 +75,8 @@ 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),@@ -103,7 +84,7 @@     failed :: Failures   } -run :: RulesConfig -> [InstructionPos Text.Text] -> Failures+run :: Configuration -> [InstructionPos Text.Text] -> Failures run config dockerfile = Seq.filter shouldKeep failed   where     AnalisisResult {..} = Foldl.fold (analyze config) dockerfile@@ -113,16 +94,18 @@         ignoreList <- SMap.lookup line ignored         return $ code `Set.member` ignoreList -analyze :: RulesConfig -> Foldl.Fold (InstructionPos Text.Text) AnalisisResult+analyze ::+  Configuration ->+  Foldl.Fold (InstructionPos Text.Text) AnalisisResult analyze config =   AnalisisResult-    <$> Hadolint.Ignore.ignored+    <$> Hadolint.Pragma.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 :: Configuration -> Rule Shell.ParsedShell onBuildFailures config =   Foldl.prefilter     isOnBuild@@ -134,8 +117,8 @@     unwrapOnbuild inst@InstructionPos {instruction = OnBuild i} = inst {instruction = i}     unwrapOnbuild inst = inst -failures :: RulesConfig -> Rule Shell.ParsedShell-failures RulesConfig {allowedRegistries, labelSchema, strictLabels} =+failures :: Configuration -> Rule Shell.ParsedShell+failures Configuration {allowedRegistries, labelSchema, strictLabels} =   Hadolint.Rule.DL3000.rule     <> Hadolint.Rule.DL3001.rule     <> Hadolint.Rule.DL3002.rule
src/Hadolint/Rule.hs view
@@ -1,14 +1,17 @@ module Hadolint.Rule where  import Control.DeepSeq (NFData)+import Data.Default+import Data.String (IsString (..))+import Data.Text (Text, unpack)+import GHC.Generics (Generic)+import Language.Docker.Syntax+import Prettyprinter (Pretty, pretty) 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 |> @@ -21,37 +24,67 @@   | DLInfoC   | DLStyleC   | DLIgnoreC-  deriving (Show, Read, Eq, Ord, Generic, NFData)+  deriving (Eq, Ord, Show, Generic, NFData)  instance Yaml.FromYAML DLSeverity where   parseYAML = withSeverity pure -withSeverity :: (DLSeverity -> Yaml.Parser a) -> Yaml.Node Yaml.Pos -> Yaml.Parser a+withSeverity ::+  (DLSeverity -> Yaml.Parser a) ->+  Yaml.Node Yaml.Pos ->+  Yaml.Parser a withSeverity f v@(Yaml.Scalar _ (Yaml.SStr b)) =-  case Hadolint.Rule.readSeverity b of+  case readEitherSeverity b of     Right s -> f s     Left _ -> Yaml.typeMismatch "severity" v withSeverity _ v = Yaml.typeMismatch "severity" v -readSeverity :: Text.Text -> Either Text.Text DLSeverity-readSeverity "error" = Right DLErrorC-readSeverity "warning" = Right DLWarningC-readSeverity "info" = Right DLInfoC-readSeverity "style" = Right DLStyleC-readSeverity "ignore" = Right DLIgnoreC-readSeverity "none" = Right DLIgnoreC-readSeverity t = Left ("Invalid severity: " <> t)+readEitherSeverity :: Text -> Either String DLSeverity+readEitherSeverity "error" = Right DLErrorC+readEitherSeverity "warning" = Right DLWarningC+readEitherSeverity "info" = Right DLInfoC+readEitherSeverity "style" = Right DLStyleC+readEitherSeverity "ignore" = Right DLIgnoreC+readEitherSeverity "none" = Right DLIgnoreC+readEitherSeverity t = Left ("Invalid severity: " ++ unpack t) -instance Semigroup DLSeverity where s1 <> s2 = min s1 s2+readMaybeSeverity :: Text -> Maybe DLSeverity+readMaybeSeverity "error" = Just DLErrorC+readMaybeSeverity "warning" = Just DLWarningC+readMaybeSeverity "info" = Just DLInfoC+readMaybeSeverity "style" = Just DLStyleC+readMaybeSeverity "ignore" = Just DLIgnoreC+readMaybeSeverity "none" = Just DLIgnoreC+readMaybeSeverity _ = Nothing +instance Semigroup DLSeverity where _ <> s2 = s2+ instance Monoid DLSeverity where mempty = DLIgnoreC -newtype RuleCode = RuleCode {unRuleCode :: Text.Text}-  deriving (Show, Eq, Ord)+instance Default DLSeverity where+  def = DLInfoC +instance Pretty DLSeverity where+  pretty DLErrorC = "error"+  pretty DLWarningC = "warning"+  pretty DLInfoC = "info"+  pretty DLStyleC = "style"+  pretty DLIgnoreC = "ignore"+++newtype RuleCode = RuleCode {unRuleCode :: Text}+  deriving (Eq, Ord)++instance Show RuleCode where+  show rc = show (unRuleCode rc)+ instance IsString RuleCode where   fromString = RuleCode . Text.pack +instance Pretty RuleCode where+  pretty rc = pretty $ show rc++ data CheckFailure = CheckFailure   { code :: RuleCode,     severity :: DLSeverity,@@ -63,47 +96,59 @@ 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+  = Email   | GitHash+  | RawText   | Rfc3339   | SemVer-  | Email-  deriving (Eq, Read, Show)+  | Spdx+  | Url+  deriving (Eq, 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 "email"   = Right Email-read "text"    = Right RawText-read ""        = Right RawText-read t         = Left ("Invalid label type: " <> t)+readEitherLabelType :: Text -> Either Text LabelType+readEitherLabelType "email" = Right Email+readEitherLabelType "hash" = Right GitHash+readEitherLabelType "text" = Right RawText+readEitherLabelType "rfc3339" = Right Rfc3339+readEitherLabelType "semver" = Right SemVer+readEitherLabelType "spdx" = Right Spdx+readEitherLabelType "url" = Right Url+readEitherLabelType "" = Right RawText+readEitherLabelType 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+    case readEitherLabelType b of       Right lt -> f lt       Left _ -> Yaml.typeMismatch "labeltype" v withLabelType _ v = Yaml.typeMismatch "labeltype" v +instance Pretty LabelType where+  pretty RawText = "text"+  pretty Url = "url"+  pretty Spdx = "spdx"+  pretty GitHash = "hash"+  pretty Rfc3339 = "rfc3339"+  pretty SemVer = "semver"+  pretty Email = "email"+ type LabelSchema = Map.Map LabelName LabelType  @@ -214,3 +259,10 @@     ".txz",     ".xz"   ]++dropQuotes :: Text -> Text+dropQuotes = Text.dropAround quotes+  where+    quotes '\"' = True+    quotes '\'' = True+    quotes _ = False
src/Hadolint/Rule/DL3000.hs view
@@ -12,18 +12,12 @@     severity = DLErrorC     message = "Use absolute WORKDIR"     check (Workdir loc)-      | "$" `Text.isPrefixOf` Text.dropAround dropQuotes loc = True-      | "/" `Text.isPrefixOf` Text.dropAround dropQuotes loc = True-      | isWindowsAbsolute (Text.dropAround dropQuotes loc) = True+      | "$" `Text.isPrefixOf` dropQuotes loc = True+      | "/" `Text.isPrefixOf` dropQuotes loc = True+      | isWindowsAbsolute (dropQuotes loc) = True       | otherwise = False     check _ = True {-# INLINEABLE rule #-}--dropQuotes :: Char -> Bool-dropQuotes chr-  | chr == '\"' = True-  | chr == '\'' = True-  | otherwise = False  isWindowsAbsolute :: Text.Text -> Bool isWindowsAbsolute path
src/Hadolint/Rule/DL3009.hs view
@@ -1,20 +1,24 @@ module Hadolint.Rule.DL3009 (rule) where +import Hadolint.Rule+import Language.Docker.Syntax import qualified Data.Map.Strict as Map+import qualified Data.Set as Set import qualified Data.Text as Text-import Hadolint.Rule import qualified Hadolint.Shell as Shell-import Language.Docker.Syntax + data Acc   = Acc       { lastFrom :: BaseImage,+        dockerClean :: Bool,         stages :: Map.Map Text.Text Linenumber,         forgets :: Map.Map Linenumber BaseImage       }   | Empty-  deriving (Show)+  deriving (Eq, Show) + rule :: Rule Shell.ParsedShell rule = veryCustomRule check (emptyState Empty) markFailures   where@@ -24,9 +28,14 @@      check line st (From from) = st |> modify (rememberStage line from)     check line st (Run (RunArgs args flags))-      | hasNoCacheMount flags && foldArguments forgotToCleanup args =-          st |> modify (rememberLine line)-      | otherwise = st+      | not (foldArguments forgotToCleanup args) =+          if foldArguments disabledDockerClean args+          then st |> modify rememberDockerClean+          else st+      | hasCacheDirectory "/var/lib/apt/lists" flags = st+      | hasCacheDirectory "/var/lib/apt" flags+        && hasCacheDirectory "/var/cache/apt" flags = st+      | otherwise = st |> modify (rememberLine line)     check _ st _ = st      -- Convert the final state into failures@@ -43,29 +52,53 @@ {-# 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+rememberStage line from@BaseImage {image = Image _ als} (Acc _ _ stages o) = Acc from True (Map.insert als line stages) o+rememberStage line from@BaseImage {image = Image _ als} Empty = Acc from True (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)+rememberLine line (Acc from clean stages o) = Acc from clean stages (Map.insert line from o)+rememberLine line Empty = Acc emptyImage True mempty (Map.singleton line emptyImage) +rememberDockerClean :: Acc -> Acc+rememberDockerClean Empty = Acc emptyImage False mempty mempty+rememberDockerClean (Acc from _ stages forget) = Acc from False stages forget+ forgotToCleanup :: Shell.ParsedShell -> Bool forgotToCleanup args-  | hasUpdate && not hasCleanup = True+  | hasUpdate args && 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)+hasUpdate :: Shell.ParsedShell -> Bool+hasUpdate args =+  any (Shell.cmdHasArgs "apt-get" ["update"]) (Shell.presentCommands args) -hasNoCacheMount :: RunFlags -> Bool-hasNoCacheMount RunFlags-  { mount =-      Just (CacheMount CacheOpts {cTarget = TargetPath {unTargetPath = p}})-  } = Text.dropWhileEnd (=='/') p /= "/var/lib/apt/lists"-hasNoCacheMount RunFlags {} = True+disabledDockerClean :: Shell.ParsedShell -> Bool+disabledDockerClean args+  | removesScript || keepsPackages = True+  | otherwise = False+  where+    removesScript =+      any+        (Shell.cmdHasArgs "rm" ["/etc/apt/apt.conf.d/docker-clean"])+        (Shell.presentCommands args)+    keepsPackages =+      any+        ( Shell.cmdHasArgs+            "echo"+            ["\'Binary::apt::APT::Keep-Downloaded-Packages \"true\";\'"]+        )+        (Shell.presentCommands args)++hasCacheDirectory :: Text.Text -> RunFlags -> Bool+hasCacheDirectory dir RunFlags { mount } =+  not ( null $ Set.filter (isCacheMount dir) mount)++isCacheMount :: Text.Text -> RunMount -> Bool+isCacheMount dir (CacheMount CacheOpts {cTarget = TargetPath {unTargetPath = t}}) = dir `Text.isPrefixOf` t+isCacheMount _ _ = False  -- | 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
src/Hadolint/Rule/DL3010.hs view
@@ -1,21 +1,138 @@ module Hadolint.Rule.DL3010 (rule) where  import Data.Foldable (toList)+import Data.Function ((&))+import Data.List.NonEmpty (NonEmpty)+import qualified Data.Set as Set+import qualified Data.Sequence as Seq import qualified Data.Text as Text import Hadolint.Rule+import qualified Hadolint.Shell as Shell import Language.Docker.Syntax -rule :: Rule args-rule = simpleRule code severity message check++data Acc+  = Acc+      { archives :: Set.Set (Linenumber, Text.Text),+        extracted :: Set.Set (Linenumber, Text.Text)+      }+  | Empty++rule :: Rule Shell.ParsedShell+rule = veryCustomRule check (emptyState Empty) markFailures   where     code = "DL3010"     severity = DLInfoC-    message = "Use ADD for extracting archives into an image"-    check (Copy (CopyArgs srcs _ _ _ NoSource)) =-      and-        [ not (format `Text.isSuffixOf` src)-          | SourcePath src <- toList srcs,-            format <- archiveFileFormatExtensions-        ]-    check _ = True+    message = "Use `ADD` for extracting archives into an image"++    check _ _ (From _) = emptyState Empty+    check line st (Copy (CopyArgs srcs tgt _ _ NoSource)) =+      st |> modify (rememberArchives line srcs tgt)+    check _ st (Run (RunArgs args _))+      | Acc archives _ <- state st,+        ex <- foldArguments (getExtractedArchives archives) args =+          st |> modify (markExtracted ex)+      | otherwise = st+    check _ st _ = st++    markFailures :: State Acc -> Failures+    markFailures (State fails (Acc _ e)) =+      Set.foldl' (Seq.|>) fails (Set.map makeFail e)+    markFailures st = failures st++    makeFail :: (Linenumber, Text.Text) -> CheckFailure+    makeFail (line, _) = CheckFailure {..} {-# INLINEABLE rule #-}+++extractsThisArchive :: (Linenumber, Text.Text) -> Shell.Command -> Bool+extractsThisArchive (_, archive) cmd =+  (isTarExtractCommand cmd || isUnzipCommand cmd) && archive `elem` arguments+  where+    arguments = map basename $ Shell.getArgsNoFlags cmd++getExtractedArchives ::+  Set.Set (Linenumber, Text.Text) ->+  Shell.ParsedShell ->+  Set.Set (Linenumber, Text.Text)+getExtractedArchives archives shell =+  Set.filter+    (\a -> any (extractsThisArchive a) cmds)+    archives+  where+    cmds = Shell.presentCommands shell++isTarExtractCommand :: Shell.Command -> Bool+isTarExtractCommand cmd@(Shell.Command name _ _) =+  name == "tar" && (any longExtractFlags args || any shortExtractFlags args)+  where+    longExtractFlags f = f `elem` ["--extract", "--get"]+    shortExtractFlags f = "-" `Text.isPrefixOf` f && "x" `Text.isInfixOf` f+    args = Shell.getArgs cmd++isUnzipCommand :: Shell.Command -> Bool+isUnzipCommand (Shell.Command name _ _) =+  name `elem`+    [ "unzip",+      "gunzip",+      "bunzip2",+      "unlzma",+      "unxz",+      "zgz",+      "uncompress",+      "zcat",+      "gzcat"+    ]++markExtracted :: Set.Set (Linenumber, Text.Text) -> Acc -> Acc+markExtracted _ Empty = Empty+markExtracted exarcv Acc {archives, extracted} =+  Acc { archives, extracted = Set.union exarcv extracted }++rememberArchives ::+  Linenumber ->+  NonEmpty SourcePath ->+  TargetPath ->+  Acc ->+  Acc+rememberArchives line paths target Empty =+  if isArchive $ unTargetPath target+    then Acc+          { archives = Set.singleton (line, basename $ unTargetPath target),+            extracted = Set.empty+          }+    else Acc+          { archives =+              paths+                & toList+                & map (basename . unSourcePath)+                & Set.fromList+                & Set.filter isArchive+                & Set.map (line,),+            extracted = Set.empty+          }+rememberArchives line paths target Acc {archives, extracted} =+  if isArchive $ unTargetPath target+    then Acc+          { archives =+              Set.insert (line, basename $ unTargetPath target) archives,+            extracted+          }+    else Acc+          { archives =+              paths+                & toList+                & map (basename . unSourcePath)+                & Set.fromList+                & Set.filter isArchive+                & Set.map (line,)+                & Set.union archives,+            extracted+          }++basename :: Text.Text -> Text.Text+basename = Text.takeWhileEnd (\c -> c /= '/' && c /= '\\') . dropQuotes++isArchive :: Text.Text -> Bool+isArchive src =+  any (`Text.isSuffixOf` dropQuotes src) archiveFileFormatExtensions
src/Hadolint/Rule/DL3019.hs view
@@ -1,10 +1,11 @@ module Hadolint.Rule.DL3019 (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+import qualified Data.Set as Set+import qualified Data.Text as Text+import qualified Hadolint.Shell as Shell  rule :: Rule ParsedShell rule = simpleRule code severity message check@@ -20,11 +21,16 @@ {-# INLINEABLE rule #-}  hasCacheMount :: RunFlags -> Bool-hasCacheMount RunFlags-  { mount =-      Just (CacheMount CacheOpts {cTarget = TargetPath {unTargetPath = p}})-  } = Text.dropWhileEnd (=='/') p == "/var/cache/apk"-hasCacheMount RunFlags {} = False+hasCacheMount RunFlags { mount } =+  not (null $ Set.filter isCacheMount mount)++isCacheMount :: RunMount -> Bool+isCacheMount (CacheMount CacheOpts { cTarget = t })= isVarCacheApk t+isCacheMount _ = False++isVarCacheApk :: TargetPath -> Bool+isVarCacheApk TargetPath { unTargetPath = p }+  = Text.dropWhileEnd (=='/') p == "/var/cache/apk"  forgotCacheOption :: Shell.Command -> Bool forgotCacheOption cmd = Shell.cmdHasArgs "apk" ["add"] cmd
src/Hadolint/Rule/DL3020.hs view
@@ -20,10 +20,13 @@ isArchive :: Text.Text -> Bool isArchive path =   or-    ( [ ftype `Text.isSuffixOf` path+    ( [ ftype `Text.isSuffixOf` dropQuotes path         | ftype <- archiveFileFormatExtensions       ]     )  isUrl :: Text.Text -> Bool-isUrl path = or ([proto `Text.isPrefixOf` path | proto <- ["https://", "http://"]])+isUrl path = or+  [ proto `Text.isPrefixOf` dropQuotes path+    | proto <- ["https://", "http://"]+  ]
src/Hadolint/Rule/DL3033.hs view
@@ -12,15 +12,40 @@     severity = DLWarningC     message = "Specify version with `yum install -y <package>-<version>`." -    check (Run (RunArgs args _)) = foldArguments (all versionFixed . yumPackages) args+    check (Run (RunArgs args _)) =+      foldArguments (all packageVersionFixed . yumPackages) args+        && foldArguments (all moduleVersionFixed . yumModules) 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"+  [ arg+    | cmd <- Shell.presentCommands args,+      not (Shell.cmdHasArgs "yum" ["module"] cmd),+      arg <- installFilter cmd+  ]++packageVersionFixed :: Text.Text -> Bool+packageVersionFixed package =+  "-" `Text.isInfixOf` package || ".rpm" `Text.isSuffixOf` package++yumModules :: Shell.ParsedShell -> [Text.Text]+yumModules args =+  [ arg+    | cmd <- Shell.presentCommands args,+      Shell.cmdHasArgs "yum" ["module"] cmd,+      arg <- installFilter cmd+  ]++moduleVersionFixed :: Text.Text -> Bool+moduleVersionFixed = Text.isInfixOf ":"++installFilter :: Shell.Command -> [Text.Text]+installFilter cmd =+  [ arg+    | Shell.cmdHasArgs "yum" ["install"] cmd,+      arg <- Shell.getArgsNoFlags cmd,+      arg /= "install",+      arg /= "module"   ]
src/Hadolint/Rule/DL3034.hs view
@@ -26,5 +26,5 @@           "si",           "patch"         ]-    hasYesOption = Shell.hasAnyFlag ["no-confirm", "y"]+    hasYesOption = Shell.hasAnyFlag ["non-interactive", "n", "no-confirm", "y"] {-# INLINEABLE rule #-}
src/Hadolint/Rule/DL3041.hs view
@@ -12,15 +12,40 @@     severity = DLWarningC     message = "Specify version with `dnf install -y <package>-<version>`." -    check (Run (RunArgs args _)) = foldArguments (all versionFixed . dnfPackages) args+    check (Run (RunArgs args _)) =+      foldArguments (all packageVersionFixed . dnfPackages) args+        && foldArguments (all moduleVersionFixed . dnfModules) 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"+    [ arg+      | cmd <- Shell.presentCommands args,+        not (Shell.cmdHasArgs "dnf" ["module"] cmd),+        arg <- installFilter cmd+    ]++packageVersionFixed :: Text.Text -> Bool+packageVersionFixed package =+  "-" `Text.isInfixOf` package || ".rpm" `Text.isSuffixOf` package++dnfModules :: Shell.ParsedShell -> [Text.Text]+dnfModules args =+  [ arg+    | cmd <- Shell.presentCommands args,+      Shell.cmdHasArgs "dnf" ["module"] cmd,+      arg <- installFilter cmd+  ]++moduleVersionFixed :: Text.Text -> Bool+moduleVersionFixed = Text.isInfixOf ":"++installFilter :: Shell.Command -> [Text.Text]+installFilter cmd =+  [ arg+    | Shell.cmdHasArgs "dnf" ["install"] cmd,+      arg <- Shell.getArgsNoFlags cmd,+      arg /= "install",+      arg /= "module"   ]
src/Hadolint/Rule/DL3059.hs view
@@ -1,29 +1,44 @@ module Hadolint.Rule.DL3059 (rule) where  import Hadolint.Rule+import qualified Hadolint.Shell as Shell import Language.Docker.Syntax   data Acc-  = Acc RunFlags+  = Acc { flags :: RunFlags, count :: Int }   | Empty-  deriving (Eq, Show)+  deriving (Eq) -rule :: Rule args+-- | This Rule catches multiple consecutive `RUN` instructions.+-- It ignores the case where multiple commands are chained together (e.g. with+-- `&&`) because in that case the programmer most likely has deliberately+-- chosen to use multiuple `RUN` instructions. Cases where --mount=xxx flags+-- differ are excluded as well.+rule :: Rule Shell.ParsedShell rule = customRule check (emptyState Empty)   where     code = "DL3059"     severity = DLInfoC     message = "Multiple consecutive `RUN` instructions. Consider consolidation." -    check line st (Run (RunArgs _ flags))-      | state st == Acc flags = st |> addFail CheckFailure {..}-      | otherwise = st |> modify (remember flags)+    check line st (Run (RunArgs ar fl))+      | state st == Empty =+          st |> modify (remember fl (foldArguments countCommands ar))+      | flags (state st) /= fl =+          st |> modify (remember fl (foldArguments countCommands ar))+      | foldArguments countCommands ar > 2 || count (state st) > 2 =+          st |> modify (remember fl (foldArguments countCommands ar))+      | otherwise = st |> addFail CheckFailure {..}+    check _ st (Comment _) = st     check _ st _ = st |> modify reset {-# INLINEABLE rule #-} -remember :: RunFlags -> Acc -> Acc-remember flags _ = Acc flags+remember :: RunFlags -> Int -> Acc -> Acc+remember fl cn _ = Acc { flags = fl, count = cn }  reset :: Acc -> Acc reset _ = Empty++countCommands :: Shell.ParsedShell -> Int+countCommands script = length $ Shell.presentCommands script
src/Hadolint/Rule/Shellcheck.hs view
@@ -3,27 +3,125 @@ import qualified Data.Set as Set import qualified Data.Text as Text import Hadolint.Rule-import Hadolint.Shell (ParsedShell)+import qualified Hadolint.Shell+import Hadolint.Pragma (parseShell) import qualified Hadolint.Shell as Shell import Language.Docker.Syntax import qualified ShellCheck.Interface -rule :: Rule ParsedShell-rule = customRule check (emptyState Shell.defaultShellOpts)++data Acc+  = Acc+      { opts :: Shell.ShellOpts,+        defaultOpts :: Shell.ShellOpts+      }+  | Empty+++rule :: Rule Shell.ParsedShell+rule = customRule check (emptyState Empty)   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+    check _ st (From _) = st |> modify newStage+    check _ st (Arg name _) = st |> modify (addVars [name])+    check _ st (Env pairs) = st |> modify (addVars (map fst pairs))+    check _ st (Shell args) =+      st |> modify (setShell (foldArguments Shell.original args))+    check _ st (Comment com) =+      case Hadolint.Pragma.parseShell com of+        Just sh -> st |> modify (shellPragma sh)+        _ -> st+    check line st (Run (RunArgs args _)) = getFailures (state st) |> foldr addFail st       where-        getFailures = foldArguments (runShellCheck (state st)) args-        runShellCheck opts script = Set.fromList [toFailure line c | c <- Shell.shellcheck opts script]+        getFailures Empty = foldArguments (runShellCheck Shell.defaultShellOpts) args+        getFailures s = foldArguments (runShellCheck (opts s)) args+        runShellCheck options script =+          Set.fromList+            [ toFailure line c+              | c <- Shell.shellcheck options script+            ]     check _ st _ = st {-# INLINEABLE rule #-} +newStage :: Acc -> Acc+newStage Empty =+  Acc+    { opts = Shell.defaultShellOpts,+      defaultOpts = Shell.defaultShellOpts+    }+newStage Acc {..} =+  Acc+    { opts = defaultOpts,+      defaultOpts+    }++addVars :: [Text.Text] -> Acc -> Acc+addVars vars Empty =+  Acc+    { opts =+        Shell.ShellOpts+          { shellName = Shell.shellName Shell.defaultShellOpts,+            envVars = Shell.envVars Shell.defaultShellOpts <> Set.fromList vars+          },+      defaultOpts = Shell.defaultShellOpts+    }+addVars vars Acc {..} =+  Acc+    { opts =+        Shell.ShellOpts+          { shellName = Shell.shellName opts,+            envVars = Shell.envVars opts <> Set.fromList vars+          },+      defaultOpts+    }++setShell :: Text.Text -> Acc -> Acc+setShell sh Empty =+  Acc+    { opts =+        Shell.ShellOpts+          { shellName = sh,+            envVars = Shell.envVars Shell.defaultShellOpts+          },+      defaultOpts = Shell.defaultShellOpts+    }+setShell sh Acc {..} =+  Acc+    { opts =+        Shell.ShellOpts+          { shellName = sh,+            envVars = Shell.envVars opts+          },+      defaultOpts+    }++shellPragma :: Text.Text -> Acc -> Acc+shellPragma sh Empty =+  Acc+    { opts =+        Shell.ShellOpts+          { shellName = sh,+            envVars = Shell.envVars Shell.defaultShellOpts+          },+      defaultOpts =+        Shell.ShellOpts+          { shellName = sh,+            envVars = Shell.envVars Shell.defaultShellOpts+          }+    }+shellPragma sh Acc {..} =+  Acc+    { opts =+        Shell.ShellOpts+          { shellName = sh,+            envVars = Shell.envVars opts+          },+      defaultOpts+    }+ -- | Converts ShellCheck errors into our own errors type-toFailure :: Linenumber -> ShellCheck.Interface.PositionedComment -> CheckFailure+toFailure :: Linenumber ->+  ShellCheck.Interface.PositionedComment ->+  CheckFailure toFailure line c =   CheckFailure     { code = RuleCode $ Text.pack ("SC" ++ show (code c)),@@ -32,13 +130,16 @@       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+    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+    message pc =+      ShellCheck.Interface.cMessage $ ShellCheck.Interface.pcComment pc++getDLSeverity :: ShellCheck.Interface.Severity -> DLSeverity+getDLSeverity s =+  case s of+    ShellCheck.Interface.WarningC -> DLWarningC+    ShellCheck.Interface.InfoC -> DLInfoC+    ShellCheck.Interface.StyleC -> DLStyleC+    _ -> DLErrorC
src/Hadolint/Shell.hs view
@@ -61,7 +61,7 @@  shellcheck :: ShellOpts -> ParsedShell -> [PositionedComment] shellcheck (ShellOpts sh env) (ParsedShell txt _ _) =-  if any (`Text.isPrefixOf` sh) nonPosixShells || hasUnsupportedShebang txt+  if any (`Text.isInfixOf` sh) nonPosixShells || hasUnsupportedShebang txt     then [] -- Do no run for non-posix shells i.e. powershell, cmd.exe     else runShellCheck   where
− test/ConfigSpec.hs
@@ -1,136 +0,0 @@-module ConfigSpec where--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--tests :: SpecWith ()-tests =-  describe "Config" $ do-    it "Parses config with only error severities" $-      let configFile =-            [ "override:",-              "  error:",-              "    - DL3000",-              "    - SC1010"-            ]-          override = Just (OverrideConfig (Just ["DL3000", "SC1010"]) Nothing Nothing Nothing)-          expected = ConfigFile override Nothing Nothing Nothing Nothing Nothing-       in assertConfig expected (Bytes.unlines configFile)--    it "Parses config with only warning severities" $-      let configFile =-            [ "override:",-              "  warning:",-              "    - DL3000",-              "    - SC1010"-            ]-          override = Just (OverrideConfig Nothing (Just ["DL3000", "SC1010"]) Nothing Nothing)-          expected = ConfigFile override Nothing Nothing Nothing Nothing Nothing-       in assertConfig expected (Bytes.unlines configFile)--    it "Parses config with only info severities" $-      let configFile =-            [ "override:",-              "  info:",-              "    - DL3000",-              "    - SC1010"-            ]-          override = Just (OverrideConfig Nothing Nothing (Just ["DL3000", "SC1010"]) Nothing)-          expected = ConfigFile override Nothing Nothing Nothing Nothing Nothing-       in assertConfig expected (Bytes.unlines configFile)--    it "Parses config with only style severities" $-      let configFile =-            [ "override:",-              "  style:",-              "    - DL3000",-              "    - SC1010"-            ]-          override = Just (OverrideConfig Nothing Nothing Nothing (Just ["DL3000", "SC1010"]))-          expected = ConfigFile override Nothing Nothing Nothing Nothing Nothing-       in assertConfig expected (Bytes.unlines configFile)--    it "Parses config with only ignores" $-      let configFile =-            [ "ignored:",-              "- DL3000",-              "- SC1010"-            ]-          expected = ConfigFile Nothing (Just ["DL3000", "SC1010"]) Nothing Nothing Nothing Nothing-       in assertConfig expected (Bytes.unlines configFile)--    it "Parses config with only trustedRegistries" $-      let configFile =-            [ "trustedRegistries:",-              "- hub.docker.com",-              "- my.shady.xyz"-            ]-          expected = ConfigFile Nothing Nothing (Just ["hub.docker.com", "my.shady.xyz"]) Nothing 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 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) Nothing-       in assertConfig expected (Bytes.unlines configFile)--    it "Parses config with failure-threshold" $-      let configFile = [ "failure-threshold: error" ]-          expected = ConfigFile Nothing Nothing Nothing Nothing Nothing (Just Rule.DLErrorC)-       in assertConfig expected (Bytes.unlines configFile)--    it "Parses full file" $-      let configFile =-            [ "override:",-              "  info:",-              "    - DL3002",-              "  style:",-              "    - DL3004",-              "  warning:",-              "    - DL3003",-              "  error:",-              "    - DL3001",-              "",-              "trustedRegistries:",-              "- hub.docker.com",-              "",-              "ignored:",-              "- DL3000",-              "",-              "strict-labels: false",-              "label-schema:",-              "  author: text",-              "  url: url",-              "",-              "failure-threshold: style"-            ]-          override = Just (OverrideConfig (Just ["DL3001"]) (Just ["DL3003"]) (Just ["DL3002"]) (Just ["DL3004"]))-          labelschema = Just (fromList [("author", Rule.RawText), ("url", Rule.Url)])-          expected = ConfigFile override (Just ["DL3000"]) (Just ["hub.docker.com"]) labelschema (Just False) (Just Rule.DLStyleC)-       in assertConfig expected (Bytes.unlines configFile)--assertConfig :: HasCallStack => ConfigFile -> Bytes.ByteString -> Assertion-assertConfig config s =-  case Yaml.decode1Strict s of-    Left (_, err) ->-      assertFailure err-    Right result ->-      checkResult result-  where-    checkResult result =-      unless (result == config) $-        assertFailure ("Config \n\n" ++ show config ++ "\n\n is not \n\n" ++ show result)
− test/DL3000.hs
@@ -1,26 +0,0 @@-module DL3000 (tests) where--import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = mempty-  describe "DL3000 - Use absolute WORKDIR." $ do-    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 "workdir absolute windows" $ ruleCatchesNot "DL3000" "WORKDIR \'C:\\\'"-    it "workdir absolute windows quotes" $ ruleCatchesNot "DL3000" "WORKDIR \"C:\\\""-    it "workdir absolute windows alternative" $-      ruleCatchesNot "DL3000" "WORKDIR C:/"-    it "workdir absolute windows quotes alternative" $-      ruleCatchesNot "DL3000" "WORKDIR \"C:/\""
− test/DL3001.hs
@@ -1,16 +0,0 @@-module DL3001 (tests) where--import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = mempty-  describe "DL3001 - 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"
− test/DL3002.hs
@@ -1,86 +0,0 @@-module DL3002 (tests) where--import Data.Text as Text-import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = mempty-  describe "DL3002 - Last user should not be root." $ do-    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
− test/DL3003.hs
@@ -1,12 +0,0 @@-module DL3003 (tests) where--import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = mempty-  describe "DL3003 - Use WORKDIR to switch to a directory." $ do-    it "ok using WORKDIR" $ ruleCatchesNot "DL3003" "WORKDIR /opt"-    it "not ok using cd" $ ruleCatches "DL3003" "RUN cd /opt"
− test/DL3004.hs
@@ -1,19 +0,0 @@-module DL3004 (tests) where--import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = mempty-  describe "DL3004 - Do not use sudo as it leads to unpredictable behavior. Use a tool like gosu to enforce root." $ do-    it "sudo" $ do-      ruleCatches "DL3004" "RUN sudo apt-get update"-      onBuildRuleCatches "DL3004" "RUN sudo apt-get update"-    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"
− test/DL3005.hs
@@ -1,13 +0,0 @@-module DL3005 (tests) where--import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = mempty-  describe "DL3005" $ do-    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"
− test/DL3006.hs
@@ -1,42 +0,0 @@-module DL3006 (tests) where--import Data.Text as Text-import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = mempty-  describe "DL3006 - Always tag the version of an image explicitly." $ do-    it "no untagged" $ ruleCatches "DL3006" "FROM debian"-    it "no untagged with name" $ ruleCatches "DL3006" "FROM debian 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-    it "scratch" $ ruleCatchesNot "DL3006" "FROM scratch"
− test/DL3007.hs
@@ -1,20 +0,0 @@-module DL3007 (tests) where--import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = mempty-  describe "DL3007 - Using latest is prone to errors if the image will ever update. Pin the version explicitly to a release tag." $ do-    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 SHA" $-      ruleCatchesNot-        "DL3007"-        "FROM hub.docker.io/debian@sha256:\-        \7959ed6f7e35f8b1aaa06d1d8259d4ee25aa85a086d5c125480c333183f9deeb"-    it "explicit tagged with name" $-      ruleCatchesNot "DL3007" "FROM debian:jessie AS builder"
− test/DL3008.hs
@@ -1,61 +0,0 @@-module DL3008 (tests) where--import Data.Text as Text-import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = mempty-  describe "DL3008 - Pin versions in apt-get install." $ do-    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 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 "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
− test/DL3009.hs
@@ -1,81 +0,0 @@-module DL3009 (tests) where--import Data.Text as Text-import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = mempty-  describe "DL3009 - Delete the apt-get lists after installing something." $ do-    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 "don't warn: BuildKit cache mount to apt lists directory" $ do-      ruleCatchesNot "DL3009" "RUN --mount=type=cache,target=/var/lib/apt/lists apt-get update && apt-get install python"-      onBuildRuleCatchesNot "DL3009" "RUN --mount=type=cache,target=/var/lib/apt/lists apt-get update && apt-get install python"
− test/DL3010.hs
@@ -1,13 +0,0 @@-module DL3010 (tests) where--import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = mempty-  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"-    it "ignore: copy from previous stage" $ ruleCatchesNot "DL3010" "COPY --from=builder /usr/local/share/some.tar /opt/some.tar"
− test/DL3011.hs
@@ -1,12 +0,0 @@-module DL3011 (tests) where--import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = mempty-  describe "EXPOSE rules" $ do-    it "invalid port" $ ruleCatches "DL3011" "EXPOSE 80000"-    it "valid port" $ ruleCatchesNot "DL3011" "EXPOSE 60000"
− test/DL3012.hs
@@ -1,18 +0,0 @@-module DL3012 (tests) where--import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = mempty-  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"
− test/DL3013.hs
@@ -1,139 +0,0 @@-module DL3013 (tests) where--import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = mempty-  describe "DL3013 - Pin versions in pip." $ 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"
− test/DL3014.hs
@@ -1,31 +0,0 @@-module DL3014 (tests) where--import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = mempty-  describe "DL3014 - Use the -y switch." $ 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"
− test/DL3015.hs
@@ -1,26 +0,0 @@-module DL3015 (tests) where--import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = mempty-  describe "DL3015 - Avoid additional packages by specifying `--no-install-recommends`." $ do-    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"
− test/DL3016.hs
@@ -1,123 +0,0 @@-module DL3016 (tests) where--import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = mempty-  describe "DL3016 - Pin versions in npm." $ 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"-    it "don't fire on loglevel flag" $ do-      ruleCatchesNot "DL3016" "RUN npm install --loglevel verbose sax@0.1.1"-      onBuildRuleCatchesNot "DL3016" "RUN npm install --loglevel verbose sax@0.1.1"
− test/DL3018.hs
@@ -1,90 +0,0 @@-module DL3018 (tests) where--import Data.Text as Text-import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = mempty-  describe "DL3018 - Pin versions in `apk add`." $ do-    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 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--    it "don't trigger when installing from .apk file" $ do-      ruleCatchesNot "DL3018" "RUN apk add mypackage-1.1.1.apk"-      onBuildRuleCatchesNot "DL3018" "RUN apk add mypackage-1.1.1.apk"
− test/DL3019.hs
@@ -1,25 +0,0 @@-module DL3019 (tests) where--import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = mempty-  describe "DL3019 - Use the --no-cache switch." $ do-    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 "don't warn: apk add with BuildKit cache mount" $ do-      ruleCatchesNot "DL3019" "RUN --mount=type=cache,target=/var/cache/apk apk add -U curl=7.77.0"-      onBuildRuleCatchesNot "DL3019" "RUN --mount=type=cache,target=/var/cache/apk apk add -U curl=7.77.0"-    it "don't warn: apk add with BuildKit cache mount in wrong dir and --no-cache" $ do-      ruleCatchesNot "DL3019" "RUN --mount=type=cache,target=/var/cache/foo apk add --no-cache -U curl=7.77.0"-      onBuildRuleCatchesNot "DL3019" "RUN --mount=type=cache,target=/var/cache/foo apk add --no-cache -U curl=7.77.0"-    it "warn: apk add with BuildKit cache mount to wrong dir" $ do-      ruleCatches "DL3019" "RUN --mount=type=cache,target=/var/cache/foo apk add -U curl=7.77.0"-      onBuildRuleCatches "DL3019" "RUN --mount=type=cache,target=/var/cache/foo apk add -U curl=7.77.0"
− test/DL3020.hs
@@ -1,18 +0,0 @@-module DL3020 (tests) where--import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = mempty-  describe "DL3020 - Use `COPY` instead of `ADD` for files and folders." $ do-    it "add for tar" $ ruleCatchesNot "DL3020" "ADD file.tar /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/"-    it "using add" $ ruleCatches "DL3020" "ADD file /usr/src/app/"-    it "warn for zip" $ ruleCatches "DL3020" "ADD file.zip /usr/src/app/"
− test/DL3021.hs
@@ -1,13 +0,0 @@-module DL3021 (tests) where--import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = mempty-  describe "DL3021 - `COPY` with more than 2 arguments requires the last argument to end with `/`" $ 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/"
− test/DL3022.hs
@@ -1,29 +0,0 @@-module DL3022 (tests) where--import Data.Text as Text-import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = mempty-  describe "DL3022 - `COPY --from` should reference a previously defined `FROM` 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
− test/DL3023.hs
@@ -1,26 +0,0 @@-module DL3023 (tests) where--import Data.Text as Text-import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = mempty-  describe "DL3023 - `COPY --from` cannot reference its own `FROM` alias" $ 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
− test/DL3024.hs
@@ -1,27 +0,0 @@-module DL3024 (tests) where--import Data.Text as Text-import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = mempty-  describe "DL3024 - 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
− test/DL3025.hs
@@ -1,36 +0,0 @@-module DL3025 (tests) where--import Data.Text as Text-import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = mempty-  describe "DL3025 - Use arguments JSON notation for `CMD` and `ENTRYPOINT` arguments" $ do-    it "warn on ENTRYPOINT" $-      let dockerFile =-            [ "FROM node as foo",-              "ENTRYPOINT something"-            ]-       in ruleCatches "DL3025" $ Text.unlines dockerFile-    it "don't warn on ENTRYPOINT json notation" $-      let dockerFile =-            [ "FROM scratch as build",-              "ENTRYPOINT [\"foo\", \"bar\"]"-            ]-       in ruleCatchesNot "DL3025" $ Text.unlines dockerFile-    it "warn on CMD" $-      let dockerFile =-            [ "FROM node as foo",-              "CMD something"-            ]-       in ruleCatches "DL3025" $ Text.unlines dockerFile-    it "don't warn on CMD json notation" $-      let dockerFile =-            [ "FROM scratch as build",-              "CMD [\"foo\", \"bar\"]",-              "CMD [ \"foo\", \"bar\" ]"-            ]-       in ruleCatchesNot "DL3025" $ Text.unlines dockerFile
− test/DL3026.hs
@@ -1,56 +0,0 @@-module DL3026 (tests) where--import Data.Map as Map-import Data.Text as Text-import Hadolint.Process-import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = mempty-  describe "DL2036 - Use only an allowed registry in the FROM image" $ 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
− test/DL3027.hs
@@ -1,19 +0,0 @@-module DL3027 (tests) where--import Data.Text as Text-import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = mempty-  describe "DL3027 - Do not use `apt` as it is meant to be a end-user tool, use `apt-get` or `apt-cache` instead" $ do-    it "apt" $-      let dockerFile =-            [ "FROM ubuntu",-              "RUN apt install python"-            ]-       in do-            ruleCatches "DL3027" $ Text.unlines dockerFile-            onBuildRuleCatches "DL3027" $ Text.unlines dockerFile
− test/DL3028.hs
@@ -1,42 +0,0 @@-module DL3028 (tests) where--import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = mempty-  describe "DL3028 - Pin versions in gem install." $-    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"
− test/DL3029.hs
@@ -1,12 +0,0 @@-module DL3029 (tests) where--import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = mempty-  describe "DL3029 - Do not use --platform flag with FROM." $ do-    it "explicit platform flag" $ ruleCatches "DL3029" "FROM --platform=linux debian:jessie"-    it "no platform flag" $ ruleCatchesNot "DL3029" "FROM debian:jessie"
− test/DL3030.hs
@@ -1,18 +0,0 @@-module DL3030 (tests) where--import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = mempty-  describe "DL3030 - Use the `-y` switch to avoid manual input `yum install -y <package>`" $ do-    it "not ok when not using `-y` switch" $ do-      ruleCatches "DL3030" "RUN yum install httpd-2.4.24 && yum clean all"-      onBuildRuleCatches "DL3030" "RUN yum install httpd-2.4.24 && yum clean all"-    it "ok when using `-y` switch" $ do-      ruleCatchesNot "DL3030" "RUN yum install -y httpd-2.4.24 && yum clean all"-      ruleCatchesNot "DL3030" "RUN bash -c `# not even a yum command`"-      onBuildRuleCatchesNot "DL3030" "RUN yum install -y httpd-2.4.24 && yum clean all"-      onBuildRuleCatchesNot "DL3030" "RUN bash -c `# not even a yum command`"
− test/DL3032.hs
@@ -1,18 +0,0 @@-module DL3032 (tests) where--import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = mempty-  describe "DL3032 - `yum clean all` missing after yum command." $ do-    it "not ok with no clean all" $ do-      ruleCatches "DL3032" "RUN yum install -y mariadb-10.4"-      onBuildRuleCatches "DL3032" "RUN yum install -y mariadb-10.4"-    it "ok with yum clean all " $ do-      ruleCatchesNot "DL3032" "RUN yum install -y mariadb-10.4 && yum clean all"-      ruleCatchesNot "DL3032" "RUN bash -c `# not even a yum command`"-      onBuildRuleCatchesNot "DL3032" "RUN yum install -y mariadb-10.4 && yum clean all"-      onBuildRuleCatchesNot "DL3032" "RUN bash -c `# not even a yum command`"
− test/DL3033.hs
@@ -1,18 +0,0 @@-module DL3033 (tests) where--import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = mempty-  describe "DL3033 - Specify version with `yum install -y <package>-<version>`" $ do-    it "not ok wihout yum version pinning" $ do-      ruleCatches "DL3033" "RUN yum install -y tomcat && yum clean all"-      onBuildRuleCatches "DL3033" "RUN yum install -y tomcat && yum clean all"-    it "ok with yum version pinning" $ do-      ruleCatchesNot "DL3033" "RUN yum install -y tomcat-9.2 && yum clean all"-      ruleCatchesNot "DL3033" "RUN bash -c `# not even a yum command`"-      onBuildRuleCatchesNot "DL3033" "RUN yum install -y tomcat-9.2 && yum clean all"-      onBuildRuleCatchesNot "DL3033" "RUN bash -c `# not even a yum command`"
− test/DL3034.hs
@@ -1,18 +0,0 @@-module DL3034 (tests) where--import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = mempty-  describe "DL3034 - Non-interactive switch missing from zypper command: `zypper install -y`" $ do-    it "not ok without non-interactive switch" $ do-      ruleCatches "DL3034" "RUN zypper install httpd=2.4.24 && zypper clean"-      onBuildRuleCatches "DL3034" "RUN zypper install httpd=2.4.24 && zypper clean"-    it "ok with non-interactive switch present" $ do-      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"-      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"
− test/DL3035.hs
@@ -1,15 +0,0 @@-module DL3035 (tests) where--import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = mempty-  describe "DL3035" $ do-    it "not ok: zypper dist-upgrade" $ do-      ruleCatches "DL3035" "RUN zypper dist-upgrade"-      ruleCatches "DL3035" "RUN zypper dup"-      onBuildRuleCatches "DL3035" "RUN zypper dist-upgrade"-      onBuildRuleCatches "DL3035" "RUN zypper dup"
− test/DL3036.hs
@@ -1,18 +0,0 @@-module DL3036 (tests) where--import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = mempty-  describe "DL3036 - `zypper clean` missing after zypper use." $ do-    it "not ok without zypper clean" $ do-      ruleCatches "DL3036" "RUN zypper install -y mariadb=10.4"-      onBuildRuleCatches "DL3036" "RUN zypper install -y mariadb=10.4"-    it "ok with zypper clean" $ do-      ruleCatchesNot "DL3036" "RUN zypper install -y mariadb=10.4 && zypper clean"-      ruleCatchesNot "DL3036" "RUN zypper install -y mariadb=10.4 && zypper cc"-      onBuildRuleCatchesNot "DL3036" "RUN zypper install -y mariadb=10.4 && zypper clean"-      onBuildRuleCatchesNot "DL3036" "RUN zypper install -y mariadb=10.4 && zypper cc"
− test/DL3037.hs
@@ -1,28 +0,0 @@-module DL3037 (tests) where--import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = mempty-  describe "DL3037 - Specify version with `zypper install -y <package>[=]<version>`." $ do-    -- NOTE: In Haskell strings, '\' has to be escaped. And in shell commands, '>'-    -- and '<' have to be escaped. Hence the double escaping.-    it "not ok without zypper version pinning" $ do-      ruleCatches "DL3037" "RUN zypper install -y tomcat && zypper clean"-      onBuildRuleCatches "DL3037" "RUN zypper install -y tomcat && zypper clean"-    it "ok with different variants of zypper version pinning" $ do-      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"-      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"
− test/DL3038.hs
@@ -1,18 +0,0 @@-module DL3038 (tests) where--import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = mempty-  describe "DL3038 - Use the `-y` switch to avoid manual input `dnf install -y <package>`" $ do-    it "not ok without dnf non-interactive flag" $ do-      ruleCatches "DL3038" "RUN dnf install httpd-2.4.24 && dnf clean all"-      onBuildRuleCatches "DL3038" "RUN dnf install httpd-2.4.24 && dnf clean all"-    it "ok with dnf non-interactive flag" $ do-      ruleCatchesNot "DL3038" "RUN dnf install -y httpd-2.4.24 && dnf clean all"-      ruleCatchesNot "DL3038" "RUN notdnf install httpd"-      onBuildRuleCatchesNot "DL3038" "RUN dnf install -y httpd-2.4.24 && dnf clean all"-      onBuildRuleCatchesNot "DL3038" "RUN notdnf install httpd"
− test/DL3040.hs
@@ -1,18 +0,0 @@-module DL3040 (tests) where--import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = mempty-  describe "DL3040 - `dnf clean all` missing after dnf command." $ do-    it "no ok without dnf clean all" $ do-      ruleCatches "DL3040" "RUN dnf install -y mariadb-10.4"-      onBuildRuleCatches "DL3040" "RUN dnf install -y mariadb-10.4"-    it "ok with dnf clean all" $ do-      ruleCatchesNot "DL3040" "RUN dnf install -y mariadb-10.4 && dnf clean all"-      ruleCatchesNot "DL3040" "RUN notdnf install mariadb"-      onBuildRuleCatchesNot "DL3040" "RUN dnf install -y mariadb-10.4 && dnf clean all"-      onBuildRuleCatchesNot "DL3040" "RUN notdnf install mariadb"
− test/DL3041.hs
@@ -1,18 +0,0 @@-module DL3041 (tests) where--import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = mempty-  describe "DL3041 - Specify version with `dnf install -y <package>-<version>`" $ do-    it "not ok without dnf version pinning" $ do-      ruleCatches "DL3041" "RUN dnf install -y tomcat && dnf clean all"-      onBuildRuleCatches "DL3041" "RUN dnf install -y tomcat && dnf clean all"-    it "ok with dnf version pinning" $ do-      ruleCatchesNot "DL3041" "RUN dnf install -y tomcat-9.0.1 && dnf clean all"-      ruleCatchesNot "DL3041" "RUN notdnf install tomcat"-      onBuildRuleCatchesNot "DL3041" "RUN dnf install -y tomcat-9.0.1 && dnf clean all"-      onBuildRuleCatchesNot "DL3041" "RUN notdnf install tomcat"
− test/DL3042.hs
@@ -1,86 +0,0 @@-module DL3042 (tests) where--import Data.Text as Text-import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = mempty-  describe "DL3042 - Avoid cache directory with `pip install --no-cache-dir <package>`." $ 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"--    -- ENV won't work with the onBuildRuleCatches[Not] wrapper:-    it "respect ENV PIP_NO_CACHE_DIR with truthy values" $ do-      ruleCatchesNot "DL3042" "ENV PIP_NO_CACHE_DIR=1\nRUN pip install MySQL_python"-      ruleCatchesNot "DL3042" "ENV PIP_NO_CACHE_DIR=on\nRUN pip install MySQL_python"-      ruleCatchesNot "DL3042" "ENV PIP_NO_CACHE_DIR=yes\nRUN pip install MySQL_python"-      ruleCatchesNot "DL3042" "ENV PIP_NO_CACHE_DIR=true\nRUN pip install MySQL_python"-    it "respect RUN PIP_NO_CACHE_DIR=... with truthy values" $ do-      ruleCatchesNot "DL3042" "RUN PIP_NO_CACHE_DIR=1 pip install MySQL_python"-      ruleCatchesNot "DL3042" "RUN PIP_NO_CACHE_DIR=on pip install MySQL_python"-      ruleCatchesNot "DL3042" "RUN PIP_NO_CACHE_DIR=yes pip install MySQL_python"-      ruleCatchesNot "DL3042" "RUN PIP_NO_CACHE_DIR=true pip install MySQL_python"-    it "respect RUN export PIP_NO_CACHE_DIR=... with truthy values" $ do-      ruleCatchesNot "DL3042" "RUN export PIP_NO_CACHE_DIR=1 && pip install MySQL_python"-      ruleCatchesNot "DL3042" "RUN export PIP_NO_CACHE_DIR=on && pip install MySQL_python"-      ruleCatchesNot "DL3042" "RUN export PIP_NO_CACHE_DIR=yes && pip install MySQL_python"-      ruleCatchesNot "DL3042" "RUN export PIP_NO_CACHE_DIR=true && pip install MySQL_python"-    it "respect ENV PIP_NO_CACHE_DIR with falsy values" $ do-      ruleCatches "DL3042" "ENV PIP_NO_CACHE_DIR=0\nRUN pip install MySQL_python"-      ruleCatches "DL3042" "ENV PIP_NO_CACHE_DIR=off\nRUN pip install MySQL_python"-      ruleCatches "DL3042" "ENV PIP_NO_CACHE_DIR=no\nRUN pip install MySQL_python"-      ruleCatches "DL3042" "ENV PIP_NO_CACHE_DIR=false\nRUN pip install MySQL_python"-    it "respect RUN PIP_NO_CACHE_DIR=... with falsy values" $ do-      ruleCatches "DL3042" "RUN PIP_NO_CACHE_DIR=0 pip install MySQL_python"-      ruleCatches "DL3042" "RUN PIP_NO_CACHE_DIR=off pip install MySQL_python"-      ruleCatches "DL3042" "RUN PIP_NO_CACHE_DIR=no pip install MySQL_python"-      ruleCatches "DL3042" "RUN PIP_NO_CACHE_DIR=false pip install MySQL_python"-    it "respect RUN export PIP_NO_CACHE_DIR=... with falsy values" $ do-      ruleCatches "DL3042" "RUN export PIP_NO_CACHE_DIR=0 && pip install MySQL_python"-      ruleCatches "DL3042" "RUN export PIP_NO_CACHE_DIR=off && pip install MySQL_python"-      ruleCatches "DL3042" "RUN export PIP_NO_CACHE_DIR=no && pip install MySQL_python"-      ruleCatches "DL3042" "RUN export PIP_NO_CACHE_DIR=false && pip install MySQL_python"--    it "don't trigger if PIP_NO_CACHE_DIR is inherited" $-      let dockerFile = Text.unlines-            [ "FROM debian:buster as base",-              "ENV PIP_NO_CACHE_DIR=1",-              "FROM base",-              "RUN pip install six"-            ]-       in do-        ruleCatchesNot "DL3042" dockerFile-    it "trigger if PIP_NO_CACHE_DIR is not inherited" $-      let dockerFile = Text.unlines-            [ "FROM debian:buster as base",-              "ENV PIP_NO_CACHE_DIR=1",-              "FROM debian:buster",-              "RUN pip install six"-            ]-       in do-        ruleCatches "DL3042" dockerFile
− test/DL3043.hs
@@ -1,43 +0,0 @@-module DL3043 (tests) where--import Data.Text as Text-import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = mempty-  describe "DL3043 - `ONBUILD`, `FROM` or `MAINTAINER` triggered from within `ONBUILD` instruction." $ 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\""
− test/DL3044.hs
@@ -1,42 +0,0 @@-module DL3044 (tests) where--import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = mempty-  describe "DL3044 - Do not refer to an environment variable within the same `ENV` statement where it is defined." $ 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}\""
− test/DL3045.hs
@@ -1,156 +0,0 @@-module DL3045 (tests) where--import Data.Text as Text-import Helpers-import Test.Hspec--tests :: SpecWith ()-tests = do-  let ?rulesConfig = mempty-  describe "DL3045 - `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 absolute destination and no `WORKDIR` set - windows with alternative paths" $ 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--    it "regression: don't crash with single character paths" $ do-      ruleCatches "DL3045" "COPY a b"-      onBuildRuleCatches "DL3045" "COPY a b"
− test/DL3046.hs
@@ -1,15 +0,0 @@-module DL3046 (tests) where--import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = mempty-  describe "DL3046 - `useradd` without flag `-l` and high UID will result in excessively large Image." $ 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"
− test/DL3047.hs
@@ -1,71 +0,0 @@-module DL3047 (tests) where--import Data.Text as Text-import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = mempty-  describe "DL3047 - `wget` without flag `--progress` will result in excessively bloated build logs when downloading larger files." $ 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
− test/DL3048.hs
@@ -1,37 +0,0 @@-module DL3048 (tests) where--import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = mempty-  describe "DL3048 - 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\""
− test/DL3049.hs
@@ -1,80 +0,0 @@-module DL3049 (tests) where--import Data.Text as Text-import qualified Data.Map as Map-import qualified Hadolint.Process-import qualified Hadolint.Rule as Rule-import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = Hadolint.Process.RulesConfig [] (Map.fromList [("foo", Rule.RawText)]) False-  describe "DL3049 - Missing label rule tests" $ 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")
− test/DL3050.hs
@@ -1,25 +0,0 @@-module DL3050 (tests) where--import qualified Data.Map as Map-import qualified Hadolint.Process-import qualified Hadolint.Rule as Rule-import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = Hadolint.Process.RulesConfig [] (Map.fromList [("required", Rule.RawText)]) True-  describe "DL3050 - Superfluous label(s) present." $ 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\""
− test/DL3051.hs
@@ -1,22 +0,0 @@-module DL3051 (tests) where--import qualified Data.Map as Map-import qualified Hadolint.Process-import qualified Hadolint.Rule as Rule-import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = Hadolint.Process.RulesConfig [] (Map.fromList [("emptylabel", Rule.RawText)]) False-  describe "DL3051 - Label `<label>` is empty." $ 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=\"\""
− test/DL3052.hs
@@ -1,22 +0,0 @@-module DL3052 (tests) where--import qualified Data.Map as Map-import qualified Hadolint.Process-import qualified Hadolint.Rule as Rule-import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = Hadolint.Process.RulesConfig [] (Map.fromList [("urllabel", Rule.Url)]) False-  describe "DL3052 - Label `<label>` is not a valid URL." $ 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\""
− test/DL3053.hs
@@ -1,22 +0,0 @@-module DL3053 (tests) where--import qualified Data.Map as Map-import qualified Hadolint.Process-import qualified Hadolint.Rule as Rule-import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = Hadolint.Process.RulesConfig [] (Map.fromList [("datelabel", Rule.Rfc3339)]) False-  describe "DL3053 - Label `<label>` is not a valid time format - must be conform to RFC3339." $ 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\""
− test/DL3054.hs
@@ -1,22 +0,0 @@-module DL3054 (tests) where--import qualified Data.Map as Map-import qualified Hadolint.Process-import qualified Hadolint.Rule as Rule-import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = Hadolint.Process.RulesConfig [] (Map.fromList [("spdxlabel", Rule.Spdx)]) False-  describe "DL3054 - Label `<label>` is not a valid SPDX license identifier." $ 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\""
− test/DL3055.hs
@@ -1,25 +0,0 @@-module DL3055 (tests) where--import qualified Data.Map as Map-import qualified Hadolint.Process-import qualified Hadolint.Rule as Rule-import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = Hadolint.Process.RulesConfig [] (Map.fromList [("githash", Rule.GitHash)]) False-  describe "DL3055 - Label `<label>` is not a valid git hash." $ 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\""
− test/DL3056.hs
@@ -1,22 +0,0 @@-module DL3056 (tests) where--import qualified Data.Map as Map-import qualified Hadolint.Process-import qualified Hadolint.Rule as Rule-import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = Hadolint.Process.RulesConfig [] (Map.fromList [("semver", Rule.SemVer)]) False-  describe "DL3056 - Label `<label>` does not conform to semantic versioning." $ 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\""
− test/DL3057.hs
@@ -1,18 +0,0 @@-module DL3057 (tests) where--import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = mempty-  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"
− test/DL3058.hs
@@ -1,22 +0,0 @@-module DL3058 (tests) where--import qualified Data.Map as Map-import qualified Hadolint.Process-import qualified Hadolint.Rule as Rule-import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = Hadolint.Process.RulesConfig [] (Map.fromList [("maintainer", Rule.Email)]) False-  describe "DL3058 - Label `<label>` is not a valid email format - must be conform to RFC5322." $ do-    it "not ok with label not containing valid email" $ do-      ruleCatches "DL3058" "LABEL maintainer=\"not-email\""-      onBuildRuleCatches "DL3058" "LABEL maintainer=\"not-email\""-    it "ok with label containing valid email" $ do-      ruleCatchesNot "DL3058" "LABEL maintainer=\"abcd@google.com\""-      onBuildRuleCatchesNot "DL3058" "LABEL maintainer=\"abcd@google.com\""-    it "ok with other label not containing valid email" $ do-      ruleCatchesNot "DL3058" "LABEL other=\"doo\""-      onBuildRuleCatchesNot "DL3058" "LABEL other=\"bar\""
− test/DL3059.hs
@@ -1,31 +0,0 @@-module DL3059 (tests) where--import Data.Text as Text-import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = mempty-  describe "DL3059 - Multiple consecutive `RUN` instructions" $ do-    it "ok with no `RUN` at all" $ do-      ruleCatchesNot "DL3059" "FROM debian:10"-    it "ok with one `RUN`" $ do-      ruleCatchesNot "DL3059" "RUN /foo.sh"-    it "ok with two not consecutive `RUN`" $ do-      ruleCatchesNot "DL3059" "RUN /foo.sh\nWORKDIR /\nRUN /bar.sh"-    it "not ok with two consecutive `RUN`s" $ do-      ruleCatches "DL3059" "RUN /foo.sh\nRUN /bar.sh"-    it "ok with two consecutive `RUN`s when flags are different 1" $ do-      ruleCatchesNot "DL3059" "RUN --mount=type=secret,id=foo /foo.sh\nRUN /bar.sh"-    it "ok with two consecutive `RUN`s when flags are different 2" $ do-      let dfile = [ "RUN --mount=type=secret,id=foo /foo.sh",-                    "RUN --mount=type=secret,id=bar /bar.sh"-                  ]-       in ruleCatchesNot "DL3059" $ Text.unlines dfile-    it "not ok with two consecutive `RUN`s when flags are equal" $ do-      let dfile = [ "RUN --mount=type=secret,id=foo /foo.sh",-                    "RUN --mount=type=secret,id=foo /bar.sh"-                  ]-       in ruleCatches "DL3059" $ Text.unlines dfile
− test/DL3060.hs
@@ -1,68 +0,0 @@-module DL3060 (tests) where--import qualified Data.Text as Text-import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = mempty-  describe "DL3060 - `yarn cache clean` missing after `yarn install`" $ do-    it "ok with non-yarn commands" $ do-      ruleCatchesNot "DL3060" "RUN foo"-      onBuildRuleCatchesNot "DL3060" "RUN foo"-    it "not ok with no cache clean" $ do-      ruleCatches "DL3060" "RUN yarn install foo"-      onBuildRuleCatches "DL3060" "RUN yarn install foo"-    it "ok with cache clean" $ do-      ruleCatchesNot "DL3060" "RUN yarn install bar && yarn cache clean"-      onBuildRuleCatchesNot "DL3060" "RUN yarn install bar && yarn cache clean"--    it "not ok when yarn install is in last stage w/o yarn clean" $-      let dockerFile =-            Text.unlines-              [ "FROM node:lts-alpine as foo",-                "RUN hey!",-                "FROM scratch",-                "RUN yarn install"-              ]-       in do-            ruleCatches "DL3060" dockerFile-            onBuildRuleCatches "DL3060" dockerFile--    it "not ok when inheriting from stage with yarn install w/o yarn clean" $-      let dockerFile =-            Text.unlines-              [ "FROM node:lts-alpine as foo",-                "RUN yarn install",-                "FROM foo",-                "RUN hey!"-              ]-       in do-            ruleCatches "DL3060" dockerFile-            onBuildRuleCatches "DL3060" dockerFile--    it "ok when inheriting from stage with yarn cache clear" $-      let dockerFile =-            Text.unlines-              [ "FROM node:lts-alpine as foo",-                "RUN yarn install && yarn cache clean",-                "FROM foo",-                "RUN hey!"-              ]-       in do-            ruleCatchesNot "DL3060" dockerFile-            onBuildRuleCatchesNot "DL3060" dockerFile--    it "ok when omitting yarn cache clean in stage that is not reused later" $-      let dockerFile =-            Text.unlines-              [ "FROM node:lts-alpine as foo",-                "RUN yarn install foo",-                "FROM scratch",-                "RUN hey!"-              ]-       in do-            ruleCatchesNot "DL3060" dockerFile-            onBuildRuleCatches "DL3060" dockerFile
− test/DL4000.hs
@@ -1,15 +0,0 @@-module DL4000 (tests) where--import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = mempty-  describe "DL4000 - MAINTAINER is deprecated." $ do-    it "has deprecated maintainer" $-      ruleCatches "DL4000" "FROM busybox\nMAINTAINER hudu@mail.com"-    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"
− test/DL4001.hs
@@ -1,69 +0,0 @@-module DL4001 (tests) where--import Data.Text as Text-import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = mempty-  describe "DL4001 - Either use Wget or Curl but not both." $ 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")
− test/DL4003.hs
@@ -1,42 +0,0 @@-module DL4003 (tests) where--import Data.Text as Text-import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = mempty-  describe "DL4003" $ do-    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"
− test/DL4004.hs
@@ -1,45 +0,0 @@-module DL4004 (tests) where--import Data.Text as Text-import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = mempty-  describe "DL4004" $ do-    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"
− test/DL4005.hs
@@ -1,19 +0,0 @@-module DL4005 (tests) where--import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = mempty-  describe "DL4005 - Use `SHELL` to change the default 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"
− test/DL4006.hs
@@ -1,104 +0,0 @@-module DL4006 (tests) where--import Data.Text as Text-import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = mempty-  describe "DL4006 - Set the `SHELL` option `-o pipefail` before RUN with a pipe in it" $ 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-    it "ignore non posix shells: pwsh" $-      let dockerFile =-            [ "FROM mcr.microsoft.com/powershell:ubuntu-16.04",-              "SHELL [ \"pwsh\", \"-c\" ]",-              "RUN Get-Variable PSVersionTable | Select-Object -ExpandProperty Value"-            ]-       in ruleCatchesNot "DL4006" $ Text.unlines dockerFile-    it "ignore non posix shells: powershell" $-      let dockerFile =-            [ "FROM mcr.microsoft.com/powershell:ubuntu-16.04",-              "SHELL [ \"powershell.exe\" ]",-              "RUN Get-Variable PSVersionTable | Select-Object -ExpandProperty Value"-            ]-       in ruleCatchesNot "DL4006" $ Text.unlines dockerFile-    it "ignore non posix shells: cmd.exe" $-      let dockerFile =-            [ "FROM mcr.microsoft.com/powershell:ubuntu-16.04",-              "SHELL [ \"cmd.exe\", \"/c\" ]",-              "RUN Get-Variable PSVersionTable | Select-Object -ExpandProperty Value"-            ]-       in ruleCatchesNot "DL4006" $ Text.unlines dockerFile
+ test/Hadolint/Config/CommandlineSpec.hs view
@@ -0,0 +1,346 @@+module Hadolint.Config.CommandlineSpec (spec) where++import Test.Hspec+import Test.HUnit hiding (Label)+import Hadolint+import Options.Applicative+  ( ParserResult (..),+    defaultPrefs,+    execParserPure,+    fullDesc,+    info,+  )+import qualified Data.Map as Map+import qualified Data.Set as Set+++spec :: SpecWith ()+spec = do+  describe "Config from Commandline" $ do+    it "parse with no arguments given" $ do+      checkCommandline [] $ CommandlineConfig+                              False+                              Nothing+                              []+                              Nothing+                              mempty++    describe "parse version flag" $ do+      it "parse -v" $ do+        checkCommandline ["-v"] $ CommandlineConfig+                                True+                                Nothing+                                []+                                Nothing+                                mempty++      it "parse --version" $ do+        checkCommandline ["--version"] $ CommandlineConfig+                                True+                                Nothing+                                []+                                Nothing+                                mempty++    describe "parse config file option" $ do+      it "parse -c hadolint.yaml" $ do+        checkCommandline ["-c", "hadolint.yaml"] $ CommandlineConfig+                                False+                                (Just "hadolint.yaml")+                                []+                                Nothing+                                mempty++      it "parse --config hadolint.yaml" $ do+        checkCommandline ["--config", "hadolint.yaml"] $ CommandlineConfig+                                False+                                (Just "hadolint.yaml")+                                []+                                Nothing+                                mempty++    describe "parse file arguments" $ do+      it "parse `Dockerfile`" $ do+        checkCommandline ["Dockerfile"] $ CommandlineConfig+                                False+                                Nothing+                                ["Dockerfile"]+                                Nothing+                                mempty++      it "parse `Dockerfile1 Dockerfile2`" $ do+        checkCommandline ["Dockerfile1", "Dockerfile2"] $ CommandlineConfig+                                False+                                Nothing+                                ["Dockerfile1", "Dockerfile2"]+                                Nothing+                                mempty++      it "parse --file-path-in-report foobar/Dockerfile" $ do+        checkCommandline+          ["--file-path-in-report", "foobar/Dockerfile"]+          ( CommandlineConfig+              False+              Nothing+              []+              (Just "foobar/Dockerfile")+              mempty+          )++    describe "parse general configuration" $ do+      it "parse --no-fail" $ do+        checkCommandline ["--no-fail"] $ CommandlineConfig+                                          False+                                          Nothing+                                          []+                                          Nothing+                                          mempty { partialNoFail = Just True }++      it "parse --no-color" $ do+        checkCommandline ["--no-color"] $ CommandlineConfig+                                            False+                                            Nothing+                                            []+                                            Nothing+                                            mempty { partialNoColor = Just True }++      it "parse -V" $ do+        checkCommandline ["-V"] $ CommandlineConfig+                                False+                                Nothing+                                []+                                Nothing+                                mempty { partialVerbose = Just True }++      it "parse --verbose" $ do+        checkCommandline ["--verbose"] $ CommandlineConfig+                                False+                                Nothing+                                []+                                Nothing+                                mempty { partialVerbose = Just True }++      it "parse -f json" $ do+        checkCommandline ["-f", "json"] $ CommandlineConfig+                                False+                                Nothing+                                []+                                Nothing+                                mempty { partialFormat = Just Json }++      it "parse --format" $ do+        checkCommandline ["--format", "sarif"] $ CommandlineConfig+                                False+                                Nothing+                                []+                                Nothing+                                mempty { partialFormat = Just Sarif }++    describe "parse severity overrides" $ do+      it "parse --error=DL3010" $ do+        checkCommandline ["--error", "DL3010"] $ CommandlineConfig+                                False+                                Nothing+                                []+                                Nothing+                                mempty { partialErrorRules = ["DL3010"] }++      it "parse --error=DL3010 --error=DL3020" $ do+        checkCommandline+          ["--error", "DL3010", "--error=DL3020"]+          ( CommandlineConfig+              False+              Nothing+              []+              Nothing+              mempty { partialErrorRules = ["DL3010", "DL3020"] }+          )++      it "parse --warning=DL3010" $ do+        checkCommandline ["--warning", "DL3010"] $ CommandlineConfig+                                False+                                Nothing+                                []+                                Nothing+                                mempty { partialWarningRules = ["DL3010"] }++      it "parse --warning=DL3010 --warning=DL3020" $ do+        checkCommandline+          ["--warning", "DL3010", "--warning=DL3020"]+          ( CommandlineConfig+              False+              Nothing+              []+              Nothing+              mempty { partialWarningRules = ["DL3010", "DL3020"] }+          )++      it "parse --info=DL3010" $ do+        checkCommandline ["--info", "DL3010"] $ CommandlineConfig+                                False+                                Nothing+                                []+                                Nothing+                                mempty { partialInfoRules = ["DL3010"] }++      it "parse --info=DL3010 --info=DL3020" $ do+        checkCommandline+          ["--info", "DL3010", "--info=DL3020"]+          ( CommandlineConfig+              False+              Nothing+              []+              Nothing+              mempty { partialInfoRules = ["DL3010", "DL3020"] }+          )++      it "parse --style=DL3010" $ do+        checkCommandline ["--style", "DL3010"] $ CommandlineConfig+                                False+                                Nothing+                                []+                                Nothing+                                mempty { partialStyleRules = ["DL3010"] }++      it "parse --style=DL3010 --style=DL3020" $ do+        checkCommandline+          ["--style", "DL3010", "--style=DL3020"]+          ( CommandlineConfig+              False+              Nothing+              []+              Nothing+              mempty { partialStyleRules = ["DL3010", "DL3020"] }+          )++      it "parse --ignore=DL3010" $ do+        checkCommandline ["--ignore", "DL3010"] $ CommandlineConfig+                                False+                                Nothing+                                []+                                Nothing+                                mempty { partialIgnoreRules = ["DL3010"] }++      it "parse --ignore=DL3010 --ignore=DL3020" $ do+        checkCommandline+          ["--ignore", "DL3010", "--ignore=DL3020"]+          ( CommandlineConfig+              False+              Nothing+              []+              Nothing+              mempty { partialIgnoreRules = ["DL3010", "DL3020"] }+          )++    describe "parse trusted registries" $ do+      it "parse --trusted-registry foobar.com" $ do+        checkCommandline+          ["--trusted-registry", "foobar.com"]+          ( CommandlineConfig+              False+              Nothing+              []+              Nothing+              mempty { partialAllowedRegistries = Set.fromList ["foobar.com"] }+          )++      it "parse --trusted-registry foobar.com --trusted-registry barfoo.io" $ do+        checkCommandline+          [ "--trusted-registry",+            "foobar.com",+            "--trusted-registry",+            "barfoo.io"+          ]+          ( CommandlineConfig+              False+              Nothing+              []+              Nothing+              mempty+                { partialAllowedRegistries =+                    Set.fromList ["foobar.com", "barfoo.io"]+                }+          )++    describe "parse required labels" $ do+      it "parse --require-label foo:email" $ do+        checkCommandline+          ["--require-label", "foo:email"]+          ( CommandlineConfig+              False+              Nothing+              []+              Nothing+              mempty { partialLabelSchema = Map.fromList [("foo", Email)] }+          )++      it "parse --require-label foo:email --require-label bar:text" $ do+        checkCommandline+          ["--require-label", "foo:email", "--require-label", "bar:text"]+          ( CommandlineConfig+              False+              Nothing+              []+              Nothing+              mempty+                { partialLabelSchema =+                    Map.fromList [("foo", Email), ("bar", RawText)]+                }+          )++    describe "parse strict labels" $ do+      it "parse --strict-labels" $ do+        checkCommandline+          ["--strict-labels"]+          ( CommandlineConfig+              False+              Nothing+              []+              Nothing+              mempty { partialStrictLabels = Just True }+          )++    describe "parse failure thresholds" $ do+      it "parse -t warning" $ do+        checkCommandline ["-t", "warning"] $ CommandlineConfig+                                False+                                Nothing+                                []+                                Nothing+                                mempty+                                  { partialFailureThreshold = Just DLWarningC }++      it "parse --failure-threshold style" $ do+        checkCommandline ["--failure-threshold", "style"] $ CommandlineConfig+                                False+                                Nothing+                                []+                                Nothing+                                mempty+                                  { partialFailureThreshold = Just DLStyleC }++    describe "fail parsing on garbage input" $ do+      it "fail parsing --blafoo" $ do+        checkCommandlineFail+          ["--blafoo"]++      it "fail parsing --require-label foo:bar" $ do+        checkCommandlineFail+          ["--require-label", "foo:bar"]+++checkCommandline :: [String] -> CommandlineConfig -> Assertion+checkCommandline args config = do+  let inf = info parseCommandline fullDesc+      res = execParserPure defaultPrefs inf args+  case res of+    Success cfg -> cfg `shouldBe` config+    _ -> assertFailure "should have parsed commandline arguments"++checkCommandlineFail :: [String] -> Assertion+checkCommandlineFail args = do+  let inf = info parseCommandline fullDesc+      res = execParserPure defaultPrefs inf args+  case res of+    Failure _ -> assertEqual "" (0 :: Int) (0 :: Int)  -- inverse asserFailure+    _ -> assertFailure "should have failed parsing commandline arguments"
+ test/Hadolint/Config/ConfigfileSpec.hs view
@@ -0,0 +1,169 @@+module Hadolint.Config.ConfigfileSpec (spec) where++import Hadolint+import Test.Hspec+import qualified Data.ByteString.Char8 as Bytes+import qualified Data.Map as Map+import qualified Data.Set as Set+import qualified Data.YAML as Yaml+++spec :: SpecWith ()+spec =+  describe "Config from Configfile" $ do+    it "parse empty config file" $ do+      let yaml = []+          conf = parseYaml yaml+      conf `shouldBe` Left "empty YAML stream"++    it "parse `no-fail: true`" $ do+      let yaml = ["no-fail: true"]+          conf = parseYaml yaml+      conf `shouldBe` Right mempty { partialNoFail = Just True }++    it "parse `no-fail: false`" $ do+      let yaml = ["no-fail: false"]+          conf = parseYaml yaml+      conf `shouldBe` Right mempty { partialNoFail = Just False }++    it "parse `no-color: true`" $ do+      let yaml = ["no-color: true"]+          conf = parseYaml yaml+      conf `shouldBe` Right mempty { partialNoColor = Just True }++    it "parse `no-color: false`" $ do+      let yaml = ["no-color: false"]+          conf = parseYaml yaml+      conf `shouldBe` Right mempty { partialNoColor = Just False }++    it "parse `verbose: true`" $ do+      let yaml = ["verbose: true"]+          conf = parseYaml yaml+      conf `shouldBe` Right mempty { partialVerbose = Just True }++    it "parse `verbose: false`" $ do+      let yaml = ["verbose: false"]+          conf = parseYaml yaml+      conf `shouldBe` Right mempty { partialVerbose = Just False }++    it "parse `output-format: json`" $ do+      let yaml = ["output-format: json"]+          conf = parseYaml yaml+      conf `shouldBe` Right mempty { partialFormat = Just Json }++    it "parse `output-format: sarif`" $ do+      let yaml = ["output-format: sarif"]+          conf = parseYaml yaml+      conf `shouldBe` Right mempty { partialFormat = Just Sarif }++    it "parse override error rules" $ do+      let yaml = [ "override:",+                   "  error:",+                   "    - DL3020",+                   "    - DL3040",+                   "    - SC1020"+                 ]+          conf = parseYaml yaml+      conf `shouldBe` Right+                        mempty+                          { partialErrorRules = ["DL3020", "DL3040", "SC1020"] }++    it "parse override warning rules" $ do+      let yaml = [ "override:",+                   "  warning:",+                   "    - DL3020",+                   "    - DL3040",+                   "    - SC1020"+                 ]+          conf = parseYaml yaml+      conf `shouldBe` Right+                        mempty+                          { partialWarningRules = ["DL3020", "DL3040", "SC1020"]+                          }++    it "parse override info rules" $ do+      let yaml = [ "override:",+                   "  info:",+                   "    - DL3020",+                   "    - DL3040",+                   "    - SC1020"+                 ]+          conf = parseYaml yaml+      conf `shouldBe` Right+                        mempty+                          { partialInfoRules = ["DL3020", "DL3040", "SC1020"] }++    it "parse override style rules" $ do+      let yaml = [ "override:",+                   "  style:",+                   "    - DL3020",+                   "    - DL3040",+                   "    - SC1020"+                 ]+          conf = parseYaml yaml+      conf `shouldBe` Right+                        mempty+                          { partialStyleRules = ["DL3020", "DL3040", "SC1020"] }++    it "parse ignored rules" $ do+      let yaml = [ "ignored:",+                   "  - DL3020",+                   "  - DL3040",+                   "  - SC1020"+                 ]+          conf = parseYaml yaml+      conf `shouldBe` Right+                        mempty+                          { partialIgnoreRules = ["DL3020", "DL3040", "SC1020"] }++    it "parse trusted registries" $ do+      let yaml = [ "trusted-registries:",+                   "  - foobar.com",+                   "  - barfoo.com"+                 ]+          conf = parseYaml yaml+      conf `shouldBe` Right+                        mempty+                          { partialAllowedRegistries =+                              Set.fromList ["foobar.com", "barfoo.com"]+                          }++    it "parse label schema" $ do+      let yaml = [ "label-schema:",+                   "  foo: email",+                   "  bar: text"+                 ]+          conf = parseYaml yaml+      conf `shouldBe` Right+                        mempty+                          { partialLabelSchema =+                              Map.fromList [("foo", Email), ("bar", RawText)]+                          }++    it "parse strict-labels: true" $ do+      let yaml = [ "strict-labels: true" ]+          conf = parseYaml yaml+      conf `shouldBe` Right mempty { partialStrictLabels = Just True }++    it "parse strict-labels: false" $ do+      let yaml = [ "strict-labels: false" ]+          conf = parseYaml yaml+      conf `shouldBe` Right mempty { partialStrictLabels = Just False }++    it "parse `failure-threshold: warning`" $ do+      let yaml = ["failure-threshold: warning"]+          conf = parseYaml yaml+      conf `shouldBe` Right mempty { partialFailureThreshold = Just DLWarningC }++    it "parse `failure-threshold: style`" $ do+      let yaml = ["failure-threshold: style"]+          conf = parseYaml yaml+      conf `shouldBe` Right mempty { partialFailureThreshold = Just DLStyleC }++-- Helper functions for parsing config files++parseYaml :: [Bytes.ByteString] -> Either String PartialConfiguration+parseYaml ls =+  case (Yaml.decode1Strict . Bytes.unlines) ls of+    Left (_, err) -> Left err+    Right res -> Right res
+ test/Hadolint/Config/ConfigurationSpec.hs view
@@ -0,0 +1,219 @@+module Hadolint.Config.ConfigurationSpec where++import Data.Default+import qualified Data.Map as Map+import qualified Data.Set as Set+import Hadolint+import Test.Hspec++spec :: SpecWith ()+spec = do+  describe "Configuration" $ do+    it "default configuration" $ do+      def+        `shouldBe` Configuration+          False+          False+          False+          TTY+          mempty+          mempty+          mempty+          mempty+          mempty+          mempty+          mempty+          False+          DLInfoC++    it "override default configuration with empty config" $ do+      applyPartialConfiguration def mempty+        `shouldBe` Configuration+          False+          False+          False+          TTY+          mempty+          mempty+          mempty+          mempty+          mempty+          mempty+          mempty+          False+          DLInfoC++    it "override default with specific configuration: no-fail" $ do+      let config =+            PartialConfiguration+              (Just True)+              Nothing+              Nothing+              mempty+              mempty+              mempty+              mempty+              mempty+              mempty+              mempty+              mempty+              Nothing+              mempty+      applyPartialConfiguration def config+        `shouldBe` Configuration+          True+          False+          False+          TTY+          mempty+          mempty+          mempty+          mempty+          mempty+          mempty+          mempty+          False+          DLInfoC++    it "override default with specific configuration: no-color" $ do+      let config =+            PartialConfiguration+              Nothing+              (Just True)+              Nothing+              mempty+              mempty+              mempty+              mempty+              mempty+              mempty+              mempty+              mempty+              Nothing+              mempty+      applyPartialConfiguration def config+        `shouldBe` Configuration+          False+          True+          False+          TTY+          mempty+          mempty+          mempty+          mempty+          mempty+          mempty+          mempty+          False+          DLInfoC++    it "empty should not override: no-color" $ do+      let config =+            PartialConfiguration+              Nothing+              (Just True)+              Nothing+              mempty+              mempty+              mempty+              mempty+              mempty+              mempty+              mempty+              mempty+              Nothing+              mempty+          config2 =+            PartialConfiguration+              Nothing+              Nothing+              Nothing+              mempty+              mempty+              mempty+              mempty+              mempty+              mempty+              mempty+              mempty+              Nothing+              mempty+      applyPartialConfiguration def (config <> config2)+        `shouldBe` Configuration+          False+          True+          False+          TTY+          mempty+          mempty+          mempty+          mempty+          mempty+          mempty+          mempty+          False+          DLInfoC++    it "override default with specific configuration: verbose" $ do+      let config =+            PartialConfiguration+              Nothing+              Nothing+              (Just True)+              mempty+              mempty+              mempty+              mempty+              mempty+              mempty+              mempty+              mempty+              Nothing+              mempty+      applyPartialConfiguration def config+        `shouldBe` Configuration+          False+          False+          True+          TTY+          mempty+          mempty+          mempty+          mempty+          mempty+          mempty+          mempty+          False+          DLInfoC++    it "override default with specific configuration: output-format json" $ do+      let config =+            PartialConfiguration+              Nothing+              Nothing+              Nothing+              (Just Json)+              mempty+              mempty+              mempty+              mempty+              mempty+              mempty+              mempty+              Nothing+              mempty+      applyPartialConfiguration def config+        `shouldBe` Configuration+          False+          False+          False+          Json+          mempty+          mempty+          mempty+          mempty+          mempty+          mempty+          mempty+          False+          DLInfoC
+ test/Hadolint/Config/EnvironmentSpec.hs view
@@ -0,0 +1,186 @@+module Hadolint.Config.EnvironmentSpec (spec) where++import Hadolint+import System.Environment+import Test.Hspec+import qualified Data.Map as Map+import qualified Data.Set as Set+++spec :: SpecWith ()+spec = do+  describe "Config from Environment" $ do+    withoutAll $ do+      it "Empty config when nothing is set" $ do+        conf <- getConfigFromEnvironment+        conf `shouldBe` mempty++    withJustEnv "HADOLINT_NOFAIL" "1" $ do+      it "parse HADOLINT_NOFAIL=1" $ do+        conf <- getConfigFromEnvironment+        conf `shouldBe` mempty { partialNoFail = Just True }++    withJustEnv "HADOLINT_NOFAIL" "0" $ do+      it "parse HADOLINT_NOFAIL=0" $ do+        conf <- getConfigFromEnvironment+        conf `shouldBe` mempty {partialNoFail = Just False }++    withJustEnv "NO_COLOR" "y" $ do+      it "parse NO_COLOR=y" $ do+        conf <- getConfigFromEnvironment+        conf `shouldBe` mempty { partialNoColor = Just True }++    withJustEnv "NO_COLOR" "n" $ do+      it "parse NO_COLOR=n" $ do+        conf <- getConfigFromEnvironment+        conf `shouldBe` mempty { partialNoColor = Just False }++    withJustEnv "HADOLINT_VERBOSE" "on" $ do+      it "parse HADOLINT_VERBOSE=on" $ do+        conf <- getConfigFromEnvironment+        conf `shouldBe` mempty { partialVerbose = Just True }++    withJustEnv "HADOLINT_VERBOSE" "off" $ do+      it "parse HADOLINT_VERBOSE=off" $ do+        conf <- getConfigFromEnvironment+        conf `shouldBe` mempty { partialVerbose = Just False }++    withJustEnv "HADOLINT_FORMAT" "json" $ do+      it "parse HADOLINT_FORMAT=json" $ do+        conf <- getConfigFromEnvironment+        conf `shouldBe` mempty { partialFormat = Just Json }++    withJustEnv "HADOLINT_FORMAT" "sarif" $ do+      it "parse HADOLINT_FORMAT=sarif" $ do+        conf <- getConfigFromEnvironment+        conf `shouldBe` mempty { partialFormat = Just Sarif }++    withJustEnv "HADOLINT_OVERRIDE_ERROR" "DL3010" $ do+      it "parse HADOLINT_OVERRIDE_ERROR=DL3010" $ do+        conf <- getConfigFromEnvironment+        conf `shouldBe` mempty { partialErrorRules = ["DL3010"] }++    withJustEnv "HADOLINT_OVERRIDE_ERROR" "DL3010,DL3011" $ do+      it "parse HADOLINT_OVERRIDE_ERROR=DL3010,DL3011" $ do+        conf <- getConfigFromEnvironment+        conf `shouldBe` mempty { partialErrorRules = ["DL3010", "DL3011"] }++    withJustEnv "HADOLINT_OVERRIDE_WARNING" "DL3010" $ do+      it "parse HADOLINT_OVERRIDE_WARNING=DL3010" $ do+        conf <- getConfigFromEnvironment+        conf `shouldBe` mempty { partialWarningRules = ["DL3010"] }++    withJustEnv "HADOLINT_OVERRIDE_WARNING" "DL3010,DL3011" $ do+      it "parse HADOLINT_OVERRIDE_WARNING=DL3010,DL3011" $ do+        conf <- getConfigFromEnvironment+        conf `shouldBe` mempty { partialWarningRules = ["DL3010", "DL3011"] }++    withJustEnv "HADOLINT_OVERRIDE_INFO" "DL3010" $ do+      it "parse HADOLINT_OVERRIDE_INFO=DL3010" $ do+        conf <- getConfigFromEnvironment+        conf `shouldBe` mempty { partialInfoRules = ["DL3010"] }++    withJustEnv "HADOLINT_OVERRIDE_INFO" "DL3010,DL3011" $ do+      it "parse HADOLINT_OVERRIDE_INFO=DL3010,DL3011" $ do+        conf <- getConfigFromEnvironment+        conf `shouldBe` mempty { partialInfoRules = ["DL3010", "DL3011"] }++    withJustEnv "HADOLINT_OVERRIDE_STYLE" "DL3010" $ do+      it "parse HADOLINT_OVERRIDE_STYLE=DL3010" $ do+        conf <- getConfigFromEnvironment+        conf `shouldBe` mempty { partialStyleRules = ["DL3010"] }++    withJustEnv "HADOLINT_OVERRIDE_STYLE" "DL3010,DL3011" $ do+      it "parse HADOLINT_OVERRIDE_STYLE=DL3010,DL3011" $ do+        conf <- getConfigFromEnvironment+        conf `shouldBe` mempty { partialStyleRules = ["DL3010", "DL3011"] }++    withJustEnv "HADOLINT_IGNORE" "DL3010" $ do+      it "parse HADOLINT_IGNORE=DL3010" $ do+        conf <- getConfigFromEnvironment+        conf `shouldBe` mempty { partialIgnoreRules = ["DL3010"] }++    withJustEnv "HADOLINT_IGNORE" "DL3010,DL3011" $ do+      it "parse HADOLINT_IGNORE=DL3010,DL3011" $ do+        conf <- getConfigFromEnvironment+        conf `shouldBe` mempty { partialIgnoreRules = ["DL3010", "DL3011"] }++    withJustEnv "HADOLINT_ALLOWED_REGISTRIES" "foobar.com" $ do+      it "parse HADOLINT_ALLOWED_REGISTRIES=foobar.com" $ do+        conf <- getConfigFromEnvironment+        conf `shouldBe` mempty+                          { partialAllowedRegistries =+                              Set.fromList ["foobar.com"]+                          }++    withJustEnv "HADOLINT_ALLOWED_REGISTRIES" "foobar.com,barfoo.com" $ do+      it "parse HADOLINT_ALLOWED_REGISTIRES=foobar.com,barfoo.com" $ do+        conf <- getConfigFromEnvironment+        conf `shouldBe` mempty+                          { partialAllowedRegistries =+                              Set.fromList ["foobar.com", "barfoo.com"]+                          }++    withJustEnv "HADOLINT_REQUIRE_LABELS" "foo:email" $ do+      it "parse HADOLINT_REQUIRE_LABELS=foo:email" $ do+        conf <- getConfigFromEnvironment+        conf `shouldBe` mempty+                          { partialLabelSchema = Map.fromList [("foo", Email)] }++    withJustEnv "HADOLINT_REQUIRE_LABELS" "foo:email,bar:text" $ do+      it "parse HADOLINT_REQUIRE_LABELS=foo:email,bar:text" $ do+        conf <- getConfigFromEnvironment+        conf `shouldBe` mempty+                          { partialLabelSchema =+                              Map.fromList [("foo", Email), ("bar", RawText)]+                          }++    withJustEnv "HADOLINT_STRICT_LABELS" "yes" $ do+      it "parse HADOLINT_STRICT_LABELS=yes" $ do+        conf <- getConfigFromEnvironment+        conf `shouldBe` mempty { partialStrictLabels = Just True }++    withJustEnv "HADOLINT_STRICT_LABELS" "false" $ do+      it "parse HADOLINT_STRICT_LABELS=false" $ do+        conf <- getConfigFromEnvironment+        conf `shouldBe` mempty { partialStrictLabels = Just False }++    withJustEnv "HADOLINT_FAILURE_THRESHOLD" "error" $ do+      it "parse HADOLINT_FAILURE_THRESHOLD=error" $ do+        conf <- getConfigFromEnvironment+        conf `shouldBe` mempty { partialFailureThreshold = Just DLErrorC }++    withJustEnv "HADOLINT_FAILURE_THRESHOLD" "style" $ do+      it "parse HADOLINT_FAILURE_THRESHOLD=style" $ do+        conf <- getConfigFromEnvironment+        conf `shouldBe` mempty { partialFailureThreshold = Just DLStyleC }++-- Helper functions for setup and teardown of the environment++withoutAll :: SpecWith () -> SpecWith ()+withoutAll = before unsetAll++withJustEnv :: String -> String -> SpecWith () -> SpecWith ()+withJustEnv name value test =+  before_ (setJustEnv name value) $ after_ (unsetEnv name) test++setJustEnv :: String -> String -> IO ()+setJustEnv name value = do+  unsetAll+  setEnv name value++unsetAll :: IO ()+unsetAll = do+  unsetEnv "HADOLINT_NOFAIL"+  unsetEnv "NO_COLOR"+  unsetEnv "HADOLINT_VERBOSE"+  unsetEnv "HADOLINT_FORMAT"+  unsetEnv "HADOLINT_OVERRIDE_ERROR"+  unsetEnv "HADOLINT_OVERRIDE_WARNING"+  unsetEnv "HADOLINT_OVERRIDE_INFO"+  unsetEnv "HADOLINT_OVERRIDE_STYLE"+  unsetEnv "HADOLINT_IGNORE"+  unsetEnv "HADOLINT_ALLOWED_REGISTRIES"+  unsetEnv "HADOLINT_REQUIRE_LABELS"+  unsetEnv "HADOLINT_STRICT_LABELS"+  unsetEnv "HADOLINT_FAILURE_THRESHOLD"
+ test/Hadolint/Config/SpecHook.hs view
@@ -0,0 +1,8 @@+module Hadolint.Config.SpecHook (hook) where++import System.Environment+import Test.Hspec+++hook :: SpecWith () -> SpecWith ()+hook = before_ (setEnv "BAR" "bar")
+ test/Hadolint/Formatter/ParseErrorSpec.hs view
@@ -0,0 +1,31 @@+module Hadolint.Formatter.ParseErrorSpec (spec) where++import Hadolint.Formatter.Format (errorMessage)+import Hadolint.Formatter.TTY (formatError)+import Language.Docker.Parser+import Test.HUnit hiding (Label)+import Test.Hspec+++spec :: SpecWith ()+spec = do+  describe "Parse Errors" $ do+    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, a pragma,\+            \ end of input, or whitespaces "+      case ast of+        Left err ->+          assertEqual "Unexpected error msg" expectedMsg (formatError err)+        Right _ -> assertFailure "AST should fail parsing"++    it "display just the error message" $ do+      let ast = parseText "RUNNN"+          expectedMsg = "missing whitespace"+      case ast of+        Left err ->+          assertEqual "Unexpected error msg" expectedMsg (errorMessage err)+        Right _ -> assertFailure "AST should fail parsing"
+ test/Hadolint/Formatter/SarifSpec.hs view
@@ -0,0 +1,32 @@+module Hadolint.Formatter.SarifSpec (spec) where++import Helpers+import Hadolint+  ( CheckFailure (..),+    DLSeverity (..),+    Result(..),+    printResults,+    OutputFormat (..),+    getShortVersion,+  )+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?noColor = True++  describe "Formatter: Sarif" $ do+    it "print empty results" $ do+      let checkFails = []+          expectation = "{\"runs\":[{\"defaultSourceLanguage\":\"dockerfi\+                        \le\",\"results\":[],\"tool\":{\"driver\":{\"full\+                        \Name\":\"Haskell Dockerfile Linter\",\"downloadU\+                        \ri\":\"https://github.com/hadolint/hadolint\",\"\+                        \shortDescription\":{\"text\":\"Dockerfile linter\+                        \, validate inline bash, written in Haskell\"},\"\+                        \name\":\"Hadolint\",\"version\":\""+                        ++ getShortVersion+                        ++ "\"}}}],\"version\":\"2.1.0\",\"$schema\+                           \\":\"http://json.schemastore.org/sarif-2.1.0\"}"+      assertFormatter Sarif checkFails expectation
+ test/Hadolint/Formatter/TTYSpec.hs view
@@ -0,0 +1,87 @@+module Hadolint.Formatter.TTYSpec (spec) where++import Helpers+import Data.List.NonEmpty as NonEmpty+import Hadolint (OutputFormat (..), printResults)+import Hadolint.Formatter.Format (Result (..))+import Hadolint.Rule (CheckFailure (..), DLSeverity (..))+import Test.Hspec+import qualified Data.Sequence as Seq+++spec :: SpecWith ()+spec = do+  let ?noColor = True+  describe "Formatter: TTY" $ do+    it "print empty results" $ do+      let results = NonEmpty.fromList [Result "<string>" mempty Seq.empty]+      printResults TTY True (Just "<string>") results `shouldReturn` ()++    it "print some result: no colors" $ do+      let checkFails = [ CheckFailure+                          { code = "DL2001",+                            severity = DLInfoC,+                            message = "test",+                            line = 1+                          }+                       ]+          expectation = unlines+                          [ "<string>:1 DL2001 info: test"+                          ]+      assertFormatter TTY checkFails expectation++    it "print some result: with colors" $ do+      let ?noColor = False+      let checkFails = [ CheckFailure+                          { code = "DL2001",+                            severity = DLInfoC,+                            message = "test",+                            line = 1+                          }+                       ]+          expectation = unlines+                          [ "<string>:1 DL2001 \ESC[92minfo\ESC[0m: test"+                          ]+      assertFormatter TTY checkFails expectation++    it "print multiple results: no colors" $ do+      let checkFails = [ CheckFailure+                          { code = "DL2001",+                            severity = DLInfoC,+                            message = "test",+                            line = 1+                          },+                         CheckFailure+                          { code = "DL2002",+                            severity = DLWarningC,+                            message = "foo",+                            line = 3+                          }+                       ]+          expectation = unlines+                          [ "<string>:1 DL2001 info: test",+                            "<string>:3 DL2002 warning: foo"+                          ]+      assertFormatter TTY checkFails expectation++    it "print multiple results: with colors" $ do+      let ?noColor = False+      let checkFails = [ CheckFailure+                          { code = "DL2001",+                            severity = DLInfoC,+                            message = "test",+                            line = 1+                          },+                         CheckFailure+                          { code = "DL2002",+                            severity = DLWarningC,+                            message = "foo",+                            line = 3+                          }+                       ]+          expectation = unlines+                          [ "<string>:1 DL2001 \ESC[92minfo\ESC[0m: test",+                            "<string>:3 DL2002 \ESC[1m\ESC[93mwarning\ESC[0m:\+                            \ foo"+                          ]+      assertFormatter TTY checkFails expectation
+ test/Hadolint/PragmaSpec.hs view
@@ -0,0 +1,57 @@+module Hadolint.PragmaSpec (spec) where++import Data.Default+import Helpers+import Test.Hspec+import qualified Data.Text as Text+++spec :: SpecWith ()+spec = do+  let ?config = def++  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
+ test/Hadolint/Rule/DL3000Spec.hs view
@@ -0,0 +1,28 @@+module Hadolint.Rule.DL3000Spec (spec) where++import Data.Default+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def++  describe "DL3000 - Use absolute WORKDIR." $ do+    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 "workdir absolute windows" $ ruleCatchesNot "DL3000" "WORKDIR \'C:\\\'"+    it "workdir absolute windows quotes" $ ruleCatchesNot "DL3000" "WORKDIR \"C:\\\""+    it "workdir absolute windows alternative" $+      ruleCatchesNot "DL3000" "WORKDIR C:/"+    it "workdir absolute windows quotes alternative" $+      ruleCatchesNot "DL3000" "WORKDIR \"C:/\""
+ test/Hadolint/Rule/DL3001Spec.hs view
@@ -0,0 +1,18 @@+module Hadolint.Rule.DL3001Spec (spec) where++import Data.Default+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def++  describe "DL3001 - 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"
+ test/Hadolint/Rule/DL3002Spec.hs view
@@ -0,0 +1,88 @@+module Hadolint.Rule.DL3002Spec (spec) where++import Data.Default+import Data.Text as Text+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def++  describe "DL3002 - Last user should not be root." $ do+    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
+ test/Hadolint/Rule/DL3003Spec.hs view
@@ -0,0 +1,14 @@+module Hadolint.Rule.DL3003Spec (spec) where++import Data.Default+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def++  describe "DL3003 - Use WORKDIR to switch to a directory." $ do+    it "ok using WORKDIR" $ ruleCatchesNot "DL3003" "WORKDIR /opt"+    it "not ok using cd" $ ruleCatches "DL3003" "RUN cd /opt"
+ test/Hadolint/Rule/DL3004Spec.hs view
@@ -0,0 +1,21 @@+module Hadolint.Rule.DL3004Spec (spec) where++import Data.Default+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def++  describe "DL3004 - Do not use sudo as it leads to unpredictable behavior. Use a tool like gosu to enforce root." $ do+    it "sudo" $ do+      ruleCatches "DL3004" "RUN sudo apt-get update"+      onBuildRuleCatches "DL3004" "RUN sudo apt-get update"+    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"
+ test/Hadolint/Rule/DL3005Spec.hs view
@@ -0,0 +1,15 @@+module Hadolint.Rule.DL3005Spec (spec) where++import Data.Default+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def++  describe "DL3005" $ do+    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"
+ test/Hadolint/Rule/DL3006Spec.hs view
@@ -0,0 +1,44 @@+module Hadolint.Rule.DL3006Spec (spec) where++import Data.Default+import Data.Text as Text+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def++  describe "DL3006 - Always tag the version of an image explicitly." $ do+    it "no untagged" $ ruleCatches "DL3006" "FROM debian"+    it "no untagged with name" $ ruleCatches "DL3006" "FROM debian 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+    it "scratch" $ ruleCatchesNot "DL3006" "FROM scratch"
+ test/Hadolint/Rule/DL3007Spec.hs view
@@ -0,0 +1,24 @@+module Hadolint.Rule.DL3007Spec (spec) where++import Data.Default+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def++  describe "DL3007 - Using latest is prone to errors if the image will ever up\+           \date. Pin the version explicitly to a release tag." $ do+    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 SHA" $+      ruleCatchesNot+        "DL3007"+        "FROM hub.docker.io/debian@sha256:\+        \7959ed6f7e35f8b1aaa06d1d8259d4ee25aa85a086d5c125480c333183f9deeb"+    it "explicit tagged with name" $+      ruleCatchesNot "DL3007" "FROM debian:jessie AS builder"
+ test/Hadolint/Rule/DL3008Spec.hs view
@@ -0,0 +1,63 @@+module Hadolint.Rule.DL3008Spec (spec) where++import Data.Default+import Data.Text as Text+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def++  describe "DL3008 - Pin versions in apt-get install." $ do+    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 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 "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
+ test/Hadolint/Rule/DL3009Spec.hs view
@@ -0,0 +1,133 @@+module Hadolint.Rule.DL3009Spec (spec) where++import Data.Default+import Data.Text as Text+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def++  describe "DL3009 - Delete the apt-get lists after installing something." $ do+    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 "don't warn: BuildKit cache mount to apt lists directory" $ do+      ruleCatchesNot+        "DL3009"+        "RUN --mount=type=cache,target=/var/lib/apt/lists \\\+        \    apt-get update && apt-get install python"+      onBuildRuleCatchesNot+        "DL3009"+        "RUN --mount=type=cache,target=/var/lib/apt/lists \\\+        \    apt-get update && apt-get install python"++    it "don't warn: BuildKit cache mount to apt directories 1" $ do+      ruleCatchesNot+        "DL3009"+        "RUN --mount=type=cache,target=/var/lib/apt \\\+        \    --mount=type=cache,target=/var/cache/apt \\\+        \    rm -f /etc/apt/apt.conf.d/docker-clean && \\\+        \    apt-get update && apt-get install python"+      onBuildRuleCatchesNot+        "DL3009"+        "RUN --mount=type=cache,target=/var/lib/apt \\\+        \    --mount=type=cache,target=/var/cache/apt \\\+        \    rm -f /etc/apt/apt.conf.d/docker-clean && \\\+        \    apt-get update && apt-get install python"++    it "don't warn: BuildKit cache mount to apt directories 2" $ do+      let dockerFile =+            [ "RUN rm -f /etc/apt/apt.conf.d/docker-clean",+              "RUN --mount=type=cache,target=/var/cache/apt \\",+              "    --mount=type=cache,target=/var/lib/apt \\",+              "    apt-get update && apt-get install foo"+            ]+      ruleCatchesNot "DL3009" $ Text.unlines dockerFile+      onBuildRuleCatchesNot "DL3009" $ Text.unlines dockerFile++    it "warn: BuildKit cache mount to apt cache directory only" $ do+      let dockerFile =+            [ "RUN --mount=type=cache,target=/var/cache/apt \\",+              "    rm -f /etc/apt/apt.conf.d/docker-clean && \\",+              "    apt-get update && apt-get install python"+            ]+       in do+            ruleCatches "DL3009" $ Text.unlines dockerFile+            onBuildRuleCatches "DL3009" $ Text.unlines dockerFile++    it "warn: BuildKit cache mount to apt lists directory only" $ do+      let dockerFile =+            [ "RUN rm -f /etc/apt/apt.conf.d/docker-clean",+              "RUN --mount=type=cache,target=/var/lib/apt \\",+              "    apt-get update && apt-get install python"+            ]+       in do+            ruleCatches "DL3009" $ Text.unlines dockerFile+            onBuildRuleCatches "DL3009" $ Text.unlines dockerFile
+ test/Hadolint/Rule/DL3010Spec.hs view
@@ -0,0 +1,61 @@+module Hadolint.Rule.DL3010Spec (spec) where++import Data.Default+import Data.Text as Text+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def++  describe "DL3010 - Use `ADD` for extracting archives into an image" $ do+    it "catch: copy archive then extract 1" $+      let dockerFile =+            [ "COPY packaged-app.tar /usr/src/app",+              "RUN tar -xf /usr/src/app/packaged-app.tar"+            ]+      in do+        ruleCatches "DL3010" $ Text.unlines dockerFile+    it "catch: copy archive then extract 2" $+      let dockerFile =+            [ "COPY packaged-app.tar /usr/src/app",+              "WORKDIR /usr/src/app",+              "RUN foo bar && echo something && tar -xf packaged-app.tar"+            ]+      in do+        ruleCatches "DL3010" $ Text.unlines dockerFile+    it "catch: copy archive then extract 3" $+      let dockerFile =+            [ "COPY foo/bar/packaged-app.tar /foo.tar",+              "RUN tar -xf /foo.tar"+            ]+      in do+        ruleCatches "DL3010" $ Text.unlines dockerFile+    it "catch: copy archive then extract windows paths 1" $+      let dockerFile =+            [ "COPY build\\foo\\bar.tar.gz \"C:\\Program Files\\Foo\"",+              "RUN tar -xf \"C:\\Program Files\\Foo\\bar.tar.gz\""+            ]+      in do+        ruleCatches "DL3010" $ Text.unlines dockerFile+    it "catch: copy archive then extract windows paths 2" $+      let dockerFile =+            [ "COPY build\\foo\\bar.tar.gz \"C:\\Program Files\\foo.tar.gz\"",+              "RUN tar -xf foo.tar.gz"+            ]+      in do+        ruleCatches "DL3010" $ Text.unlines dockerFile+    it "ignore: copy archive without extract" $+      let dockerFile =+            [ "COPY packaged-app.tar /usr/src/app",+              "FROM debian:11 as newstage"+            ]+      in do+        ruleCatchesNot "DL3010" $ Text.unlines dockerFile+    it "ignore: non archive" $+      ruleCatchesNot "DL3010" "COPY package.json /usr/src/app"+    it "ignore: copy from previous stage" $+      ruleCatchesNot "DL3010"+        "COPY --from=builder /usr/local/share/some.tar /opt/some.tar"
+ test/Hadolint/Rule/DL3011Spec.hs view
@@ -0,0 +1,14 @@+module Hadolint.Rule.DL3011Spec (spec) where++import Data.Default+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def++  describe "EXPOSE rules" $ do+    it "invalid port" $ ruleCatches "DL3011" "EXPOSE 80000"+    it "valid port" $ ruleCatchesNot "DL3011" "EXPOSE 60000"
+ test/Hadolint/Rule/DL3012Spec.hs view
@@ -0,0 +1,20 @@+module Hadolint.Rule.DL3012Spec (spec) where++import Data.Default+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def++  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"
+ test/Hadolint/Rule/DL3013Spec.hs view
@@ -0,0 +1,141 @@+module Hadolint.Rule.DL3013Spec (spec) where++import Data.Default+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def++  describe "DL3013 - Pin versions in pip." $ 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"
+ test/Hadolint/Rule/DL3014Spec.hs view
@@ -0,0 +1,33 @@+module Hadolint.Rule.DL3014Spec (spec) where++import Data.Default+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def++  describe "DL3014 - Use the -y switch." $ 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"
+ test/Hadolint/Rule/DL3015Spec.hs view
@@ -0,0 +1,28 @@+module Hadolint.Rule.DL3015Spec (spec) where++import Data.Default+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def++  describe "DL3015 - Avoid additional packages by specifying `--no-install-recommends`." $ do+    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"
+ test/Hadolint/Rule/DL3016Spec.hs view
@@ -0,0 +1,124 @@+module Hadolint.Rule.DL3016Spec (spec) where++import Data.Default+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def+  describe "DL3016 - Pin versions in npm." $ 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"+    it "don't fire on loglevel flag" $ do+      ruleCatchesNot "DL3016" "RUN npm install --loglevel verbose sax@0.1.1"+      onBuildRuleCatchesNot "DL3016" "RUN npm install --loglevel verbose sax@0.1.1"
+ test/Hadolint/Rule/DL3018Spec.hs view
@@ -0,0 +1,92 @@+module Hadolint.Rule.DL3018Spec (spec) where++import Data.Default+import Data.Text as Text+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def++  describe "DL3018 - Pin versions in `apk add`." $ do+    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 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++    it "don't trigger when installing from .apk file" $ do+      ruleCatchesNot "DL3018" "RUN apk add mypackage-1.1.1.apk"+      onBuildRuleCatchesNot "DL3018" "RUN apk add mypackage-1.1.1.apk"
+ test/Hadolint/Rule/DL3019Spec.hs view
@@ -0,0 +1,27 @@+module Hadolint.Rule.DL3019Spec (spec) where++import Data.Default+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def++  describe "DL3019 - Use the --no-cache switch." $ do+    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 "don't warn: apk add with BuildKit cache mount" $ do+      ruleCatchesNot "DL3019" "RUN --mount=type=cache,target=/var/cache/apk apk add -U curl=7.77.0"+      onBuildRuleCatchesNot "DL3019" "RUN --mount=type=cache,target=/var/cache/apk apk add -U curl=7.77.0"+    it "don't warn: apk add with BuildKit cache mount in wrong dir and --no-cache" $ do+      ruleCatchesNot "DL3019" "RUN --mount=type=cache,target=/var/cache/foo apk add --no-cache -U curl=7.77.0"+      onBuildRuleCatchesNot "DL3019" "RUN --mount=type=cache,target=/var/cache/foo apk add --no-cache -U curl=7.77.0"+    it "warn: apk add with BuildKit cache mount to wrong dir" $ do+      ruleCatches "DL3019" "RUN --mount=type=cache,target=/var/cache/foo apk add -U curl=7.77.0"+      onBuildRuleCatches "DL3019" "RUN --mount=type=cache,target=/var/cache/foo apk add -U curl=7.77.0"
+ test/Hadolint/Rule/DL3020Spec.hs view
@@ -0,0 +1,23 @@+module Hadolint.Rule.DL3020Spec (spec) where++import Data.Default+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def++  describe "DL3020 - Use `COPY` instead of `ADD` for files and folders." $ do+    it "add for tar" $ ruleCatchesNot "DL3020" "ADD file.tar /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/"+    it "using add" $ ruleCatches "DL3020" "ADD file /usr/src/app/"+    it "warn for zip" $ ruleCatches "DL3020" "ADD file.zip /usr/src/app/"+    it "add for tgz with quotes" $ ruleCatchesNot "DL3020" "ADD \"file.tgz\" /usr/src/app/"+    it "add for url with quotes" $ ruleCatchesNot "DL3020" "ADD \"http://file.com\" /usr/src/app/"+    it "warn for zip with quotes" $ ruleCatches "DL3020" "ADD \"file.zip\" /usr/src/app/"
+ test/Hadolint/Rule/DL3021Spec.hs view
@@ -0,0 +1,15 @@+module Hadolint.Rule.DL3021Spec (spec) where++import Data.Default+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def++  describe "DL3021 - `COPY` with more than 2 arguments requires the last argument to end with `/`" $ 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/"
+ test/Hadolint/Rule/DL3022Spec.hs view
@@ -0,0 +1,31 @@+module Hadolint.Rule.DL3022Spec (spec) where++import Data.Default+import Data.Text as Text+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def++  describe "DL3022 - `COPY --from` should reference a previously defined `FROM` 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
+ test/Hadolint/Rule/DL3023Spec.hs view
@@ -0,0 +1,28 @@+module Hadolint.Rule.DL3023Spec (spec) where++import Data.Default+import Data.Text as Text+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def++  describe "DL3023 - `COPY --from` cannot reference its own `FROM` alias" $ 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
+ test/Hadolint/Rule/DL3024Spec.hs view
@@ -0,0 +1,29 @@+module Hadolint.Rule.DL3024Spec (spec) where++import Data.Default+import Data.Text as Text+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def++  describe "DL3024 - 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
+ test/Hadolint/Rule/DL3025Spec.hs view
@@ -0,0 +1,38 @@+module Hadolint.Rule.DL3025Spec (spec) where++import Data.Default+import Data.Text as Text+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def++  describe "DL3025 - Use arguments JSON notation for `CMD` and `ENTRYPOINT` arguments" $ do+    it "warn on ENTRYPOINT" $+      let dockerFile =+            [ "FROM node as foo",+              "ENTRYPOINT something"+            ]+       in ruleCatches "DL3025" $ Text.unlines dockerFile+    it "don't warn on ENTRYPOINT json notation" $+      let dockerFile =+            [ "FROM scratch as build",+              "ENTRYPOINT [\"foo\", \"bar\"]"+            ]+       in ruleCatchesNot "DL3025" $ Text.unlines dockerFile+    it "warn on CMD" $+      let dockerFile =+            [ "FROM node as foo",+              "CMD something"+            ]+       in ruleCatches "DL3025" $ Text.unlines dockerFile+    it "don't warn on CMD json notation" $+      let dockerFile =+            [ "FROM scratch as build",+              "CMD [\"foo\", \"bar\"]",+              "CMD [ \"foo\", \"bar\" ]"+            ]+       in ruleCatchesNot "DL3025" $ Text.unlines dockerFile
+ test/Hadolint/Rule/DL3026Spec.hs view
@@ -0,0 +1,62 @@+module Hadolint.Rule.DL3026Spec (spec) where++import Data.Default+import Data.Text as Text+import Hadolint (Configuration (..))+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def++  describe "DL2036 - Use only an allowed registry in the FROM image" $ 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 ?config = def { allowedRegistries = ["docker.io"] }++      ruleCatches "DL3026" $ Text.unlines dockerFile++    it "does not warn on allowed registries" $ do+      let dockerFile =+            [ "FROM random.com/debian"+            ]+      let ?config = def { allowedRegistries = ["x.com", "random.com"] }++      ruleCatchesNot "DL3026" $ Text.unlines dockerFile++    it "doesn't warn on scratch image" $ do+      let dockerFile =+            [ "FROM scratch"+            ]+      let ?config = def { allowedRegistries = ["x.com", "random.com"] }++      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 ?config = def { allowedRegistries = ["docker.io"] }++      ruleCatchesNot "DL3026" $ Text.unlines dockerFile++    it "allows using previous stages" $ do+      let dockerFile =+            [ "FROM random.com/foo AS builder1",+              "FROM builder1 AS builder2"+            ]+      let ?config = def { allowedRegistries = ["random.com"] }++      ruleCatchesNot "DL3026" $ Text.unlines dockerFile
+ test/Hadolint/Rule/DL3027Spec.hs view
@@ -0,0 +1,21 @@+module Hadolint.Rule.DL3027Spec (spec) where++import Data.Default+import Data.Text as Text+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def++  describe "DL3027 - Do not use `apt` as it is meant to be a end-user tool, use `apt-get` or `apt-cache` instead" $ do+    it "apt" $+      let dockerFile =+            [ "FROM ubuntu",+              "RUN apt install python"+            ]+       in do+            ruleCatches "DL3027" $ Text.unlines dockerFile+            onBuildRuleCatches "DL3027" $ Text.unlines dockerFile
+ test/Hadolint/Rule/DL3028Spec.hs view
@@ -0,0 +1,44 @@+module Hadolint.Rule.DL3028Spec (spec) where++import Data.Default+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def++  describe "DL3028 - Pin versions in gem install." $+    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"
+ test/Hadolint/Rule/DL3029Spec.hs view
@@ -0,0 +1,14 @@+module Hadolint.Rule.DL3029Spec (spec) where++import Data.Default+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def++  describe "DL3029 - Do not use --platform flag with FROM." $ do+    it "explicit platform flag" $ ruleCatches "DL3029" "FROM --platform=linux debian:jessie"+    it "no platform flag" $ ruleCatchesNot "DL3029" "FROM debian:jessie"
+ test/Hadolint/Rule/DL3030Spec.hs view
@@ -0,0 +1,19 @@+module Hadolint.Rule.DL3030Spec (spec) where++import Data.Default+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def+  describe "DL3030 - Use the `-y` switch to avoid manual input `yum install -y <package>`" $ do+    it "not ok when not using `-y` switch" $ do+      ruleCatches "DL3030" "RUN yum install httpd-2.4.24 && yum clean all"+      onBuildRuleCatches "DL3030" "RUN yum install httpd-2.4.24 && yum clean all"+    it "ok when using `-y` switch" $ do+      ruleCatchesNot "DL3030" "RUN yum install -y httpd-2.4.24 && yum clean all"+      ruleCatchesNot "DL3030" "RUN bash -c `# not even a yum command`"+      onBuildRuleCatchesNot "DL3030" "RUN yum install -y httpd-2.4.24 && yum clean all"+      onBuildRuleCatchesNot "DL3030" "RUN bash -c `# not even a yum command`"
+ test/Hadolint/Rule/DL3032Spec.hs view
@@ -0,0 +1,20 @@+module Hadolint.Rule.DL3032Spec (spec) where++import Data.Default+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def++  describe "DL3032 - `yum clean all` missing after yum command." $ do+    it "not ok with no clean all" $ do+      ruleCatches "DL3032" "RUN yum install -y mariadb-10.4"+      onBuildRuleCatches "DL3032" "RUN yum install -y mariadb-10.4"+    it "ok with yum clean all " $ do+      ruleCatchesNot "DL3032" "RUN yum install -y mariadb-10.4 && yum clean all"+      ruleCatchesNot "DL3032" "RUN bash -c `# not even a yum command`"+      onBuildRuleCatchesNot "DL3032" "RUN yum install -y mariadb-10.4 && yum clean all"+      onBuildRuleCatchesNot "DL3032" "RUN bash -c `# not even a yum command`"
+ test/Hadolint/Rule/DL3033Spec.hs view
@@ -0,0 +1,28 @@+module Hadolint.Rule.DL3033Spec (spec) where++import Data.Default+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def++  describe "DL3033 - Specify version with `yum install -y <package>-<version>`" $ do+    it "not ok wihout yum version pinning" $ do+      ruleCatches "DL3033" "RUN yum install -y tomcat && yum clean all"+      onBuildRuleCatches "DL3033" "RUN yum install -y tomcat && yum clean all"+    it "ok with yum version pinning" $ do+      ruleCatchesNot "DL3033" "RUN yum install -y tomcat-9.2 && yum clean all"+      ruleCatchesNot "DL3033" "RUN bash -c `# not even a yum command`"+      onBuildRuleCatchesNot "DL3033" "RUN yum install -y tomcat-9.2 && yum clean all"+      onBuildRuleCatchesNot "DL3033" "RUN bash -c `# not even a yum command`"+    it "not ok wihout yum version pinning - modules" $ do+      ruleCatches "DL3033" "RUN yum module install -y tomcat && yum clean all"+      onBuildRuleCatches "DL3033" "RUN yum module install -y tomcat && yum clean all"+    it "ok with yum version pinning - modules" $ do+      ruleCatchesNot "DL3033" "RUN yum module install -y tomcat:9 && yum clean all"+      ruleCatchesNot "DL3033" "RUN bash -c `# not even a yum command`"+      onBuildRuleCatchesNot "DL3033" "RUN yum module install -y tomcat:9 && yum clean all"+      onBuildRuleCatchesNot "DL3033" "RUN bash -c `# not even a yum command`"
+ test/Hadolint/Rule/DL3034Spec.hs view
@@ -0,0 +1,24 @@+module Hadolint.Rule.DL3034Spec (spec) where++import Data.Default+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def++  describe "DL3034 - Non-interactive switch missing from zypper command: `zypper install -y`" $ do+    it "not ok without non-interactive switch" $ do+      ruleCatches "DL3034" "RUN zypper install httpd=2.4.24 && zypper clean"+      onBuildRuleCatches "DL3034" "RUN zypper install httpd=2.4.24 && zypper clean"+    it "ok with non-interactive switch present" $ do+      ruleCatchesNot "DL3034" "RUN zypper install -n httpd=2.4.24 && zypper clean"+      ruleCatchesNot "DL3034" "RUN zypper install --non-interactive 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"+      onBuildRuleCatchesNot "DL3034" "RUN zypper install -n httpd=2.4.24 && zypper clean"+      onBuildRuleCatchesNot "DL3034" "RUN zypper install --non-interactive 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"
+ test/Hadolint/Rule/DL3035Spec.hs view
@@ -0,0 +1,17 @@+module Hadolint.Rule.DL3035Spec (spec) where++import Data.Default+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def++  describe "DL3035" $ do+    it "not ok: zypper dist-upgrade" $ do+      ruleCatches "DL3035" "RUN zypper dist-upgrade"+      ruleCatches "DL3035" "RUN zypper dup"+      onBuildRuleCatches "DL3035" "RUN zypper dist-upgrade"+      onBuildRuleCatches "DL3035" "RUN zypper dup"
+ test/Hadolint/Rule/DL3036Spec.hs view
@@ -0,0 +1,20 @@+module Hadolint.Rule.DL3036Spec (spec) where++import Data.Default+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def++  describe "DL3036 - `zypper clean` missing after zypper use." $ do+    it "not ok without zypper clean" $ do+      ruleCatches "DL3036" "RUN zypper install -y mariadb=10.4"+      onBuildRuleCatches "DL3036" "RUN zypper install -y mariadb=10.4"+    it "ok with zypper clean" $ do+      ruleCatchesNot "DL3036" "RUN zypper install -y mariadb=10.4 && zypper clean"+      ruleCatchesNot "DL3036" "RUN zypper install -y mariadb=10.4 && zypper cc"+      onBuildRuleCatchesNot "DL3036" "RUN zypper install -y mariadb=10.4 && zypper clean"+      onBuildRuleCatchesNot "DL3036" "RUN zypper install -y mariadb=10.4 && zypper cc"
+ test/Hadolint/Rule/DL3037Spec.hs view
@@ -0,0 +1,30 @@+module Hadolint.Rule.DL3037Spec (spec) where++import Data.Default+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def++  describe "DL3037 - Specify version with `zypper install -y <package>[=]<version>`." $ do+    -- NOTE: In Haskell strings, '\' has to be escaped. And in shell commands, '>'+    -- and '<' have to be escaped. Hence the double escaping.+    it "not ok without zypper version pinning" $ do+      ruleCatches "DL3037" "RUN zypper install -y tomcat && zypper clean"+      onBuildRuleCatches "DL3037" "RUN zypper install -y tomcat && zypper clean"+    it "ok with different variants of zypper version pinning" $ do+      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"+      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"
+ test/Hadolint/Rule/DL3038Spec.hs view
@@ -0,0 +1,20 @@+module Hadolint.Rule.DL3038Spec (spec) where++import Data.Default+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def++  describe "DL3038 - Use the `-y` switch to avoid manual input `dnf install -y <package>`" $ do+    it "not ok without dnf non-interactive flag" $ do+      ruleCatches "DL3038" "RUN dnf install httpd-2.4.24 && dnf clean all"+      onBuildRuleCatches "DL3038" "RUN dnf install httpd-2.4.24 && dnf clean all"+    it "ok with dnf non-interactive flag" $ do+      ruleCatchesNot "DL3038" "RUN dnf install -y httpd-2.4.24 && dnf clean all"+      ruleCatchesNot "DL3038" "RUN notdnf install httpd"+      onBuildRuleCatchesNot "DL3038" "RUN dnf install -y httpd-2.4.24 && dnf clean all"+      onBuildRuleCatchesNot "DL3038" "RUN notdnf install httpd"
+ test/Hadolint/Rule/DL3040Spec.hs view
@@ -0,0 +1,20 @@+module Hadolint.Rule.DL3040Spec (spec) where++import Data.Default+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def++  describe "DL3040 - `dnf clean all` missing after dnf command." $ do+    it "no ok without dnf clean all" $ do+      ruleCatches "DL3040" "RUN dnf install -y mariadb-10.4"+      onBuildRuleCatches "DL3040" "RUN dnf install -y mariadb-10.4"+    it "ok with dnf clean all" $ do+      ruleCatchesNot "DL3040" "RUN dnf install -y mariadb-10.4 && dnf clean all"+      ruleCatchesNot "DL3040" "RUN notdnf install mariadb"+      onBuildRuleCatchesNot "DL3040" "RUN dnf install -y mariadb-10.4 && dnf clean all"+      onBuildRuleCatchesNot "DL3040" "RUN notdnf install mariadb"
+ test/Hadolint/Rule/DL3041Spec.hs view
@@ -0,0 +1,28 @@+module Hadolint.Rule.DL3041Spec (spec) where++import Data.Default+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def++  describe "DL3041 - Specify version with `dnf install -y <package>-<version>`" $ do+    it "not ok without dnf version pinning" $ do+      ruleCatches "DL3041" "RUN dnf install -y tomcat && dnf clean all"+      onBuildRuleCatches "DL3041" "RUN dnf install -y tomcat && dnf clean all"+    it "ok with dnf version pinning" $ do+      ruleCatchesNot "DL3041" "RUN dnf install -y tomcat-9.0.1 && dnf clean all"+      ruleCatchesNot "DL3041" "RUN notdnf install tomcat"+      onBuildRuleCatchesNot "DL3041" "RUN dnf install -y tomcat-9.0.1 && dnf clean all"+      onBuildRuleCatchesNot "DL3041" "RUN notdnf install tomcat"+    it "not ok without dnf version pinning - moudles" $ do+      ruleCatches "DL3041" "RUN dnf module install -y tomcat && dnf clean all"+      onBuildRuleCatches "DL3041" "RUN dnf module install -y tomcat && dnf clean all"+    it "ok with dnf version pinning - modules" $ do+      ruleCatchesNot "DL3041" "RUN dnf module install -y tomcat:9 && dnf clean all"+      ruleCatchesNot "DL3041" "RUN notdnf module install tomcat"+      onBuildRuleCatchesNot "DL3041" "RUN dnf module install -y tomcat:9 && dnf clean all"+      onBuildRuleCatchesNot "DL3041" "RUN notdnf module install tomcat"
+ test/Hadolint/Rule/DL3042Spec.hs view
@@ -0,0 +1,88 @@+module Hadolint.Rule.DL3042Spec (spec) where++import Data.Default+import Data.Text as Text+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def++  describe "DL3042 - Avoid cache directory with `pip install --no-cache-dir <package>`." $ 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"++    -- ENV won't work with the onBuildRuleCatches[Not] wrapper:+    it "respect ENV PIP_NO_CACHE_DIR with truthy values" $ do+      ruleCatchesNot "DL3042" "ENV PIP_NO_CACHE_DIR=1\nRUN pip install MySQL_python"+      ruleCatchesNot "DL3042" "ENV PIP_NO_CACHE_DIR=on\nRUN pip install MySQL_python"+      ruleCatchesNot "DL3042" "ENV PIP_NO_CACHE_DIR=yes\nRUN pip install MySQL_python"+      ruleCatchesNot "DL3042" "ENV PIP_NO_CACHE_DIR=true\nRUN pip install MySQL_python"+    it "respect RUN PIP_NO_CACHE_DIR=... with truthy values" $ do+      ruleCatchesNot "DL3042" "RUN PIP_NO_CACHE_DIR=1 pip install MySQL_python"+      ruleCatchesNot "DL3042" "RUN PIP_NO_CACHE_DIR=on pip install MySQL_python"+      ruleCatchesNot "DL3042" "RUN PIP_NO_CACHE_DIR=yes pip install MySQL_python"+      ruleCatchesNot "DL3042" "RUN PIP_NO_CACHE_DIR=true pip install MySQL_python"+    it "respect RUN export PIP_NO_CACHE_DIR=... with truthy values" $ do+      ruleCatchesNot "DL3042" "RUN export PIP_NO_CACHE_DIR=1 && pip install MySQL_python"+      ruleCatchesNot "DL3042" "RUN export PIP_NO_CACHE_DIR=on && pip install MySQL_python"+      ruleCatchesNot "DL3042" "RUN export PIP_NO_CACHE_DIR=yes && pip install MySQL_python"+      ruleCatchesNot "DL3042" "RUN export PIP_NO_CACHE_DIR=true && pip install MySQL_python"+    it "respect ENV PIP_NO_CACHE_DIR with falsy values" $ do+      ruleCatches "DL3042" "ENV PIP_NO_CACHE_DIR=0\nRUN pip install MySQL_python"+      ruleCatches "DL3042" "ENV PIP_NO_CACHE_DIR=off\nRUN pip install MySQL_python"+      ruleCatches "DL3042" "ENV PIP_NO_CACHE_DIR=no\nRUN pip install MySQL_python"+      ruleCatches "DL3042" "ENV PIP_NO_CACHE_DIR=false\nRUN pip install MySQL_python"+    it "respect RUN PIP_NO_CACHE_DIR=... with falsy values" $ do+      ruleCatches "DL3042" "RUN PIP_NO_CACHE_DIR=0 pip install MySQL_python"+      ruleCatches "DL3042" "RUN PIP_NO_CACHE_DIR=off pip install MySQL_python"+      ruleCatches "DL3042" "RUN PIP_NO_CACHE_DIR=no pip install MySQL_python"+      ruleCatches "DL3042" "RUN PIP_NO_CACHE_DIR=false pip install MySQL_python"+    it "respect RUN export PIP_NO_CACHE_DIR=... with falsy values" $ do+      ruleCatches "DL3042" "RUN export PIP_NO_CACHE_DIR=0 && pip install MySQL_python"+      ruleCatches "DL3042" "RUN export PIP_NO_CACHE_DIR=off && pip install MySQL_python"+      ruleCatches "DL3042" "RUN export PIP_NO_CACHE_DIR=no && pip install MySQL_python"+      ruleCatches "DL3042" "RUN export PIP_NO_CACHE_DIR=false && pip install MySQL_python"++    it "don't trigger if PIP_NO_CACHE_DIR is inherited" $+      let dockerFile = Text.unlines+            [ "FROM debian:buster as base",+              "ENV PIP_NO_CACHE_DIR=1",+              "FROM base",+              "RUN pip install six"+            ]+       in do+        ruleCatchesNot "DL3042" dockerFile+    it "trigger if PIP_NO_CACHE_DIR is not inherited" $+      let dockerFile = Text.unlines+            [ "FROM debian:buster as base",+              "ENV PIP_NO_CACHE_DIR=1",+              "FROM debian:buster",+              "RUN pip install six"+            ]+       in do+        ruleCatches "DL3042" dockerFile
+ test/Hadolint/Rule/DL3043Spec.hs view
@@ -0,0 +1,45 @@+module Hadolint.Rule.DL3043Spec (spec) where++import Data.Default+import Data.Text as Text+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def++  describe "DL3043 - `ONBUILD`, `FROM` or `MAINTAINER` triggered from within `ONBUILD` instruction." $ 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\""
+ test/Hadolint/Rule/DL3044Spec.hs view
@@ -0,0 +1,44 @@+module Hadolint.Rule.DL3044Spec (spec) where++import Data.Default+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def++  describe "DL3044 - Do not refer to an environment variable within the same `ENV` statement where it is defined." $ 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}\""
+ test/Hadolint/Rule/DL3045Spec.hs view
@@ -0,0 +1,158 @@+module Hadolint.Rule.DL3045Spec (spec) where++import Data.Default+import Data.Text as Text+import Helpers+import Test.Hspec++spec :: SpecWith ()+spec = do+  let ?config = def++  describe "DL3045 - `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 absolute destination and no `WORKDIR` set - windows with alternative paths" $ 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++    it "regression: don't crash with single character paths" $ do+      ruleCatches "DL3045" "COPY a b"+      onBuildRuleCatches "DL3045" "COPY a b"
+ test/Hadolint/Rule/DL3046Spec.hs view
@@ -0,0 +1,17 @@+module Hadolint.Rule.DL3046Spec (spec) where++import Data.Default+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def++  describe "DL3046 - `useradd` without flag `-l` and high UID will result in excessively large Image." $ 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"
+ test/Hadolint/Rule/DL3047Spec.hs view
@@ -0,0 +1,73 @@+module Hadolint.Rule.DL3047Spec (spec) where++import Data.Default+import Data.Text as Text+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def++  describe "DL3047 - `wget` without flag `--progress` will result in excessively bloated build logs when downloading larger files." $ 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
+ test/Hadolint/Rule/DL3048Spec.hs view
@@ -0,0 +1,39 @@+module Hadolint.Rule.DL3048Spec (spec) where++import Data.Default+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def++  describe "DL3048 - 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\""
+ test/Hadolint/Rule/DL3049Spec.hs view
@@ -0,0 +1,82 @@+module Hadolint.Rule.DL3049Spec (spec) where++import Data.Default+import Hadolint (Configuration (..))+import Data.Text as Text+import qualified Data.Map as Map+import qualified Hadolint.Rule as Rule+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def { labelSchema = Map.fromList [("foo", Rule.RawText)] }++  describe "DL3049 - Missing label rule spec" $ do+  -- single stage spec+    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 spec+    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")
+ test/Hadolint/Rule/DL3050Spec.hs view
@@ -0,0 +1,31 @@+module Hadolint.Rule.DL3050Spec (spec) where++import Data.Default+import Hadolint (Configuration (..))+import qualified Data.Map as Map+import qualified Hadolint.Rule as Rule+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config =+        def+          { labelSchema = Map.fromList [("required", Rule.RawText)],+            strictLabels = True+          }++  describe "DL3050 - Superfluous label(s) present." $ 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\""
+ test/Hadolint/Rule/DL3051Spec.hs view
@@ -0,0 +1,24 @@+module Hadolint.Rule.DL3051Spec (spec) where++import Data.Default+import Hadolint (Configuration (..))+import qualified Data.Map as Map+import qualified Hadolint.Rule as Rule+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config =+        def { labelSchema = Map.fromList [("emptylabel", Rule.RawText)] }+  describe "DL3051 - Label `<label>` is empty." $ 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=\"\""
+ test/Hadolint/Rule/DL3052Spec.hs view
@@ -0,0 +1,23 @@+module Hadolint.Rule.DL3052Spec (spec) where++import Data.Default+import Hadolint (Configuration (..), LabelType (..))+import qualified Data.Map as Map+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def { labelSchema = Map.fromList [("urllabel", Url)] }++  describe "DL3052 - Label `<label>` is not a valid URL." $ 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\""
+ test/Hadolint/Rule/DL3053Spec.hs view
@@ -0,0 +1,24 @@+module Hadolint.Rule.DL3053Spec (spec) where++import Data.Default+import Hadolint (Configuration (..))+import qualified Data.Map as Map+import qualified Hadolint.Rule as Rule+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def { labelSchema = Map.fromList [("datelabel", Rule.Rfc3339)] }++  describe "DL3053 - Label `<label>` is not a valid time format - must be conform to RFC3339." $ 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\""
+ test/Hadolint/Rule/DL3054Spec.hs view
@@ -0,0 +1,24 @@+module Hadolint.Rule.DL3054Spec (spec) where++import Data.Default+import Hadolint (Configuration (..))+import qualified Data.Map as Map+import qualified Hadolint.Rule as Rule+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def { labelSchema = Map.fromList [("spdxlabel", Rule.Spdx)] }++  describe "DL3054 - Label `<label>` is not a valid SPDX license identifier." $ 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\""
+ test/Hadolint/Rule/DL3055Spec.hs view
@@ -0,0 +1,27 @@+module Hadolint.Rule.DL3055Spec (spec) where++import Data.Default+import Hadolint (Configuration (..))+import qualified Data.Map as Map+import qualified Hadolint.Rule as Rule+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def { labelSchema = Map.fromList [("githash", Rule.GitHash)] }++  describe "DL3055 - Label `<label>` is not a valid git hash." $ 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\""
+ test/Hadolint/Rule/DL3056Spec.hs view
@@ -0,0 +1,24 @@+module Hadolint.Rule.DL3056Spec (spec) where++import Data.Default+import Hadolint (Configuration (..))+import qualified Data.Map as Map+import qualified Hadolint.Rule as Rule+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def { labelSchema = Map.fromList [("semver", Rule.SemVer)] }++  describe "DL3056 - Label `<label>` does not conform to semantic versioning." $ 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\""
+ test/Hadolint/Rule/DL3057Spec.hs view
@@ -0,0 +1,20 @@+module Hadolint.Rule.DL3057Spec (spec) where++import Data.Default+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def++  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"
+ test/Hadolint/Rule/DL3058Spec.hs view
@@ -0,0 +1,24 @@+module Hadolint.Rule.DL3058Spec (spec) where++import Data.Default+import Hadolint (Configuration (..))+import qualified Data.Map as Map+import qualified Hadolint.Rule as Rule+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def { labelSchema = Map.fromList [("maintainer", Rule.Email)] }++  describe "DL3058 - Label `<label>` is not a valid email format - must be conform to RFC5322." $ do+    it "not ok with label not containing valid email" $ do+      ruleCatches "DL3058" "LABEL maintainer=\"not-email\""+      onBuildRuleCatches "DL3058" "LABEL maintainer=\"not-email\""+    it "ok with label containing valid email" $ do+      ruleCatchesNot "DL3058" "LABEL maintainer=\"abcd@google.com\""+      onBuildRuleCatchesNot "DL3058" "LABEL maintainer=\"abcd@google.com\""+    it "ok with other label not containing valid email" $ do+      ruleCatchesNot "DL3058" "LABEL other=\"doo\""+      onBuildRuleCatchesNot "DL3058" "LABEL other=\"bar\""
+ test/Hadolint/Rule/DL3059Spec.hs view
@@ -0,0 +1,73 @@+module Hadolint.Rule.DL3059Spec (spec) where++import Data.Default+import Data.Text as Text+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def++  describe "DL3059 - Multiple consecutive `RUN` instructions" $ do+    it "ok with no `RUN` at all" $ do+      ruleCatchesNot "DL3059" "FROM debian:10"+    it "ok with one `RUN`" $ do+      ruleCatchesNot "DL3059" "RUN /foo.sh"+    it "ok with two not consecutive `RUN`" $ do+      ruleCatchesNot "DL3059" "RUN /foo.sh\nWORKDIR /\nRUN /bar.sh"+    it "not ok with two consecutive `RUN`s" $ do+      ruleCatches "DL3059" "RUN /foo.sh\nRUN /bar.sh"+    it "not ok with two `RUN`s separated by a comment" $ do+      ruleCatches "DL3059" "RUN /foo.sh\n# a comment\nRUN /bar.sh"+    it "not ok with two `RUN`s separated by two comment" $ do+      ruleCatches "DL3059" "RUN /foo.sh\n# a comment\n# another comment\nRUN\+                           \ /bar.sh"+    it "ok with one `RUN` after a comment" $ do+      ruleCatchesNot "DL3059" "# a comment\nRUN /foo.sh"+    it "ok with two consecutive `RUN`s when flags are different 1" $ do+      ruleCatchesNot "DL3059" "RUN --mount=type=secret,id=foo /foo.sh\nRUN\+                              \ /bar.sh"+    it "ok with two consecutive `RUN`s when flags are different 2" $ do+      let dfile = [ "RUN --mount=type=secret,id=foo /foo.sh",+                    "RUN --mount=type=secret,id=bar /bar.sh"+                  ]+       in ruleCatchesNot "DL3059" $ Text.unlines dfile+    it "not ok with two consecutive `RUN`s when flags are equal" $ do+      let dfile = [ "RUN --mount=type=secret,id=foo /foo.sh",+                    "RUN --mount=type=secret,id=foo /bar.sh"+                  ]+       in ruleCatches "DL3059" $ Text.unlines dfile++    it "ok with two consecutive `RUN`s when commands are chained 1" $ do+      let dfile = [ "RUN foo && bar",+                    "RUN foobar"+                  ]+       in ruleCatchesNot "DL3059" $ Text.unlines dfile+    it "ok with two consecutive `RUN`s when commands are chained 2" $ do+      let dfile = [ "RUN foo; bar",+                    "RUN foobar"+                  ]+       in ruleCatchesNot "DL3059" $ Text.unlines dfile+    it "ok with two consecutive `RUN`s when commands are chained 3" $ do+      let dfile = [ "RUN foobar",+                    "RUN foo && bar"+                  ]+       in ruleCatchesNot "DL3059" $ Text.unlines dfile+    it "ok with two consecutive `RUN`s when commands are chained 4" $ do+      let dfile = [ "RUN foobar",+                    "RUN foo; bar"+                  ]+       in ruleCatchesNot "DL3059" $ Text.unlines dfile+    it "ok with two consecutive `RUN`s when commands are chained 5" $ do+      let dfile = [ "RUN foo && bar",+                    "RUN foo; bar"+                  ]+       in ruleCatchesNot "DL3059" $ Text.unlines dfile+    it "not ok when more than one `RUN` has just one command in a row" $ do+      let dfile = [ "RUN foo && bar",+                    "RUN foo",+                    "RUN bar"+                  ]+       in ruleCatches "DL3059" $ Text.unlines dfile
+ test/Hadolint/Rule/DL3060Spec.hs view
@@ -0,0 +1,70 @@+module Hadolint.Rule.DL3060Spec (spec) where++import Data.Default+import qualified Data.Text as Text+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def++  describe "DL3060 - `yarn cache clean` missing after `yarn install`" $ do+    it "ok with non-yarn commands" $ do+      ruleCatchesNot "DL3060" "RUN foo"+      onBuildRuleCatchesNot "DL3060" "RUN foo"+    it "not ok with no cache clean" $ do+      ruleCatches "DL3060" "RUN yarn install foo"+      onBuildRuleCatches "DL3060" "RUN yarn install foo"+    it "ok with cache clean" $ do+      ruleCatchesNot "DL3060" "RUN yarn install bar && yarn cache clean"+      onBuildRuleCatchesNot "DL3060" "RUN yarn install bar && yarn cache clean"++    it "not ok when yarn install is in last stage w/o yarn clean" $+      let dockerFile =+            Text.unlines+              [ "FROM node:lts-alpine as foo",+                "RUN hey!",+                "FROM scratch",+                "RUN yarn install"+              ]+       in do+            ruleCatches "DL3060" dockerFile+            onBuildRuleCatches "DL3060" dockerFile++    it "not ok when inheriting from stage with yarn install w/o yarn clean" $+      let dockerFile =+            Text.unlines+              [ "FROM node:lts-alpine as foo",+                "RUN yarn install",+                "FROM foo",+                "RUN hey!"+              ]+       in do+            ruleCatches "DL3060" dockerFile+            onBuildRuleCatches "DL3060" dockerFile++    it "ok when inheriting from stage with yarn cache clear" $+      let dockerFile =+            Text.unlines+              [ "FROM node:lts-alpine as foo",+                "RUN yarn install && yarn cache clean",+                "FROM foo",+                "RUN hey!"+              ]+       in do+            ruleCatchesNot "DL3060" dockerFile+            onBuildRuleCatchesNot "DL3060" dockerFile++    it "ok when omitting yarn cache clean in stage that is not reused later" $+      let dockerFile =+            Text.unlines+              [ "FROM node:lts-alpine as foo",+                "RUN yarn install foo",+                "FROM scratch",+                "RUN hey!"+              ]+       in do+            ruleCatchesNot "DL3060" dockerFile+            onBuildRuleCatches "DL3060" dockerFile
+ test/Hadolint/Rule/DL4000Spec.hs view
@@ -0,0 +1,16 @@+module Hadolint.Rule.DL4000Spec (spec) where++import Data.Default+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def+  describe "DL4000 - MAINTAINER is deprecated." $ do+    it "has deprecated maintainer" $+      ruleCatches "DL4000" "FROM busybox\nMAINTAINER hudu@mail.com"+    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"
+ test/Hadolint/Rule/DL4001Spec.hs view
@@ -0,0 +1,71 @@+module Hadolint.Rule.DL4001Spec (spec) where++import Data.Default+import Data.Text as Text+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def++  describe "DL4001 - Either use Wget or Curl but not both." $ 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")
+ test/Hadolint/Rule/DL4003Spec.hs view
@@ -0,0 +1,44 @@+module Hadolint.Rule.DL4003Spec (spec) where++import Data.Default+import Data.Text as Text+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def++  describe "DL4003" $ do+    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"
+ test/Hadolint/Rule/DL4004Spec.hs view
@@ -0,0 +1,47 @@+module Hadolint.Rule.DL4004Spec (spec) where++import Data.Default+import Data.Text as Text+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def++  describe "DL4004" $ do+    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"
+ test/Hadolint/Rule/DL4005Spec.hs view
@@ -0,0 +1,21 @@+module Hadolint.Rule.DL4005Spec (spec) where++import Data.Default+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def++  describe "DL4005 - Use `SHELL` to change the default 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"
+ test/Hadolint/Rule/DL4006Spec.hs view
@@ -0,0 +1,106 @@+module Hadolint.Rule.DL4006Spec (spec) where++import Data.Default+import Data.Text as Text+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def++  describe "DL4006 - Set the `SHELL` option `-o pipefail` before RUN with a pipe in it" $ 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+    it "ignore non posix shells: pwsh" $+      let dockerFile =+            [ "FROM mcr.microsoft.com/powershell:ubuntu-16.04",+              "SHELL [ \"pwsh\", \"-c\" ]",+              "RUN Get-Variable PSVersionTable | Select-Object -ExpandProperty Value"+            ]+       in ruleCatchesNot "DL4006" $ Text.unlines dockerFile+    it "ignore non posix shells: powershell" $+      let dockerFile =+            [ "FROM mcr.microsoft.com/powershell:ubuntu-16.04",+              "SHELL [ \"powershell.exe\" ]",+              "RUN Get-Variable PSVersionTable | Select-Object -ExpandProperty Value"+            ]+       in ruleCatchesNot "DL4006" $ Text.unlines dockerFile+    it "ignore non posix shells: cmd.exe" $+      let dockerFile =+            [ "FROM mcr.microsoft.com/powershell:ubuntu-16.04",+              "SHELL [ \"cmd.exe\", \"/c\" ]",+              "RUN Get-Variable PSVersionTable | Select-Object -ExpandProperty Value"+            ]+       in ruleCatchesNot "DL4006" $ Text.unlines dockerFile
+ test/Hadolint/Rule/ShellcheckSpec.hs view
@@ -0,0 +1,171 @@+module Hadolint.Rule.ShellcheckSpec (spec) where++import Data.Default+import Data.Text as Text+import Helpers+import Test.Hspec+++spec :: SpecWith ()+spec = do+  let ?config = def++  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 non-posix shells: pwsh" $+      let dockerFile =+            Text.unlines+              [ "SHELL [\"pwsh\", \"-c\"]",+                "RUN Get-Variable PSVersionTable | Select-Object -ExpandProperty Value"+              ]+       in do+            assertChecks dockerFile passesShellcheck+            assertOnBuildChecks dockerFile passesShellcheck+    it "Does not complain on non-posix shells: cmd.exe" $+      let dockerFile =+            Text.unlines+              [ "SHELL [\"cmd.exe\", \"/c\"]",+                "RUN Get-Variable PSVersionTable | Select-Object -ExpandProperty Value"+              ]+       in do+            assertChecks dockerFile passesShellcheck+            assertOnBuildChecks dockerFile passesShellcheck++    it "Does not complain on non-posix shells, powershell - absolute path" $+      let dockerFile =+            Text.unlines+              [ "SHELL [\"C:\\\\Windows\\\\System32\\\\WindowsPowerShell\\\\\+                \v1.0\\\\powershell.exe\", \"-noprofile\", \"-noninteractive\"\+                \, \"-command\"]",+                "RUN Get-Variable PSVersionTable | Select-Object \+                \-ExpandProperty Value"+              ]+       in do+            assertChecks dockerFile passesShellcheck+            assertOnBuildChecks dockerFile passesShellcheck++    it "Respects shell pragma" $+      let dockerFile =+            Text.unlines+              [ "FROM mcr.microsoft.com/foo/bar/windows:10",+                "# hadolint shell = powershell.exe",+                "RUN Get-Variable PSVersionTable | Select-Object \+                \-ExpandProperty Value"+              ]+       in do+            assertChecks dockerFile passesShellcheck+            assertOnBuildChecks dockerFile passesShellcheck+    it "Respects global shell pragma" $+      let dockerFile =+            Text.unlines+              [ "# hadolint shell=powershell.exe",+                "FROM mcr.microsoft.com/foo/bar/windows:10",+                "RUN Get-Variable PSVersionTable | Select-Object \+                \-ExpandProperty Value",+                "FROM mcr.microsoft.com/foo/bar/windows:10",+                "RUN Get-Variable PSVersionTable | Select-Object \+                \-ExpandProperty Value"+              ]+       in do+            assertChecks dockerFile passesShellcheck+            assertOnBuildChecks dockerFile passesShellcheck
+ test/Hadolint/ShellSpec.hs view
@@ -0,0 +1,53 @@+module Hadolint.ShellSpec (spec) where++import Hadolint.Shell+import Test.Hspec+++spec :: SpecWith ()+spec =+  describe "Shell unit tests" $ do+  --+    it "getFlagArgs" $ do+      let cmd = Command "useradd"+                        [ CmdPart "12345" 2,+                          CmdPart "67890" 4,+                          CmdPart "luser" 5 ]+                        [ CmdPart "u" 1,+                          CmdPart "u" 3 ]+       in getFlagArg "u" cmd `shouldBe` [ "12345",+                                          "67890" ]+      let cmd = Command "useradd"+                        [ CmdPart "12345" 2,+                          CmdPart "luser" 3 ]+                        [ CmdPart "u" 1 ]+       in getFlagArg "u" cmd `shouldBe` [ "12345" ]++      let cmd = Command "useradd"+                        [ CmdPart "12345" 2,+                          CmdPart "luser" 3 ]+                        [ CmdPart "u" 1 ]+       in getFlagArg "f" cmd `shouldBe` []+  --+    it "hasFlag" $ do+      let cmd = Command "useradd"+                        [ CmdPart "luser" 1 ]+                        [ CmdPart "l" 0 ]+       in hasFlag "l" cmd `shouldBe` True+      let cmd = Command "useradd"+                        [ CmdPart "luser" 1 ]+                        [ CmdPart "l" 0 ]+       in hasFlag "f" cmd `shouldBe` False+  --+    it "dropFlagArg" $ do+      let cmd = Command "npm"+                        [ CmdPart "verbose" 2,+                          CmdPart "install" 3,+                          CmdPart "bla@1.0.0" 4]+                        [ CmdPart "loglevel" 1]+      let res = Command "npm"+                        [ CmdPart "install" 3,+                          CmdPart "bla@1.0.0" 4]+                        [ CmdPart "loglevel" 1]+       in do+        dropFlagArg ["loglevel"] cmd `shouldBe` res
test/Helpers.hs view
@@ -1,30 +1,34 @@ module Helpers where -import qualified Control.Foldl as Foldl import Control.Monad (unless, when)-import qualified Data.Sequence as Seq-import qualified Data.Text as Text+import Hadolint (Configuration (..), OutputFormat (..), printResults)+import Hadolint.Formatter.Format (Result (..)) import Hadolint.Formatter.TTY (formatCheck)-import qualified Hadolint.Process import Hadolint.Rule (CheckFailure (..), Failures, RuleCode (..)) import Language.Docker.Parser import Language.Docker.Syntax+import System.IO.Silently import Test.HUnit hiding (Label) import Test.Hspec+import qualified Control.Foldl as Foldl+import qualified Data.List.NonEmpty as NonEmpty+import qualified Data.Sequence as Seq+import qualified Data.Text as Text+import qualified Hadolint.Process   assertChecks ::-  (HasCallStack, ?rulesConfig :: Hadolint.Process.RulesConfig) =>+  (HasCallStack, ?config :: Configuration) =>   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+    Right dockerFile -> makeAssertions $ Hadolint.Process.run ?config dockerFile  assertOnBuildChecks ::-  (HasCallStack, ?rulesConfig :: Hadolint.Process.RulesConfig) =>+  (HasCallStack, ?config :: Configuration) =>   Text.Text ->   (Failures -> IO a) ->   IO a@@ -33,7 +37,7 @@     Left err -> assertFailure $ show err     Right dockerFile -> checkOnBuild dockerFile   where-    checkOnBuild dockerFile = makeAssertions $ Hadolint.Process.run ?rulesConfig (fmap wrapInOnBuild dockerFile)+    checkOnBuild dockerFile = makeAssertions $ Hadolint.Process.run ?config (fmap wrapInOnBuild dockerFile)     wrapInOnBuild (InstructionPos (Run args) so li) = InstructionPos (OnBuild (Run args)) so li     wrapInOnBuild i = i @@ -44,7 +48,7 @@  -- Assert a failed check exists for rule ruleCatches ::-  (HasCallStack, ?rulesConfig :: Hadolint.Process.RulesConfig) =>+  (HasCallStack, ?config :: Configuration) =>   RuleCode ->   Text.Text ->   Assertion@@ -55,7 +59,7 @@       assertBool "Incorrect line number for result" $ not $ hasInvalidLines checks  onBuildRuleCatches ::-  (HasCallStack, ?rulesConfig :: Hadolint.Process.RulesConfig) =>+  (HasCallStack, ?config :: Configuration) =>   RuleCode ->   Text.Text ->   Assertion@@ -66,7 +70,7 @@       assertBool "Incorrect line number for result" $ not $ hasInvalidLines checks  ruleCatchesNot ::-  (HasCallStack, ?rulesConfig :: Hadolint.Process.RulesConfig) =>+  (HasCallStack, ?config :: Configuration) =>   RuleCode ->   Text.Text ->   Assertion@@ -75,7 +79,7 @@     f = failsWith 0 ruleCode  onBuildRuleCatchesNot ::-  (HasCallStack, ?rulesConfig :: Hadolint.Process.RulesConfig) =>+  (HasCallStack, ?config :: Configuration) =>   RuleCode ->   Text.Text ->   Assertion@@ -118,3 +122,17 @@         <> (Text.unpack . formatChecksNoColor $ matched)   where     matched = Seq.filter (\CheckFailure {code = RuleCode rc} -> "SC" `Text.isPrefixOf` rc) checks+++assertFormatter ::+  (HasCallStack, ?noColor :: Bool) =>+  OutputFormat ->+  [CheckFailure] ->+  String ->+  Assertion+assertFormatter formatter failures expectation = do+  let results =+        NonEmpty.fromList [Result "<string>" mempty (Seq.fromList failures)]+  (cap, _) <- capture+                (printResults formatter ?noColor (Just "<string>") results)+  cap `shouldBe` expectation
+ test/RegressionSpec.hs view
@@ -0,0 +1,32 @@+module RegressionSpec (spec) where++import Data.Default+import qualified Data.Text as Text+import Helpers+import Test.HUnit hiding (Label)+import Test.Hspec++spec :: SpecWith ()+spec = do+  let ?config = def  -- default implicit parameter running the checkers+  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)
− test/ShellSpec.hs
@@ -1,52 +0,0 @@-module ShellSpec where--import Hadolint.Shell-import Test.Hspec--tests :: SpecWith ()-tests =-  describe "Shell unit tests" $ do-  ---    it "getFlagArgs" $ do-      let cmd = Command "useradd"-                        [ CmdPart "12345" 2,-                          CmdPart "67890" 4,-                          CmdPart "luser" 5 ]-                        [ CmdPart "u" 1,-                          CmdPart "u" 3 ]-       in getFlagArg "u" cmd `shouldBe` [ "12345",-                                          "67890" ]-      let cmd = Command "useradd"-                        [ CmdPart "12345" 2,-                          CmdPart "luser" 3 ]-                        [ CmdPart "u" 1 ]-       in getFlagArg "u" cmd `shouldBe` [ "12345" ]--      let cmd = Command "useradd"-                        [ CmdPart "12345" 2,-                          CmdPart "luser" 3 ]-                        [ CmdPart "u" 1 ]-       in getFlagArg "f" cmd `shouldBe` []-  ---    it "hasFlag" $ do-      let cmd = Command "useradd"-                        [ CmdPart "luser" 1 ]-                        [ CmdPart "l" 0 ]-       in hasFlag "l" cmd `shouldBe` True-      let cmd = Command "useradd"-                        [ CmdPart "luser" 1 ]-                        [ CmdPart "l" 0 ]-       in hasFlag "f" cmd `shouldBe` False-  ---    it "dropFlagArg" $ do-      let cmd = Command "npm"-                        [ CmdPart "verbose" 2,-                          CmdPart "install" 3,-                          CmdPart "bla@1.0.0" 4]-                        [ CmdPart "loglevel" 1]-      let res = Command "npm"-                        [ CmdPart "install" 3,-                          CmdPart "bla@1.0.0" 4]-                        [ CmdPart "loglevel" 1]-       in do-        dropFlagArg ["loglevel"] cmd `shouldBe` res
− test/Shellcheck.hs
@@ -1,130 +0,0 @@-module Shellcheck (tests) where--import Data.Text as Text-import Helpers-import Test.Hspec---tests :: SpecWith ()-tests = do-  let ?rulesConfig = mempty-  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 non-posix shells: pwsh" $-      let dockerFile =-            Text.unlines-              [ "SHELL [\"pwsh\", \"-c\"]",-                "RUN Get-Variable PSVersionTable | Select-Object -ExpandProperty Value"-              ]-       in do-            assertChecks dockerFile passesShellcheck-            assertOnBuildChecks dockerFile passesShellcheck-    it "Does not complain on non-posix shells: cmd.exe" $-      let dockerFile =-            Text.unlines-              [ "SHELL [\"cmd.exe\", \"/c\"]",-                "RUN Get-Variable PSVersionTable | Select-Object -ExpandProperty Value"-              ]-       in do-            assertChecks dockerFile passesShellcheck-            assertOnBuildChecks dockerFile passesShellcheck
test/Spec.hs view
@@ -1,233 +1,1 @@-import qualified ConfigSpec-import qualified Data.Text as Text-import qualified DL3000-import qualified DL3001-import qualified DL3002-import qualified DL3003-import qualified DL3004-import qualified DL3005-import qualified DL3006-import qualified DL3007-import qualified DL3008-import qualified DL3009-import qualified DL3010-import qualified DL3011-import qualified DL3012-import qualified DL3013-import qualified DL3014-import qualified DL3015-import qualified DL3016-import qualified DL3018-import qualified DL3019-import qualified DL3020-import qualified DL3021-import qualified DL3022-import qualified DL3023-import qualified DL3024-import qualified DL3025-import qualified DL3026-import qualified DL3027-import qualified DL3028-import qualified DL3029-import qualified DL3030-import qualified DL3032-import qualified DL3033-import qualified DL3034-import qualified DL3035-import qualified DL3036-import qualified DL3037-import qualified DL3038-import qualified DL3040-import qualified DL3041-import qualified DL3042-import qualified DL3043-import qualified DL3044-import qualified DL3045-import qualified DL3046-import qualified DL3047-import qualified DL3048-import qualified DL3049-import qualified DL3050-import qualified DL3051-import qualified DL3052-import qualified DL3053-import qualified DL3054-import qualified DL3055-import qualified DL3056-import qualified DL3057-import qualified DL3058-import qualified DL3059-import qualified DL3060-import qualified DL4001-import qualified DL4003-import qualified DL4004-import qualified DL4005-import qualified DL4006-import Hadolint.Formatter.Format (errorMessage)-import Hadolint.Formatter.TTY (formatError)-import Helpers-import Language.Docker.Parser-import qualified Shellcheck-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 "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, a pragma, end of input, or whitespaces "-        case ast of-          Left err -> assertEqual "Unexpected error msg" expectedMsg (formatError err)-          Right _ -> assertFailure "AST should fail parsing"-    describe "errorMessage" $-      it "display just the error message" $ do-        let ast = parseText "RUNNN"-            expectedMsg = "missing whitespace"-        case ast of-          Left err -> assertEqual "Unexpected error msg" expectedMsg (errorMessage 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 "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-    -- Run rule tests-    Shellcheck.tests-    DL3000.tests-    DL3001.tests-    DL3002.tests-    DL3003.tests-    DL3004.tests-    DL3005.tests-    DL3006.tests-    DL3007.tests-    DL3008.tests-    DL3009.tests-    DL3010.tests-    DL3011.tests-    DL3012.tests-    DL3013.tests-    DL3014.tests-    DL3015.tests-    DL3016.tests-    DL3018.tests-    DL3019.tests-    DL3020.tests-    DL3021.tests-    DL3022.tests-    DL3023.tests-    DL3024.tests-    DL3025.tests-    DL3026.tests-    DL3027.tests-    DL3028.tests-    DL3029.tests-    DL3030.tests-    DL3032.tests-    DL3033.tests-    DL3034.tests-    DL3035.tests-    DL3036.tests-    DL3037.tests-    DL3038.tests-    DL3040.tests-    DL3041.tests-    DL3042.tests-    DL3043.tests-    DL3044.tests-    DL3045.tests-    DL3046.tests-    DL3047.tests-    DL3048.tests-    DL3049.tests-    DL3050.tests-    DL3051.tests-    DL3052.tests-    DL3053.tests-    DL3054.tests-    DL3055.tests-    DL3056.tests-    DL3057.tests-    DL3058.tests-    DL3059.tests-    DL3060.tests-    DL4001.tests-    DL4003.tests-    DL4004.tests-    DL4005.tests-    DL4006.tests+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}