diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -24,6 +24,7 @@
 ```bash
 hadolint <Dockerfile>
 hadolint --ignore DL3003 --ignore DL3006 <Dockerfile> # exclude specific rules
+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
@@ -78,6 +79,20 @@
   - SC1010
 ```
 
+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:
+
+```yaml
+ignored:
+  - DL3000
+  - SC1010
+
+trustedRegistries:
+  - docker.io
+  - my-company.com:5000
+```
+
 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`
 
@@ -154,6 +169,7 @@
 | [DL3023](https://github.com/hadolint/hadolint/wiki/DL3023)   | `COPY --from` cannot reference its own `FROM` alias                                                                                                 |
 | [DL3024](https://github.com/hadolint/hadolint/wiki/DL3024)   | `FROM` aliases (stage names) must be unique                                                                                                         |
 | [DL3025](https://github.com/hadolint/hadolint/wiki/DL3025)   | Use arguments JSON notation for CMD and ENTRYPOINT arguments                                                                                        |
+| [DL3025](https://github.com/hadolint/hadolint/wiki/DL3026)   | Use only an allowed registry in the FROM image                                                                                                      |
 | [DL4000](https://github.com/hadolint/hadolint/wiki/DL4000)   | MAINTAINER is deprecated.                                                                                                                           |
 | [DL4001](https://github.com/hadolint/hadolint/wiki/DL4001)   | Either use Wget or Curl but not both.                                                                                                               |
 | [DL4003](https://github.com/hadolint/hadolint/wiki/DL4003)   | Multiple `CMD` instructions found.                                                                                                                  |
diff --git a/app/Main.hs b/app/Main.hs
--- a/app/Main.hs
+++ b/app/Main.hs
@@ -4,35 +4,39 @@
 
 module Main where
 
-import Hadolint.Rules
-import Language.Docker (parseFile)
-import Language.Docker.Syntax (Dockerfile)
-
 import Control.Applicative
 import Control.Monad (filterM)
-import Data.Maybe (listToMaybe)
+import Data.Coerce (coerce)
+import Data.Maybe (fromMaybe, listToMaybe)
 import Data.Semigroup ((<>))
-import qualified Data.Version as V (showVersion)
+import qualified Data.Set as Set
+import Data.String
+import Data.Text (Text)
+import qualified Data.Version
 import qualified Data.Yaml as Yaml
-import Development.GitRev (gitDescribe)
+import qualified Development.GitRev
 import GHC.Generics
+import qualified Language.Docker as Docker
+import Language.Docker.Syntax (Dockerfile)
 import Options.Applicative hiding (ParseError)
-import qualified Paths_hadolint as Version -- version from hadolint.cabal file
+import qualified Paths_hadolint -- version from hadolint.cabal file
 import System.Directory
        (XdgDirectory(..), doesFileExist, getCurrentDirectory,
         getXdgDirectory)
 import System.Exit (exitFailure, exitSuccess)
 import System.FilePath ((</>))
 
-import Data.Text (Text)
 import qualified Hadolint.Formatter.Checkstyle as Checkstyle
 import qualified Hadolint.Formatter.Codeclimate as Codeclimate
-import Hadolint.Formatter.Format (toResult)
+import qualified Hadolint.Formatter.Format as Format
 import qualified Hadolint.Formatter.Json as Json
 import qualified Hadolint.Formatter.TTY as TTY
+import qualified Hadolint.Rules as Rules
 
 type IgnoreRule = Text
 
+type TrustedRegistry = Text
+
 data OutputFormat
     = Json
     | TTY
@@ -45,16 +49,19 @@
     , format :: OutputFormat
     , ignoreRules :: [IgnoreRule]
     , dockerfiles :: [String]
+    , rulesConfig :: Rules.RulesConfig
     } deriving (Show)
 
-newtype ConfigFile = ConfigFile
-    { ignored :: [IgnoreRule]
+data ConfigFile = ConfigFile
+    { ignored :: Maybe [IgnoreRule]
+    , trustedRegistries :: Maybe [TrustedRegistry]
     } deriving (Show, Eq, Generic)
 
 instance Yaml.FromJSON ConfigFile
 
-ignoreFilter :: [IgnoreRule] -> RuleCheck -> Bool
-ignoreFilter ignoredRules (RuleCheck (Metadata code _ _) _ _ _) = code `notElem` ignoredRules
+ignoreFilter :: [IgnoreRule] -> Rules.RuleCheck -> Bool
+ignoreFilter ignoredRules (Rules.RuleCheck (Rules.Metadata code _ _) _ _ _) =
+    code `notElem` ignoredRules
 
 toOutputFormat :: String -> Maybe OutputFormat
 toOutputFormat "json" = Just Json
@@ -75,7 +82,8 @@
     version <*>
     outputFormat <*>
     ignoreList <*>
-    files
+    files <*>
+    parseRulesConfig
   where
     version = switch (long "version" <> short 'v' <> help "Show version")
     --
@@ -99,6 +107,15 @@
     --
     -- | Parse a list of dockerfile names
     files = many (argument str (metavar "DOCKERFILE..." <> action "file"))
+    --
+    -- | Parses all the optional rules configuration
+    parseRulesConfig =
+        Rules.RulesConfig . 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)"))
 
 main :: IO ()
 main = execParser opts >>= applyConfig >>= lint
@@ -110,12 +127,13 @@
              header "hadolint - Dockerfile Linter written in Haskell")
 
 applyConfig :: LintOptions -> IO LintOptions
-applyConfig o@LintOptions {ignoreRules = (_:_)} = return o
-applyConfig o = do
-    theConfig <- findConfig
-    case theConfig of
-        Nothing -> return o
-        Just config -> parseAndApply config
+applyConfig o
+    | not (null (ignoreRules o)) && rulesConfig o /= mempty = return o
+    | otherwise = do
+        theConfig <- findConfig
+        case theConfig of
+            Nothing -> return o
+            Just config -> parseAndApply config
   where
     findConfig = do
         localConfigFile <- (</> ".hadolint.yaml") <$> getCurrentDirectory
@@ -125,15 +143,37 @@
         result <- Yaml.decodeFileEither config
         case result of
             Left err -> printError err config
-            Right (ConfigFile ignore) -> return o {ignoreRules = ignore}
+            Right (ConfigFile ignore trusted) -> return (override ignore trusted)
+    -- | Applies the configuration found in the file to the passed LintOptions
+    override ignore trusted = applyTrusted trusted . applyIgnore ignore $ o
+    applyIgnore ignore opts =
+        case ignoreRules opts of
+            [] -> opts {ignoreRules = fromMaybe [] ignore}
+            _ -> opts
+    applyTrusted trusted opts
+        | null (Rules.allowedRegistries (rulesConfig opts)) =
+            opts {rulesConfig = toRules trusted <> rulesConfig opts}
+        | otherwise = opts
+    -- | Converts a list of TrustedRegistry to a RulesConfig record
+    toRules (Just trusted) = Rules.RulesConfig (Set.fromList . coerce $ trusted)
+    toRules _ = mempty
     printError err config =
         case err of
-            Yaml.AesonException _ ->
+            Yaml.AesonException e ->
                 error $
-                "Error parsing your config file in  '" ++
-                config ++
-                "':\nIt should contain an 'ignored' key with a list of strings. For example:\n\n" ++
-                unlines ["ignored:", "\t- DL3000", "\t- SC1099"]
+                unlines
+                    [ "Error parsing your config file in  '" ++ config ++ "':"
+                    , "It should contain one of the keys 'ignored' or 'trustedRegistries'. For example:\n"
+                    , "ignored:"
+                    , "\t- DL3000"
+                    , "\t- SC1099\n\n"
+                    , "The key 'trustedRegistries' should contain the names of the allowed docker registries:\n"
+                    , "allowedRegistries:"
+                    , "\t- docker.io"
+                    , "\t- my-company.com"
+                    , ""
+                    , e
+                    ]
             _ ->
                 error $
                 "Error parsing your config file in  '" ++
@@ -146,14 +186,17 @@
 
 getVersion :: String
 getVersion
-    | $(gitDescribe) == "UNKNOWN" =
-        "Haskell Dockerfile Linter " ++ V.showVersion Version.version ++ "-no-git"
-    | otherwise = "Haskell Dockerfile Linter " ++ $(gitDescribe)
+    | $(Development.GitRev.gitDescribe) == "UNKNOWN" =
+        "Haskell Dockerfile Linter " ++ Data.Version.showVersion Paths_hadolint.version ++ "-no-git"
+    | otherwise = "Haskell Dockerfile Linter " ++ $(Development.GitRev.gitDescribe)
 
+-- | Performs the process of parsing the dockerfile and analyzing it with all the applicable
+-- rules, depending on the list of ignored rules.
+-- Depending on the preferred printing format, it will output the results to stdout
 lint :: LintOptions -> IO ()
 lint LintOptions {showVersion = True} = putStrLn getVersion >> exitSuccess
 lint LintOptions {dockerfiles = []} = putStrLn "Please provide a Dockerfile" >> exitFailure
-lint LintOptions {ignoreRules = ignoreList, dockerfiles = dFiles, format} = do
+lint LintOptions {ignoreRules = ignoreList, dockerfiles = dFiles, format, rulesConfig} = do
     processedFiles <- mapM (lintDockerfile ignoreList) dFiles
     let allResults = results processedFiles
     printResult allResults
@@ -161,9 +204,9 @@
         then exitFailure
         else exitSuccess
   where
-    results = foldMap toResult -- Parse and check rules for each dockerfile,
-                               -- then convert them to a Result and combine with
-                               -- the result of the previous dockerfile results
+    results = foldMap Format.toResult -- Parse and check rules for each dockerfile,
+                                      -- then convert them to a Result and combine with
+                                      -- the result of the previous dockerfile results
     printResult res =
         case format of
             TTY -> TTY.printResult res
@@ -171,17 +214,18 @@
             Checkstyle -> Checkstyle.printResult res
             CodeclimateJson -> Codeclimate.printResult res >> exitSuccess
     lintDockerfile ignoreRules dockerFile = do
-        ast <- parseFile $ parseFilename dockerFile
+        ast <- Docker.parseFile (parseFilename dockerFile)
         return (processedFile ast)
       where
         processedFile = fmap processRules
-        processRules fileLines = filter ignoredRules (analyzeAll fileLines)
+        processRules fileLines = filter ignoredRules (analyzeAll rulesConfig fileLines)
         ignoredRules = ignoreFilter ignoreRules
 
-analyzeAll :: Dockerfile -> [RuleCheck]
-analyzeAll = analyze rules
+-- | Returns the result of applying all the rules to the given dockerfile
+analyzeAll :: Rules.RulesConfig -> Dockerfile -> [Rules.RuleCheck]
+analyzeAll config = Rules.analyze (Rules.rules ++ Rules.optionalRules config)
 
--- Helper to analyze AST quickly in GHCI
-analyzeEither :: Either t Dockerfile -> [RuleCheck]
-analyzeEither (Left _) = []
-analyzeEither (Right dockerFile) = analyzeAll dockerFile
+-- | Helper to analyze AST quickly in GHCI
+analyzeEither :: Rules.RulesConfig -> Either t Dockerfile -> [Rules.RuleCheck]
+analyzeEither _ (Left _) = []
+analyzeEither config (Right dockerFile) = analyzeAll config dockerFile
diff --git a/hadolint.cabal b/hadolint.cabal
--- a/hadolint.cabal
+++ b/hadolint.cabal
@@ -2,10 +2,10 @@
 --
 -- see: https://github.com/sol/hpack
 --
--- hash: 0723e0fb6351589066a83e427a444fd8297f43911d23ac2c3a053888a957f94b
+-- hash: 1117d08fad1cb1f1cbc95bffaff37881c65142fe9772c00690c26a5e0070a4c6
 
 name:           hadolint
-version:        1.9.0
+version:        1.10.1
 synopsis:       Dockerfile Linter JavaScript API
 description:    A smarter Dockerfile linter that helps you build best practice Docker images.
 category:       Development
@@ -27,27 +27,27 @@
 library
   hs-source-dirs:
       src
-  ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints
+  ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints -optP-Wno-nonportable-include-path
   build-depends:
       ShellCheck >=0.5.0
     , aeson
     , base >=4.8 && <5
     , bytestring
     , containers
-    , language-docker >=6.0.1 && <7
+    , language-docker >=6.0.3 && <7
     , megaparsec >=6.4
     , mtl
     , split >=0.2
     , text
     , void
   exposed-modules:
-      Hadolint.Bash
-      Hadolint.Rules
+      Hadolint.Formatter.Checkstyle
+      Hadolint.Formatter.Codeclimate
       Hadolint.Formatter.Format
       Hadolint.Formatter.Json
       Hadolint.Formatter.TTY
-      Hadolint.Formatter.Codeclimate
-      Hadolint.Formatter.Checkstyle
+      Hadolint.Rules
+      Hadolint.Shell
   other-modules:
       Paths_hadolint
   default-language: Haskell2010
@@ -56,14 +56,15 @@
   main-is: Main.hs
   hs-source-dirs:
       app
-  ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints
+  ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints -optP-Wno-nonportable-include-path
   build-depends:
       base >=4.8 && <5
+    , containers
     , directory >=1.3.0
     , filepath
     , gitrev >=1.3.1
     , hadolint
-    , language-docker >=6.0.1 && <7
+    , language-docker >=6.0.3 && <7
     , megaparsec >=6.4
     , optparse-applicative >=0.14.0
     , text
@@ -79,7 +80,7 @@
   main-is: Spec.hs
   hs-source-dirs:
       test
-  ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints
+  ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints -optP-Wno-nonportable-include-path
   build-depends:
       HUnit >=1.2
     , ShellCheck >=0.5.0
@@ -88,7 +89,7 @@
     , bytestring >=0.10
     , hadolint
     , hspec
-    , language-docker >=6.0.1 && <7
+    , language-docker >=6.0.3 && <7
     , megaparsec >=6.4
     , split >=0.2
     , text
diff --git a/src/Hadolint/Bash.hs b/src/Hadolint/Bash.hs
deleted file mode 100644
--- a/src/Hadolint/Bash.hs
+++ /dev/null
@@ -1,115 +0,0 @@
-module Hadolint.Bash where
-
-import Control.Monad.Writer (Writer, execWriter, tell)
-import Data.Functor.Identity (runIdentity)
-import Data.List (nub)
-import Data.Maybe (mapMaybe)
-import qualified Data.Text as Text
-import qualified ShellCheck.AST
-import ShellCheck.AST (Id(..), Token(..))
-import qualified ShellCheck.ASTLib
-import ShellCheck.Checker
-import ShellCheck.Interface
-import qualified ShellCheck.Parser
-
-data ParsedBash = ParsedBash
-    { original :: Text.Text
-    , parsed :: ParseResult
-    }
-
-shellcheck :: ParsedBash -> [Comment]
-shellcheck (ParsedBash txt _) = map comment $ crComments $ runIdentity $ checkScript si spec
-  where
-    comment (PositionedComment _ _ c) = c
-    si = mockedSystemInterface [("", "")]
-    spec = CheckSpec filename script sourced exclusions (Just Bash)
-    script = "#!/bin/bash\n" ++ Text.unpack txt
-    filename = "" -- filename can be ommited because we only want the parse results back
-    sourced = False
-    exclusions = []
-
-parseShell :: Text.Text -> ParsedBash
-parseShell txt =
-    ParsedBash
-    { original = txt
-    , parsed =
-          runIdentity $
-          ShellCheck.Parser.parseScript
-              (mockedSystemInterface [("", "")])
-              ParseSpec
-              { psFilename = "" -- There is no filename
-              , psScript = "#!/bin/bash\n" ++ Text.unpack txt
-              , psCheckSourced = False
-              }
-    }
-
-extractTokensWith :: (Token -> Maybe Token) -> ParsedBash -> [Token]
-extractTokensWith extractor (ParsedBash _ ast) =
-    case prRoot ast of
-        Nothing -> []
-        Just script -> nub . execWriter $ ShellCheck.AST.doAnalysis extract script
-  where
-    extract :: Token -> Writer [Token] ()
-    extract token =
-      case extractor token of
-        Nothing -> return ()
-        Just t -> tell [t]
-
-findPipes :: ParsedBash -> [Token]
-findPipes = extractTokensWith pipesExtractor
-  where
-    pipesExtractor pipe@T_Pipe{} = Just pipe
-    pipesExtractor _ = Nothing
-
-hasPipes :: ParsedBash -> Bool
-hasPipes = not . null . findPipes
-
-findCommands :: ParsedBash -> [Token]
-findCommands = extractTokensWith commandsExtractor
-  where
-    commandsExtractor = ShellCheck.ASTLib.getCommand
-
-allCommands :: (Token -> Bool) -> ParsedBash -> Bool
-allCommands check script = all check (findCommands script)
-
-noCommands :: (Token -> Bool) -> ParsedBash -> Bool
-noCommands check = allCommands (not . check)
-
-getCommandName :: Token -> Maybe String
-getCommandName = ShellCheck.ASTLib.getCommandName
-
-findCommandNames :: ParsedBash -> [String]
-findCommandNames = mapMaybe getCommandName . findCommands
-
-cmdHasArgs :: String -> [String] -> Token -> Bool
-cmdHasArgs command arguments token@T_SimpleCommand {}
-    | ShellCheck.ASTLib.getCommandName token /= Just command = False
-    | otherwise = not $ null [arg | arg <- getAllArgs token, arg `elem` arguments]
-cmdHasArgs _ _ _ = False
-
-getAllArgs :: Token -> [String]
-getAllArgs (T_SimpleCommand _ _ (_:allArgs)) = concatMap ShellCheck.ASTLib.oversimplify allArgs
-getAllArgs _ = []
-
-getArgsNoFlags :: Token -> [String]
-getArgsNoFlags cmd@(T_SimpleCommand _ _ (_:allArgs)) = concatMap ShellCheck.ASTLib.oversimplify args
-  where
-    flags = [t | (t, _) <- getAllFlags cmd]
-    args = [a | a <- allArgs, a `notElem` flags]
-getArgsNoFlags _ = []
-
-getAllFlags :: Token -> [(Token, String)]
-getAllFlags cmd@T_SimpleCommand {} = [(t, f) | (t, f) <- ShellCheck.ASTLib.getAllFlags cmd, f /= ""]
-getAllFlags _ = []
-
-hasFlag :: String -> Token -> Bool
-hasFlag flag = any (\(_, f) -> f == flag) . getAllFlags
-
-dropFlagArg :: [String] -> Token -> Token
-dropFlagArg flags cmd@(T_SimpleCommand cid b allArgs) = T_SimpleCommand cid b filterdArgs
-  where
-    filterdArgs = [arg | arg <- allArgs, isNotNextToken arg]
-    isNotNextToken arg = ShellCheck.AST.getId arg `notElem` findTokensToDrop
-    findTokensToDrop = [next (ShellCheck.AST.getId t) | (t, f) <- getAllFlags cmd, f `elem` flags]
-    next (Id i) = Id (i + 2)
-dropFlagArg _ token = token
diff --git a/src/Hadolint/Rules.hs b/src/Hadolint/Rules.hs
--- a/src/Hadolint/Rules.hs
+++ b/src/Hadolint/Rules.hs
@@ -4,13 +4,13 @@
 module Hadolint.Rules where
 
 import Control.Arrow ((&&&))
-import Data.List (dropWhile, isInfixOf, isPrefixOf, mapAccumL)
+import Data.List (dropWhile, isInfixOf, isPrefixOf, mapAccumL, nub)
 import Data.List.NonEmpty (toList)
 import Data.Maybe (catMaybes)
-import qualified Hadolint.Bash as Bash
+import qualified Hadolint.Shell as Shell
 import Language.Docker.Syntax
 
-import Data.Semigroup ((<>))
+import Data.Semigroup (Semigroup, (<>))
 import qualified Data.Set as Set
 import qualified Data.Text as Text
 import Data.Void (Void)
@@ -37,32 +37,57 @@
     , success :: Bool
     } deriving (Eq)
 
+-- | Contains the required parameters for optional rules
+newtype RulesConfig = RulesConfig
+    { allowedRegistries :: Set.Set Registry -- ^ The docker registries that are allowed in FROM
+    } deriving (Show, Eq)
+
 instance Ord RuleCheck where
     a `compare` b = linenumber a `compare` linenumber b
 
+instance Semigroup RulesConfig where
+    RulesConfig a <> RulesConfig b = RulesConfig (a <> b)
+
+instance Monoid RulesConfig where
+    mempty = RulesConfig mempty
+    mappend = (<>)
+
 type IgnoreRuleParser = Megaparsec.Parsec Void Text.Text
 
-type ParsedFile = [InstructionPos Bash.ParsedBash]
+type ParsedFile = [InstructionPos Shell.ParsedShell]
 
+-- | A function to check individual dockerfile instructions.
+-- It gets the current state and a line number.
+-- It should return the new state and whether or not the check passes for the given instruction.
+type SimpleCheckerWithState state
+     = state -> Linenumber -> Instruction Shell.ParsedShell -> (state, Bool)
+
+-- | A function to check individual dockerfile instructions.
+-- It gets the current line number.
+-- It should return True if the check passes for the given instruction.
+type SimpleCheckerWithLine = (Linenumber -> Instruction Shell.ParsedShell -> Bool)
+
+-- | A function to check individual dockerfile instructions.
+-- It should return the new state and a list of Metadata records.
+-- Each Metadata record signifies a failing check for the given instruction.
+type CheckerWithState state
+     = state -> Linenumber -> Instruction Shell.ParsedShell -> (state, [Metadata])
+
 link :: Metadata -> Text.Text
 link (Metadata code _ _)
     | "SC" `Text.isPrefixOf` code = "https://github.com/koalaman/shellcheck/wiki/" <> code
     | "DL" `Text.isPrefixOf` code = "https://github.com/hadolint/hadolint/wiki/" <> code
     | otherwise = "https://github.com/hadolint/hadolint"
 
--- a Rule takes a Dockerfile with parsed bash and returns the executed checks
+-- a Rule takes a Dockerfile with parsed shell and returns the executed checks
 type Rule = ParsedFile -> [RuleCheck]
 
 -- Apply a function on each instruction and create a check
 -- for the according line number
-mapInstructions ::
-       Metadata
-    -> (state -> Linenumber -> Instruction Bash.ParsedBash -> (state, Bool))
-    -> state
-    -> Rule
-mapInstructions metadata f initialState dockerfile =
+mapInstructions :: CheckerWithState state -> state -> Rule
+mapInstructions f initialState dockerfile =
     let (_, results) = mapAccumL applyRule initialState dockerfile
-    in results
+    in concat results
   where
     applyRule state (InstructionPos (OnBuild i) source linenumber) =
         applyWithState state source linenumber i -- All rules applying to instructions also apply to ONBUILD,
@@ -72,37 +97,34 @@
         applyWithState state source linenumber i -- Otherwise, normal instructions are not unwrapped
     applyWithState state source linenumber instruction =
         let (newState, res) = f state linenumber instruction
-        in (newState, RuleCheck metadata source linenumber res)
+        in (newState, [RuleCheck m source linenumber False | m <- res])
 
 instructionRule ::
-       Text.Text -> Severity -> Text.Text -> (Instruction Bash.ParsedBash -> Bool) -> Rule
+       Text.Text -> Severity -> Text.Text -> (Instruction Shell.ParsedShell -> Bool) -> Rule
 instructionRule code severity message check =
     instructionRuleLine code severity message (const check)
 
-instructionRuleLine ::
-       Text.Text
-    -> Severity
-    -> Text.Text
-    -> (Linenumber -> Instruction Bash.ParsedBash -> Bool)
-    -> Rule
+instructionRuleLine :: Text.Text -> Severity -> Text.Text -> SimpleCheckerWithLine -> Rule
 instructionRuleLine code severity message check =
     instructionRuleState code severity message checkAndDropState ()
   where
     checkAndDropState state line instr = (state, check line instr)
 
 instructionRuleState ::
-       Text.Text
-    -> Severity
-    -> Text.Text
-    -> (state -> Linenumber -> Instruction Bash.ParsedBash -> (state, Bool))
-    -> state
-    -> Rule
-instructionRuleState code severity message = mapInstructions (Metadata code severity message)
+       Text.Text -> Severity -> Text.Text -> SimpleCheckerWithState state -> state -> Rule
+instructionRuleState code severity message f = mapInstructions constMetadataCheck
+  where
+    meta = Metadata code severity message
+    constMetadataCheck st ln instr =
+        let (newSt, success) = f st ln instr
+        in if not success
+               then (newSt, [meta])
+               else (newSt, [])
 
 withState :: a -> b -> (a, b)
 withState st res = (st, res)
 
-argumentsRule :: (Bash.ParsedBash -> a) -> Arguments Bash.ParsedBash -> a
+argumentsRule :: (Shell.ParsedShell -> a) -> Arguments Shell.ParsedShell -> a
 argumentsRule applyRule args =
     case args of
         ArgumentsText as -> applyRule as
@@ -110,13 +132,17 @@
 
 -- Enforce rules on a dockerfile and return failed checks
 analyze :: [Rule] -> Dockerfile -> [RuleCheck]
-analyze list dockerfile = filter failed $ concat [r parsedFile | r <- list]
+analyze list dockerfile =
+    [ result -- Keep the result
+    | rule <- list -- for each rule in the list
+    , result <- rule parsedFile -- after applying the rule to the file
+    , notIgnored result -- and only keep failures that were not ignored
+    ]
   where
-    failed RuleCheck {metadata = Metadata {code}, linenumber, success} =
-        not success && not (wasIgnored code linenumber)
+    notIgnored RuleCheck {metadata = Metadata {code}, linenumber} = not (wasIgnored code linenumber)
     wasIgnored c ln = not $ null [line | (line, codes) <- allIgnores, line == ln, c `elem` codes]
     allIgnores = ignored dockerfile
-    parsedFile = map (fmap Bash.parseShell) dockerfile
+    parsedFile = map (fmap Shell.parseShell) dockerfile
 
 ignored :: Dockerfile -> [(Linenumber, [Text.Text])]
 ignored dockerfile =
@@ -145,7 +171,7 @@
 rules :: [Rule]
 rules =
     [ absoluteWorkdir
-    , shellcheckBash
+    , shellcheck
     , invalidCmd
     , copyInsteadAdd
     , copyEndingSlash
@@ -178,22 +204,8 @@
     , usePipefail
     ]
 
-commentMetadata :: ShellCheck.Interface.Comment -> Metadata
-commentMetadata (ShellCheck.Interface.Comment severity code message) =
-    Metadata (Text.pack ("SC" ++ show code)) severity (Text.pack message)
-
-shellcheckBash :: ParsedFile -> [RuleCheck]
-shellcheckBash = concatMap check
-  where
-    check (InstructionPos (Run args) source linenumber) =
-        argumentsRule (applyRule source linenumber) args
-    check _ = []
-    applyRule source linenumber args =
-        rmDup [RuleCheck m source linenumber False | m <- convert args]
-    convert args = [commentMetadata c | c <- Bash.shellcheck args]
-    rmDup :: [RuleCheck] -> [RuleCheck]
-    rmDup [] = []
-    rmDup (x:xs) = x : rmDup (filter (\y -> metadata x /= metadata y) xs)
+optionalRules :: RulesConfig -> [Rule]
+optionalRules RulesConfig {allowedRegistries} = [registryIsAllowed allowedRegistries]
 
 allFromImages :: ParsedFile -> [(Linenumber, BaseImage)]
 allFromImages dockerfile = [(l, f) | (l, From f) <- instr]
@@ -236,6 +248,27 @@
 fromAlias (TaggedImage _ _ alias) = alias
 fromAlias (DigestedImage _ _ alias) = alias
 
+-------------
+--  RULES  --
+-------------
+shellcheck :: Rule
+shellcheck = mapInstructions check Shell.defaultShellOpts
+  where
+    check :: CheckerWithState Shell.ShellOpts
+    check _ _ (From _) = (Shell.defaultShellOpts, []) -- Reset the state
+    check st _ (Arg name _) = (Shell.addVars [name] st, [])
+    check st _ (Env pairs) = (Shell.addVars (map fst pairs) st, [])
+    check st _ (Shell (ArgumentsList script)) = (Shell.setShell (Shell.original script) st, [])
+    check st _ (Shell (ArgumentsText script)) = (Shell.setShell (Shell.original script) st, [])
+    check st _ (Run (ArgumentsList script)) = (st, doCheck st script)
+    check st _ (Run (ArgumentsText script)) = (st, doCheck st script)
+    check st _ _ = (st, [])
+    doCheck opts script = nub [commentMetadata c | c <- Shell.shellcheck opts script]
+    -- | Converts ShellCheck errors into our own errors type
+    commentMetadata :: ShellCheck.Interface.Comment -> Metadata
+    commentMetadata (ShellCheck.Interface.Comment severity code message) =
+        Metadata (Text.pack ("SC" ++ show code)) severity (Text.pack message)
+
 absoluteWorkdir :: Rule
 absoluteWorkdir = instructionRule code severity message check
   where
@@ -258,8 +291,8 @@
     check _ = True
 
 -- Check if a command contains a program call in the Run instruction
-usingProgram :: String -> Bash.ParsedBash -> Bool
-usingProgram prog args = not $ null [cmd | cmd <- Bash.findCommandNames args, cmd == prog]
+usingProgram :: String -> Shell.ParsedShell -> Bool
+usingProgram prog args = not $ null [cmd | cmd <- Shell.findCommandNames args, cmd == prog]
 
 multipleCmds :: Rule
 multipleCmds = instructionRuleState code severity message check False
@@ -299,7 +332,7 @@
             newState = Set.union state newArgs
         in withState newState (Set.size newState < 2)
     extractCommands args =
-        Set.fromList [w | w <- Bash.findCommandNames args, w == "curl" || w == "wget"]
+        Set.fromList [w | w <- Shell.findCommandNames args, w == "curl" || w == "wget"]
 
 invalidCmd :: Rule
 invalidCmd = instructionRule code severity message check
@@ -311,7 +344,7 @@
         \`vim`, `shutdown`, `service`, `ps`, `free`, `top`, `kill`, `mount`, `ifconfig`"
     check (Run args) = argumentsRule detectInvalid args
     check _ = True
