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 qualified Hadolint.Shell as Shell


data Acc
  = Acc
      { Acc -> BaseImage
lastFrom :: BaseImage,
        Acc -> Bool
dockerClean :: Bool,
        Acc -> Map Text Linenumber
stages :: Map.Map Text.Text Linenumber,
        Acc -> Map Linenumber BaseImage
forgets :: Map.Map Linenumber BaseImage
      }
  | Empty
  deriving (Acc -> Acc -> Bool
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
/= :: Acc -> Acc -> Bool
$c/= :: Acc -> Acc -> Bool
== :: Acc -> Acc -> Bool
$c== :: Acc -> Acc -> Bool
Eq, Linenumber -> Acc -> ShowS
[Acc] -> ShowS
Acc -> String
forall a.
(Linenumber -> a -> ShowS)
-> (a -> String) -> ([a] -> ShowS) -> Show a
showList :: [Acc] -> ShowS
$cshowList :: [Acc] -> ShowS
show :: Acc -> String
$cshow :: Acc -> String
showsPrec :: Linenumber -> Acc -> ShowS
$cshowsPrec :: Linenumber -> Acc -> ShowS
Show)


rule :: Rule Shell.ParsedShell
rule :: Rule ParsedShell
rule = Rule ParsedShell
dl3009 forall a. Semigroup a => a -> a -> a
<> forall args. Rule args -> Rule args
onbuild Rule ParsedShell
dl3009
{-# INLINEABLE rule #-}

dl3009 :: Rule Shell.ParsedShell
dl3009 :: Rule ParsedShell
dl3009 = forall a args.
(Linenumber -> State a -> Instruction args -> State a)
-> State a -> (State a -> Seq CheckFailure) -> Rule args
veryCustomRule Linenumber -> State Acc -> Instruction ParsedShell -> State Acc
check (forall a. a -> State a
emptyState Acc
Empty) State Acc -> Seq CheckFailure
markFailures
  where
    code :: RuleCode
code = RuleCode
"DL3009"
    severity :: DLSeverity
severity = DLSeverity
DLInfoC
    message :: Text
message = Text
"Delete the apt-get lists after installing something"

    check :: Linenumber -> State Acc -> Instruction ParsedShell -> State Acc
check Linenumber
line State Acc
st (From BaseImage
from) = State Acc
st forall a b. a -> (a -> b) -> b
|> forall a. (a -> a) -> State a -> State a
modify (Linenumber -> BaseImage -> Acc -> Acc
rememberStage Linenumber
line BaseImage
from)
    check Linenumber
line State Acc
st (Run (RunArgs Arguments ParsedShell
args RunFlags
flags))
      | Bool -> Bool
not (forall a b. (a -> b) -> Arguments a -> b
foldArguments ParsedShell -> Bool
forgotToCleanup Arguments ParsedShell
args) =
          if forall a b. (a -> b) -> Arguments a -> b
foldArguments ParsedShell -> Bool
disabledDockerClean Arguments ParsedShell
args
          then State Acc
st forall a b. a -> (a -> b) -> b
|> forall a. (a -> a) -> State a -> State a
modify Acc -> Acc
rememberDockerClean
          else State Acc
st
      | Text -> RunFlags -> Bool
hasCacheDirectory Text
"/var/lib/apt/lists" RunFlags
flags = State Acc
st
      | Text -> RunFlags -> Bool
hasCacheDirectory Text
"/var/lib/apt" RunFlags
flags
        Bool -> Bool -> Bool
&& Text -> RunFlags -> Bool
hasCacheDirectory Text
"/var/cache/apt" RunFlags
flags = State Acc
st
      | Bool
otherwise = State Acc
st forall a b. a -> (a -> b) -> b
|> forall a. (a -> a) -> State a -> State a
modify (Linenumber -> Acc -> Acc
rememberLine Linenumber
line)
    check Linenumber
_ State Acc
st Instruction ParsedShell
_ = State Acc
st

    -- Convert the final state into failures
    markFailures :: State Acc -> Seq CheckFailure
markFailures (State Seq CheckFailure
fails Acc
Empty) = Seq CheckFailure
fails
    markFailures (State Seq CheckFailure
_ Acc {Bool
Map Linenumber BaseImage
Map Text Linenumber
BaseImage
forgets :: Map Linenumber BaseImage
stages :: Map Text Linenumber
dockerClean :: Bool
lastFrom :: BaseImage
forgets :: Acc -> Map Linenumber BaseImage
stages :: Acc -> Map Text Linenumber
dockerClean :: Acc -> Bool
lastFrom :: Acc -> BaseImage
..}) = Map Linenumber BaseImage
forgets forall a b. a -> (a -> b) -> b
|> forall m k a. Monoid m => (k -> a -> m) -> Map k a -> m
Map.foldMapWithKey forall {f :: * -> *}.
(Applicative f, Monoid (f CheckFailure)) =>
Linenumber -> BaseImage -> f CheckFailure
mapFail
      where
        mapFail :: Linenumber -> BaseImage -> f CheckFailure
mapFail Linenumber
line BaseImage
from
          | BaseImage
from forall a. Eq a => a -> a -> Bool
== BaseImage
lastFrom = forall (f :: * -> *) a. Applicative f => a -> f a
pure CheckFailure {Linenumber
Text
RuleCode
DLSeverity
line :: Linenumber
message :: Text
severity :: DLSeverity
code :: RuleCode
line :: Linenumber
message :: Text
severity :: DLSeverity
code :: RuleCode
..}
          | BaseImage {$sel:alias:BaseImage :: BaseImage -> Maybe ImageAlias
alias = Just (ImageAlias Text
als)} <- BaseImage
from, -- Check if this stage is used later
            Just Linenumber
_ <- forall k a. Ord k => k -> Map k a -> Maybe a
Map.lookup Text
als Map Text Linenumber
stages =
            -- If the same alias is used in another stage, fail
            forall (f :: * -> *) a. Applicative f => a -> f a
pure CheckFailure {Linenumber
Text
RuleCode
DLSeverity
line :: Linenumber
message :: Text
severity :: DLSeverity
code :: RuleCode
line :: Linenumber
message :: Text
severity :: DLSeverity
code :: RuleCode
..}
          | Bool
otherwise = forall a. Monoid a => a
mempty
{-# INLINEABLE dl3009 #-}

rememberStage :: Linenumber -> BaseImage -> Acc -> Acc
rememberStage :: Linenumber -> BaseImage -> Acc -> Acc
rememberStage Linenumber
line from :: BaseImage
from@BaseImage {$sel:image:BaseImage :: BaseImage -> Image
image = Image Maybe Registry
_ Text
als} (Acc BaseImage
_ Bool
_ Map Text Linenumber
stages Map Linenumber BaseImage
o) = BaseImage
-> Bool -> Map Text Linenumber -> Map Linenumber BaseImage -> Acc
Acc BaseImage
from Bool
True (forall k a. Ord k => k -> a -> Map k a -> Map k a
Map.insert Text
als Linenumber
line Map Text Linenumber
stages) Map Linenumber BaseImage
o
rememberStage Linenumber
line from :: BaseImage
from@BaseImage {$sel:image:BaseImage :: BaseImage -> Image
image = Image Maybe Registry
_ Text
als} Acc
Empty = BaseImage
-> Bool -> Map Text Linenumber -> Map Linenumber BaseImage -> Acc
Acc BaseImage
from Bool
True (forall k a. k -> a -> Map k a
Map.singleton Text
als Linenumber
line) forall k a. Map k a
Map.empty

rememberLine :: Linenumber -> Acc -> Acc
rememberLine :: Linenumber -> Acc -> Acc
rememberLine Linenumber
line (Acc BaseImage
from Bool
clean Map Text Linenumber
stages Map Linenumber BaseImage
o) = BaseImage
-> Bool -> Map Text Linenumber -> Map Linenumber BaseImage -> Acc
Acc BaseImage
from Bool
clean Map Text Linenumber
stages (forall k a. Ord k => k -> a -> Map k a -> Map k a
Map.insert Linenumber
line BaseImage
from Map Linenumber BaseImage
o)
rememberLine Linenumber
line Acc
Empty = BaseImage
-> Bool -> Map Text Linenumber -> Map Linenumber BaseImage -> Acc
Acc BaseImage
emptyImage Bool
True forall a. Monoid a => a
mempty (forall k a. k -> a -> Map k a
Map.singleton Linenumber
line BaseImage
emptyImage)

rememberDockerClean :: Acc -> Acc
rememberDockerClean :: Acc -> Acc
rememberDockerClean Acc
Empty = BaseImage
-> Bool -> Map Text Linenumber -> Map Linenumber BaseImage -> Acc
Acc BaseImage
emptyImage Bool
False forall a. Monoid a => a
mempty forall a. Monoid a => a
mempty
rememberDockerClean (Acc BaseImage
from Bool
_ Map Text Linenumber
stages Map Linenumber BaseImage
forget) = BaseImage
-> Bool -> Map Text Linenumber -> Map Linenumber BaseImage -> Acc
Acc BaseImage
from Bool
False Map Text Linenumber
stages Map Linenumber BaseImage
forget

forgotToCleanup :: Shell.ParsedShell -> Bool
forgotToCleanup :: ParsedShell -> Bool
forgotToCleanup ParsedShell
args
  | ParsedShell -> Bool
hasUpdate ParsedShell
args Bool -> Bool -> Bool
&& Bool -> Bool
not Bool
hasCleanup = Bool
True
  | Bool
otherwise = Bool
False
  where
    hasCleanup :: Bool
hasCleanup =
      forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
any (Text -> [Text] -> Command -> Bool
Shell.cmdHasArgs Text
"rm" [Text
"-rf", Text
"/var/lib/apt/lists/*"]) (ParsedShell -> [Command]
Shell.presentCommands ParsedShell
args)

hasUpdate :: Shell.ParsedShell -> Bool
hasUpdate :: ParsedShell -> Bool
hasUpdate ParsedShell
args =
  forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
any (Text -> [Text] -> Command -> Bool
Shell.cmdHasArgs Text
"apt-get" [Text
"update"]) (ParsedShell -> [Command]
Shell.presentCommands ParsedShell
args)

disabledDockerClean :: Shell.ParsedShell -> Bool
disabledDockerClean :: ParsedShell -> Bool
disabledDockerClean ParsedShell
args
  | Bool
removesScript Bool -> Bool -> Bool
|| Bool
keepsPackages = Bool
True
  | Bool
otherwise = Bool
False
  where
    removesScript :: Bool
removesScript =
      forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
any
        (Text -> [Text] -> Command -> Bool
Shell.cmdHasArgs Text
"rm" [Text
"/etc/apt/apt.conf.d/docker-clean"])
        (ParsedShell -> [Command]
Shell.presentCommands ParsedShell
args)
    keepsPackages :: Bool
keepsPackages =
      forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
any
        ( Text -> [Text] -> Command -> Bool
Shell.cmdHasArgs
            Text
"echo"
            [Text
"\'Binary::apt::APT::Keep-Downloaded-Packages \"true\";\'"]
        )
        (ParsedShell -> [Command]
Shell.presentCommands ParsedShell
args)

hasCacheDirectory :: Text.Text -> RunFlags -> Bool
hasCacheDirectory :: Text -> RunFlags -> Bool
hasCacheDirectory Text
dir RunFlags { Set RunMount
$sel:mount:RunFlags :: RunFlags -> Set RunMount
mount :: Set RunMount
mount } =
  Bool -> Bool
not ( forall (t :: * -> *) a. Foldable t => t a -> Bool
null forall a b. (a -> b) -> a -> b
$ forall a. (a -> Bool) -> Set a -> Set a
Set.filter (Text -> RunMount -> Bool
isCacheMount Text
dir) Set RunMount
mount)

isCacheMount :: Text.Text -> RunMount -> Bool
isCacheMount :: Text -> RunMount -> Bool
isCacheMount Text
dir (CacheMount CacheOpts {$sel:cTarget:CacheOpts :: CacheOpts -> TargetPath
cTarget = TargetPath {$sel:unTargetPath:TargetPath :: TargetPath -> Text
unTargetPath = Text
t}}) = Text
dir Text -> Text -> Bool
`Text.isPrefixOf` Text
t
isCacheMount Text
_ RunMount
_ = Bool
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
emptyImage :: BaseImage
emptyImage :: BaseImage
emptyImage = (BaseImage {$sel:image:BaseImage :: Image
image = Image
"scratch", $sel:tag:BaseImage :: Maybe Tag
tag = forall a. Maybe a
Nothing, $sel:digest:BaseImage :: Maybe Digest
digest = forall a. Maybe a
Nothing, $sel:alias:BaseImage :: Maybe ImageAlias
alias = forall a. Maybe a
Nothing, $sel:platform:BaseImage :: Maybe Text
platform = forall a. Maybe a
Nothing})