{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE TupleSections #-} -- | -- Module : Aura.Pkgbuild.Security -- Copyright : (c) Colin Woodbury, 2012 - 2020 -- License : GPL3 -- Maintainer: Colin Woodbury -- -- Analyse PKGBUILDs for potentially malicious bash code. module Aura.Pkgbuild.Security ( BannedTerm(..), BanCategory(..) , parsedPB, bannedTerms , reportExploit ) where import Aura.Languages import Aura.Types (Language, Pkgbuild(..)) import Aura.Utils (hush) import Data.Text.Prettyprint.Doc (Doc) import Data.Text.Prettyprint.Doc.Render.Terminal (AnsiStyle) import Language.Bash.Parse (parse) import Language.Bash.Syntax import Language.Bash.Word import RIO hiding (Word) import RIO.Lens (each, _Just) import qualified RIO.Map as M import qualified RIO.Text as T --- -- | A bash term which should never appear in a PKGBUILD. If one does, it's -- either a sign of maintainer negligence or malicious behaviour. data BannedTerm = BannedTerm Text BanCategory deriving (Eq, Ord, Show, Generic) banCatL :: Lens' BannedTerm BanCategory banCatL f (BannedTerm t bc) = BannedTerm t <$> f bc -- | The reason why the bash term is black-listed. data BanCategory = Downloading | ScriptRunning | Permissions | InlinedBash | StrangeBashism | CleverRedirect deriving (Eq, Ord, Show) blacklist :: Map Text BannedTerm blacklist = M.fromList $ downloading <> running <> permissions where downloading = map (\t -> (t, BannedTerm t Downloading)) ["curl", "wget", "rsync", "scp"] running = map (\t -> (t, BannedTerm t ScriptRunning)) ["sh", "bash", "eval", "zsh", "fish"] permissions = map (\t -> (t, BannedTerm t Permissions)) ["sudo", "ssh"] -- TODO wasteful conversion! -- | Attempt to parse a PKGBUILD. Should succeed for all reasonable PKGBUILDs. parsedPB :: Pkgbuild -> Maybe List parsedPB (Pkgbuild pb) = hush . parse "PKGBUILD" . T.unpack $ decodeUtf8Lenient pb -- | Discover any banned terms lurking in a parsed PKGBUILD, paired with -- the surrounding context lines. bannedTerms :: List -> [(ShellCommand, BannedTerm)] bannedTerms = simpleCommands >=> bannedCommand banned :: Word -> Maybe BannedTerm banned w = M.lookup (T.pack $ unquote w) blacklist -- | Extract all `SimpleCommand`s from a parsed bash AST. simpleCommands :: List -> [ShellCommand] simpleCommands (List ss) = ss >>= statements >>= p where p :: ShellCommand -> [ShellCommand] p sc@(SimpleCommand _ _) = [sc] p sc = lists sc >>= simpleCommands statements :: Statement -> [ShellCommand] statements (Statement ao _) = andor ao andor :: AndOr -> [ShellCommand] andor (Last pl) = pipeline pl andor (And pl ao) = pipeline pl <> andor ao andor (Or pl ao) = pipeline pl <> andor ao pipeline :: Pipeline -> [ShellCommand] pipeline (Pipeline _ _ _ cs) = map command cs command :: Command -> ShellCommand command (Command sc _) = sc lists :: ShellCommand -> [List] lists (SimpleCommand _ _) = [] lists (AssignBuiltin _ _) = [] lists (FunctionDef _ l) = [l] lists (Coproc _ c) = lists $ command c lists (Subshell l) = [l] lists (Group l) = [l] lists (Arith _) = [] lists (Cond _) = [] lists (For _ _ l) = [l] lists (ArithFor _ l) = [l] lists (Select _ _ l) = [l] lists (Case _ ccs) = map caseClause ccs lists (If l1 l2 ml) = l1 : l2 : maybeToList ml lists (Until l1 l2) = [l1, l2] lists (While l1 l2) = [l1, l2] caseClause :: CaseClause -> List caseClause (CaseClause _ l _) = l bannedCommand :: ShellCommand -> [(ShellCommand, BannedTerm)] bannedCommand s@(SimpleCommand [] (g:c:_)) | g == [Char 'g', Char 'i', Char 't'] && c == [Char 'c', Char 'l', Char 'o', Char 'n', Char 'e'] = [(s, BannedTerm "git" Downloading)] bannedCommand s@(SimpleCommand [] (c:_)) = maybeToList $ (s,) <$> banned c bannedCommand s@(SimpleCommand as _) = as ^.. each . rValueL . to r . each where r rv@(RValue w) = maybeToList ((s,) <$> (banned w & _Just . banCatL .~ CleverRedirect)) <> q rv r rv = q rv q :: RValue -> [(ShellCommand, BannedTerm)] q rv = map (s,) $ join (rWords rv) >>= p p :: Span -> [BannedTerm] p (CommandSubst str) = maybeToList (hush $ parse "CommandSubst" str) >>= simpleCommands >>= map snd . bannedCommand p (ArithSubst str) = [BannedTerm (T.pack str) StrangeBashism] p (ProcessSubst _ str) = [BannedTerm (T.pack str) StrangeBashism] p sp = join (sWords sp) >>= p rWords :: RValue -> [Word] rWords (RValue w) = [w] rWords (RArray ws) = ws >>= \(mw, w) -> w : maybeToList mw sWords :: Span -> [Word] sWords (Single w) = [w] sWords (Double w) = [w] sWords (ANSIC w) = [w] sWords (Locale w) = [w] sWords (Backquote w) = [w] sWords (ParamSubst ps) = subWords ps sWords _ = [] subWords :: ParamSubst -> [Word] subWords (Bare (Parameter _ mw)) = maybeToList mw subWords (Brace _ (Parameter _ mw)) = maybeToList mw subWords (Alt _ (Parameter _ mw) _ _ w) = w : maybeToList mw subWords (Substring _ (Parameter _ mw) w1 w2) = w1 : w2 : maybeToList mw subWords (Prefix _ _) = [] subWords (Indices (Parameter _ mw)) = maybeToList mw subWords (Length (Parameter _ mw)) = maybeToList mw subWords (Delete _ (Parameter _ mw) _ _ w) = w : maybeToList mw subWords (Replace _ (Parameter _ mw) _ _ w1 w2) = w1 : w2 : maybeToList mw subWords (LetterCase _ (Parameter _ mw) _ _ w) = w : maybeToList mw bannedCommand _ = [] ------------ -- REPORTING ------------ -- | Dispatch different error messages depending on the category of a `BannedTerm`. reportExploit :: BannedTerm -> (Language -> Doc AnsiStyle) reportExploit (BannedTerm t bc) = case bc of Downloading -> security_2 t ScriptRunning -> security_3 t Permissions -> security_4 t InlinedBash -> security_8 t StrangeBashism -> security_9 t CleverRedirect -> security_10 t -------- -- UTILS -------- rValueL :: Lens' Assign RValue rValueL f (Assign p ao r) = Assign p ao <$> f r