-    detectInvalid args = null [arg | arg <- Bash.findCommandNames args, arg `elem` invalidCmds]
+    detectInvalid args = null [arg | arg <- Shell.findCommandNames args, arg `elem` invalidCmds]
     invalidCmds = ["ssh", "vim", "shutdown", "service", "ps", "free", "top", "kill", "mount"]
 
 noRootUser :: Rule
@@ -371,7 +404,8 @@
     code = "DL3005"
     severity = ErrorC
     message = "Do not use apt-get upgrade or dist-upgrade"
-    check (Run args) = argumentsRule (Bash.noCommands (Bash.cmdHasArgs "apt-get" ["upgrade"])) args
+    check (Run args) =
+        argumentsRule (Shell.noCommands (Shell.cmdHasArgs "apt-get" ["upgrade"])) args
     check _ = True
 
 noUntagged :: Rule
@@ -408,16 +442,16 @@
     check _ = True
     versionFixed package = "=" `isInfixOf` package
 
-aptGetPackages :: Bash.ParsedBash -> [String]
+aptGetPackages :: Shell.ParsedShell -> [String]
 aptGetPackages args =
     [ arg
-    | cmd <- dropTarget <$> Bash.findCommands args
-    , arg <- Bash.getArgsNoFlags cmd
-    , Bash.cmdHasArgs "apt-get" ["install"] cmd
+    | cmd <- dropTarget <$> Shell.findCommands args
+    , arg <- Shell.getArgsNoFlags cmd
+    , Shell.cmdHasArgs "apt-get" ["install"] cmd
     , arg /= "install"
     ]
   where
