packages feed

hadolint-2.9.2: src/Hadolint/Rule/DL3016.hs

module Hadolint.Rule.DL3016 (rule) where

import Data.List (isPrefixOf)
import qualified Data.Text as Text
import Hadolint.Rule
import Hadolint.Shell (ParsedShell)
import qualified Hadolint.Shell as Shell
import Language.Docker.Syntax (Instruction (..), RunArgs (..))


rule :: Rule ParsedShell
rule = dl3016 <> onbuild dl3016
{-# INLINEABLE rule #-}

-- | Rule for pinning NPM packages to version, tag, or commit
--  supported formats by Hadolint
--    npm install (with no args, in package dir)
--    npm install [<@scope>/]<name>
--    npm install [<@scope>/]<name>@<tag>
--    npm install [<@scope>/]<name>@<version>
--    npm install git[+http|+https]://<git-host>/<git-user>/<repo-name>[#<commit>|#semver:<semver>]
--    npm install git+ssh://<git-host>:<git-user>/<repo-name>[#<commit>|#semver:<semver>]
dl3016 :: Rule ParsedShell
dl3016 = simpleRule code severity message check
  where
    code = "DL3016"
    severity = DLWarningC
    message =
      "Pin versions in npm. Instead of `npm install <package>` use `npm install \
      \<package>@<version>`"

    check (Run (RunArgs args _)) = foldArguments (Shell.noCommands forgotToPinVersion) args
    check _ = True
{-# INLINEABLE dl3016 #-}


forgotToPinVersion :: Shell.Command -> Bool
forgotToPinVersion cmd =
  isNpmInstall cmd && installIsFirst cmd && not (all versionFixed (packages cmd))

isNpmInstall :: Shell.Command -> Bool
isNpmInstall = Shell.cmdHasArgs "npm" ["install"]

installIsFirst :: Shell.Command -> Bool
installIsFirst cmd = ["install"] `isPrefixOf` Shell.getArgsNoFlags
  (Shell.dropFlagArg ignoreFlags cmd)

packages :: Shell.Command -> [Text.Text]
packages cmd = stripInstallPrefix
  (Shell.getArgsNoFlags (Shell.dropFlagArg ignoreFlags cmd))

versionFixed :: Text.Text -> Bool
versionFixed package
  | hasGitPrefix package = isVersionedGit package
  | hasTarballSuffix package = True
  | isFolder package = True
  | otherwise = hasVersionSymbol package

hasGitPrefix :: Text.Text -> Bool
hasGitPrefix package = or [p `Text.isPrefixOf` package | p <- gitPrefixes]

hasTarballSuffix :: Text.Text -> Bool
hasTarballSuffix package = or [p `Text.isSuffixOf` package | p <- tarballSuffixes]

isFolder :: Text.Text -> Bool
isFolder package = or [p `Text.isPrefixOf` package | p <- pathPrefixes]

isVersionedGit :: Text.Text -> Bool
isVersionedGit package = "#" `Text.isInfixOf` package

hasVersionSymbol :: Text.Text -> Bool
hasVersionSymbol package = "@" `Text.isInfixOf` dropScope package
  where
    dropScope pkg =
      if "@" `Text.isPrefixOf` pkg
        then Text.dropWhile ('/' <) pkg
        else pkg

stripInstallPrefix :: [Text.Text] -> [Text.Text]
stripInstallPrefix cmd = dropWhile (== "install") (dropWhile (/= "install") cmd)


ignoreFlags :: [Text.Text]
ignoreFlags = ["loglevel"]

gitPrefixes :: [Text.Text]
gitPrefixes = ["git://", "git+ssh://", "git+http://", "git+https://"]

pathPrefixes :: [Text.Text]
pathPrefixes = ["/", "./", "../", "~/"]

tarballSuffixes :: [Text.Text]
tarballSuffixes = [".tar", ".tar.gz", ".tgz"]