packages feed

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

    check (Run (RunArgs args _)) = foldArguments (Shell.noCommands forgotToPinVersion) args
    check _ = True

    forgotToPinVersion cmd =
      isNpmInstall cmd && installIsFirst cmd && not (all versionFixed (packages cmd))

    isNpmInstall = Shell.cmdHasArgs "npm" ["install"]
    installIsFirst cmd = ["install"] `isPrefixOf` Shell.getArgsNoFlags cmd
    packages cmd = stripInstallPrefix (Shell.getArgsNoFlags cmd)

    versionFixed package
      | hasGitPrefix package = isVersionedGit package
      | hasTarballSuffix package = True
      | isFolder package = True
      | otherwise = hasVersionSymbol package

    gitPrefixes = ["git://", "git+ssh://", "git+http://", "git+https://"]
    pathPrefixes = ["/", "./", "../", "~/"]
    tarballSuffixes = [".tar", ".tar.gz", ".tgz"]

    hasGitPrefix package = or [p `Text.isPrefixOf` package | p <- gitPrefixes]
    hasTarballSuffix package = or [p `Text.isSuffixOf` package | p <- tarballSuffixes]
    isFolder package = or [p `Text.isPrefixOf` package | p <- pathPrefixes]
    isVersionedGit package = "#" `Text.isInfixOf` package

    hasVersionSymbol package = "@" `Text.isInfixOf` dropScope package
      where
        dropScope pkg =
          if "@" `Text.isPrefixOf` pkg
            then Text.dropWhile ('/' <) pkg
            else pkg
{-# INLINEABLE rule #-}

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