-    dropTarget = Bash.dropFlagArg ["t", "target-release"]
+    dropTarget = Shell.dropFlagArg ["t", "target-release"]
 
 aptGetCleanup :: Rule
 aptGetCleanup dockerfile = instructionRuleState code severity message check Nothing dockerfile
@@ -440,8 +474,8 @@
         | not (hasUpdate args) || not (imageIsUsed line baseimage) = True
         | otherwise = hasCleanup args
     hasCleanup args =
-        any (Bash.cmdHasArgs "rm" ["-rf", "/var/lib/apt/lists/*"]) (Bash.findCommands args)
-    hasUpdate args = any (Bash.cmdHasArgs "apt-get" ["update"]) (Bash.findCommands args)
+        any (Shell.cmdHasArgs "rm" ["-rf", "/var/lib/apt/lists/*"]) (Shell.findCommands args)
+    hasUpdate args = any (Shell.cmdHasArgs "apt-get" ["update"]) (Shell.findCommands args)
     imageIsUsed line baseimage = isLastImage line baseimage || imageIsUsedLater line baseimage
     isLastImage line baseimage =
         case reverse (allFromImages dockerfile) of
@@ -459,7 +493,7 @@
     code = "DL3017"
     severity = ErrorC
     message = "Do not use apk upgrade"
-    check (Run args) = argumentsRule (Bash.noCommands (Bash.cmdHasArgs "apk" ["upgrade"])) args
+    check (Run args) = argumentsRule (Shell.noCommands (Shell.cmdHasArgs "apk" ["upgrade"])) args
     check _ = True
 
 apkAddVersionPinned :: Rule
@@ -473,16 +507,16 @@
     check _ = True
     versionFixed package = "=" `isInfixOf` package
 
-apkAddPackages :: Bash.ParsedBash -> [String]
+apkAddPackages :: Shell.ParsedShell -> [String]
 apkAddPackages args =
     [ arg
-    | cmd <- dropTarget <$> Bash.findCommands args
-    , arg <- Bash.getArgsNoFlags cmd
-    , Bash.cmdHasArgs "apk" ["add"] cmd
+    | cmd <- dropTarget <$> Shell.findCommands args
+    , arg <- Shell.getArgsNoFlags cmd
+    , Shell.cmdHasArgs "apk" ["add"] cmd
     , arg /= "add"
     ]
   where
-    dropTarget = Bash.dropFlagArg ["t", "virtual"]
+    dropTarget = Shell.dropFlagArg ["t", "virtual"]
 
 apkAddNoCache :: Rule
 apkAddNoCache = instructionRule code severity message check
@@ -492,9 +526,9 @@
     message =
         "Use the `--no-cache` switch to avoid the need to use `--update` and remove \
         \`/var/cache/apk/*` when done installing packages"
-    check (Run args) = argumentsRule (Bash.noCommands forgotCacheOption) args
+    check (Run args) = argumentsRule (Shell.noCommands forgotCacheOption) args
     check _ = True
-    forgotCacheOption cmd = Bash.cmdHasArgs "apk" ["add"] cmd && not (Bash.hasFlag "no-cache" cmd)
+    forgotCacheOption cmd = Shell.cmdHasArgs "apk" ["add"] cmd && not (Shell.hasFlag "no-cache" cmd)
 
 useAdd :: Rule
 useAdd = instructionRule code severity message check
@@ -530,20 +564,20 @@
     message =
         "Pin versions in pip. Instead of `pip install <package>` use `pip install \
         \<package>==<version>`"
-    check (Run args) = argumentsRule (Bash.noCommands forgotToPinVersion) args
+    check (Run args) = argumentsRule (Shell.noCommands forgotToPinVersion) args
     check _ = True
     forgotToPinVersion cmd = isPipInstall cmd && not (all versionFixed (packages cmd))
     -- Check if the command is a pip* install command, and that specific pacakges are being listed
     isPipInstall cmd =
-        case Bash.getCommandName cmd of
+        case Shell.getCommandName cmd of
             Just ('p':'i':'p':_) -> relevantInstall cmd
             _ -> False
     -- If the user is installing requirements from a file or just the local module, then we are not interested
     -- in running this rule
     relevantInstall cmd =
-        ["install"] `isInfixOf` Bash.getAllArgs cmd &&
-        not (["-r"] `isInfixOf` Bash.getAllArgs cmd || ["."] `isInfixOf` Bash.getAllArgs cmd)
-    packages cmd = stripInstallPrefix (Bash.getArgsNoFlags cmd)
+        ["install"] `isInfixOf` Shell.getAllArgs cmd &&
+        not (["-r"] `isInfixOf` Shell.getAllArgs cmd || ["."] `isInfixOf` Shell.getAllArgs cmd)
+    packages cmd = stripInstallPrefix (Shell.getArgsNoFlags cmd)
     versionFixed package = hasVersionSymbol package || isVersionedGit package
     isVersionedGit package = "git+http" `isInfixOf` package && "@" `isInfixOf` package
     versionSymbols = ["==", ">=", "<=", ">", "<", "!=", "~=", "==="]
@@ -570,11 +604,11 @@
     message =
         "Pin versions in npm. Instead of `npm install <package>` use `npm install \
         \<package>@<version>`"
-    check (Run args) = argumentsRule (Bash.noCommands forgotToPinVersion) args
+    check (Run args) = argumentsRule (Shell.noCommands forgotToPinVersion) args
     check _ = True
     forgotToPinVersion cmd = isNpmInstall cmd && not (all versionFixed (packages cmd))
-    isNpmInstall = Bash.cmdHasArgs "npm" ["install"]
-    packages cmd = stripInstallPrefix (Bash.getArgsNoFlags cmd)
+    isNpmInstall = Shell.cmdHasArgs "npm" ["install"]
+    packages cmd = stripInstallPrefix (Shell.getArgsNoFlags cmd)
     versionFixed package =
         if hasGitPrefix package
             then isVersionedGit package
@@ -595,14 +629,14 @@
     code = "DL3014"
     severity = WarningC
     message = "Use the `-y` switch to avoid manual input `apt-get -y install <package>`"
-    check (Run args) = argumentsRule (Bash.noCommands forgotAptYesOption) args
+    check (Run args) = argumentsRule (Shell.noCommands forgotAptYesOption) args
     check _ = True
     forgotAptYesOption cmd = isAptGetInstall cmd && not (hasYesOption cmd)
-    isAptGetInstall = Bash.cmdHasArgs "apt-get" ["install"]
+    isAptGetInstall = Shell.cmdHasArgs "apt-get" ["install"]
     hasYesOption cmd =
         "y" `elem` allFlags cmd ||
         "yes" `elem` allFlags cmd || length (filter (== "q") (allFlags cmd)) > 1
-    allFlags cmd = snd <$> Bash.getAllFlags cmd
+    allFlags cmd = snd <$> Shell.getAllFlags cmd
 
 aptGetNoRecommends :: Rule
 aptGetNoRecommends = instructionRule code severity message check
@@ -610,11 +644,11 @@
     code = "DL3015"
     severity = InfoC
     message = "Avoid additional packages by specifying `--no-install-recommends`"
-    check (Run args) = argumentsRule (Bash.noCommands forgotNoInstallRecommends) args
+    check (Run args) = argumentsRule (Shell.noCommands forgotNoInstallRecommends) args
     check _ = True
     forgotNoInstallRecommends cmd = isAptGetInstall cmd && not (hasRecommendsOption cmd)
-    isAptGetInstall = Bash.cmdHasArgs "apt-get" ["install"]
-    hasRecommendsOption = Bash.hasFlag "no-install-recommends"
+    isAptGetInstall = Shell.cmdHasArgs "apt-get" ["install"]
+    hasRecommendsOption = Shell.hasFlag "no-install-recommends"
 
 isArchive :: Text.Text -> Bool
 isArchive path =
@@ -702,7 +736,7 @@
     code = "DL4005"
     severity = WarningC
     message = "Use SHELL to change the default shell"
-    check (Run args) = argumentsRule (Bash.noCommands (Bash.cmdHasArgs "ln" ["/bin/sh"])) args
+    check (Run args) = argumentsRule (Shell.noCommands (Shell.cmdHasArgs "ln" ["/bin/sh"])) args
     check _ = True
 
 useJsonArgs :: Rule
@@ -725,15 +759,29 @@
     check _ _ (Shell args) = (argumentsRule hasPipefailOption args, True)
     check False _ (Run args) = (False, argumentsRule notHasPipes args)
     check st _ _ = (st, True)
-    notHasPipes script = not (Bash.hasPipes script)
+    notHasPipes script = not (Shell.hasPipes script)
     hasPipefailOption script =
         not $
         null
             [ True
-            | cmd <- Bash.findCommands script
+            | cmd <- Shell.findCommands script
             , validShell <- ["/bin/bash", "/bin/zsh", "/bin/ash", "bash", "zsh", "ash"]
-            , Bash.getCommandName cmd == Just validShell
-            , Bash.hasFlag "o" cmd
-            , arg <- Bash.getAllArgs cmd
+            , Shell.getCommandName cmd == Just validShell
+            , Shell.hasFlag "o" cmd
+            , arg <- Shell.getAllArgs cmd
             , arg == "pipefail"
             ]
+
+registryIsAllowed :: Set.Set Registry -> Rule
+registryIsAllowed allowed = instructionRule code severity message check
+  where
+    code = "DL3026"
+    severity = ErrorC
+    message = "Use only an allowed registry in the FROM image"
+    check (From (UntaggedImage img _)) = Set.null allowed || isAllowed img
+    check (From (TaggedImage img _ _)) = Set.null allowed || isAllowed img
+    check _ = True
+    isAllowed Image {registryName = Just registry} = Set.member registry allowed
+    isAllowed Image {registryName = Nothing, imageName} =
+        imageName == "scratch" ||
+        Set.member "docker.io" allowed || Set.member "hub.docker.com" allowed
diff --git a/src/Hadolint/Shell.hs b/src/Hadolint/Shell.hs
new file mode 100644
--- /dev/null
+++ b/src/Hadolint/Shell.hs
@@ -0,0 +1,155 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Hadolint.Shell where
+
+import Control.Monad.Writer (Writer, execWriter, tell)
+import Data.Functor.Identity (runIdentity)
+import Data.List (nub)
+import Data.Maybe (listToMaybe, mapMaybe)
+import Data.Semigroup ((<>))
+import qualified Data.Set as Set
+import qualified Data.Text as Text
+import qualified ShellCheck.AST
+import ShellCheck.AST (Id(..), Token(..))
+import qualified ShellCheck.ASTLib
+import ShellCheck.Checker
+import ShellCheck.Interface
+import qualified ShellCheck.Parser
+
+data ParsedShell = ParsedShell
+    { original :: Text.Text
+    , parsed :: ParseResult
+    }
+
+data ShellOpts = ShellOpts
+    { shellName :: Text.Text
+    , envVars :: Set.Set Text.Text
+    }
+
+defaultShellOpts :: ShellOpts
+defaultShellOpts = ShellOpts "/bin/sh -c" defaultVars
+  where
+    defaultVars =
+        Set.fromList
+            [ "HTTP_PROXY"
+            , "http_proxy"
+            , "HTTPS_PROXY"
+            , "https_proxy"
+            , "FTP_PROXY"
+            , "ftp_proxy"
+            , "NO_PROXY"
+            , "no_proxy"
+            ]
+
+addVars :: [Text.Text] -> ShellOpts -> ShellOpts
+addVars vars (ShellOpts n v) = ShellOpts n (v <> Set.fromList vars)
+
+setShell :: Text.Text -> ShellOpts -> ShellOpts
+setShell s (ShellOpts _ v) = ShellOpts s v
+
+shellcheck :: ShellOpts -> ParsedShell -> [Comment]
+shellcheck (ShellOpts sh env) (ParsedShell txt _) = map comment runShellCheck
+  where
+    runShellCheck = crComments $ runIdentity $ checkScript si spec
+    comment (PositionedComment _ _ c) = c
+    si = mockedSystemInterface [("", "")]
+    spec = CheckSpec filename script sourced exclusions Nothing
+    script = "#!" ++ extractShell sh ++ "\n" ++ printVars ++ Text.unpack txt
+    filename = "" -- filename can be ommited because we only want the parse results back
+    sourced = False
+    exclusions =
+        [ 2187 -- exclude the warning about the ash shell not being supported
+        ]
+    -- | Shellcheck complains when the shebang has more than one argument, so we only take the first
+    extractShell s =
+        case listToMaybe . Text.words $ s of
+            Nothing -> ""
+            Just shell -> Text.unpack shell
+    -- | Inject all the collected env vars as exported variables so they can be used
+    printVars = Text.unpack . Text.unlines . Set.toList $ Set.map (\v -> "export " <> v <> "=1") env
+
+parseShell :: Text.Text -> ParsedShell
+parseShell txt =
+    ParsedShell
+    { original = txt
+    , parsed =
+          runIdentity $
+          ShellCheck.Parser.parseScript
+              (mockedSystemInterface [("", "")])
+              ParseSpec
+              { psFilename = "" -- There is no filename
+              , psScript = "#!/bin/bash\n" ++ Text.unpack txt
+              , psCheckSourced = False
+              }
+    }
+
+extractTokensWith :: (Token -> Maybe Token) -> ParsedShell -> [Token]
+extractTokensWith extractor (ParsedShell _ ast) =
+    case prRoot ast of
+        Nothing -> []
+        Just script -> nub . execWriter $ ShellCheck.AST.doAnalysis extract script
+  where
+    extract :: Token -> Writer [Token] ()
+    extract token =
+        case extractor token of
+            Nothing -> return ()
+            Just t -> tell [t]
+
+findPipes :: ParsedShell -> [Token]
+findPipes = extractTokensWith pipesExtractor
+  where
+    pipesExtractor pipe@T_Pipe {} = Just pipe
+    pipesExtractor _ = Nothing
+
+hasPipes :: ParsedShell -> Bool
+hasPipes = not . null . findPipes
+
+findCommands :: ParsedShell -> [Token]
+findCommands = extractTokensWith commandsExtractor
+  where
+    commandsExtractor = ShellCheck.ASTLib.getCommand
+
+allCommands :: (Token -> Bool) -> ParsedShell -> Bool
+allCommands check script = all check (findCommands script)
+
+noCommands :: (Token -> Bool) -> ParsedShell -> Bool
+noCommands check = allCommands (not . check)
+
+getCommandName :: Token -> Maybe String
+getCommandName = ShellCheck.ASTLib.getCommandName
+
+findCommandNames :: ParsedShell -> [String]
+findCommandNames = mapMaybe getCommandName . findCommands
+
+cmdHasArgs :: String -> [String] -> Token -> Bool
+cmdHasArgs command arguments token@T_SimpleCommand {}
+    | ShellCheck.ASTLib.getCommandName token /= Just command = False
+    | otherwise = not $ null [arg | arg <- getAllArgs token, arg `elem` arguments]
+cmdHasArgs _ _ _ = False
+
+getAllArgs :: Token -> [String]
+getAllArgs (T_SimpleCommand _ _ (_:allArgs)) = concatMap ShellCheck.ASTLib.oversimplify allArgs
+getAllArgs _ = []
+
+getArgsNoFlags :: Token -> [String]
+getArgsNoFlags cmd@(T_SimpleCommand _ _ (_:allArgs)) = concatMap ShellCheck.ASTLib.oversimplify args
+  where
+    flags = [t | (t, _) <- getAllFlags cmd]
+    args = [a | a <- allArgs, a `notElem` flags]
+getArgsNoFlags _ = []
+
+getAllFlags :: Token -> [(Token, String)]
+getAllFlags cmd@T_SimpleCommand {} = [(t, f) | (t, f) <- ShellCheck.ASTLib.getAllFlags cmd, f /= ""]
+getAllFlags _ = []
+
+hasFlag :: String -> Token -> Bool
+hasFlag flag = any (\(_, f) -> f == flag) . getAllFlags
+
+dropFlagArg :: [String] -> Token -> Token
+dropFlagArg flags cmd@(T_SimpleCommand cid b allArgs) = T_SimpleCommand cid b filterdArgs
+  where
+    filterdArgs = [arg | arg <- allArgs, isNotNextToken arg]
+    isNotNextToken arg = ShellCheck.AST.getId arg `notElem` findTokensToDrop
+    findTokensToDrop = [next (ShellCheck.AST.getId t) | (t, f) <- getAllFlags cmd, f `elem` flags]
+    next (Id i) = Id (i + 2)
+dropFlagArg _ token = token
diff --git a/test/Spec.hs b/test/Spec.hs
--- a/test/Spec.hs
+++ b/test/Spec.hs
@@ -1,4 +1,5 @@
 {-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE OverloadedLists #-}
 import Test.HUnit hiding (Label)
 import Test.Hspec
 
@@ -474,6 +475,101 @@
                 ruleCatchesNot useShell "RUN ln -s foo bar && unrelated && something_with /bin/sh"
                 onBuildRuleCatchesNot useShell "RUN ln -s foo bar && unrelated && something_with /bin/sh"
         --
+        --
+        describe "Shellcheck" $ do
+            it "runs shellchek on RUN instructions" $ do
+                ruleCatches shellcheck "RUN echo $MISSING_QUOTES"
+                onBuildRuleCatches shellcheck "RUN echo $MISSING_QUOTES"
+            it "not warns on valid scripts" $ do
+                ruleCatchesNot shellcheck "RUN echo foo"
+                onBuildRuleCatchesNot shellcheck "RUN echo foo"
+
+            it "Does not complain on default env vars" $
+                let dockerFile = Text.unlines
+                        [ "RUN echo \"$HTTP_PROXY\""
+                        , "RUN echo \"$http_proxy\""
+                        , "RUN echo \"$HTTPS_PROXY\""
+                        , "RUN echo \"$https_proxy\""
+                        , "RUN echo \"$FTP_PROXY\""
+                        , "RUN echo \"$ftp_proxy\""
+                        , "RUN echo \"$NO_PROXY\""
+                        , "RUN echo \"$no_proxy\""
+                        ]
+                in do
+                  ruleCatchesNot shellcheck dockerFile
+                  onBuildRuleCatchesNot shellcheck dockerFile
+
+            it "Complain on missing env vars" $
+                let dockerFile = Text.unlines
+                        [ "RUN echo \"$RTTP_PROXY\""
+                        ]
+                in do
+                  ruleCatches shellcheck dockerFile
+                  onBuildRuleCatches shellcheck dockerFile
+
+            it "Is aware of ARGS and ENV" $
+                let dockerFile = Text.unlines
+                        [ "ARG foo=bar"
+                        , "ARG another_foo"
+                        , "ENV bar=10 baz=20"
+                        , "RUN echo \"$foo\""
+                        , "RUN echo \"$another_foo\""
+                        , "RUN echo \"$bar\""
+                        , "RUN echo \"$baz\""
+                        ]
+                in do
+                  ruleCatchesNot shellcheck dockerFile
+                  onBuildRuleCatchesNot shellcheck dockerFile
+
+            it "Resets env vars after a FROM" $
+                let dockerFile = Text.unlines
+                        [ "ARG foo=bar"
+                        , "ARG another_foo"
+                        , "ENV bar=10 baz=20"
+                        , "FROM debian"
+                        , "RUN echo \"$foo\""
+                        ]
+                in do
+                  ruleCatches shellcheck dockerFile
+                  onBuildRuleCatches shellcheck dockerFile
+
+            it "Defaults the shell to sh" $
+                let dockerFile = Text.unlines
+                        [ "RUN echo $RANDOM" -- $RANDOM is not available in sh
+                        ]
+                in do
+                  ruleCatches shellcheck dockerFile
+                  onBuildRuleCatches shellcheck dockerFile
+
+            it "Can change the shell check to bash" $
+                let dockerFile = Text.unlines
+                        [ "SHELL [\"/bin/bash\", \"-eo\", \"pipefail\", \"-c\"]"
+                        , "RUN echo $RANDOM" -- $RANDOM is available in bash
+                        ]
+                in do
+                  ruleCatchesNot shellcheck dockerFile
+                  onBuildRuleCatchesNot shellcheck dockerFile
+
+            it "Resets the SHELL to sh after a FROM" $
+                let dockerFile = Text.unlines
+                        [ "SHELL [\"/bin/bash\", \"-eo\", \"pipefail\", \"-c\"]"
+                        , "FROM debian"
+                        , "RUN echo $RANDOM"
+                        ]
+                in do
+                  ruleCatches shellcheck dockerFile
+                  onBuildRuleCatches shellcheck dockerFile
+
+            it "Does not complain on ash shell" $
+                let dockerFile = Text.unlines
+                        [ "SHELL [\"/bin/ash\", \"-o\", \"pipefail\", \"-c\"]"
+                        , "RUN echo hello"
+                        ]
+                in do
+                  ruleCatchesNot shellcheck dockerFile
+                  onBuildRuleCatchesNot shellcheck dockerFile
+        --
+        --
         describe "COPY rules" $ do
             it "use add" $ ruleCatches useAdd "COPY packaged-app.tar /usr/src/app"
             it "use not add" $ ruleCatchesNot useAdd "COPY package.json /usr/src/app"
@@ -824,6 +920,35 @@
                         , "RUN wget -O - https://some.site | wc -l file > /number"
                         ]
                 in ruleCatches usePipefail $ Text.unlines dockerFile
+        --
+        describe "Allowed docker registries" $ do
+            it "warn on non-allowed registry" $
+                let dockerFile =
+                        [ "FROM random.com/debian"
+                        ]
+                in ruleCatches (registryIsAllowed ["docker.io"]) $ Text.unlines dockerFile
+            it "don't warn on empty allowed registries" $
+                let dockerFile =
+                        [ "FROM random.com/debian"
+                        ]
+                in ruleCatchesNot (registryIsAllowed []) $ Text.unlines dockerFile
+            it "don't warn on allowed registries" $
+                let dockerFile =
+                        [ "FROM random.com/debian"
+                        ]
+                in ruleCatchesNot (registryIsAllowed ["x.com", "random.com"]) $ Text.unlines dockerFile
+            it "doesn't warn on scratch image" $
+                let dockerFile =
+                        [ "FROM scratch"
+                        ]
+                in ruleCatchesNot (registryIsAllowed ["x.com", "random.com"]) $ Text.unlines dockerFile
+            it "allows boths all forms of docker.io" $
+                let dockerFile =
+                        [ "FROM ubuntu:18.04 AS builder1"
+                        , "FROM zemanlx/ubuntu:18.04 AS builder2"
+                        , "FROM docker.io/zemanlx/ubuntu:18.04 AS builder3"
+                        ]
+                in ruleCatchesNot (registryIsAllowed ["docker.io"]) $ Text.unlines dockerFile
 
 assertChecks :: HasCallStack => Rule -> Text.Text -> ([RuleCheck] -> IO a) -> IO a
 assertChecks rule s makeAssertions =
@@ -860,9 +985,11 @@
 ruleCatchesNot :: HasCallStack => Rule -> Text.Text -> Assertion
 ruleCatchesNot rule s = assertChecks rule s f
   where
-    f checks = assertEqual "Found check of rule" 0 $ length checks
+    f checks = assertEqual ("Error: " ++ errorMessages checks) 0 $ length checks
+    errorMessages checks = Text.unpack . Text.unlines $ map (message . metadata) checks
 
 onBuildRuleCatchesNot :: HasCallStack => Rule -> Text.Text -> Assertion
 onBuildRuleCatchesNot rule s = assertOnBuildChecks rule s f
   where
-    f checks = assertEqual "Found check of rule" 0 $ length checks
+    f checks = assertEqual ("Error: " ++ errorMessages checks) 0 $ length checks
+    errorMessages checks = Text.unpack . Text.unlines $ map (message . metadata) checks
