pan-os-syslog 0.1.0.0 → 0.2.0.0
raw patch · 17 files changed
+3249/−1273 lines, 17 filesdep +byte-orderdep +bytebuilddep +bytestringdep ~basedep ~byteslicedep ~chronosnew-component:exe:pan-os-syslog-to-avroPVP ok
version bump matches the API change (PVP)
Dependencies added: byte-order, bytebuild, bytestring, containers, contiguous, directory, json-syntax, optparse-generic, text, text-short, transformers, uuid-bytes, wide-word, zlib
Dependency ranges changed: base, byteslice, chronos, ip, primitive, run-st
API changes (from Hackage documentation)
- Panos.Syslog.Unsafe: instance GHC.Exception.Type.Exception Panos.Syslog.Unsafe.Field
- Panos.Syslog.Unsafe: instance GHC.Show.Show Panos.Syslog.Unsafe.Field
+ Panos.Syslog: LogCorrelation :: !Correlation -> Log
+ Panos.Syslog: LogUser :: !User -> Log
+ Panos.Syslog: data Correlation
+ Panos.Syslog: data User
+ Panos.Syslog.Correlation: category :: Correlation -> Bytes
+ Panos.Syslog.Correlation: deviceGroupHierarchyLevel1 :: Correlation -> Word64
+ Panos.Syslog.Correlation: deviceGroupHierarchyLevel2 :: Correlation -> Word64
+ Panos.Syslog.Correlation: deviceGroupHierarchyLevel3 :: Correlation -> Word64
+ Panos.Syslog.Correlation: deviceGroupHierarchyLevel4 :: Correlation -> Word64
+ Panos.Syslog.Correlation: deviceName :: Correlation -> Bytes
+ Panos.Syslog.Correlation: evidence :: Correlation -> Bytes
+ Panos.Syslog.Correlation: objectId :: Correlation -> Word64
+ Panos.Syslog.Correlation: objectName :: Correlation -> Bytes
+ Panos.Syslog.Correlation: serialNumber :: Correlation -> Bytes
+ Panos.Syslog.Correlation: severity :: Correlation -> Bytes
+ Panos.Syslog.Correlation: sourceAddress :: Correlation -> IP
+ Panos.Syslog.Correlation: sourceUser :: Correlation -> Bytes
+ Panos.Syslog.Correlation: syslogHost :: Correlation -> Bytes
+ Panos.Syslog.Correlation: timeGenerated :: Correlation -> Datetime
+ Panos.Syslog.Threat: fileDigest :: Threat -> Bytes
+ Panos.Syslog.Threat: ipProtocol :: Threat -> Bytes
+ Panos.Syslog.Threat: repeatCount :: Threat -> Word64
+ Panos.Syslog.Threat: sessionId :: Threat -> Word64
+ Panos.Syslog.Threat: urlCategoryList :: Threat -> Maybe Bytes
+ Panos.Syslog.Traffic: actionSource :: Traffic -> Bytes
+ Panos.Syslog.Traffic: repeatCount :: Traffic -> Word64
+ Panos.Syslog.Traffic: ruleUuid :: Traffic -> Word128
+ Panos.Syslog.Traffic: sessionId :: Traffic -> Word64
+ Panos.Syslog.Unsafe: Correlation :: {-# UNPACK #-} !ByteArray -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Datetime -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Datetime -> {-# UNPACK #-} !IP -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Bytes -> Correlation
+ Panos.Syslog.Unsafe: LogCorrelation :: !Correlation -> Log
+ Panos.Syslog.Unsafe: LogUser :: !User -> Log
+ Panos.Syslog.Unsafe: User :: {-# UNPACK #-} !ByteArray -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Datetime -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Datetime -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !IP -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> User
+ Panos.Syslog.Unsafe: [$sel:actionFlags:User] :: User -> {-# UNPACK #-} !Word64
+ Panos.Syslog.Unsafe: [$sel:category:Correlation] :: Correlation -> {-# UNPACK #-} !Bounds
+ Panos.Syslog.Unsafe: [$sel:dataSource:User] :: User -> {-# UNPACK #-} !Bounds
+ Panos.Syslog.Unsafe: [$sel:dataSourceName:User] :: User -> {-# UNPACK #-} !Bounds
+ Panos.Syslog.Unsafe: [$sel:deviceGroupHierarchyLevel1:Correlation] :: Correlation -> {-# UNPACK #-} !Word64
+ Panos.Syslog.Unsafe: [$sel:deviceGroupHierarchyLevel1:User] :: User -> {-# UNPACK #-} !Word64
+ Panos.Syslog.Unsafe: [$sel:deviceGroupHierarchyLevel2:Correlation] :: Correlation -> {-# UNPACK #-} !Word64
+ Panos.Syslog.Unsafe: [$sel:deviceGroupHierarchyLevel2:User] :: User -> {-# UNPACK #-} !Word64
+ Panos.Syslog.Unsafe: [$sel:deviceGroupHierarchyLevel3:Correlation] :: Correlation -> {-# UNPACK #-} !Word64
+ Panos.Syslog.Unsafe: [$sel:deviceGroupHierarchyLevel3:User] :: User -> {-# UNPACK #-} !Word64
+ Panos.Syslog.Unsafe: [$sel:deviceGroupHierarchyLevel4:Correlation] :: Correlation -> {-# UNPACK #-} !Word64
+ Panos.Syslog.Unsafe: [$sel:deviceGroupHierarchyLevel4:User] :: User -> {-# UNPACK #-} !Word64
+ Panos.Syslog.Unsafe: [$sel:deviceName:Correlation] :: Correlation -> {-# UNPACK #-} !Bounds
+ Panos.Syslog.Unsafe: [$sel:deviceName:User] :: User -> {-# UNPACK #-} !Bounds
+ Panos.Syslog.Unsafe: [$sel:evidence:Correlation] :: Correlation -> {-# UNPACK #-} !Bytes
+ Panos.Syslog.Unsafe: [$sel:message:Correlation] :: Correlation -> {-# UNPACK #-} !ByteArray
+ Panos.Syslog.Unsafe: [$sel:message:User] :: User -> {-# UNPACK #-} !ByteArray
+ Panos.Syslog.Unsafe: [$sel:objectId:Correlation] :: Correlation -> {-# UNPACK #-} !Word64
+ Panos.Syslog.Unsafe: [$sel:objectName:Correlation] :: Correlation -> {-# UNPACK #-} !Bounds
+ Panos.Syslog.Unsafe: [$sel:receiveTime:Correlation] :: Correlation -> {-# UNPACK #-} !Datetime
+ Panos.Syslog.Unsafe: [$sel:receiveTime:User] :: User -> {-# UNPACK #-} !Datetime
+ Panos.Syslog.Unsafe: [$sel:repeatCount:User] :: User -> {-# UNPACK #-} !Word64
+ Panos.Syslog.Unsafe: [$sel:ruleUuid:Threat] :: Threat -> {-# UNPACK #-} !Word128
+ Panos.Syslog.Unsafe: [$sel:ruleUuid:Traffic] :: Traffic -> {-# UNPACK #-} !Word128
+ Panos.Syslog.Unsafe: [$sel:sequenceNumber:User] :: User -> {-# UNPACK #-} !Word64
+ Panos.Syslog.Unsafe: [$sel:serialNumber:Correlation] :: Correlation -> {-# UNPACK #-} !Bounds
+ Panos.Syslog.Unsafe: [$sel:serialNumber:User] :: User -> {-# UNPACK #-} !Bounds
+ Panos.Syslog.Unsafe: [$sel:severity:Correlation] :: Correlation -> {-# UNPACK #-} !Bounds
+ Panos.Syslog.Unsafe: [$sel:sourceAddress:Correlation] :: Correlation -> {-# UNPACK #-} !IP
+ Panos.Syslog.Unsafe: [$sel:sourceIp:User] :: User -> {-# UNPACK #-} !IP
+ Panos.Syslog.Unsafe: [$sel:sourceUser:Correlation] :: Correlation -> {-# UNPACK #-} !Bounds
+ Panos.Syslog.Unsafe: [$sel:subtype:Correlation] :: Correlation -> {-# UNPACK #-} !Bounds
+ Panos.Syslog.Unsafe: [$sel:subtype:User] :: User -> {-# UNPACK #-} !Bounds
+ Panos.Syslog.Unsafe: [$sel:syslogHost:Correlation] :: Correlation -> {-# UNPACK #-} !Bounds
+ Panos.Syslog.Unsafe: [$sel:syslogHost:User] :: User -> {-# UNPACK #-} !Bounds
+ Panos.Syslog.Unsafe: [$sel:timeGenerated:Correlation] :: Correlation -> {-# UNPACK #-} !Datetime
+ Panos.Syslog.Unsafe: [$sel:timeGenerated:User] :: User -> {-# UNPACK #-} !Datetime
+ Panos.Syslog.Unsafe: [$sel:urlCategoryList:Threat] :: Threat -> {-# UNPACK #-} !Bytes
+ Panos.Syslog.Unsafe: [$sel:user:User] :: User -> {-# UNPACK #-} !Bounds
+ Panos.Syslog.Unsafe: [$sel:virtualSystem:Correlation] :: Correlation -> {-# UNPACK #-} !Bounds
+ Panos.Syslog.Unsafe: [$sel:virtualSystem:User] :: User -> {-# UNPACK #-} !Bounds
+ Panos.Syslog.Unsafe: [$sel:virtualSystemName:User] :: User -> {-# UNPACK #-} !Bounds
+ Panos.Syslog.Unsafe: data Correlation
+ Panos.Syslog.Unsafe: data User
+ Panos.Syslog.User: dataSource :: User -> Bytes
+ Panos.Syslog.User: dataSourceName :: User -> Bytes
+ Panos.Syslog.User: deviceGroupHierarchyLevel1 :: User -> Word64
+ Panos.Syslog.User: deviceGroupHierarchyLevel2 :: User -> Word64
+ Panos.Syslog.User: deviceGroupHierarchyLevel3 :: User -> Word64
+ Panos.Syslog.User: deviceGroupHierarchyLevel4 :: User -> Word64
+ Panos.Syslog.User: deviceName :: User -> Bytes
+ Panos.Syslog.User: repeatCount :: User -> Word64
+ Panos.Syslog.User: sequenceNumber :: User -> Word64
+ Panos.Syslog.User: serialNumber :: User -> Bytes
+ Panos.Syslog.User: sourceIp :: User -> IP
+ Panos.Syslog.User: subtype :: User -> Bytes
+ Panos.Syslog.User: syslogHost :: User -> Bytes
+ Panos.Syslog.User: timeGenerated :: User -> Datetime
+ Panos.Syslog.User: user :: User -> Bytes
+ Panos.Syslog.User: virtualSystem :: User -> Bytes
+ Panos.Syslog.User: virtualSystemName :: User -> Bytes
- Panos.Syslog.Unsafe: Threat :: {-# UNPACK #-} !ByteArray -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Datetime -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Datetime -> {-# UNPACK #-} !IP -> {-# UNPACK #-} !IP -> {-# UNPACK #-} !IP -> {-# UNPACK #-} !IP -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Word16 -> {-# UNPACK #-} !Word16 -> {-# UNPACK #-} !Word16 -> {-# UNPACK #-} !Word16 -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Word32 -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !ByteArray -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !ByteArray -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bytes -> {-# UNPACK #-} !Bytes -> {-# UNPACK #-} !Bytes -> {-# UNPACK #-} !Bytes -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Bytes -> Threat
+ Panos.Syslog.Unsafe: Threat :: {-# UNPACK #-} !ByteArray -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Datetime -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Datetime -> {-# UNPACK #-} !IP -> {-# UNPACK #-} !IP -> {-# UNPACK #-} !IP -> {-# UNPACK #-} !IP -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Word16 -> {-# UNPACK #-} !Word16 -> {-# UNPACK #-} !Word16 -> {-# UNPACK #-} !Word16 -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Word32 -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !ByteArray -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !ByteArray -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bytes -> {-# UNPACK #-} !Bytes -> {-# UNPACK #-} !Bytes -> {-# UNPACK #-} !Bytes -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Bytes -> {-# UNPACK #-} !Bytes -> {-# UNPACK #-} !Word128 -> Threat
- Panos.Syslog.Unsafe: Traffic :: {-# UNPACK #-} !ByteArray -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Datetime -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Datetime -> {-# UNPACK #-} !IP -> {-# UNPACK #-} !IP -> {-# UNPACK #-} !IP -> {-# UNPACK #-} !IP -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Word16 -> {-# UNPACK #-} !Word16 -> {-# UNPACK #-} !Word16 -> {-# UNPACK #-} !Word16 -> {-# UNPACK #-} !Word32 -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Datetime -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> Traffic
+ Panos.Syslog.Unsafe: Traffic :: {-# UNPACK #-} !ByteArray -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Datetime -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Datetime -> {-# UNPACK #-} !IP -> {-# UNPACK #-} !IP -> {-# UNPACK #-} !IP -> {-# UNPACK #-} !IP -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Word16 -> {-# UNPACK #-} !Word16 -> {-# UNPACK #-} !Word16 -> {-# UNPACK #-} !Word16 -> {-# UNPACK #-} !Word32 -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Datetime -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Word64 -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Bounds -> {-# UNPACK #-} !Word128 -> Traffic
Files
- app/Main.hs +458/−0
- common/Sample.hs +153/−1
- pan-os-syslog.cabal +45/−4
- src/Panos/Syslog.hs +2/−0
- src/Panos/Syslog/Correlation.hs +104/−0
- src/Panos/Syslog/Internal/Common.hs +857/−0
- src/Panos/Syslog/Internal/Correlation.hs +104/−0
- src/Panos/Syslog/Internal/System.hs +108/−0
- src/Panos/Syslog/Internal/Threat.hs +486/−0
- src/Panos/Syslog/Internal/Traffic.hs +265/−0
- src/Panos/Syslog/Internal/User.hs +104/−0
- src/Panos/Syslog/System.hs +8/−6
- src/Panos/Syslog/Threat.hs +64/−13
- src/Panos/Syslog/Traffic.hs +60/−17
- src/Panos/Syslog/Unsafe.hs +154/−1229
- src/Panos/Syslog/User.hs +129/−0
- test/Main.hs +148/−3
+ app/Main.hs view
@@ -0,0 +1,458 @@+{-# language BangPatterns #-}+{-# language DeriveGeneric #-}+{-# language DuplicateRecordFields #-}+{-# language LambdaCase #-}+{-# language NamedFieldPuns #-}+{-# language NumericUnderscores #-}+{-# language OverloadedRecordDot #-}+{-# language OverloadedStrings #-}+{-# language PatternSynonyms #-}+{-# language ScopedTypeVariables #-}++import Control.Exception (catchJust)+import Control.Monad.ST.Run (runByteArrayST)+import Control.Monad.Trans.State.Strict (State)+import Data.Bits ((.&.))+import Data.Bool (bool)+import Data.ByteString (ByteString)+import Data.Bytes (Bytes)+import Data.Bytes.Builder (Builder)+import Data.Bytes.Chunks (Chunks(ChunksNil,ChunksCons))+import Data.Char (toUpper)+import Data.Foldable (for_)+import Data.Int (Int64)+import Data.Map.Strict (Map)+import Data.Primitive (ByteArray,SmallMutableArray)+import Data.Primitive (SmallArray)+import Data.Primitive.PrimVar (PrimVar)+import Data.Primitive.PrimVar (readPrimVar,writePrimVar,newPrimVar)+import Data.Text (Text)+import Data.Text.Short (ShortText)+import Data.WideWord (Word128(Word128))+import Foreign.C.Types (CChar)+import GHC.Exts (RealWorld)+import GHC.Generics (Generic)+import Json (pattern (:->))+import Net.Types (IP(IP),IPv6(IPv6))+import Panos.Syslog (Log(LogTraffic),decode)+import Panos.Syslog (Traffic)+import System.Directory (createDirectoryIfMissing)+import System.IO (Handle)+import System.IO.Error (isEOFError)++import qualified Data.Primitive.ByteArray.BigEndian as BigEndian+import qualified Codec.Compression.Zlib.Raw as Deflate+import qualified Data.Primitive as PM+import qualified Data.Primitive.Contiguous as Contiguous+import qualified Data.Map.Strict as Map+import qualified Panos.Syslog.Traffic as Traffic+import qualified Data.Primitive.Ptr as PM+import qualified Data.Bytes.Text.Ascii as Ascii+import qualified Data.ByteString as ByteString+import qualified Data.Bytes as Bytes+import qualified Chronos+import qualified Data.Bytes.Chunks as Chunks+import qualified Data.Bytes.Builder as Builder+import qualified Data.Bytes.Builder.Avro as Avro+import qualified Data.Text as T+import qualified Data.Text.Short as TS+import qualified Control.Monad.Trans.State.Strict as State+import qualified System.IO as IO+import qualified Json+import qualified Data.ByteString.Lazy as LBS+import qualified GHC.Exts as Exts+import qualified Net.IP as IP+import qualified Options.Generic as Opts++data Nullability = Nullable | NonNullable++data Patchedness = Patched | Unpatched++data Atom+ = Signed32+ | Signed64+ | Ip+ | String+ | Timestamp++data Ty+ = Record -- We do not allow records to be nullable+ [Field]+ | Atomic Nullability Atom++data Field = Field+ { name :: !ShortText+ -- ^ Field name must not be the empty string+ , ty :: !Ty+ }++-- x fieldToJson :: Field -> Json.Value+-- x fieldToJson s = case s.ty of+-- x Record snext -> Json.object3+-- x ("name" :-> s.name)+-- x ("type" :-> Json.shortText "record")+-- x ("fields" :-> schemaToJson snext)++makeRecordType :: ShortText -> SmallArray Json.Value -> Json.Value+makeRecordType tyName fields = Json.object3+ ("type" :-> Json.shortText "record")+ ("name" :-> Json.shortText tyName)+ ("fields" :-> Json.Array fields)++encodeAtom :: Atom -> Json.Value+encodeAtom = \case+ Timestamp -> Json.object2+ ("type" :-> Json.shortText "long")+ ("logicalType" :-> Json.shortText "timestamp-millis")+ Signed64 -> Json.shortText "long"+ Signed32 -> Json.shortText "int"+ String -> Json.shortText "string"+ Ip -> Json.shortText "exts.IP"++encodeAtomWithNullability :: Atom -> Nullability -> Json.Value+encodeAtomWithNullability a n = case n of+ NonNullable -> enc+ Nullable -> Json.Array (Contiguous.doubleton (Json.shortText "null") enc)+ where+ enc = encodeAtom a++-- In this, the prefix is only used to name record components.+fieldToJson :: ShortText -> Field -> Json.Value+fieldToJson prefix x = case x.ty of+ Record children -> + let titleName = TS.fromText (uppercaseHead (TS.toText x.name))+ fullName = prefix <> titleName+ convertedFields = Exts.fromList (fmap (fieldToJson fullName) children)+ in Json.object2+ ("type" :-> makeRecordType fullName convertedFields)+ ("name" :-> Json.shortText x.name)+ Atomic nullability atom ->+ let memberName = "name" :-> Json.shortText x.name+ memberType = "type" :-> encodeAtomWithNullability atom nullability+ in Json.object2 memberName memberType++-- This converts the nesting source.nat.ip to SourceNatIp+fieldToFlatJson :: Text -> Field -> [Json.Value]+fieldToFlatJson prefix x = case x.ty of+ Record children -> fieldToFlatJson (prefix <> uppercaseHead (TS.toText x.name)) =<< children+ Atomic nullability atom ->+ let memberName = "name" :-> Json.text (prefix <> uppercaseHead (TS.toText x.name))+ memberType = "type" :-> encodeAtomWithNullability atom nullability+ in pure (Json.object2 memberName memberType)++uppercaseHead :: Text -> Text+uppercaseHead t = case T.uncons t of+ Just (c,cs) -> T.cons (toUpper c) cs+ Nothing -> T.empty++replaceIpWithS64 :: [Field] -> [Field]+replaceIpWithS64 = map replaceIpWithS64Single++replaceIpWithS64Single :: Field -> Field+replaceIpWithS64Single x = case x.ty of + Record children -> Field x.name (Record (replaceIpWithS64 children))+ Atomic nullability atom -> case atom of+ Ip -> Field x.name (Atomic nullability Signed64)+ _ -> Field x.name (Atomic nullability atom)++theSchema :: [Field]+theSchema =+ [ Field "timestamp" $ Atomic NonNullable Timestamp+ , Field "chronopartition" $ Atomic NonNullable Signed32+ , Field "network" $ Record+ [ Field "application" $ Atomic Nullable String+ , Field "iana_number" $ Atomic NonNullable Signed32+ ]+ , Field "source" $ Record+ [ Field "ip" $ Atomic NonNullable Ip+ , Field "port" $ Atomic NonNullable Signed32+ , Field "packets" $ Atomic NonNullable Signed64+ , Field "bytes" $ Atomic NonNullable Signed64+ , Field "user" $ Record+ [ Field "original" $ Atomic Nullable String+ ]+ ]+ , Field "destination" $ Record+ [ Field "ip" $ Atomic NonNullable Ip+ , Field "port" $ Atomic NonNullable Signed32+ , Field "packets" $ Atomic NonNullable Signed64+ , Field "bytes" $ Atomic NonNullable Signed64+ , Field "user" $ Record+ [ Field "original" $ Atomic Nullable String+ ]+ ]+ ]++-- Avro is weird about how users have to use the "fixed" type. You have+-- to provide a "name" for it so that a Java code generation tool can+-- use that as the class name. You cannot reuse the same name more than+-- once. And you cannot define all the names in advance. You have to define+-- them inline as you are defining the fields for a document. So, this+-- function is a hack to work around this strange limitation. Here, we+-- find the first occurrence of "exts.IP" and replace it with a definition+-- that names that type. All future occurrences then refer to the original.+patchFirstExtsIP :: Json.Value -> Json.Value+patchFirstExtsIP v0 = State.evalState (go v0) Unpatched+ where+ go :: Json.Value -> State Patchedness Json.Value+ go v = State.get >>= \case+ Patched -> pure v+ Unpatched -> case v of+ Json.Array xs -> Json.Array <$> traverse go xs+ Json.Object mbrs -> Json.Object+ <$> traverse (\Json.Member{key,value} -> Json.Member key <$> go value) mbrs+ Json.String "exts.IP" -> do+ State.put Patched+ pure $ Json.object4+ ("name" :-> Json.shortText "IP") + ("namespace" :-> Json.shortText "exts") + ("type" :-> Json.shortText "fixed") + ("size" :-> Json.int 16) + x -> pure x++data Settings = Settings+ { nested :: !Bool+ , ipAsSigned64 :: !Bool+ , timeBucket :: !(Maybe Text)+ , compress :: !Bool+ }+ deriving (Generic, Show)++instance Opts.ParseRecord Settings++data Compression = CompressionNone | CompressionDeflate++main :: IO ()+main = do+ Settings{nested,ipAsSigned64,timeBucket=timeBucketStr,compress} <- Opts.getRecord "pan-os-syslog-to-avro"+ timeBucket :: Int64 <- case timeBucketStr of+ Just "1d" -> pure 86400+ Just "24h" -> pure 86400+ Just "1h" -> pure 3600+ Just "60m" -> pure 3600+ Just "5m" -> pure 300+ Just "none" -> pure 0+ Nothing -> pure 0+ _ -> fail "Unsupported timeBucket. Try: 5m, 60m, 1d"+ let compression = case compress of+ True -> CompressionDeflate+ False -> CompressionNone+ let ipEncoding = case ipAsSigned64 of+ True -> IpEncodingS64+ False -> IpEncodingU128+ let theSchema' = case ipAsSigned64 of+ True -> replaceIpWithS64 theSchema+ False -> theSchema+ let encodedSchema = Builder.run 512 $ Json.encode $ patchFirstExtsIP $ case nested of+ False -> Json.object3+ ("type" :-> Json.shortText "record")+ ("name" :-> Json.shortText "docroot")+ ("fields" :-> Json.Array (Exts.fromList (fieldToFlatJson T.empty =<< theSchema')))+ True -> Json.object3+ ("type" :-> Json.shortText "record")+ ("name" :-> Json.shortText "docroot")+ ("fields" :-> Json.Array (Exts.fromList (map (fieldToJson "") theSchema')))+ let syncMarker@(Word128 syncA syncB) = 0xabcd_0123_9876_fedc_4567_4321_3456_cdef+ putStrLn "Schema"+ putStrLn "======"+ Chunks.hPut IO.stdout encodedSchema+ putStrLn ""+ putStrLn "Sync Marker"+ putStrLn "==========="+ Chunks.hPut IO.stdout+ ( Builder.run 16+ (Builder.word64PaddedUpperHex syncA <> Builder.word64PaddedUpperHex syncB)+ )+ putStrLn ""+ sinks <- handleUntilEof timeBucket compression encodedSchema syncMarker ipEncoding+ for_ sinks $ \Sink{handle=h,buffer,position} -> do+ ix <- readPrimVar position+ case ix of+ 0 -> pure ()+ _ -> do+ dst' <- PM.freezeSmallArray buffer 0 ix+ Chunks.hPut h (encodeTrafficLogBatch compression ipEncoding syncMarker dst')+ IO.hClose h+ pure ()++pushAvroHeader :: + Compression+ -> Word128 -- sync marker+ -> Chunks -- encoded schema+ -> Handle -- sink+ -> IO ()+pushAvroHeader !compression !syncMarker !encSchema !sink = Chunks.hPut sink $ Builder.run 1024 $+ Builder.ascii4 'O' 'b' 'j' '\x01'+ <>+ Avro.map2+ "avro.schema"+ (Avro.chunks encSchema)+ "avro.codec"+ (Avro.text (case compression of {CompressionNone -> "null"; CompressionDeflate -> "deflate"}))+ <>+ Avro.word128 syncMarker++data Sink = Sink+ { buffer :: !(SmallMutableArray RealWorld Traffic)+ , position :: !(PrimVar RealWorld Int)+ , handle :: !Handle+ }++initializeSinkIfNotExists :: Compression -> Word128 -> Chunks -> Map Int64 Sink -> Int64 -> IO (Sink, Map Int64 Sink)+initializeSinkIfNotExists !compression !syncMarker !encSchema !sinks !k = case Map.lookup k sinks of+ Nothing -> do+ buffer <- PM.newSmallArray batchSize errorThunk+ position <- newPrimVar 0+ createDirectoryIfMissing False "output"+ let partitionDir = ("output/chronopartition=" ++ show k)+ createDirectoryIfMissing False partitionDir+ h <- IO.openFile (partitionDir ++ "/data.avro") IO.WriteMode+ pushAvroHeader compression syncMarker encSchema h+ let newSink = Sink{buffer,position,handle=h}+ let sinks' = Map.insert k newSink sinks+ pure (newSink, sinks')+ Just s -> pure (s,sinks)++computeBucket ::+ Int64+ -> Int64 -- seconds since epoch+ -> Int64+computeBucket !sz !seconds = case sz of+ 0 -> 0+ _ -> div (sz * div seconds sz) 300++handleUntilEof ::+ Int64 -- time bucket+ -> Compression -- compression codec+ -> Chunks -- encoded schema+ -> Word128 -- sync marker+ -> IpEncoding+ -> IO (Map Int64 Sink)+handleUntilEof !bucketSize !compression !encSchema !syncMarker !ipEncoding = do+ let go :: Map Int64 Sink -> Int -> IO (Map Int64 Sink)+ go !sinks !fileIx = catchJust (bool Nothing (Just ()) . isEOFError) (Just <$> ByteString.getLine) (\() -> pure Nothing) >>= \case+ Nothing -> pure sinks+ Just b0 -> do+ b1 <- b2b b0+ case decode (Bytes.fromByteArray b1) of+ Right (LogTraffic tr) -> do+ let secondsSinceEpoch = div (Chronos.getTime (Chronos.datetimeToTime (Traffic.timeGenerated tr))) 1_000_000_000+ let bucket = computeBucket bucketSize secondsSinceEpoch+ (Sink{buffer=dst,position,handle=h},sinks') <- initializeSinkIfNotExists compression syncMarker encSchema sinks bucket+ !dstIx0 <- readPrimVar position+ !dstIx <- if dstIx0 == batchSize+ then do+ dst' <- PM.freezeSmallArray dst 0 batchSize+ Chunks.hPut h (encodeTrafficLogBatch compression ipEncoding syncMarker dst')+ pure 0+ else pure dstIx0+ case ipEncoding of+ IpEncodingU128+ | isKnownIpProtocol (Traffic.ipProtocol tr) -> do+ PM.writeSmallArray dst dstIx tr+ writePrimVar position (dstIx + 1)+ go sinks (fileIx + 1)+ | otherwise -> go sinks (fileIx + 1)+ IpEncodingS64+ | IP.isIPv4 (Traffic.sourceAddress tr)+ , IP.isIPv4 (Traffic.destinationAddress tr)+ , isKnownIpProtocol (Traffic.ipProtocol tr) -> do+ PM.writeSmallArray dst dstIx tr+ writePrimVar position (dstIx + 1)+ go sinks' (fileIx + 1)+ | otherwise -> go sinks' (fileIx + 1)+ Right _ -> go sinks (fileIx + 1)+ Left err -> fail ("On line " ++ show fileIx ++ ", " ++ show err)+ go Map.empty (0 :: Int)++batchSize :: Int+batchSize = 8192++isKnownIpProtocol :: Bytes -> Bool+isKnownIpProtocol !proto = proto == Ascii.fromString "tcp" || proto == Ascii.fromString "udp"++data IpEncoding = IpEncodingU128 | IpEncodingS64++syncMarkerToBytes :: Word128 -> Bytes+syncMarkerToBytes !w = Bytes.fromByteArray $ runByteArrayST $ do+ dst <- PM.newByteArray 16+ BigEndian.writeByteArray dst 0 w+ PM.unsafeFreezeByteArray dst++encodeTrafficLogBatch :: Compression -> IpEncoding -> Word128 -> SmallArray Traffic -> Chunks+encodeTrafficLogBatch !compression !ipEnc !syncMarker !xs =+ -- Note: we have to subtract the length of the sync marker from+ -- the payload length. + let payload = Builder.run 512 (foldMap (encodeTrafficLog ipEnc) xs)+ in case compression of+ CompressionNone ->+ Builder.runOnto 128+ (Avro.int (PM.sizeofSmallArray xs) <> Avro.int (Chunks.length payload))+ (payload <> ChunksCons (syncMarkerToBytes syncMarker) ChunksNil)+ CompressionDeflate ->+ let compressedPayload = Deflate.compress (LBS.fromStrict (Chunks.concatByteString payload))+ in Builder.runOnto 128+ (Avro.int (PM.sizeofSmallArray xs) <> Avro.int64 (LBS.length compressedPayload))+ (ChunksCons (Bytes.fromLazyByteString compressedPayload) (ChunksCons (syncMarkerToBytes syncMarker) ChunksNil))++encodeIp :: IpEncoding -> IP -> Builder+encodeIp !enc !ip = case enc of+ IpEncodingS64 -> Avro.int64 (extractIPv4 ip)+ IpEncodingU128 -> Avro.word128 (ipToW128 ip)++encodeTrafficLog :: IpEncoding -> Traffic -> Builder+encodeTrafficLog ipEnc t =+ let millisecondsSinceEpoch = div (Chronos.getTime (Chronos.datetimeToTime (Traffic.timeGenerated t))) 1_000_000 in+ Avro.int64 millisecondsSinceEpoch+ <>+ Avro.int64 (div millisecondsSinceEpoch 300_000)+ <>+ encodeNullableString Traffic.application t+ <>+ Avro.int32 (if Traffic.ipProtocol t == Ascii.fromString "tcp" then 6 else 17)+ <>+ encodeIp ipEnc (Traffic.sourceAddress t)+ <>+ Avro.word16 (Traffic.sourcePort t)+ <>+ Avro.int64 (fromIntegral (Traffic.packetsReceived t))+ <>+ Avro.int64 (fromIntegral (Traffic.bytesReceived t))+ <>+ encodeNullableString Traffic.sourceUser t+ <>+ encodeIp ipEnc (Traffic.destinationAddress t)+ <>+ Avro.word16 (Traffic.destinationPort t)+ <>+ Avro.int64 (fromIntegral (Traffic.packetsSent t))+ <>+ Avro.int64 (fromIntegral (Traffic.bytesSent t))+ <>+ encodeNullableString Traffic.destinationUser t++extractIPv4 :: IP -> Int64+extractIPv4 (IP (IPv6 (Word128 _ b))) = fromIntegral (b .&. 0x0000_0000_FFFF_FFFF)++encodeNullableString :: (Traffic -> Bytes) -> Traffic -> Builder+encodeNullableString project t =+ let x = project t+ in case Bytes.null x of+ True -> Builder.word8 0x00+ False -> Builder.word8 0x02 <> Avro.bytes x++ipToW128 :: IP -> Word128+ipToW128 (IP (IPv6 w)) = w++errorThunk :: Traffic+{-# noinline errorThunk #-}+errorThunk = errorWithoutStackTrace "pan-os-syslog-to-avro: implementation mistake"++b2b :: ByteString -> IO ByteArray+b2b !b = ByteString.useAsCStringLen b $ \(ptr,len) -> do+ arr <- PM.newByteArray len+ PM.copyPtrToMutablePrimArray (castArray arr) 0 ptr len+ PM.unsafeFreezeByteArray arr++castArray :: PM.MutableByteArray s -> PM.MutablePrimArray s CChar+castArray (PM.MutableByteArray x) = PM.MutablePrimArray x
common/Sample.hs view
@@ -1,8 +1,11 @@ {-# language TypeApplications #-} module Sample- ( traffic_8_1_A+ ( correlation_A+ , traffic_8_1_A , traffic_8_1_B+ , traffic_9_0_A+ , traffic_prisma_A , threat_8_1_A , threat_8_1_B , threat_8_1_C@@ -12,7 +15,14 @@ , threat_8_1_G , threat_8_1_H , threat_8_1_I+ , threat_9_0_A+ , threat_9_1_A+ , threat_9_1_B+ , threat_prisma_A+ , threat_prisma_B+ , threat_prisma_C , system_8_1_A+ , user_A ) where import Data.Bytes (Bytes)@@ -63,6 +73,35 @@ , "0,0,0,0" ] +traffic_9_0_A :: Bytes+traffic_9_0_A = pack $ concat+ [ "<14> Mar 9 14:13:44 NY-PAN-FW-5.example.com 1,2020/03/09 14:13:44,"+ , "019624168632,TRAFFIC,end,2304,2020/03/09 14:13:44,192.0.2.73,"+ , "192.0.2.117,0.0.0.0,0.0.0.0,NEAR to FAR,,,insufficient-data,"+ , "vsys1,NEAR-ZONE,FAR-ZONE,tunnel.163,ethernet1/4,Forward-The-Logs,"+ , "2020/03/09 14:13:44,385404,1,56451,8475,0,0,0x401c,tcp,allow,1760,"+ , "1051,709,19,2020/03/09 14:13:24,16,any,0,6412095715,0x0,United States,"+ , "10.0.0.0-10.255.255.255,0,12,7,tcp-fin,11,0,0,0,,NY-PAN-FW-5,"+ , "my-policy,,,0,,0,,N/A,0,0,0,0,321ef4bf-801e-1c89-b341-efb2898ba2be,0"+ ]++-- This is PAN-OS 10.x.+traffic_prisma_A :: Bytes+traffic_prisma_A = pack $ concat+ [ "<14>1 2021-10-27T19:21:00.034Z stream-logfwd20-548106107-12876850-z5tm-harness-44k2 "+ , "logforwarder - panwlogs - 2021-10-27T19:20:59.000000Z,no-serial,TRAFFIC,"+ , "end,10.0,2021-10-27T19:20:40.000000Z,192.0.2.12,192.0.2.18,"+ , "192.0.2.112,192.0.2.118,INSIDE-TO-OUTSIDE,,,"+ , "web-browsing,vsys1,Zone-One,Zone-Two,tunnel.101,"+ , "ethernet1/1,Send-The-Logs,170172,1,42493,80,46254,80,tcp,"+ , "allow,892,818,74,12,2021-10-27T19:19:08.000000Z,1,low-risk,"+ , "9091497,10.0.0.0-10.255.255.255,JP,11,1,threat,28,31,0,0,,"+ , "The-Device-Name,from-policy,,,0,,0,"+ , "1970-01-01T00:00:00.000000Z,N/A,0,0,0,0,"+ , "89092ab0-606f-4c5a-b44c-c271420f3972,0,0,,,,,,,,,,,,,,,,,,,"+ , ",,,,,,,,,,,,,,,,2021-10-27T19:20:41.652000Z,,"+ ]+ -- Threat log for web browsing threat_8_1_A :: Bytes threat_8_1_A = pack $ concat@@ -215,6 +254,80 @@ , "AppThreat-3218-3729,0x0,0,4294967295," ] +-- Web browsing threat log from PAN-OS 9.0+threat_9_0_A :: Bytes+threat_9_0_A = pack $ concat+ [ "Mar 9 14:47:44 firewall1.example.com 1,2020/03/09 14:47:44,001701012545,"+ , "THREAT,url,2304,2020/03/09 14:47:44,192.0.2.101,192.0.2.102,"+ , "192.0.2.103,192.0.2.104,FOO to BAR,example\\jdoe,,ssl,vsys1,FOO,BAR,"+ , "ethernet1/6,ethernet1/7,My-Log-Forwarding,2020/03/09 14:47:44,455102,"+ , "1,62475,443,31963,443,0x40f000,tcp,alert,\"dt.adsafeprotected.com/\","+ , "(9999),web-advertisements,informational,client-to-server,2314781488,"+ , "0x2000000000000000,United States,United States,0,,0,,,0,,,,,,,,0,11,"+ , "0,0,0,,firewall1,,,,,0,,0,,N/A,unknown,AppThreat-0-0,0x0,0,4294967295,"+ , ",\"web-advertisements,low-risk\",edd29e10-d927-1753-867f-0108b01b80de,0"+ ]++threat_prisma_A :: Bytes+threat_prisma_A = pack $ concat+ [ "<14>1 2021-10-28T02:58:53.586Z stream-foobar-baz logforwarder - "+ , "panwlogs - 2021-10-28T02:58:52.000000Z,no-serial,THREAT,url,"+ , "10.0,2021-10-28T02:58:50.000000Z,192.0.2.99,190.0.2.100,192.0.2.101,"+ , "192.0.2.102,IN-TO-OUT,,,google-play,vsys1,trust,untrust,tunnel.105,"+ , "ethernet1/1,The-Forward,28182,1,59580,443,3502,443,tcp,block-url,"+ , "play.google.com/,shopping,Informational,client to server,2719193,"+ , "10.0.0.0-10.255.255.255,US,,0,0,,,,28,31,0,0,,big-ol-device,,,"+ , "unknown,0,,0,1970-01-01T00:00:00.000000Z,N/A,unknown,0,0,,"+ , "\"shopping,low-risk\",89ea2a40-656e-4c5a-b44c-c27b52de3982,0,"+ , ",,,,,,,,,,,,,,,,,,,,,,,,,,,2021-10-28T02:58:50.719000Z,"+ ]++threat_prisma_B :: Bytes+threat_prisma_B = pack $ concat+ [ "<14>1 2021-11-01T15:14:08.069Z stream-logfwd-xyz logforwarder - panwlogs "+ , "- 2021-11-01T15:14:06.000000Z,no-serial,THREAT,spyware,10.0,"+ , "2021-11-01T15:13:56.000000Z,192.0.2.20,8.8.8.8,192.0.2.111,8.8.8.8,"+ , "in-to-out-dns,domain\\exampleuser,,dns,vsys1,ZoneA,ZoneB,tunnel.101,"+ , "ethernet1/1,My-Forwarding,1355037,1,55517,53,45783,53,udp,drop,"+ , "example.com,Phishing:example.com(109010001),Low,client-to-server,"+ , "12088427,10.0.0.0-10.255.255.255,US,1152921504606899096,,,0,,,,,0,"+ , "28,31,0,0,,Big-Firewall,,,0,,0,1970-01-01T00:00:00.000000Z,N/A,"+ , "dns-phishing,0,0x0,ceb0229f-7a4a-5c62-8960-f66b27e2e01e,0,"+ , ",,,,,,,,,,,,,,,,,,,,,,,,,,,,0,2021-11-01T15:13:57.399000Z,"+ ]++threat_prisma_C :: Bytes+threat_prisma_C = pack $ concat+ [ "<14>1 2021-11-02T00:53:49.421Z stream-logfwd-xyz logforwarder - panwlogs "+ , "- 2021-11-02T00:53:48.000000Z,no-serial,THREAT,wildfire,10.0,"+ , "2021-11-02T00:53:37.000000Z,192.0.2.20,192.0.2.21,192.0.2.22,192.0.2.23,"+ , "in-to-out,foo\\bar,,office365-enterprise-access,vsys1,trust,"+ , "untrust,tunnel.101,ethernet1/1,My-Forwarding,1294093,1,"+ , "64810,443,32485,443,tcp,allow,Exchange.asmx,"+ , "Adobe Portable Document Format (PDF)(52021),Informational,"+ , "server to client,13548583,10.0.0.0-10.255.255.255,US,0,"+ , "85dde742f22216725f0ff6b172ca68f4f30f02029f08b3079cec34da804db793,"+ , "wildfire.paloaltonetworks.com,69,pdf,,,,47136308381,28,31,0,0,,"+ , "Big-Firewall,,,0,,0,1970-01-01T00:00:00.000000Z,"+ , "N/A,unknown,0,0x0,f0ebabfb-ef45-451a-89a5-30f87238d62b,"+ , "0,,,,,,,,,,,,,,,,,,,,,,,,,,,,,0,2021-11-02T00:53:37.797000Z,"+ ]+++-- This is a log from a firewall whose license had expired.+threat_9_1_A :: Bytes+threat_9_1_A = pack $ concat+ [ "<14>Mar 2 11:30:47 MY-PA-5250-3 1,2021/03/02 11:30:47,012911492893,"+ , "THREAT,url,2305,2021/03/02 11:30:47,192.0.2.22,192.0.2.17,0.0.0.0,"+ , "0.0.0.0,rule99,foo\\_bar,,ssl,vsys1,MY-trust,MY-dmz,ethernet1/1,"+ , "ethernet1/2,My-Log-Forwarding,2021/03/02 11:30:47,38578210,1,63421,"+ , "5986,0,0,0x10b000,tcp,allow,\"192.0.2.17_solarwinds_zero_configuration:5986/\","+ , "(9999),license-expired,informational,client-to-server,4901795984744840635,"+ , "0xa000000000000000,10.0.0.0-10.255.255.255,10.0.0.0-10.255.255.255,0,,0,,,0,"+ , ",,,,,,,0,56,0,0,0,,MY-PA-5250-1,,,,,0,,0,,N/A,unknown,AppThreat-0-0,0x0,0,"+ , "4196987195,,\"license-expired\",9871ab80-6061-4f07-a06e-0036b0206f73,0,"+ ]+ -- System log (IKE delete) system_8_1_A :: Bytes system_8_1_A = pack $ concat@@ -223,4 +336,43 @@ , "To-FOO-BAR-NET,0,0,general,informational,\"IKE protocol IPSec SA " , "delete message sent to peer. SPI:0xA1CD910F.\",18249042," , "0x8000000000000000,0,0,0,0,,NY-DC-FW-2"+ ]++-- User ID log (login)+user_A :: Bytes+user_A = pack $ concat+ [ "1,2021/09/19 11:55:38,013201027000,USERID,login,2305,2021/09/19 11:55:38,"+ , "vsys1,192.0.2.112,BIGDAWG10$@EXAMPLE.COM,server.example.com,0,1,2700,0,0,"+ , "active-directory,,6991364276409747475,0x0,92,0,0,0,,MY-PA5220-A,1,,"+ , "2021/09/19 11:55:25,1,0x0,BIGDAWG10$@EXAMPLE.COM"+ ]++-- Correlation log+correlation_A :: Bytes+correlation_A = pack $ concat+ [ "<14>Mar 7 14:43:42 panorama.example.com 1,2022/03/07 14:43:17,013201019829,"+ , "CORRELATION,,,2022/03/07 14:43:17,192.0.2.75,,,compromised-host,medium,"+ , "146,0,0,0,,panorama,1869504768,Beacon Detection,6005,Host visited known "+ , "malware URL (11 times)."+ ]++-- Threat log+threat_9_1_B :: Bytes+threat_9_1_B = pack $ concat+ [ "<14>Jul 25 09:39:39 Panorama 1,2022/07/25 09:39:39,012801204178,"+ , "THREAT,url,2560,2022/07/25 09:40:03,192.0.2.254,192.0.2.201,"+ , "192.0.2.13,192.0.2.12,trust-to-untrust-allow-any,,,ssl,vsys1,"+ , "ABC-trust,ABC-untrust,ethernet1/2.14,ethernet1/3,"+ , "Forward_To_Panorama,2022/07/25 09:40:03,31669,1,53686,443,15492,"+ , "443,0x403400,tcp,block-url,\"t.co/\",(9999),social-networking,"+ , "informational,client-to-server,30720475,0xa000000000000000,"+ , "10.0.0.0-10.255.255.255,United States,,,0,,,0,,,,,,,,0,15,0,0,0,"+ , ",NYC-ABC-PA-220,,,,,0,,0,,N/A,unknown,AppThreat-0-0,0x0,0,"+ , "4294967295,,\"social-networking,low-risk\","+ , "a40f8ec5-109d-4dc5-b09d-33b848f5bd09,0,,0.0.0.0,"+ , ",,,,,,,,,,,,,,,,,,,,,,,,,,0,2022-07-25T09:40:03.909-04:00,"+ , ",,,encrypted-tunnel,networking,browser-based,"+ , "4,\"used-by-malware,able-to-transfer-file,"+ , "has-known-vulnerability,tunnel-other-application,pervasive-use\","+ , ",ssl,no,no" ]
pan-os-syslog.cabal view
@@ -1,6 +1,6 @@ cabal-version: 2.2 name: pan-os-syslog-version: 0.1.0.0+version: 0.2.0.0 synopsis: Parse syslog traffic from PAN-OS description: Parse syslog traffic from PAN-OS. The data types in this library are@@ -44,20 +44,61 @@ library exposed-modules: Panos.Syslog+ Panos.Syslog.Correlation Panos.Syslog.System- Panos.Syslog.Traffic Panos.Syslog.Threat+ Panos.Syslog.Traffic Panos.Syslog.Unsafe+ Panos.Syslog.User+ other-modules:+ -- These were split into separate modules to improve compile times.+ -- GHC was using 8GB of memory to compile the megamodule that used+ -- to contain them.+ Panos.Syslog.Internal.Common+ Panos.Syslog.Internal.Correlation+ Panos.Syslog.Internal.System+ Panos.Syslog.Internal.Threat+ Panos.Syslog.Internal.Traffic+ Panos.Syslog.Internal.User build-depends: , base >=4.12.0.0 && <5 , byteslice >=0.1.3 && <0.3 , bytesmith >=0.3.1 && <0.4- , chronos >=1.0.6 && <1.1+ , chronos >=1.1.3 && <1.2 , ip >=1.6 && <1.8- , primitive >=0.7 && <0.8+ , primitive >=0.7 && <0.10 , primitive-addr >=0.1.0.2 && <2 , run-st >=0.1 && <0.2+ , uuid-bytes >=0.1.1 && <0.2+ , wide-word >=0.1.0.9 && <0.2 hs-source-dirs: src+ ghc-options: -Wall -O2+ default-language: Haskell2010++executable pan-os-syslog-to-avro+ hs-source-dirs: app+ main-is: Main.hs+ build-depends:+ , base+ , pan-os-syslog+ , primitive >=0.9+ , byteslice+ , optparse-generic >=1.5.2+ , bytebuild >=0.3.14+ , json-syntax >=0.2.7+ , bytestring >=0.11.5.3+ , text-short >=0.1.5+ , text >=2.0+ , contiguous >=0.6.4+ , wide-word >=0.1.5+ , transformers >=0.6.1+ , ip >= 1.7.6+ , chronos >=1.1.5+ , containers >=0.6+ , zlib >=0.6.3+ , byte-order >=0.1.3+ , run-st >=0.1.3.2+ , directory >=1.3.8.1 ghc-options: -Wall -O2 default-language: Haskell2010
src/Panos/Syslog.hs view
@@ -4,6 +4,8 @@ , U.Traffic , U.Threat , U.System+ , U.User+ , U.Correlation , U.Field -- * Decoding , U.decode
+ src/Panos/Syslog/Correlation.hs view
@@ -0,0 +1,104 @@+{-# language BangPatterns #-}+{-# language DerivingStrategies #-}+{-# language DuplicateRecordFields #-}+{-# language GeneralizedNewtypeDeriving #-}+{-# language MagicHash #-}+{-# language NamedFieldPuns #-}+{-# language NumericUnderscores #-}+{-# language OverloadedRecordDot #-}++-- | Fields for correlation logs.+module Panos.Syslog.Correlation+ ( category+ , deviceGroupHierarchyLevel1+ , deviceGroupHierarchyLevel2+ , deviceGroupHierarchyLevel3+ , deviceGroupHierarchyLevel4+ , deviceName+ , evidence+ , objectId+ , objectName+ , serialNumber+ , severity+ , sourceAddress+ , sourceUser+ , syslogHost+ , timeGenerated+ ) where++import Chronos (Datetime)+import Data.Bytes.Types (Bytes(..))+import Data.Word (Word64)+import Net.Types (IP)+import Panos.Syslog.Unsafe (Correlation(Correlation),Bounds(Bounds))++import qualified Panos.Syslog.Unsafe as U++-- | The hostname of the firewall on which the session was logged.+deviceName :: Correlation -> Bytes+deviceName (Correlation{deviceName=Bounds off len,message=msg}) =+ Bytes{offset=off,length=len,array=msg}++objectName :: Correlation -> Bytes+objectName (Correlation{objectName=Bounds off len,message=msg}) =+ Bytes{offset=off,length=len,array=msg}++sourceUser :: Correlation -> Bytes+sourceUser (Correlation{sourceUser=Bounds off len,message=msg}) =+ Bytes{offset=off,length=len,array=msg}++-- | Time the log was generated on the dataplane.+timeGenerated :: Correlation -> Datetime+{-# inline timeGenerated #-}+timeGenerated u = u.timeGenerated++-- | Serial number of the firewall that generated the log. These+-- occassionally contain non-numeric characters, so do not attempt+-- to parse this as a decimal number.+serialNumber :: Correlation -> Bytes+serialNumber (Correlation{serialNumber=Bounds off len,message=msg}) =+ Bytes{offset=off,length=len,array=msg}++-- | The hostname from the syslog header appended to the PAN-OS log.+-- This field is not documented by Palo Alto Network and technically+-- is not part of the log, but in practice, it is always present.+-- This is similar to @deviceName@.+syslogHost :: Correlation -> Bytes+syslogHost (Correlation{syslogHost=Bounds off len,message=msg}) =+ Bytes{offset=off,length=len,array=msg}++severity :: Correlation -> Bytes+severity (Correlation{severity=Bounds off len,message=msg}) =+ Bytes{offset=off,length=len,array=msg}++category :: Correlation -> Bytes+category (Correlation{category=Bounds off len,message=msg}) =+ Bytes{offset=off,length=len,array=msg}++evidence :: Correlation -> Bytes+evidence u = u.evidence++objectId :: Correlation -> Word64+objectId u = u.objectId++deviceGroupHierarchyLevel1 :: Correlation -> Word64+{-# inline deviceGroupHierarchyLevel1 #-}+deviceGroupHierarchyLevel1 u = u.deviceGroupHierarchyLevel1++deviceGroupHierarchyLevel2 :: Correlation -> Word64+{-# inline deviceGroupHierarchyLevel2 #-}+deviceGroupHierarchyLevel2 u = u.deviceGroupHierarchyLevel2++deviceGroupHierarchyLevel3 :: Correlation -> Word64+{-# inline deviceGroupHierarchyLevel3 #-}+deviceGroupHierarchyLevel3 u = u.deviceGroupHierarchyLevel3++deviceGroupHierarchyLevel4 :: Correlation -> Word64+{-# inline deviceGroupHierarchyLevel4 #-}+deviceGroupHierarchyLevel4 u = u.deviceGroupHierarchyLevel4++-- | Original session source IP address.+sourceAddress :: Correlation -> IP+sourceAddress u = u.sourceAddress++
+ src/Panos/Syslog/Internal/Common.hs view
@@ -0,0 +1,857 @@+{-# language BangPatterns #-}+{-# language DerivingStrategies #-}+{-# language DuplicateRecordFields #-}+{-# language GeneralizedNewtypeDeriving #-}+{-# language LambdaCase #-}+{-# language MagicHash #-}+{-# language NamedFieldPuns #-}+{-# language NumericUnderscores #-}+{-# language ScopedTypeVariables #-}+module Panos.Syslog.Internal.Common+ ( -- * Parsers+ untilComma+ , skipDigitsThroughComma+ , skipThroughComma+ , w64Comma+ , w16Comma+ , parserDatetime+ , parserOptionallyQuoted+ , parserOptionallyQuoted_+ , finalOptionallyQuoted+ -- * Types+ , Field(..)+ , Bounds(..)+ -- * Fields+ , actionField+ , actionFlagsField+ , actionSourceField+ , applicationField+ , bytesField+ , bytesReceivedField+ , bytesSentField+ , categoryField+ , cloudField+ , contentTypeField+ , contentVersionField+ , dataSourceField+ , dataSourceNameField+ , dataSourceTypeField+ , descriptionField+ , destinationAddressField+ , destinationCountryField+ , destinationPortField+ , destinationUserField+ , destinationVmUuidField+ , destinationZoneField+ , deviceGroupHierarchyLevel1Field+ , deviceGroupHierarchyLevel2Field+ , deviceGroupHierarchyLevel3Field+ , deviceGroupHierarchyLevel4Field+ , deviceNameField+ , directionField+ , elapsedTimeField+ , eventIdField+ , evidenceField+ , fileDigestField+ , fileTypeField+ , flagsField+ , forwardedForField+ , futureUseAField+ , futureUseBField+ , futureUseCField+ , futureUseDField+ , futureUseEField+ , futureUseFField+ , futureUseGField+ , http2ConnectionField+ , httpHeadersField+ , httpMethodField+ , inboundInterfaceField+ , ipField+ , ipProtocolField+ , leftoversField+ , linkChangeCountField+ , logActionField+ , miscellaneousField+ , moduleField+ , monitorTagField+ , natDestinationIpField+ , natDestinationPortField+ , natSourceIpField+ , natSourcePortField+ , objectField+ , objectIdField+ , objectNameField+ , outboundInterfaceField+ , packetsField+ , packetsReceivedField+ , packetsSentField+ , parentSessionIdField+ , parentStartTimeField+ , payloadProtocolField+ , pcapIdField+ , prismaDataField+ , receiveTimeDateField+ , receiveTimeTimeField+ , recipientField+ , refererField+ , repeatCountField+ , reportIdField+ , ruleNameField+ , ruleUuidField+ , sctpAssociationIdField+ , sctpChunksField+ , sctpChunksReceivedField+ , sctpChunksSentField+ , senderField+ , sequenceNumberField+ , serialNumberField+ , sessionEndReasonField+ , sessionIdField+ , severityField+ , sourceAddressField+ , sourceCountryField+ , sourceIpField+ , sourcePortField+ , sourceUserField+ , sourceVmUuidField+ , sourceZoneField+ , startTimeDateField+ , startTimeTimeField+ , subjectField+ , subtypeField+ , syslogDatetimeField+ , syslogHostField+ , syslogPriorityField+ , threatCategoryField+ , threatIdField+ , timeGeneratedDateField+ , timeGeneratedTimeField+ , timeoutField+ , tunnelIdField+ , tunnelTypeField+ , typeField+ , urlCategoryListField+ , urlIndexField+ , userAgentField+ , userField+ , virtualSystemField+ , virtualSystemIdField+ , virtualSystemNameField+ ) where++import Chronos (DayOfMonth(..),Date(..),Offset(..),OffsetDatetime(..))+import Chronos (Year(..),Month(..),Datetime(..),TimeOfDay(..))+import Control.Exception (Exception)+import Control.Monad.ST.Run (runByteArrayST)+import Data.Bytes.Parser (Parser)+import Data.Bytes.Types (Bytes(..),UnmanagedBytes(UnmanagedBytes))+import Data.Char (ord)+import Data.Primitive (ByteArray)+import Data.Primitive.Addr (Addr(Addr))+import Data.Word (Word64,Word16,Word8)+import GHC.Exts (Ptr(Ptr),Int(I#),Int#,Addr#)++import qualified Chronos+import qualified Control.Exception+import qualified Data.Primitive as PM+import qualified Data.Primitive.Ptr as PM+import qualified Data.Bytes.Parser as P+import qualified Data.Bytes.Parser.Unsafe as Unsafe+import qualified Data.Bytes.Parser.Latin as Latin+import qualified GHC.Pack++-- In PAN-OS 10, datetimes started being encoded with ISO-8601+-- rather than with the YYYY/MM/DD HH:mm:ss scheme. So, we+-- allow either.+parserDatetime :: e -> e -> Parser e s Datetime+{-# noinline parserDatetime #-}+parserDatetime edate etime = do+ _ <- P.take edate 4+ Latin.any edate >>= \case+ '-' -> do+ Unsafe.unconsume 5+ OffsetDatetime t (Offset f) <- P.orElse Chronos.parserUtf8BytesIso8601 (P.fail edate)+ Latin.char etime ','+ case f of+ 0 -> pure t+ _ -> P.fail edate+ '/' -> do+ Unsafe.unconsume 5+ year <- Latin.decWord edate+ Latin.char edate '/'+ monthPlusOne <- Latin.decWord edate+ let month = monthPlusOne - 1+ if month > 11+ then P.fail edate+ else pure ()+ Latin.char edate '/'+ day <- Latin.decWord edate+ Latin.char etime ' '+ hour <- Latin.decWord etime+ Latin.char etime ':'+ minute <- Latin.decWord etime+ Latin.char etime ':'+ second <- Latin.decWord etime+ Latin.char etime ','+ pure $ Datetime+ (Date+ (Year (fromIntegral year))+ (Month (fromIntegral month))+ (DayOfMonth (fromIntegral day))+ )+ (TimeOfDay+ (fromIntegral hour)+ (fromIntegral minute)+ (1_000_000_000 * fromIntegral second)+ )+ _ -> P.fail edate++-- This does not require that any digits are+-- actually present.+skipDigitsThroughComma :: e -> Parser e s ()+{-# inline skipDigitsThroughComma #-}+skipDigitsThroughComma e =+ Latin.skipDigits *> Latin.char e ','++skipThroughComma :: e -> Parser e s ()+{-# inline skipThroughComma #-}+skipThroughComma e = Latin.skipTrailedBy e ','++w64Comma :: e -> Parser e s Word64+{-# inline w64Comma #-}+w64Comma e = do+ w <- Latin.decWord64 e+ Latin.char e ','+ pure w++w16Comma :: e -> Parser e s Word16+{-# inline w16Comma #-}+w16Comma e = Latin.decWord16 e <* Latin.char e ','+++untilComma :: e -> Parser e s Bounds+{-# inline untilComma #-}+untilComma e = do+ start <- Unsafe.cursor+ Latin.skipTrailedBy e ','+ endSucc <- Unsafe.cursor+ let end = endSucc - 1+ pure (Bounds start (end - start))++data Bounds = Bounds+ {-# UNPACK #-} !Int -- offset+ {-# UNPACK #-} !Int -- length+++-- | The field that was being parsed when a parse failure occurred.+-- This is typically for useful for libary developers, but to present+-- it to the end user, call @show@ or @throwIO@.+newtype Field = Field UnmanagedBytes++instance Show Field where+ showsPrec _ (Field (UnmanagedBytes (Addr addr) _)) s =+ '"' : GHC.Pack.unpackAppendCString# addr ('"' : s)++instance Exception Field where+ displayException (Field (UnmanagedBytes (Addr addr) _)) =+ GHC.Pack.unpackCString# addr++syslogPriorityField :: Field+syslogPriorityField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "syslogPriority"#++syslogHostField :: Field+syslogHostField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "syslogHost"#++syslogDatetimeField :: Field+syslogDatetimeField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "syslogDatetime"#++prismaDataField :: Field+prismaDataField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "prismaData"#++receiveTimeDateField :: Field+receiveTimeDateField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "receiveTime:date"#++receiveTimeTimeField :: Field+receiveTimeTimeField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "receiveTime:time"#++serialNumberField :: Field+serialNumberField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "serialNumber"#++typeField :: Field+typeField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "type"#++subtypeField :: Field+subtypeField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "subtype"#++timeGeneratedDateField :: Field+timeGeneratedDateField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "timeGenerated:date"#++timeGeneratedTimeField :: Field+timeGeneratedTimeField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "timeGenerated:time"#++sourceAddressField :: Field+sourceAddressField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "sourceAddress"#++destinationAddressField :: Field+destinationAddressField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "destinationAddress"#++natSourceIpField :: Field+natSourceIpField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "natSourceIp"#++natDestinationIpField :: Field+natDestinationIpField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "natDestinationIp"#++ruleNameField :: Field+ruleNameField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "ruleName"#++sourceUserField :: Field+sourceUserField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "sourceUser"#++destinationUserField :: Field+destinationUserField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "destinationUser"#++applicationField :: Field+applicationField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "application"#++virtualSystemField :: Field+virtualSystemField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "virtualSystem"#++sourceZoneField :: Field+sourceZoneField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "sourceZone"#++destinationZoneField :: Field+destinationZoneField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "destinationZone"#++inboundInterfaceField :: Field+inboundInterfaceField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "inboundInterface"#++outboundInterfaceField :: Field+outboundInterfaceField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "outboundInterface"#++logActionField :: Field+logActionField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "logAction"#++sessionIdField :: Field+sessionIdField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "sessionId"#++repeatCountField :: Field+repeatCountField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "repeatCount"#++sourcePortField :: Field+sourcePortField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "sourcePort"#++destinationPortField :: Field+destinationPortField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "destinationPort"#++natSourcePortField :: Field+natSourcePortField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "natSourcePort"#++natDestinationPortField :: Field+natDestinationPortField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "natDestinationPort"#++flagsField :: Field+flagsField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "flags"#++ipProtocolField :: Field+ipProtocolField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "ipProtocol"#++urlCategoryListField :: Field+urlCategoryListField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "urlCategoryList"#++actionField :: Field+actionField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "action"#++bytesField :: Field+bytesField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "bytes"#++bytesSentField :: Field+bytesSentField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "bytesSent"#++bytesReceivedField :: Field+bytesReceivedField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "bytesReceived"#++packetsField :: Field+packetsField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "packets"#++startTimeDateField :: Field+startTimeDateField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "startTime:date"#++startTimeTimeField :: Field+startTimeTimeField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "startTime:time"#++elapsedTimeField :: Field+elapsedTimeField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "elapsedTime"#++categoryField :: Field+categoryField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "category"#++sequenceNumberField :: Field+sequenceNumberField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "sequenceNumber"#++actionFlagsField :: Field+actionFlagsField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "actionFlags"#++sourceCountryField :: Field+sourceCountryField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "sourceCountry"#++destinationCountryField :: Field+destinationCountryField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "destinationCountry"#++packetsSentField :: Field+packetsSentField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "packetsSent"#++packetsReceivedField :: Field+packetsReceivedField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "packetsReceived"#++sessionEndReasonField :: Field+sessionEndReasonField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "sessionEndReason"#++deviceGroupHierarchyLevel1Field :: Field+deviceGroupHierarchyLevel1Field = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "deviceGroupHierarchyLevel1"#++deviceGroupHierarchyLevel2Field :: Field+deviceGroupHierarchyLevel2Field = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "deviceGroupHierarchyLevel2"#++deviceGroupHierarchyLevel3Field :: Field+deviceGroupHierarchyLevel3Field = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "deviceGroupHierarchyLevel3"#++deviceGroupHierarchyLevel4Field :: Field+deviceGroupHierarchyLevel4Field = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "deviceGroupHierarchyLevel4"#++virtualSystemNameField :: Field+virtualSystemNameField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "virtualSystemName"#++payloadProtocolField :: Field+payloadProtocolField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "field:payloadProtocol"#++senderField :: Field+senderField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "field:sender"#++recipientField :: Field+recipientField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "field:recipient"#++refererField :: Field+refererField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "field:referer"#++pcapIdField :: Field+pcapIdField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "field:pcapId"#++directionField :: Field+directionField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "field:direction"#++contentTypeField :: Field+contentTypeField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "field:contentType"#++severityField :: Field+severityField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "field:severity"#++cloudField :: Field+cloudField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "field:cloud"#++threatCategoryField :: Field+threatCategoryField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "field:threatCategory"#++urlIndexField :: Field+urlIndexField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "field:urlIndex"#++fileDigestField :: Field+fileDigestField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "field:fileDigest"#++fileTypeField :: Field+fileTypeField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "field:fileType"#++forwardedForField :: Field+forwardedForField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "field:forwardedFor"#++userAgentField :: Field+userAgentField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "field:userAgent"#++subjectField :: Field+subjectField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "field:subject"#++contentVersionField :: Field+contentVersionField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "field:contentVersion"#++httpMethodField :: Field+httpMethodField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "field:httpMethod"#++httpHeadersField :: Field+httpHeadersField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "field:httpHeaders"#++reportIdField :: Field+reportIdField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "field:reportId"#++miscellaneousField :: Field+miscellaneousField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "field:miscellaneous"#++threatIdField :: Field+threatIdField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "field:threatId"#++deviceNameField :: Field+deviceNameField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "deviceName"#++actionSourceField :: Field+actionSourceField = Field (UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "actionSource"#++futureUseAField :: Field+futureUseAField = Field (UnmanagedBytes (Addr x#) (I# (cstringLen# x#)))+ where !x# = "futureUse:A"#++futureUseBField :: Field+futureUseBField = Field (UnmanagedBytes (Addr x#) (I# (cstringLen# x#)))+ where !x# = "futureUse:B"#++futureUseCField :: Field+futureUseCField = Field (UnmanagedBytes (Addr x#) (I# (cstringLen# x#)))+ where !x# = "futureUse:C"#++futureUseDField :: Field+futureUseDField = Field (UnmanagedBytes (Addr x#) (I# (cstringLen# x#)))+ where !x# = "futureUse:D"#++futureUseEField :: Field+futureUseEField = Field (UnmanagedBytes (Addr x#) (I# (cstringLen# x#)))+ where !x# = "futureUse:E"#++futureUseFField :: Field+futureUseFField = Field (UnmanagedBytes (Addr x#) (I# (cstringLen# x#)))+ where !x# = "futureUse:F"#++futureUseGField :: Field+futureUseGField = Field (UnmanagedBytes (Addr x#) (I# (cstringLen# x#)))+ where !x# = "futureUse:G"#++leftoversField :: Field+leftoversField = Field (UnmanagedBytes (Addr x#) (I# (cstringLen# x#)))+ where !x# = "framing:leftovers"#++sourceVmUuidField :: Field+sourceVmUuidField = Field (UnmanagedBytes (Addr x#) (I# (cstringLen# x#)))+ where !x# = "field:source_uuid"#++destinationVmUuidField :: Field+destinationVmUuidField = Field (UnmanagedBytes (Addr x#) (I# (cstringLen# x#)))+ where !x# = "field:dst_uuid"#++tunnelIdField :: Field+tunnelIdField = Field (UnmanagedBytes (Addr x#) (I# (cstringLen# x#)))+ where !x# = "field:tunnelid"#++monitorTagField :: Field+monitorTagField = Field (UnmanagedBytes (Addr x#) (I# (cstringLen# x#)))+ where !x# = "field:monitortag"#++parentSessionIdField :: Field+parentSessionIdField = Field (UnmanagedBytes (Addr x#) (I# (cstringLen# x#)))+ where !x# = "field:parent_session_id"#++parentStartTimeField :: Field+parentStartTimeField = Field (UnmanagedBytes (Addr x#) (I# (cstringLen# x#)))+ where !x# = "field:parent_start_time"#++tunnelTypeField :: Field+tunnelTypeField = Field (UnmanagedBytes (Addr x#) (I# (cstringLen# x#)))+ where !x# = "field:tunnel"#++sctpAssociationIdField :: Field+sctpAssociationIdField = Field (UnmanagedBytes (Addr x#) (I# (cstringLen# x#)))+ where !x# = "field:assoc_id"#++sctpChunksField :: Field+sctpChunksField = Field (UnmanagedBytes (Addr x#) (I# (cstringLen# x#)))+ where !x# = "field:chunks"#++sctpChunksSentField :: Field+sctpChunksSentField = Field (UnmanagedBytes (Addr x#) (I# (cstringLen# x#)))+ where !x# = "field:chunks_sent"#++sctpChunksReceivedField :: Field+sctpChunksReceivedField = Field (UnmanagedBytes (Addr x#) (I# (cstringLen# x#)))+ where !x# = "field:chunks_received"#++ruleUuidField :: Field+ruleUuidField = Field (UnmanagedBytes (Addr x#) (I# (cstringLen# x#)))+ where !x# = "field:rule_uuid"#++http2ConnectionField :: Field+http2ConnectionField = Field (UnmanagedBytes (Addr x#) (I# (cstringLen# x#)))+ where !x# = "field:http_2_connection"#++linkChangeCountField :: Field+linkChangeCountField = Field (UnmanagedBytes (Addr x#) (I# (cstringLen# x#)))+ where !x# = "field:link_change_count_field"#++moduleField :: Field+moduleField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "field:module"#++descriptionField :: Field+descriptionField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "field:description"#++eventIdField :: Field+eventIdField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "field:eventId"#++objectField :: Field+objectField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "field:object"#++userField :: Field+userField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "field:user"#++dataSourceNameField :: Field+dataSourceNameField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "field:datasourcename"#++timeoutField :: Field+timeoutField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "field:timeout"#++ipField :: Field+ipField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "field:ip"#++dataSourceField :: Field+dataSourceField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "field:datasource"#++dataSourceTypeField :: Field+dataSourceTypeField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "field:datasourcetype"#++sourceIpField :: Field+sourceIpField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "field:source_ip"#++evidenceField :: Field+evidenceField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "field:evidence"#++objectIdField :: Field+objectIdField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "field:objectid"#++objectNameField :: Field+objectNameField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "field:objectname"#++virtualSystemIdField :: Field+virtualSystemIdField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))+ where !x# = "field:virtualsystemid"#++-- TODO: switch to the known-key cstrlen that comes with GHC +cstringLen# :: Addr# -> Int#+{-# noinline cstringLen# #-}+cstringLen# ptr = go 0 where+ go !ix@(I# ix#) = if PM.indexOffPtr (Ptr ptr) ix == (0 :: Word8)+ then ix#+ else go (ix + 1)++parserOptionallyQuoted_ :: e -> Parser e s ()+parserOptionallyQuoted_ e = Latin.any e >>= \case+ '"' -> do+ _ <- consumeQuoted e 0+ pure ()+ ',' -> pure ()+ _ -> Latin.skipTrailedBy e ','++-- Precondition: the cursor is placed at the beginning of the+-- possibly-quoted content. That is, the comma preceeding has+-- already been consumed. This is very similar to parserOptionallyQuoted,+-- but it differs slightly because might not be a trailing comma. If+-- there is, it gets left alone.+finalOptionallyQuoted :: e -> Parser e s Bytes+finalOptionallyQuoted e = Latin.opt >>= \case+ Nothing -> do+ !array <- Unsafe.expose+ pure $! Bytes{array,offset=0,length=0}+ Just c -> case c of+ '"' -> do+ -- First, we do a run through just to see if anything+ -- actually needs to be escaped.+ start <- Unsafe.cursor+ !n <- consumeFinalQuoted e 0+ !array <- Unsafe.expose+ !endSucc <- Unsafe.cursor+ let end = endSucc - 1+ if n == 0+ then pure Bytes{array,offset=start,length=(end - start)}+ else do+ let !r = escapeQuotes Bytes{array,offset=start,length=(end - start)}+ pure $! Bytes{array=r,offset=0,length=PM.sizeofByteArray r}+ ',' -> do+ Unsafe.unconsume 1+ !arr <- Unsafe.expose+ pure $! Bytes arr 0 0+ _ -> do+ !startSucc <- Unsafe.cursor+ Latin.skipUntil ','+ !end <- Unsafe.cursor+ !arr <- Unsafe.expose+ let start = startSucc - 1+ pure $! Bytes arr start (end - start)++-- Precondition: the cursor is placed at the beginning of the+-- possibly-quoted content. That is, the comma preceeding has+-- already been consumed.+parserOptionallyQuoted :: e -> Parser e s Bytes+parserOptionallyQuoted e = Latin.any e >>= \case+ '"' -> do+ -- First, we do a run through just to see if anything+ -- actually needs to be escaped.+ start <- Unsafe.cursor+ !n <- consumeQuoted e 0+ !array <- Unsafe.expose+ !endSuccSucc <- Unsafe.cursor+ let end = endSuccSucc - 2+ if n == 0+ then pure Bytes{array,offset=start,length=(end - start)}+ else do+ let !r = escapeQuotes Bytes{array,offset=start,length=(end - start)}+ pure $! Bytes{array=r,offset=0,length=PM.sizeofByteArray r}+ ',' -> do+ !array <- Unsafe.expose+ pure $! Bytes{array,offset=0,length=0}+ _ -> do+ !startSucc <- Unsafe.cursor+ Latin.skipTrailedBy e ','+ !endSucc <- Unsafe.cursor+ !arr <- Unsafe.expose+ let start = startSucc - 1+ let end = endSucc - 1+ pure $! (Bytes arr start (end - start))++-- Precondition: the input is a valid CSV-style quoted-escaped+-- string. That is, any double quote character is guaranteed to+-- be followed by another one.+escapeQuotes :: Bytes -> ByteArray+escapeQuotes (Bytes arr off0 len0) = runByteArrayST $ do+ marr <- PM.newByteArray len0+ let go !soff !doff !len = if len > 0+ then do+ let w :: Word8 = PM.indexByteArray arr soff+ PM.writeByteArray marr doff w+ if w /= c2w '"'+ then go (soff + 1) (doff + 1) (len - 1)+ else go (soff + 2) (doff + 1) (len - 2)+ else pure doff+ finalSz <- go off0 0 len0+ marr' <- PM.resizeMutableByteArray marr finalSz+ PM.unsafeFreezeByteArray marr'++-- When this parser completed, the position in the input will be+-- just after the comma that followed the quoted field.+-- This is defined recursively.+consumeQuoted ::+ e+ -> Int -- the number of escaped quotes we have encountered+ -> Parser e s Int+consumeQuoted e !n = do+ Latin.skipTrailedBy e '"'+ Latin.any e >>= \case+ ',' -> pure n+ '"' -> consumeQuoted e (n + 1)+ _ -> P.fail e++-- Like consumeQuoted except that we are expected end-of-input+-- instead of a comma at the end.+consumeFinalQuoted ::+ e+ -> Int -- the number of escaped quotes we have encountered+ -> Parser e s Int+consumeFinalQuoted e !n = do+ Latin.skipTrailedBy e '"'+ Latin.opt >>= \case+ Nothing -> pure n+ Just c -> case c of+ '"' -> consumeFinalQuoted e (n + 1)+ ',' -> do+ Unsafe.unconsume 1+ pure n+ _ -> P.fail e++c2w :: Char -> Word8+{-# inline c2w #-}+c2w = fromIntegral . ord
+ src/Panos/Syslog/Internal/Correlation.hs view
@@ -0,0 +1,104 @@+{-# language BangPatterns #-}+{-# language DerivingStrategies #-}+{-# language DuplicateRecordFields #-}+{-# language GeneralizedNewtypeDeriving #-}+{-# language LambdaCase #-}+{-# language MagicHash #-}+{-# language MultiWayIf #-}+{-# language NamedFieldPuns #-}+{-# language NumericUnderscores #-}+{-# language ScopedTypeVariables #-}+++module Panos.Syslog.Internal.Correlation+ ( Correlation(..)+ , parserCorrelation+ ) where++import Chronos (Datetime)+import Data.Bytes.Parser (Parser)+import Data.Bytes.Types (Bytes(..))+import Data.Primitive (ByteArray)+import Data.Word (Word64)+import Panos.Syslog.Internal.Common+import Net.Types (IP)++import qualified Data.Bytes.Parser.Latin as Latin+import qualified Data.Bytes.Parser.Unsafe as Unsafe+import qualified Net.IP as IP++-- | A PAN-OS correlation log. Read-only accessors are found in+-- @Panos.Syslog.Correlation@.+data Correlation = Correlation+ { message :: {-# UNPACK #-} !ByteArray+ -- The original log+ , syslogHost :: {-# UNPACK #-} !Bounds+ -- The host as presented in the syslog preamble that+ -- prefixes the message.+ , receiveTime :: {-# UNPACK #-} !Datetime+ -- In log, presented as: 2019/06/18 15:10:20+ , serialNumber :: {-# UNPACK #-} !Bounds+ -- In log, presented as: 002610378847+ , subtype :: {-# UNPACK #-} !Bounds+ -- Presented as: dhcp, dnsproxy, dos, general, etc.+ , timeGenerated :: {-# UNPACK #-} !Datetime+ -- Presented as: 2019/11/04 08:39:05+ , sourceAddress :: {-# UNPACK #-} !IP+ , sourceUser :: {-# UNPACK #-} !Bounds+ , virtualSystem :: {-# UNPACK #-} !Bounds+ , category :: {-# UNPACK #-} !Bounds+ , severity :: {-# UNPACK #-} !Bounds+ , deviceGroupHierarchyLevel1 :: {-# UNPACK #-} !Word64+ , deviceGroupHierarchyLevel2 :: {-# UNPACK #-} !Word64+ , deviceGroupHierarchyLevel3 :: {-# UNPACK #-} !Word64+ , deviceGroupHierarchyLevel4 :: {-# UNPACK #-} !Word64+ , deviceName :: {-# UNPACK #-} !Bounds+ , objectName :: {-# UNPACK #-} !Bounds+ , objectId :: {-# UNPACK #-} !Word64+ , evidence :: {-# UNPACK #-} !Bytes+ }++parserCorrelation :: Bounds -> Datetime -> Bounds -> Parser Field s Correlation+parserCorrelation !syslogHost receiveTime !serialNumber = do+ !message <- Unsafe.expose+ subtype <- untilComma subtypeField -- usually empty for correlations+ skipThroughComma futureUseAField+ -- The datetime parser consumes the trailing comma+ timeGenerated <- parserDatetime timeGeneratedDateField timeGeneratedTimeField+ sourceAddress <- IP.parserUtf8Bytes sourceIpField+ Latin.char sourceAddressField ','+ sourceUser <- untilComma sourceUserField+ virtualSystem <- untilComma virtualSystemField+ category <- untilComma categoryField+ severity <- untilComma severityField+ deviceGroupHierarchyLevel1 <- w64Comma deviceGroupHierarchyLevel1Field+ deviceGroupHierarchyLevel2 <- w64Comma deviceGroupHierarchyLevel2Field+ deviceGroupHierarchyLevel3 <- w64Comma deviceGroupHierarchyLevel3Field+ deviceGroupHierarchyLevel4 <- w64Comma deviceGroupHierarchyLevel4Field+ skipThroughComma virtualSystemNameField+ deviceName <- untilComma deviceNameField+ skipThroughComma virtualSystemIdField+ objectName <- untilComma objectNameField+ objectId <- w64Comma objectIdField+ evidence <- finalOptionallyQuoted evidenceField+ pure Correlation+ { message+ , syslogHost+ , receiveTime+ , serialNumber+ , subtype+ , timeGenerated+ , virtualSystem+ , category+ , severity+ , deviceGroupHierarchyLevel1+ , deviceGroupHierarchyLevel2+ , deviceGroupHierarchyLevel3+ , deviceGroupHierarchyLevel4+ , deviceName+ , objectName+ , objectId+ , evidence+ , sourceAddress+ , sourceUser+ }
+ src/Panos/Syslog/Internal/System.hs view
@@ -0,0 +1,108 @@+{-# language BangPatterns #-}+{-# language DerivingStrategies #-}+{-# language DuplicateRecordFields #-}+{-# language GeneralizedNewtypeDeriving #-}+{-# language LambdaCase #-}+{-# language MagicHash #-}+{-# language NamedFieldPuns #-}+{-# language NumericUnderscores #-}+{-# language ScopedTypeVariables #-}+module Panos.Syslog.Internal.System+ ( System(..)+ , parserSystem+ ) where++import Panos.Syslog.Internal.Common++import Chronos (Datetime)+import Data.Bytes.Parser (Parser)+import Data.Bytes.Types (Bytes(..))+import Data.Primitive (ByteArray)+import Data.Word (Word64)++import qualified Data.Bytes.Parser.Latin as Latin+import qualified Data.Bytes.Parser.Unsafe as Unsafe++-- | A PAN-OS system log. Read-only accessors are found in+-- @Panos.Syslog.System@.+data System = System+ { message :: {-# UNPACK #-} !ByteArray+ -- The original log+ , syslogHost :: {-# UNPACK #-} !Bounds+ -- The host as presented in the syslog preamble that+ -- prefixes the message.+ , receiveTime :: {-# UNPACK #-} !Datetime+ -- In log, presented as: 2019/06/18 15:10:20+ , serialNumber :: {-# UNPACK #-} !Bounds+ -- In log, presented as: 002610378847+ , subtype :: {-# UNPACK #-} !Bounds+ -- Presented as: dhcp, dnsproxy, dos, general, etc.+ , timeGenerated :: {-# UNPACK #-} !Datetime+ -- Presented as: 2019/11/04 08:39:05+ , virtualSystem :: {-# UNPACK #-} !Bounds+ , eventId :: {-# UNPACK #-} !Bounds+ , object :: {-# UNPACK #-} !Bounds+ , module_ :: {-# UNPACK #-} !Bounds+ , severity :: {-# UNPACK #-} !Bounds+ , descriptionBounds :: {-# UNPACK #-} !Bounds+ , descriptionByteArray :: {-# UNPACK #-} !ByteArray+ , sequenceNumber :: {-# UNPACK #-} !Word64+ , actionFlags :: {-# UNPACK #-} !Word64+ , deviceGroupHierarchyLevel1 :: {-# UNPACK #-} !Word64+ , deviceGroupHierarchyLevel2 :: {-# UNPACK #-} !Word64+ , deviceGroupHierarchyLevel3 :: {-# UNPACK #-} !Word64+ , deviceGroupHierarchyLevel4 :: {-# UNPACK #-} !Word64+ , virtualSystemName :: {-# UNPACK #-} !Bounds+ , deviceName :: {-# UNPACK #-} !Bounds+ }++parserSystem :: Bounds -> Datetime -> Bounds -> Parser Field s System+parserSystem syslogHost receiveTime serialNumber = do+ subtype <- untilComma subtypeField+ skipThroughComma futureUseAField+ -- The datetime parser consumes the trailing comma+ timeGenerated <- parserDatetime timeGeneratedDateField timeGeneratedTimeField+ virtualSystem <- untilComma virtualSystemField+ eventId <- untilComma eventIdField+ object <- untilComma objectField+ skipThroughComma futureUseBField+ skipThroughComma futureUseCField+ module_ <- untilComma moduleField+ severity <- untilComma severityField+ Bytes{array=descriptionByteArray,offset=descrOff,length=descrLen} <-+ parserOptionallyQuoted descriptionField+ let descriptionBounds = Bounds descrOff descrLen+ sequenceNumber <- w64Comma sequenceNumberField+ -- TODO: handle action flags+ Latin.char actionFlagsField '0'+ Latin.char actionFlagsField 'x'+ _ <- untilComma actionFlagsField+ let actionFlags = 0+ deviceGroupHierarchyLevel1 <- w64Comma deviceGroupHierarchyLevel1Field+ deviceGroupHierarchyLevel2 <- w64Comma deviceGroupHierarchyLevel2Field+ deviceGroupHierarchyLevel3 <- w64Comma deviceGroupHierarchyLevel3Field+ deviceGroupHierarchyLevel4 <- w64Comma deviceGroupHierarchyLevel4Field+ virtualSystemName <- untilComma virtualSystemNameField+ deviceName <- finalField+ message <- Unsafe.expose+ pure System+ { subtype , timeGenerated+ , sequenceNumber + , deviceGroupHierarchyLevel1 , deviceGroupHierarchyLevel2 + , deviceGroupHierarchyLevel3 , deviceGroupHierarchyLevel4 + , virtualSystemName , deviceName , receiveTime+ , serialNumber, actionFlags, message+ , syslogHost, virtualSystem, eventId, object, module_+ , severity, descriptionBounds, descriptionByteArray+ }++-- There should not be any more commas left in the input.+-- This takes until it finds a comma or until end of input+-- is reached.+finalField :: Parser e s Bounds+{-# inline finalField #-}+finalField = do+ start <- Unsafe.cursor+ Latin.skipUntil ','+ end <- Unsafe.cursor+ pure (Bounds start (end - start))
+ src/Panos/Syslog/Internal/Threat.hs view
@@ -0,0 +1,486 @@+{-# language BangPatterns #-}+{-# language DerivingStrategies #-}+{-# language DuplicateRecordFields #-}+{-# language GeneralizedNewtypeDeriving #-}+{-# language LambdaCase #-}+{-# language MagicHash #-}+{-# language MultiWayIf #-}+{-# language NamedFieldPuns #-}+{-# language NumericUnderscores #-}+{-# language ScopedTypeVariables #-}+module Panos.Syslog.Internal.Threat+ ( Threat(..)+ , parserThreat+ ) where++import Panos.Syslog.Internal.Common++import Chronos (Datetime)+import Control.Monad (when)+import Data.Bytes.Parser (Parser)+import Data.Bytes.Types (Bytes(..))+import Data.Char (isAsciiUpper,isAsciiLower)+import Data.Primitive (ByteArray)+import Data.Word (Word64,Word32,Word16)+import GHC.Exts (Int(I#),Ptr(Ptr))+import Net.Types (IP(IP),IPv6(IPv6))+import Data.WideWord (Word128)++import qualified Data.Bytes as Bytes+import qualified Data.Bytes.Parser as P+import qualified Data.Bytes.Parser.Latin as Latin+import qualified Data.Bytes.Parser.Unsafe as Unsafe+import qualified Data.Primitive as PM+import qualified GHC.Exts as Exts+import qualified Net.IP as IP+import qualified UUID++-- | A PAN-OS threat log. Read-only accessors are found in+-- @Panos.Syslog.Threat@.+data Threat = Threat+ { message :: {-# UNPACK #-} !ByteArray+ -- The original log+ , syslogHost :: {-# UNPACK #-} !Bounds+ -- The host as presented in the syslog preamble that+ -- prefixes the message.+ , receiveTime :: {-# UNPACK #-} !Datetime+ -- In log, presented as: 2019/06/18 15:10:20+ , serialNumber :: {-# UNPACK #-} !Bounds+ -- In log, presented as: 002610378847+ , subtype :: {-# UNPACK #-} !Bounds+ -- Presented as: data, file, flood, packet, scan, spyware, url,+ -- virus, vulnerability, wildfire, or wildfire-virus.+ , timeGenerated :: {-# UNPACK #-} !Datetime+ -- Presented as: 2018/04/11 23:19:22+ , sourceAddress :: {-# UNPACK #-} !IP+ , destinationAddress :: {-# UNPACK #-} !IP+ , natSourceIp :: {-# UNPACK #-} !IP+ , natDestinationIp :: {-# UNPACK #-} !IP+ , ruleName :: {-# UNPACK #-} !Bounds+ , sourceUser :: {-# UNPACK #-} !Bounds+ , destinationUser :: {-# UNPACK #-} !Bounds+ , application :: {-# UNPACK #-} !Bounds+ , virtualSystem :: {-# UNPACK #-} !Bounds+ , sourceZone :: {-# UNPACK #-} !Bounds+ , destinationZone :: {-# UNPACK #-} !Bounds+ , inboundInterface :: {-# UNPACK #-} !Bounds+ , outboundInterface :: {-# UNPACK #-} !Bounds+ , logAction :: {-# UNPACK #-} !Bounds+ , sessionId :: {-# UNPACK #-} !Word64+ , repeatCount :: {-# UNPACK #-} !Word64+ , sourcePort :: {-# UNPACK #-} !Word16+ , destinationPort :: {-# UNPACK #-} !Word16+ , natSourcePort :: {-# UNPACK #-} !Word16+ , natDestinationPort :: {-# UNPACK #-} !Word16+ , action :: {-# UNPACK #-} !Bounds+ , ipProtocol :: {-# UNPACK #-} !Bounds+ , flags :: {-# UNPACK #-} !Word32+ , miscellaneousBounds :: {-# UNPACK #-} !Bounds+ , miscellaneousByteArray :: {-# UNPACK #-} !ByteArray+ , threatName :: {-# UNPACK #-} !Bounds+ , threatId :: {-# UNPACK #-} !Word64+ , category :: {-# UNPACK #-} !Bounds+ , severity :: {-# UNPACK #-} !Bounds+ , direction :: {-# UNPACK #-} !Bounds+ , sequenceNumber :: {-# UNPACK #-} !Word64+ , actionFlags :: {-# UNPACK #-} !Word64+ -- Presented as: 0x8000000000000000+ , sourceCountry :: {-# UNPACK #-} !Bounds+ , destinationCountry :: {-# UNPACK #-} !Bounds+ , contentType :: {-# UNPACK #-} !Bounds+ , pcapId :: {-# UNPACK #-} !Word64+ , fileDigest :: {-# UNPACK #-} !Bounds+ -- Only used by wildfire subtype+ -- TODO: make the file digest a 128-bit or 256-bit word+ , cloud :: {-# UNPACK #-} !Bounds+ -- Only used by wildfire subtype+ , urlIndex :: {-# UNPACK #-} !Word64+ -- Only used by wildfire subtype+ , userAgentBounds :: {-# UNPACK #-} !Bounds+ , userAgentByteArray :: {-# UNPACK #-} !ByteArray+ -- Only used by url filtering subtype. This field may have+ -- escaped characters, so we include the possibility of+ -- using a byte array distinct from the original log.+ , fileType :: {-# UNPACK #-} !Bounds+ -- Only used by wildfire subtype+ , forwardedFor :: {-# UNPACK #-} !Bounds+ -- Only used by url filtering subtype+ , referer :: {-# UNPACK #-} !Bytes+ -- Only used by url filtering subtype+ , sender :: {-# UNPACK #-} !Bytes+ -- Only used by wildfire subtype+ , subject :: {-# UNPACK #-} !Bytes+ -- Only used by wildfire subtype+ , recipient :: {-# UNPACK #-} !Bytes+ -- Only used by wildfire subtype+ , reportId :: {-# UNPACK #-} !Bounds+ -- Only used by wildfire subtype+ , deviceGroupHierarchyLevel1 :: {-# UNPACK #-} !Word64+ , deviceGroupHierarchyLevel2 :: {-# UNPACK #-} !Word64+ , deviceGroupHierarchyLevel3 :: {-# UNPACK #-} !Word64+ , deviceGroupHierarchyLevel4 :: {-# UNPACK #-} !Word64+ , virtualSystemName :: {-# UNPACK #-} !Bounds+ , deviceName :: {-# UNPACK #-} !Bounds+ -- TODO: skipping over uuid fields for now+ , httpMethod :: {-# UNPACK #-} !Bounds+ , tunnelId :: {-# UNPACK #-} !Word64+ , parentSessionId :: {-# UNPACK #-} !Word64+ -- Only used by url subtype+ , threatCategory :: {-# UNPACK #-} !Bounds+ , contentVersion :: {-# UNPACK #-} !Bounds+ -- TODO: skipping some fields here+ , sctpAssociationId :: {-# UNPACK #-} !Word64+ , payloadProtocolId :: {-# UNPACK #-} !Word64+ -- TODO: skipping over other fields here+ , httpHeaders :: {-# UNPACK #-} !Bytes+ , urlCategoryList :: {-# UNPACK #-} !Bytes+ , ruleUuid :: {-# UNPACK #-} !Word128+ }+++parserThreat :: Bounds -> Datetime -> Bounds -> Parser Field s Threat+parserThreat !syslogHost receiveTime !serialNumber = do+ !message <- Unsafe.expose+ subtype@(Bounds subtypeOff subtypeLen) <- untilComma subtypeField+ let !subtypeB = Bytes message subtypeOff subtypeLen+ let !isWildfire =+ if | Bytes.equalsCString (Ptr "wildfire"# ) subtypeB -> 1 :: Int+ | otherwise -> 0 :: Int+ let !isUrl =+ if | Bytes.equalsCString (Ptr "url"# ) subtypeB -> 1 :: Int+ | otherwise -> 0 :: Int+ let !isSpyware =+ if | Bytes.equalsCString (Ptr "spyware"# ) subtypeB -> 1 :: Int+ | otherwise -> 0 :: Int+ Bounds futureUseAOff futureUseALen <- untilComma futureUseAField+ let !futureUseA = Bytes message futureUseAOff futureUseALen+ let !version =+ if | Bytes.equalsCString (Ptr "10.0"# ) futureUseA -> 100 :: Int+ | Bytes.equalsCString (Ptr "10.1"# ) futureUseA -> 101 :: Int+ | otherwise -> 0 :: Int+ -- the datetime parser also grabs the trailing comma+ timeGenerated <- parserDatetime timeGeneratedDateField timeGeneratedTimeField+ sourceAddress <- IP.parserUtf8Bytes sourceAddressField+ Latin.char sourceAddressField ','+ destinationAddress <- IP.parserUtf8Bytes destinationAddressField+ Latin.char destinationAddressField ','+ -- Use the ip address zero when no NAT address is present.+ natSourceIp <- Latin.trySatisfy (==',') >>= \case+ True -> pure (IP (IPv6 0))+ False -> do+ natSourceIp <- IP.parserUtf8Bytes natSourceIpField+ Latin.char natSourceIpField ','+ pure natSourceIp+ natDestinationIp <- Latin.trySatisfy (==',') >>= \case+ True -> pure (IP (IPv6 0))+ False -> do+ natDestinationIp <- IP.parserUtf8Bytes natDestinationIpField+ Latin.char natDestinationIpField ','+ pure natDestinationIp+ ruleName <- untilComma ruleNameField+ sourceUser <- untilComma sourceUserField+ destinationUser <- untilComma destinationUserField+ application <- untilComma applicationField+ virtualSystem <- untilComma virtualSystemField+ sourceZone <- untilComma sourceZoneField+ destinationZone <- untilComma destinationZoneField+ inboundInterface <- untilComma inboundInterfaceField+ outboundInterface <- untilComma outboundInterfaceField+ logAction <- untilComma logActionField+ -- According to Palo Alto's documentation, the field after log_action+ -- is a future use field. However, in some (possibly all) PAN-OS 10+ -- logs, this field is missing. In all other logs, it is a+ -- YYYY/MM/DD HH:mm:ss timestamp. If the field is exactly 19 bytes long,+ -- we assume that it is the unused field. Otherwise, we assume that the+ -- unused field is missing, and we try to interpret these bytes as the+ -- session_id.+ futureCursorB <- Unsafe.cursor+ Bounds _ futureLenB <- untilComma futureUseBField+ sessionId <- case futureLenB of+ 19 -> w64Comma sessionIdField+ _ -> do+ Unsafe.jump futureCursorB+ w64Comma sessionIdField+ repeatCount <- w64Comma repeatCountField+ sourcePort <- w16Comma sourcePortField+ destinationPort <- w16Comma destinationPortField+ natSourcePort <- w16Comma natSourcePortField+ natDestinationPort <- w16Comma natDestinationPortField+ -- Note: Flags are ignored. Also, in either PAN-OS 10 or Prisma+ -- (not sure which one causes this), flags are missing.+ Latin.trySatisfy (=='0') >>= \case+ True -> do+ Latin.char flagsField 'x'+ _ <- untilComma flagsField+ pure ()+ False -> pure ()+ let flags = 0+ ipProtocol <- untilComma ipProtocolField+ action <- untilComma actionField+ Bytes{array=miscellaneousByteArray,offset=miscOff,length=miscLen} <-+ parserOptionallyQuoted miscellaneousField+ let miscellaneousBounds = Bounds miscOff miscLen+ -- Detect presence of threat name and id by looking for trailing+ -- close paren on field.+ threatIdCursor <- Unsafe.cursor+ Bounds threatIdOff threatIdLen <- untilComma threatIdField+ Unsafe.jump threatIdCursor+ (threatName,threatId) <- case Bytes.isByteSuffixOf 0x29 (Bytes message threatIdOff threatIdLen) of+ True -> parserThreatId+ False -> pure (Bounds threatIdOff 0, 0)+ category <-+ if | version < 100 || isUrl == 1 -> untilComma categoryField+ | otherwise -> do+ off <- Unsafe.cursor+ pure (Bounds off 0)+ severity <- untilComma severityField+ direction <- untilComma directionField+ sequenceNumber <- w64Comma sequenceNumberField+ -- Note: Action flags are ignored. See note on Flags as well.+ Latin.trySatisfy (=='0') >>= \case+ True -> do+ Latin.char actionFlagsField 'x'+ _ <- untilComma actionFlagsField+ pure ()+ False -> pure ()+ let actionFlags = 0+ sourceCountry <- untilComma sourceCountryField+ destinationCountry <- untilComma destinationCountryField+ -- This future use is always either zero or empty before PAN-OS 10.x,+ -- and in newer versions it is suppressed entirely. However, the+ -- following field is content_type, which never starts with zero. + if | version >= 100, isUrl == 1 -> pure ()+ | version >= 100, isSpyware == 1 -> do+ -- I cannot figure out why, in PAN-OS 10, there is an extra field+ -- in spyware logs.+ Latin.skipDigits1 futureUseEField+ Latin.char2 futureUseEField ',' ','+ | otherwise -> do+ Latin.skipDigits+ Latin.char futureUseEField ','+ -- In PAN-OS 10.x, content_type only shows up in URL logs.+ -- I've added spyware here as well, but I'm not sure if this+ -- is correct. Content type definitely does not show up in+ -- wildfire logs.+ contentType <-+ if | version < 100 || isUrl == 1 || isSpyware == 1 -> untilComma contentTypeField+ | otherwise -> do+ off <- Unsafe.cursor+ pure (Bounds off 0)+ -- Wildfire logs in PAN-OS 10.x do not have a pcap_id field.+ pcapId <-+ if | version < 100 || isUrl == 1 || isSpyware == 1 -> w64Comma pcapIdField+ | otherwise -> pure 0+ -- In PAN-OS 10.x, file_digest and cloud only show up in wildfire logs.+ fileDigest <-+ if | version < 100 || isWildfire == 1 || isSpyware == 1 -> untilComma fileDigestField+ | otherwise -> do+ off <- Unsafe.cursor+ pure (Bounds off 0)+ cloud <-+ if | version < 100 || isWildfire == 1 || isSpyware == 1 -> untilComma cloudField+ | otherwise -> do+ off <- Unsafe.cursor+ pure (Bounds off 0)+ urlIndex <-+ if | version < 100 || isUrl == 1 || isWildfire == 1 -> w64Comma urlIndexField+ | otherwise -> pure 0+ Bytes{array=userAgentByteArray,offset=uaOff,length=uaLen} <-+ if | version < 100 || isUrl == 1 -> parserOptionallyQuoted userAgentField+ | otherwise -> do+ off <- Unsafe.cursor+ pure (Bytes message off 0)+ let userAgentBounds = Bounds uaOff uaLen+ fileType <-+ if | version < 100 || isWildfire == 1 || isSpyware == 1 -> untilComma fileTypeField+ | otherwise -> do+ off <- Unsafe.cursor+ pure (Bounds off 0)+ forwardedFor <-+ if | version < 100 || isUrl == 1 -> untilComma forwardedForField+ | otherwise -> do+ off <- Unsafe.cursor+ pure (Bounds off 0)+ referer <-+ if | version < 100 || isUrl == 1 -> parserOptionallyQuoted refererField+ | otherwise -> do+ off <- Unsafe.cursor+ pure (Bytes message off 0)+ sender <-+ if | version < 100 || isWildfire == 1 -> parserOptionallyQuoted senderField+ | otherwise -> do+ off <- Unsafe.cursor+ pure (Bytes message off 0)+ subject <-+ if | version < 100 || isWildfire == 1 -> parserOptionallyQuoted subjectField+ | otherwise -> do+ off <- Unsafe.cursor+ pure (Bytes message off 0)+ recipient <-+ if | version < 100 || isWildfire == 1 -> parserOptionallyQuoted recipientField+ | otherwise -> do+ off <- Unsafe.cursor+ pure (Bytes message off 0)+ reportId <-+ if | version < 100 || isWildfire == 1 || isSpyware == 1 -> untilComma reportIdField+ | otherwise -> do+ off <- Unsafe.cursor+ pure (Bounds off 0)+ deviceGroupHierarchyLevel1 <- w64Comma deviceGroupHierarchyLevel1Field+ deviceGroupHierarchyLevel2 <- w64Comma deviceGroupHierarchyLevel2Field+ deviceGroupHierarchyLevel3 <- w64Comma deviceGroupHierarchyLevel3Field+ deviceGroupHierarchyLevel4 <- w64Comma deviceGroupHierarchyLevel4Field+ virtualSystemName <- untilComma virtualSystemNameField+ deviceName <- untilComma deviceNameField+ -- On PAN-OS 10+, this future use field is omitted.+ when (version < 100) (parserOptionallyQuoted_ futureUseFField)+ skipThroughComma sourceVmUuidField+ skipThroughComma destinationVmUuidField+ httpMethod <-+ if | version < 100 || isUrl == 1 || isSpyware == 1 -> untilComma httpMethodField+ | otherwise -> do+ off <- Unsafe.cursor+ pure (Bounds off 0)+ tunnelId <- w64Comma tunnelIdField+ skipThroughComma monitorTagField+ parentSessionId <- w64Comma parentSessionIdField+ skipThroughComma parentStartTimeField+ skipThroughComma tunnelTypeField+ threatCategory <- untilComma threatCategoryField+ contentVersion <-+ if | version < 100 -> untilComma contentVersionField+ | otherwise -> do+ off <- Unsafe.cursor+ pure (Bounds off 0)+ when (version < 100) (skipThroughComma futureUseGField)+ sctpAssociationId <- w64Comma sctpAssociationIdField+ payloadProtocolId <-+ if | version < 100 -> do+ w64Comma payloadProtocolField+ | isUrl == 1 -> do+ w64Comma payloadProtocolField+ | isSpyware == 1 || isWildfire == 1 -> do+ -- In PAN-OS 10.x, this field looks like it is always zero,+ -- represented in hexadecimal, in spyware logs.+ Latin.char4 payloadProtocolField '0' 'x' '0' ','+ pure 0+ | otherwise -> pure 0+ -- TODO: Escape or parse HTTP Headers correctly+ httpHeaders <-+ if | version < 100 || isUrl == 1 -> do+ finalOptionallyQuoted httpHeadersField+ | otherwise -> do+ off <- Unsafe.cursor+ pure (Bytes message off 0)+ -- In PAN-OS 8.1, threat logs end after http headers.+ -- PAN-OS 9.0 adds three more fields.+ P.isEndOfInput >>= \case+ False -> do+ !urlCategoryList <-+ if | version < 100 || isUrl == 1 -> do+ Latin.char urlCategoryListField ','+ parserOptionallyQuoted urlCategoryListField+ | otherwise -> do+ off <- Unsafe.cursor+ pure (Bytes message off 0)+ ruleUuid <- UUID.parserHyphenated ruleUuidField+ Latin.char http2ConnectionField ','+ Latin.skipDigits1 http2ConnectionField+ pure Threat+ { subtype , timeGenerated , sourceAddress , destinationAddress + , natSourceIp , natDestinationIp , ruleName , sourceUser + , destinationUser , application , virtualSystem , sourceZone + , destinationZone , inboundInterface , outboundInterface , logAction + , sessionId , repeatCount , sourcePort , destinationPort + , natSourcePort , natDestinationPort , ipProtocol + , action , category + , sequenceNumber , sourceCountry , destinationCountry + , deviceGroupHierarchyLevel1 , deviceGroupHierarchyLevel2 + , deviceGroupHierarchyLevel3 , deviceGroupHierarchyLevel4 + , virtualSystemName , deviceName , receiveTime+ , serialNumber, actionFlags, flags, message+ , syslogHost, threatId, severity, direction, threatName+ , contentType, pcapId+ , fileDigest, cloud, urlIndex+ , userAgentBounds, sctpAssociationId+ , userAgentByteArray, fileType+ , forwardedFor, referer+ , sender, subject, recipient+ , reportId, httpMethod, contentVersion+ , threatCategory, miscellaneousBounds, miscellaneousByteArray+ , payloadProtocolId, parentSessionId, tunnelId+ , httpHeaders, ruleUuid, urlCategoryList+ }+ True -> pure Threat+ { subtype , timeGenerated , sourceAddress , destinationAddress + , natSourceIp , natDestinationIp , ruleName , sourceUser + , destinationUser , application , virtualSystem , sourceZone + , destinationZone , inboundInterface , outboundInterface , logAction + , sessionId , repeatCount , sourcePort , destinationPort + , natSourcePort , natDestinationPort , ipProtocol + , action , category + , sequenceNumber , sourceCountry , destinationCountry + , deviceGroupHierarchyLevel1 , deviceGroupHierarchyLevel2 + , deviceGroupHierarchyLevel3 , deviceGroupHierarchyLevel4 + , virtualSystemName , deviceName , receiveTime+ , serialNumber, actionFlags, flags, message+ , syslogHost, threatId, severity, direction, threatName+ , contentType, pcapId+ , fileDigest, cloud, urlIndex+ , userAgentBounds, sctpAssociationId+ , userAgentByteArray, fileType+ , forwardedFor, referer+ , sender, subject, recipient+ , reportId, httpMethod, contentVersion+ , threatCategory, miscellaneousBounds, miscellaneousByteArray+ , payloadProtocolId, parentSessionId, tunnelId+ , httpHeaders+ , ruleUuid = 0+ , urlCategoryList = Bytes message 0 0+ }++-- Threat IDs are weird. There are three different kinds of+-- strings that can show up here:+--+-- * (9999)+-- * Microsoft RPC Endpoint Mapper Detection(30845)+-- * Windows Executable (EXE)(52020)+--+-- URL logs have a threat id of 9999, and there is no description.+-- Everything else has a human-readable description. Sometimes,+-- this description is suffixed by a space and a parenthesized+-- acronym (EXE, DLL, etc.).+parserThreatId :: Parser Field s (Bounds,Word64)+parserThreatId = Latin.any threatIdField >>= \case+ '(' -> do+ theId <- Latin.decWord64 threatIdField+ Latin.char threatIdField ')'+ Latin.char threatIdField ','+ pure (Bounds 0 0, theId)+ _ -> do+ startSucc <- Unsafe.cursor+ Latin.skipTrailedBy threatIdField '('+ end <- Latin.trySatisfy (\c -> isAsciiUpper c || isAsciiLower c) >>= \case+ True -> do+ endSuccSucc <- Unsafe.cursor+ Latin.skipTrailedBy threatIdField '('+ arr <- Unsafe.expose+ -- We go back an extra character to remove the trailing+ -- space. I do not believe this can lead to negative-length+ -- slices, but the line of reasoning is muddy.+ case indexCharArray arr (endSuccSucc - 3) of+ ' ' -> pure (endSuccSucc - 3)+ _ -> P.fail threatIdField+ False -> do+ endSucc <- Unsafe.cursor+ pure (endSucc - 1)+ theId <- Latin.decWord64 threatIdField+ Latin.char threatIdField ')'+ Latin.char threatIdField ','+ let start = startSucc - 1+ pure (Bounds start (end - start), theId)++indexCharArray :: ByteArray -> Int -> Char+indexCharArray (PM.ByteArray x) (I# i) =+ Exts.C# (Exts.indexCharArray# x i)+
+ src/Panos/Syslog/Internal/Traffic.hs view
@@ -0,0 +1,265 @@+{-# language BangPatterns #-}+{-# language DerivingStrategies #-}+{-# language DuplicateRecordFields #-}+{-# language GeneralizedNewtypeDeriving #-}+{-# language LambdaCase #-}+{-# language MagicHash #-}+{-# language NamedFieldPuns #-}+{-# language NumericUnderscores #-}+{-# language ScopedTypeVariables #-}+module Panos.Syslog.Internal.Traffic+ ( Traffic(..)+ , parserTraffic+ ) where++import Panos.Syslog.Internal.Common++import Chronos (Datetime)+import Data.Bytes.Parser (Parser)+import Data.Primitive (ByteArray)+import Data.Word (Word64,Word32,Word16)+import GHC.Exts (Ptr(Ptr))+import Net.Types (IP(IP),IPv6(IPv6))+import Data.WideWord (Word128)++import qualified Data.Bytes.Parser as P+import qualified Data.Bytes.Parser.Latin as Latin+import qualified Data.Bytes.Parser.Unsafe as Unsafe+import qualified Net.IP as IP+import qualified UUID++-- | A PAN-OS traffic log. Read-only accessors are found in+-- @Panos.Syslog.Traffic@.+data Traffic = Traffic+ { message :: {-# UNPACK #-} !ByteArray+ -- The original log+ , syslogHost :: {-# UNPACK #-} !Bounds+ -- The host as presented in the syslog preamble that+ -- prefixes the message.+ , receiveTime :: {-# UNPACK #-} !Datetime+ -- In log, presented as: 2019/06/18 15:10:20+ , serialNumber :: {-# UNPACK #-} !Bounds+ -- In log, presented as: 002610378847+ , subtype :: {-# UNPACK #-} !Bounds+ -- Presented as: start, end, drop, and deny+ , timeGenerated :: {-# UNPACK #-} !Datetime+ -- Presented as: 2018/04/11 23:19:22+ , sourceAddress :: {-# UNPACK #-} !IP+ , destinationAddress :: {-# UNPACK #-} !IP+ , natSourceIp :: {-# UNPACK #-} !IP+ , natDestinationIp :: {-# UNPACK #-} !IP+ , ruleName :: {-# UNPACK #-} !Bounds+ , sourceUser :: {-# UNPACK #-} !Bounds+ , destinationUser :: {-# UNPACK #-} !Bounds+ , application :: {-# UNPACK #-} !Bounds+ , virtualSystem :: {-# UNPACK #-} !Bounds+ , sourceZone :: {-# UNPACK #-} !Bounds+ , destinationZone :: {-# UNPACK #-} !Bounds+ , inboundInterface :: {-# UNPACK #-} !Bounds+ , outboundInterface :: {-# UNPACK #-} !Bounds+ , logAction :: {-# UNPACK #-} !Bounds+ , sessionId :: {-# UNPACK #-} !Word64+ , repeatCount :: {-# UNPACK #-} !Word64+ , sourcePort :: {-# UNPACK #-} !Word16+ , destinationPort :: {-# UNPACK #-} !Word16+ , natSourcePort :: {-# UNPACK #-} !Word16+ , natDestinationPort :: {-# UNPACK #-} !Word16+ , flags :: {-# UNPACK #-} !Word32+ -- Presented as: 0x400053+ , ipProtocol :: {-# UNPACK #-} !Bounds+ -- Presented as: tcp, udp, etc.+ , action :: {-# UNPACK #-} !Bounds+ , bytes :: {-# UNPACK #-} !Word64+ , bytesSent :: {-# UNPACK #-} !Word64+ , bytesReceived :: {-# UNPACK #-} !Word64+ , packets :: {-# UNPACK #-} !Word64+ , startTime :: {-# UNPACK #-} !Datetime+ , elapsedTime :: {-# UNPACK #-} !Word64+ , category :: {-# UNPACK #-} !Bounds+ , sequenceNumber :: {-# UNPACK #-} !Word64+ , actionFlags :: {-# UNPACK #-} !Word64+ -- Presented as: 0x8000000000000000+ , sourceCountry :: {-# UNPACK #-} !Bounds+ , destinationCountry :: {-# UNPACK #-} !Bounds+ , packetsSent :: {-# UNPACK #-} !Word64+ , packetsReceived :: {-# UNPACK #-} !Word64+ , sessionEndReason :: {-# UNPACK #-} !Bounds+ , deviceGroupHierarchyLevel1 :: {-# UNPACK #-} !Word64+ , deviceGroupHierarchyLevel2 :: {-# UNPACK #-} !Word64+ , deviceGroupHierarchyLevel3 :: {-# UNPACK #-} !Word64+ , deviceGroupHierarchyLevel4 :: {-# UNPACK #-} !Word64+ , virtualSystemName :: {-# UNPACK #-} !Bounds+ , deviceName :: {-# UNPACK #-} !Bounds+ , actionSource :: {-# UNPACK #-} !Bounds+ , ruleUuid :: {-# UNPACK #-} !Word128+ }+++parserTraffic :: Bounds -> Datetime -> Bounds -> Parser Field s Traffic+parserTraffic !syslogHost receiveTime !serialNumber = do+ subtype <- untilComma subtypeField+ skipThroughComma futureUseAField+ -- The datetime parser consumes the trailing comma+ timeGenerated <- parserDatetime timeGeneratedDateField timeGeneratedTimeField+ sourceAddress <- IP.parserUtf8Bytes sourceAddressField+ Latin.char sourceAddressField ','+ destinationAddress <- IP.parserUtf8Bytes destinationAddressField+ Latin.char destinationAddressField ','+ -- Use the ip address zero when no NAT address is present.+ natSourceIp <- Latin.trySatisfy (==',') >>= \case+ True -> pure (IP (IPv6 0))+ False -> do+ natSourceIp <- IP.parserUtf8Bytes natSourceIpField+ Latin.char natSourceIpField ','+ pure natSourceIp+ natDestinationIp <- Latin.trySatisfy (==',') >>= \case+ True -> pure (IP (IPv6 0))+ False -> do+ natDestinationIp <- IP.parserUtf8Bytes natDestinationIpField+ Latin.char natDestinationIpField ','+ pure natDestinationIp+ ruleName <- untilComma ruleNameField+ sourceUser <- untilComma sourceUserField+ destinationUser <- untilComma destinationUserField+ application <- untilComma applicationField+ virtualSystem <- untilComma virtualSystemField+ sourceZone <- untilComma sourceZoneField+ destinationZone <- untilComma destinationZoneField+ inboundInterface <- untilComma inboundInterfaceField+ outboundInterface <- untilComma outboundInterfaceField+ logAction <- untilComma logActionField+ -- According to Palo Alto's documentation, the field after log_action+ -- is a future use field. However, in some (possibly all) PAN-OS 10+ -- logs, this field is missing. In all other logs, it is a+ -- YYYY/MM/DD HH:mm:ss timestamp. If the field is exactly 19 bytes long,+ -- we assume that it is the unused field. Otherwise, we assume that the+ -- unused field is missing, and we try to interpret these bytes as the+ -- session_id.+ futureCursorB <- Unsafe.cursor+ Bounds _ futureLenB <- untilComma futureUseBField+ sessionId <- case futureLenB of+ 19 -> w64Comma sessionIdField+ _ -> do+ Unsafe.jump futureCursorB+ w64Comma sessionIdField+ repeatCount <- w64Comma repeatCountField+ sourcePort <- w16Comma sourcePortField+ destinationPort <- w16Comma destinationPortField+ natSourcePort <- w16Comma natSourcePortField+ natDestinationPort <- w16Comma natDestinationPortField+ -- Note: Flags are ignored. Also, in either PAN-OS 10 or Prisma+ -- (not sure which one causes this), flags are missing.+ Latin.trySatisfy (=='0') >>= \case+ True -> do+ Latin.char flagsField 'x'+ _ <- untilComma flagsField+ pure ()+ False -> pure ()+ let flags = 0+ ipProtocol <- untilComma ipProtocolField+ action <- untilComma actionField+ bytes <- w64Comma bytesField+ bytesSent <- w64Comma bytesSentField+ bytesReceived <- w64Comma bytesReceivedField+ packets <- w64Comma packetsField+ startTime <- parserDatetime startTimeDateField startTimeTimeField+ elapsedTime <- w64Comma elapsedTimeField+ category <- untilComma categoryField+ futureCursorC <- Unsafe.cursor+ Bounds _ futureLenC <- untilComma futureUseCField+ -- Here, we find another future use fields that is missing in PAN-OS 10.x.+ -- In older versions, this was always the single digit 0. So, we treat+ -- length-1 fields as future use.+ sequenceNumber <- case futureLenC of+ 0 -> w64Comma sequenceNumberField+ 1 -> w64Comma sequenceNumberField+ _ -> do+ Unsafe.jump futureCursorC+ w64Comma sequenceNumberField+ -- Note: Action flags are ignored. See note on Flags as well.+ Latin.trySatisfy (=='0') >>= \case+ True -> do+ Latin.char actionFlagsField 'x'+ _ <- untilComma actionFlagsField+ pure ()+ False -> pure ()+ let actionFlags = 0+ sourceCountry <- untilComma sourceCountryField+ destinationCountry <- untilComma destinationCountryField+ -- Future use field is optional. Here, we hop forward to what is either+ -- the session_end_reason or the packets_received. We use the first byte+ -- of this to figure out if the future use field is missing.+ futureCursorE <- Unsafe.cursor+ skipThroughComma futureUseEField+ skipThroughComma futureUseEField+ Latin.any futureUseEField >>= \case+ c | c >= '0', c <= '9' -> do+ -- Future use field was present. Skip it.+ Unsafe.jump futureCursorE+ skipThroughComma futureUseEField+ _ -> Unsafe.jump futureCursorE+ packetsSent <- w64Comma packetsSentField+ packetsReceived <- w64Comma packetsReceivedField+ sessionEndReason <- untilComma sessionEndReasonField+ deviceGroupHierarchyLevel1 <- w64Comma deviceGroupHierarchyLevel1Field+ deviceGroupHierarchyLevel2 <- w64Comma deviceGroupHierarchyLevel2Field+ deviceGroupHierarchyLevel3 <- w64Comma deviceGroupHierarchyLevel3Field+ deviceGroupHierarchyLevel4 <- w64Comma deviceGroupHierarchyLevel4Field+ virtualSystemName <- untilComma virtualSystemNameField+ deviceName <- untilComma deviceNameField+ actionSource <- untilComma actionSourceField+ skipThroughComma sourceVmUuidField+ skipThroughComma destinationVmUuidField+ skipThroughComma tunnelIdField+ skipThroughComma monitorTagField+ skipThroughComma parentSessionIdField+ skipThroughComma parentStartTimeField+ skipThroughComma tunnelTypeField+ skipThroughComma sctpAssociationIdField+ skipDigitsThroughComma sctpChunksField+ skipDigitsThroughComma sctpChunksSentField+ Latin.skipDigits1 sctpChunksReceivedField+ message <- Unsafe.expose+ -- In PAN-OS 8.1, traffic logs end after SCTP chunks received.+ -- PAN-OS 9.0 adds two additional fields: rule uuid and a number+ -- that has something to do with HTTP/2.+ P.isEndOfInput >>= \case+ False -> do+ Latin.char ruleUuidField ','+ ruleUuid <- UUID.parserHyphenated ruleUuidField+ Latin.char http2ConnectionField ','+ Latin.skipDigits1 http2ConnectionField+ pure Traffic+ { subtype , timeGenerated , sourceAddress , destinationAddress + , natSourceIp , natDestinationIp , ruleName , sourceUser + , destinationUser , application , virtualSystem , sourceZone + , destinationZone , inboundInterface , outboundInterface , logAction + , sessionId , repeatCount , sourcePort , destinationPort + , natSourcePort , natDestinationPort , ipProtocol + , action , bytes , bytesSent , bytesReceived + , packets , startTime , elapsedTime , category + , sequenceNumber , sourceCountry , destinationCountry + , packetsSent , sessionEndReason + , deviceGroupHierarchyLevel1 , deviceGroupHierarchyLevel2 + , deviceGroupHierarchyLevel3 , deviceGroupHierarchyLevel4 + , virtualSystemName , deviceName , actionSource , receiveTime+ , serialNumber, packetsReceived, actionFlags, flags, message+ , syslogHost, ruleUuid+ }+ True -> pure Traffic+ { subtype , timeGenerated , sourceAddress , destinationAddress + , natSourceIp , natDestinationIp , ruleName , sourceUser + , destinationUser , application , virtualSystem , sourceZone + , destinationZone , inboundInterface , outboundInterface , logAction + , sessionId , repeatCount , sourcePort , destinationPort + , natSourcePort , natDestinationPort , ipProtocol + , action , bytes , bytesSent , bytesReceived + , packets , startTime , elapsedTime , category + , sequenceNumber , sourceCountry , destinationCountry + , packetsSent , sessionEndReason + , deviceGroupHierarchyLevel1 , deviceGroupHierarchyLevel2 + , deviceGroupHierarchyLevel3 , deviceGroupHierarchyLevel4 + , virtualSystemName , deviceName , actionSource , receiveTime+ , serialNumber, packetsReceived, actionFlags, flags, message+ , syslogHost, ruleUuid = 0+ }
+ src/Panos/Syslog/Internal/User.hs view
@@ -0,0 +1,104 @@+{-# language BangPatterns #-}+{-# language DerivingStrategies #-}+{-# language DuplicateRecordFields #-}+{-# language GeneralizedNewtypeDeriving #-}+{-# language LambdaCase #-}+{-# language MagicHash #-}+{-# language NamedFieldPuns #-}+{-# language NumericUnderscores #-}+{-# language ScopedTypeVariables #-}+module Panos.Syslog.Internal.User+ ( User(..)+ , parserUser+ ) where++import Panos.Syslog.Internal.Common++import Chronos (Datetime)+import Data.Bytes.Parser (Parser)+import Data.Primitive (ByteArray)+import Data.Word (Word64)+import Net.Types (IP)++import qualified Data.Bytes.Parser as P+import qualified Data.Bytes.Parser.Latin as Latin+import qualified Data.Bytes.Parser.Unsafe as Unsafe+import qualified Net.IP as IP++-- | A PAN-OS user id log. Read-only accessors are found in+-- @Panos.Syslog.User@.+data User = User+ { message :: {-# UNPACK #-} !ByteArray+ -- The original log+ , syslogHost :: {-# UNPACK #-} !Bounds+ -- The host as presented in the syslog preamble that+ -- prefixes the message.+ , receiveTime :: {-# UNPACK #-} !Datetime+ -- In log, presented as: 2019/06/18 15:10:20+ , serialNumber :: {-# UNPACK #-} !Bounds+ -- In log, presented as: 002610378847+ , subtype :: {-# UNPACK #-} !Bounds+ -- Presented as: dhcp, dnsproxy, dos, general, etc.+ , timeGenerated :: {-# UNPACK #-} !Datetime+ -- Presented as: 2019/11/04 08:39:05+ , virtualSystem :: {-# UNPACK #-} !Bounds+ , sourceIp :: {-# UNPACK #-} !IP+ , user :: {-# UNPACK #-} !Bounds+ , dataSourceName :: {-# UNPACK #-} !Bounds+ , repeatCount :: {-# UNPACK #-} !Word64+ , dataSource :: {-# UNPACK #-} !Bounds+ , sequenceNumber :: {-# UNPACK #-} !Word64+ , actionFlags :: {-# UNPACK #-} !Word64+ , deviceGroupHierarchyLevel1 :: {-# UNPACK #-} !Word64+ , deviceGroupHierarchyLevel2 :: {-# UNPACK #-} !Word64+ , deviceGroupHierarchyLevel3 :: {-# UNPACK #-} !Word64+ , deviceGroupHierarchyLevel4 :: {-# UNPACK #-} !Word64+ , virtualSystemName :: {-# UNPACK #-} !Bounds+ , deviceName :: {-# UNPACK #-} !Bounds+ }++parserUser :: Bounds -> Datetime -> Bounds -> Parser Field s User+parserUser !syslogHost receiveTime !serialNumber = do+ subtype <- untilComma subtypeField -- login or logout+ skipThroughComma futureUseAField+ -- The datetime parser consumes the trailing comma+ timeGenerated <- parserDatetime timeGeneratedDateField timeGeneratedTimeField+ virtualSystem <- untilComma virtualSystemField+ sourceIp <- IP.parserUtf8Bytes sourceIpField+ Latin.char ipField ','+ user <- untilComma userField+ dataSourceName <- untilComma dataSourceNameField+ skipThroughComma eventIdField+ repeatCount <- w64Comma repeatCountField+ skipThroughComma timeoutField+ skipThroughComma sourcePortField+ skipThroughComma destinationPortField+ dataSource <- untilComma dataSourceField+ skipThroughComma dataSourceTypeField+ sequenceNumber <- w64Comma sequenceNumberField+ -- TODO: handle action flags+ Latin.char actionFlagsField '0'+ Latin.char actionFlagsField 'x'+ _ <- untilComma actionFlagsField+ let actionFlags = 0+ deviceGroupHierarchyLevel1 <- w64Comma deviceGroupHierarchyLevel1Field+ deviceGroupHierarchyLevel2 <- w64Comma deviceGroupHierarchyLevel2Field+ deviceGroupHierarchyLevel3 <- w64Comma deviceGroupHierarchyLevel3Field+ deviceGroupHierarchyLevel4 <- w64Comma deviceGroupHierarchyLevel4Field+ virtualSystemName <- untilComma virtualSystemNameField+ deviceName <- untilComma deviceNameField+ -- Ignore all fields after device name since they are low-value.+ _ <- P.remaining+ message <- Unsafe.expose+ pure User+ { subtype+ , timeGenerated+ , sourceIp+ , sequenceNumber + , deviceGroupHierarchyLevel1 , deviceGroupHierarchyLevel2 + , deviceGroupHierarchyLevel3 , deviceGroupHierarchyLevel4 + , virtualSystemName , deviceName , receiveTime+ , serialNumber, actionFlags, message+ , syslogHost, virtualSystem+ , dataSourceName, dataSource, user, repeatCount+ }
src/Panos/Syslog/System.hs view
@@ -1,10 +1,11 @@ {-# language BangPatterns #-}+{-# language DerivingStrategies #-}+{-# language DuplicateRecordFields #-}+{-# language GeneralizedNewtypeDeriving #-} {-# language MagicHash #-} {-# language NamedFieldPuns #-}-{-# language DuplicateRecordFields #-} {-# language NumericUnderscores #-}-{-# language DerivingStrategies #-}-{-# language GeneralizedNewtypeDeriving #-}+{-# language OverloadedRecordDot #-} -- | Fields for system logs. module Panos.Syslog.System@@ -71,12 +72,14 @@ -- | Time the log was generated on the dataplane. timeGenerated :: System -> Datetime-timeGenerated = U.timeGenerated+{-# inline timeGenerated #-}+timeGenerated u = u.timeGenerated -- | A 64-bit log entry identifier incremented sequentially; -- each log type has a unique number space. sequenceNumber :: System -> Word64-sequenceNumber = U.sequenceNumber+{-# inline sequenceNumber #-}+sequenceNumber u = u.sequenceNumber -- | Serial number of the firewall that generated the log. These -- occassionally contain non-numeric characters, so do not attempt@@ -84,4 +87,3 @@ serialNumber :: System -> Bytes serialNumber (System{serialNumber=Bounds off len,message=msg}) = Bytes{offset=off,length=len,array=msg}-
src/Panos/Syslog/Threat.hs view
@@ -1,10 +1,11 @@ {-# language BangPatterns #-}+{-# language DerivingStrategies #-}+{-# language DuplicateRecordFields #-}+{-# language GeneralizedNewtypeDeriving #-} {-# language MagicHash #-} {-# language NamedFieldPuns #-}-{-# language DuplicateRecordFields #-} {-# language NumericUnderscores #-}-{-# language DerivingStrategies #-}-{-# language GeneralizedNewtypeDeriving #-}+{-# language OverloadedRecordDot #-} -- | Fields for threat logs. module Panos.Syslog.Threat@@ -18,9 +19,11 @@ , destinationUser , destinationZone , deviceName+ , fileDigest , httpHeaders , httpMethod , inboundInterface+ , ipProtocol , miscellaneous , natDestinationIp , natDestinationPort@@ -29,10 +32,12 @@ , outboundInterface , recipient , referer+ , repeatCount , ruleName , sender , sequenceNumber , serialNumber+ , sessionId , severity , sourceAddress , sourceCountry@@ -45,6 +50,7 @@ , threatId , threatName , timeGenerated+ , urlCategoryList , virtualSystemName ) where @@ -53,6 +59,7 @@ import Data.Word (Word64,Word16) import Chronos (Datetime) import Net.Types (IP)+import qualified Data.Bytes as Bytes import qualified Panos.Syslog.Unsafe as U -- | Subtype of threat log. Values include: @data@, @file@, @flood@,@@ -97,6 +104,15 @@ deviceName (Threat{deviceName=Bounds off len,message=msg}) = Bytes{offset=fromIntegral off,length=fromIntegral len,array=msg} +-- | The file digest string shows the binary hash of the file sent+-- to be analyzed by the WildFire service.+--+-- This is sometimes MD5 and sometimes SHA256. Rather than attempting+-- to decode it, it is just left as bytes.+fileDigest :: Threat -> Bytes+fileDigest (Threat{fileDigest=Bounds off len,message=msg}) =+ Bytes{offset=fromIntegral off,length=fromIntegral len,array=msg}+ -- | Palo Alto Networks identifier for the threat. It is a description -- string followed by a 64-bit numerical identifier in parentheses for -- some subtypes.@@ -113,7 +129,8 @@ -- | Time the log was generated on the dataplane. timeGenerated :: Threat -> Datetime-timeGenerated = U.timeGenerated+{-# inline timeGenerated #-}+timeGenerated u = u.timeGenerated -- | For URL Subtype, it is the URL Category; For WildFire subtype, -- it is the verdict on the file and is either @malicious@, @grayware@,@@ -147,7 +164,8 @@ -- type has a unique number space. This field is not supported on -- PA-7000 Series firewalls. sequenceNumber :: Threat -> Word64-sequenceNumber = U.sequenceNumber+{-# inline sequenceNumber #-}+sequenceNumber u = u.sequenceNumber -- | Serial number of the firewall that generated the log. These -- occassionally contain non-numeric characters, so do not attempt@@ -188,19 +206,23 @@ -- | Original session destination IP address. destinationAddress :: Threat -> IP-destinationAddress = U.destinationAddress+{-# inline destinationAddress #-}+destinationAddress u = u.destinationAddress -- | Original session source IP address. sourceAddress :: Threat -> IP-sourceAddress = U.sourceAddress+{-# inline sourceAddress #-}+sourceAddress u = u.sourceAddress -- | Source port utilized by the session. sourcePort :: Threat -> Word16-sourcePort = U.sourcePort+{-# inline sourcePort #-}+sourcePort u = u.sourcePort -- | Destination port utilized by the session. destinationPort :: Threat -> Word16-destinationPort = U.destinationPort+{-# inline destinationPort #-}+destinationPort u = u.destinationPort sender :: Threat -> Bytes sender = U.sender@@ -213,19 +235,23 @@ -- | Post-NAT destination port. natDestinationPort :: Threat -> Word16-natDestinationPort = U.natDestinationPort+{-# inline natDestinationPort #-}+natDestinationPort u = u.natDestinationPort -- | If Source NAT performed, the post-NAT Source IP address. natSourceIp :: Threat -> IP-natSourceIp = U.natSourceIp+{-# inline natSourceIp #-}+natSourceIp u = u.natSourceIp -- | If Destination NAT performed, the post-NAT Destination IP address. natDestinationIp :: Threat -> IP-natDestinationIp = U.natDestinationIp+{-# inline natDestinationIp #-}+natDestinationIp u = u.natDestinationIp -- | Post-NAT source port. natSourcePort :: Threat -> Word16-natSourcePort = U.natSourcePort+{-# inline natSourcePort #-}+natSourcePort u = u.natSourcePort -- | Source country or Internal region for private addresses; -- maximum length is 32 bytes.@@ -238,3 +264,28 @@ destinationCountry :: Threat -> Bytes destinationCountry (Threat{destinationCountry=Bounds off len,message=msg}) = Bytes{offset=off,length=len,array=msg}++-- | Lists the URL Filtering categories that the firewall used to enforce policy.+--+-- This field only exists in PAN-OS 9.0 and up.+urlCategoryList :: Threat -> Maybe Bytes+{-# inline urlCategoryList #-}+urlCategoryList (Threat{urlCategoryList=x}) = case Bytes.length x of+ 0 -> Nothing+ _ -> Just x++-- | IP protocol associated with the session.+ipProtocol :: Threat -> Bytes+ipProtocol (Threat{ipProtocol=Bounds off len,message=msg}) =+ Bytes{offset=off,length=len,array=msg}+++-- | Number of total bytes (transmit and receive) for the session.+repeatCount :: Threat -> Word64+{-# inline repeatCount #-}+repeatCount u = u.repeatCount++-- | An internal numerical identifier applied to each session.+sessionId :: Threat -> Word64+{-# inline sessionId #-}+sessionId u = u.sessionId
src/Panos/Syslog/Traffic.hs view
@@ -1,15 +1,17 @@ {-# language BangPatterns #-}+{-# language DerivingStrategies #-}+{-# language DuplicateRecordFields #-}+{-# language GeneralizedNewtypeDeriving #-} {-# language MagicHash #-} {-# language NamedFieldPuns #-}-{-# language DuplicateRecordFields #-} {-# language NumericUnderscores #-}-{-# language DerivingStrategies #-}-{-# language GeneralizedNewtypeDeriving #-}+{-# language OverloadedRecordDot #-} -- | Fields for traffic logs. module Panos.Syslog.Traffic ( -- * Fields action+ , actionSource , application , bytes , bytesReceived@@ -32,10 +34,13 @@ , packets , packetsReceived , packetsSent+ , repeatCount , ruleName+ , ruleUuid , sequenceNumber , serialNumber , sessionEndReason+ , sessionId , sourceAddress , sourceCountry , sourcePort@@ -58,6 +63,7 @@ import Panos.Syslog.Unsafe (Traffic(Traffic),Bounds(Bounds)) import Data.Word (Word64,Word16) import Net.Types (IP)+import Data.WideWord (Word128) import qualified Panos.Syslog.Unsafe as U -- | IP protocol associated with the session.@@ -90,6 +96,14 @@ ruleName (Traffic{ruleName=Bounds off len,message=msg}) = Bytes{offset=off,length=len,array=msg} +-- | Specifies whether the action taken to allow or block an+-- application was defined in the application or in policy. The+-- actions can be @allow@, @deny@, @drop@, @reset-server@, @reset-client@+-- or @reset-both@ for the session.+actionSource :: Traffic -> Bytes+actionSource (Traffic{actionSource=Bounds off len,message=msg}) =+ Bytes{offset=off,length=len,array=msg}+ -- | Interface that the session was sourced from. inboundInterface :: Traffic -> Bytes inboundInterface (Traffic{inboundInterface=Bounds off len,message=msg}) =@@ -154,44 +168,59 @@ -- | Source port utilized by the session. sourcePort :: Traffic -> Word16-sourcePort = U.sourcePort+{-# inline sourcePort #-}+sourcePort u = u.sourcePort -- | Time the log was generated on the dataplane. timeGenerated :: Traffic -> Datetime-timeGenerated = U.timeGenerated+{-# inline timeGenerated #-}+timeGenerated u = u.timeGenerated -- | A 64-bit log entry identifier incremented sequentially; -- each log type has a unique number space. sequenceNumber :: Traffic -> Word64-sequenceNumber = U.sequenceNumber+{-# inline sequenceNumber #-}+sequenceNumber u = u.sequenceNumber -- | Post-NAT source port. natSourcePort :: Traffic -> Word16-natSourcePort = U.natSourcePort+{-# inline natSourcePort #-}+natSourcePort u = u.natSourcePort +-- | The UUID that permanently identifies the rule.+ruleUuid :: Traffic -> Word128+{-# inline ruleUuid #-}+ruleUuid u = u.ruleUuid+ -- | Destination port utilized by the session. destinationPort :: Traffic -> Word16-destinationPort = U.destinationPort+{-# inline destinationPort #-}+destinationPort u = u.destinationPort -- | Post-NAT destination port. natDestinationPort :: Traffic -> Word16-natDestinationPort = U.natDestinationPort+{-# inline natDestinationPort #-}+natDestinationPort u = u.natDestinationPort -- | If Source NAT performed, the post-NAT Source IP address. natSourceIp :: Traffic -> IP-natSourceIp = U.natSourceIp+{-# inline natSourceIp #-}+natSourceIp u = u.natSourceIp -- | If Destination NAT performed, the post-NAT Destination IP address. natDestinationIp :: Traffic -> IP-natDestinationIp = U.natDestinationIp+{-# inline natDestinationIp #-}+natDestinationIp u = u.natDestinationIp -- | Original session source IP address. sourceAddress :: Traffic -> IP-sourceAddress = U.sourceAddress+{-# inline sourceAddress #-}+sourceAddress u = u.sourceAddress -- | Original session destination IP address. destinationAddress :: Traffic -> IP-destinationAddress = U.destinationAddress+{-# inline destinationAddress #-}+destinationAddress u = u.destinationAddress -- | Number of total packets (transmit and receive) for the session. packets :: Traffic -> Word64@@ -209,6 +238,11 @@ bytes :: Traffic -> Word64 bytes = U.bytes +-- | Number of total bytes (transmit and receive) for the session.+repeatCount :: Traffic -> Word64+{-# inline repeatCount #-}+repeatCount u = u.repeatCount+ -- | Serial number of the firewall that generated the log. These -- occassionally contain non-numeric characters, so do not attempt -- to parse this as a decimal number.@@ -217,16 +251,20 @@ Bytes{offset=off,length=len,array=msg} deviceGroupHierarchyLevel1 :: Traffic -> Word64-deviceGroupHierarchyLevel1 = U.deviceGroupHierarchyLevel1+{-# inline deviceGroupHierarchyLevel1 #-}+deviceGroupHierarchyLevel1 u = u.deviceGroupHierarchyLevel1 deviceGroupHierarchyLevel2 :: Traffic -> Word64-deviceGroupHierarchyLevel2 = U.deviceGroupHierarchyLevel2+{-# inline deviceGroupHierarchyLevel2 #-}+deviceGroupHierarchyLevel2 u = u.deviceGroupHierarchyLevel2 deviceGroupHierarchyLevel3 :: Traffic -> Word64-deviceGroupHierarchyLevel3 = U.deviceGroupHierarchyLevel3+{-# inline deviceGroupHierarchyLevel3 #-}+deviceGroupHierarchyLevel3 u = u.deviceGroupHierarchyLevel3 deviceGroupHierarchyLevel4 :: Traffic -> Word64-deviceGroupHierarchyLevel4 = U.deviceGroupHierarchyLevel4+{-# inline deviceGroupHierarchyLevel4 #-}+deviceGroupHierarchyLevel4 u = u.deviceGroupHierarchyLevel4 -- | The reason a session terminated. sessionEndReason :: Traffic -> Bytes@@ -265,3 +303,8 @@ destinationCountry :: Traffic -> Bytes destinationCountry (Traffic{destinationCountry=Bounds off len,message=msg}) = Bytes{offset=off,length=len,array=msg}++-- | An internal numerical identifier applied to each session.+sessionId :: Traffic -> Word64+{-# inline sessionId #-}+sessionId u = u.sessionId
src/Panos/Syslog/Unsafe.hs view
@@ -14,1232 +14,157 @@ , Traffic(..) , Threat(..) , System(..)- , Field(..)- , Bounds(..)- -- * Decoding- , decode- ) where--import Chronos (DayOfMonth(..),Date(..))-import Chronos (Year(..),Month(..),Datetime(..),TimeOfDay(..))-import Control.Exception (Exception)-import Control.Monad.ST.Run (runByteArrayST)-import Data.Bytes.Parser (Parser)-import Data.Bytes.Types (Bytes(..),UnmanagedBytes(UnmanagedBytes))-import Data.Char (ord,isAsciiUpper,isAsciiLower)-import Data.Primitive (ByteArray)-import Data.Primitive.Addr (Addr(Addr))-import Data.Word (Word64,Word32,Word16,Word8)-import GHC.Exts (Ptr(Ptr),Int(I#),Int#,Addr#)-import Net.Types (IP)--import qualified Control.Exception-import qualified Data.Bytes.Parser as P-import qualified Data.Bytes.Parser.Ascii as Ascii-import qualified Data.Bytes.Parser.Latin as Latin-import qualified Data.Bytes.Parser.Unsafe as Unsafe-import qualified Data.Primitive as PM-import qualified Data.Primitive.Ptr as PM-import qualified GHC.Exts as Exts-import qualified GHC.Pack-import qualified Net.IP as IP-import qualified Net.IPv4 as IPv4---- | Sum that represents all known PAN-OS syslog types. Use 'decode'--- to parse a byte sequence into a structured log.-data Log- = LogTraffic !Traffic- | LogThreat !Threat- | LogSystem !System- | LogOther--data Bounds = Bounds- {-# UNPACK #-} !Int -- offset- {-# UNPACK #-} !Int -- length---- | A PAN-OS system log. Read-only accessors are found in--- @Panos.Syslog.System@.-data System = System- { message :: {-# UNPACK #-} !ByteArray- -- The original log- , syslogHost :: {-# UNPACK #-} !Bounds- -- The host as presented in the syslog preamble that- -- prefixes the message.- , receiveTime :: {-# UNPACK #-} !Datetime- -- In log, presented as: 2019/06/18 15:10:20- , serialNumber :: {-# UNPACK #-} !Bounds- -- In log, presented as: 002610378847- , subtype :: {-# UNPACK #-} !Bounds- -- Presented as: dhcp, dnsproxy, dos, general, etc.- , timeGenerated :: {-# UNPACK #-} !Datetime- -- Presented as: 2019/11/04 08:39:05- , virtualSystem :: {-# UNPACK #-} !Bounds- , eventId :: {-# UNPACK #-} !Bounds- , object :: {-# UNPACK #-} !Bounds- , module_ :: {-# UNPACK #-} !Bounds- , severity :: {-# UNPACK #-} !Bounds- , descriptionBounds :: {-# UNPACK #-} !Bounds- , descriptionByteArray :: {-# UNPACK #-} !ByteArray- , sequenceNumber :: {-# UNPACK #-} !Word64- , actionFlags :: {-# UNPACK #-} !Word64- , deviceGroupHierarchyLevel1 :: {-# UNPACK #-} !Word64- , deviceGroupHierarchyLevel2 :: {-# UNPACK #-} !Word64- , deviceGroupHierarchyLevel3 :: {-# UNPACK #-} !Word64- , deviceGroupHierarchyLevel4 :: {-# UNPACK #-} !Word64- , virtualSystemName :: {-# UNPACK #-} !Bounds- , deviceName :: {-# UNPACK #-} !Bounds- }---- | A PAN-OS traffic log. Read-only accessors are found in--- @Panos.Syslog.Traffic@.-data Traffic = Traffic- { message :: {-# UNPACK #-} !ByteArray- -- The original log- , syslogHost :: {-# UNPACK #-} !Bounds- -- The host as presented in the syslog preamble that- -- prefixes the message.- , receiveTime :: {-# UNPACK #-} !Datetime- -- In log, presented as: 2019/06/18 15:10:20- , serialNumber :: {-# UNPACK #-} !Bounds- -- In log, presented as: 002610378847- , subtype :: {-# UNPACK #-} !Bounds- -- Presented as: start, end, drop, and deny- , timeGenerated :: {-# UNPACK #-} !Datetime- -- Presented as: 2018/04/11 23:19:22- , sourceAddress :: {-# UNPACK #-} !IP- , destinationAddress :: {-# UNPACK #-} !IP- , natSourceIp :: {-# UNPACK #-} !IP- , natDestinationIp :: {-# UNPACK #-} !IP- , ruleName :: {-# UNPACK #-} !Bounds- , sourceUser :: {-# UNPACK #-} !Bounds- , destinationUser :: {-# UNPACK #-} !Bounds- , application :: {-# UNPACK #-} !Bounds- , virtualSystem :: {-# UNPACK #-} !Bounds- , sourceZone :: {-# UNPACK #-} !Bounds- , destinationZone :: {-# UNPACK #-} !Bounds- , inboundInterface :: {-# UNPACK #-} !Bounds- , outboundInterface :: {-# UNPACK #-} !Bounds- , logAction :: {-# UNPACK #-} !Bounds- , sessionId :: {-# UNPACK #-} !Word64- , repeatCount :: {-# UNPACK #-} !Word64- , sourcePort :: {-# UNPACK #-} !Word16- , destinationPort :: {-# UNPACK #-} !Word16- , natSourcePort :: {-# UNPACK #-} !Word16- , natDestinationPort :: {-# UNPACK #-} !Word16- , flags :: {-# UNPACK #-} !Word32- -- Presented as: 0x400053- , ipProtocol :: {-# UNPACK #-} !Bounds- -- Presented as: tcp, udp, etc.- , action :: {-# UNPACK #-} !Bounds- , bytes :: {-# UNPACK #-} !Word64- , bytesSent :: {-# UNPACK #-} !Word64- , bytesReceived :: {-# UNPACK #-} !Word64- , packets :: {-# UNPACK #-} !Word64- , startTime :: {-# UNPACK #-} !Datetime- , elapsedTime :: {-# UNPACK #-} !Word64- , category :: {-# UNPACK #-} !Bounds- , sequenceNumber :: {-# UNPACK #-} !Word64- , actionFlags :: {-# UNPACK #-} !Word64- -- Presented as: 0x8000000000000000- , sourceCountry :: {-# UNPACK #-} !Bounds- , destinationCountry :: {-# UNPACK #-} !Bounds- , packetsSent :: {-# UNPACK #-} !Word64- , packetsReceived :: {-# UNPACK #-} !Word64- , sessionEndReason :: {-# UNPACK #-} !Bounds- , deviceGroupHierarchyLevel1 :: {-# UNPACK #-} !Word64- , deviceGroupHierarchyLevel2 :: {-# UNPACK #-} !Word64- , deviceGroupHierarchyLevel3 :: {-# UNPACK #-} !Word64- , deviceGroupHierarchyLevel4 :: {-# UNPACK #-} !Word64- , virtualSystemName :: {-# UNPACK #-} !Bounds- , deviceName :: {-# UNPACK #-} !Bounds- , actionSource :: {-# UNPACK #-} !Bounds- }---- | A PAN-OS threat log. Read-only accessors are found in--- @Panos.Syslog.Threat@.-data Threat = Threat- { message :: {-# UNPACK #-} !ByteArray- -- The original log- , syslogHost :: {-# UNPACK #-} !Bounds- -- The host as presented in the syslog preamble that- -- prefixes the message.- , receiveTime :: {-# UNPACK #-} !Datetime- -- In log, presented as: 2019/06/18 15:10:20- , serialNumber :: {-# UNPACK #-} !Bounds- -- In log, presented as: 002610378847- , subtype :: {-# UNPACK #-} !Bounds- -- Presented as: data, file, flood, packet, scan, spyware, url,- -- virus, vulnerability, wildfire, or wildfire-virus.- , timeGenerated :: {-# UNPACK #-} !Datetime- -- Presented as: 2018/04/11 23:19:22- , sourceAddress :: {-# UNPACK #-} !IP- , destinationAddress :: {-# UNPACK #-} !IP- , natSourceIp :: {-# UNPACK #-} !IP- , natDestinationIp :: {-# UNPACK #-} !IP- , ruleName :: {-# UNPACK #-} !Bounds- , sourceUser :: {-# UNPACK #-} !Bounds- , destinationUser :: {-# UNPACK #-} !Bounds- , application :: {-# UNPACK #-} !Bounds- , virtualSystem :: {-# UNPACK #-} !Bounds- , sourceZone :: {-# UNPACK #-} !Bounds- , destinationZone :: {-# UNPACK #-} !Bounds- , inboundInterface :: {-# UNPACK #-} !Bounds- , outboundInterface :: {-# UNPACK #-} !Bounds- , logAction :: {-# UNPACK #-} !Bounds- , sessionId :: {-# UNPACK #-} !Word64- , repeatCount :: {-# UNPACK #-} !Word64- , sourcePort :: {-# UNPACK #-} !Word16- , destinationPort :: {-# UNPACK #-} !Word16- , natSourcePort :: {-# UNPACK #-} !Word16- , natDestinationPort :: {-# UNPACK #-} !Word16- , action :: {-# UNPACK #-} !Bounds- , ipProtocol :: {-# UNPACK #-} !Bounds- , flags :: {-# UNPACK #-} !Word32- , miscellaneousBounds :: {-# UNPACK #-} !Bounds- , miscellaneousByteArray :: {-# UNPACK #-} !ByteArray- , threatName :: {-# UNPACK #-} !Bounds- , threatId :: {-# UNPACK #-} !Word64- , category :: {-# UNPACK #-} !Bounds- , severity :: {-# UNPACK #-} !Bounds- , direction :: {-# UNPACK #-} !Bounds- , sequenceNumber :: {-# UNPACK #-} !Word64- , actionFlags :: {-# UNPACK #-} !Word64- -- Presented as: 0x8000000000000000- , sourceCountry :: {-# UNPACK #-} !Bounds- , destinationCountry :: {-# UNPACK #-} !Bounds- , contentType :: {-# UNPACK #-} !Bounds- , pcapId :: {-# UNPACK #-} !Word64- , fileDigest :: {-# UNPACK #-} !Bounds- -- Only used by wildfire subtype- -- TODO: make the file digest a 128-bit or 256-bit word- , cloud :: {-# UNPACK #-} !Bounds- -- Only used by wildfire subtype- , urlIndex :: {-# UNPACK #-} !Word64- -- Only used by wildfire subtype- , userAgentBounds :: {-# UNPACK #-} !Bounds- , userAgentByteArray :: {-# UNPACK #-} !ByteArray- -- Only used by url filtering subtype. This field may have- -- escaped characters, so we include the possibility of- -- using a byte array distinct from the original log.- , fileType :: {-# UNPACK #-} !Bounds- -- Only used by wildfire subtype- , forwardedFor :: {-# UNPACK #-} !Bounds- -- Only used by url filtering subtype- , referer :: {-# UNPACK #-} !Bytes- -- Only used by url filtering subtype- , sender :: {-# UNPACK #-} !Bytes- -- Only used by wildfire subtype- , subject :: {-# UNPACK #-} !Bytes- -- Only used by wildfire subtype- , recipient :: {-# UNPACK #-} !Bytes- -- Only used by wildfire subtype- , reportId :: {-# UNPACK #-} !Bounds- -- Only used by wildfire subtype- , deviceGroupHierarchyLevel1 :: {-# UNPACK #-} !Word64- , deviceGroupHierarchyLevel2 :: {-# UNPACK #-} !Word64- , deviceGroupHierarchyLevel3 :: {-# UNPACK #-} !Word64- , deviceGroupHierarchyLevel4 :: {-# UNPACK #-} !Word64- , virtualSystemName :: {-# UNPACK #-} !Bounds- , deviceName :: {-# UNPACK #-} !Bounds- -- TODO: skipping over uuid fields for now- , httpMethod :: {-# UNPACK #-} !Bounds- , tunnelId :: {-# UNPACK #-} !Word64- , parentSessionId :: {-# UNPACK #-} !Word64- -- Only used by url subtype- , threatCategory :: {-# UNPACK #-} !Bounds- , contentVersion :: {-# UNPACK #-} !Bounds- -- TODO: skipping some fields here- , sctpAssociationId :: {-# UNPACK #-} !Word64- , payloadProtocolId :: {-# UNPACK #-} !Word64- -- TODO: skipping over other fields here- , httpHeaders :: {-# UNPACK #-} !Bytes- }---- | The field that was being parsed when a parse failure occurred.--- This is typically for useful for libary developers, but to present--- it to the end user, call @show@ or @throwIO@.-newtype Field = Field UnmanagedBytes--instance Show Field where- showsPrec _ (Field (UnmanagedBytes (Addr addr) _)) s =- '"' : GHC.Pack.unpackAppendCString# addr ('"' : s)--instance Exception Field where- displayException (Field (UnmanagedBytes (Addr addr) _)) =- GHC.Pack.unpackCString# addr--syslogPriorityField :: Field-syslogPriorityField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "syslogPriority"#--syslogHostField :: Field-syslogHostField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "syslogHost"#--syslogDatetimeField :: Field-syslogDatetimeField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "syslogDatetime"#--receiveTimeDateField :: Field-receiveTimeDateField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "receiveTime:date"#--receiveTimeTimeField :: Field-receiveTimeTimeField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "receiveTime:time"#--serialNumberField :: Field-serialNumberField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "serialNumber"#--typeField :: Field-typeField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "type"#--subtypeField :: Field-subtypeField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "subtype"#--timeGeneratedDateField :: Field-timeGeneratedDateField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "timeGenerated:date"#--timeGeneratedTimeField :: Field-timeGeneratedTimeField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "timeGenerated:time"#--sourceAddressField :: Field-sourceAddressField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "sourceAddress"#--destinationAddressField :: Field-destinationAddressField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "destinationAddress"#--natSourceIpField :: Field-natSourceIpField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "natSourceIp"#--natDestinationIpField :: Field-natDestinationIpField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "natDestinationIp"#--ruleNameField :: Field-ruleNameField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "ruleName"#--sourceUserField :: Field-sourceUserField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "sourceUser"#--destinationUserField :: Field-destinationUserField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "destinationUser"#--applicationField :: Field-applicationField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "application"#--virtualSystemField :: Field-virtualSystemField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "virtualSystem"#--sourceZoneField :: Field-sourceZoneField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "sourceZone"#--destinationZoneField :: Field-destinationZoneField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "destinationZone"#--inboundInterfaceField :: Field-inboundInterfaceField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "inboundInterface"#--outboundInterfaceField :: Field-outboundInterfaceField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "outboundInterface"#--logActionField :: Field-logActionField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "logAction"#--sessionIdField :: Field-sessionIdField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "sessionId"#--repeatCountField :: Field-repeatCountField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "repeatCount"#--sourcePortField :: Field-sourcePortField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "sourcePort"#--destinationPortField :: Field-destinationPortField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "destinationPort"#--natSourcePortField :: Field-natSourcePortField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "natSourcePort"#--natDestinationPortField :: Field-natDestinationPortField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "natDestinationPort"#--flagsField :: Field-flagsField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "flags"#--ipProtocolField :: Field-ipProtocolField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "ipProtocol"#--actionField :: Field-actionField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "action"#--bytesField :: Field-bytesField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "bytes"#--bytesSentField :: Field-bytesSentField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "bytesSent"#--bytesReceivedField :: Field-bytesReceivedField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "bytesReceived"#--packetsField :: Field-packetsField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "packets"#--startTimeDateField :: Field-startTimeDateField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "startTime:date"#--startTimeTimeField :: Field-startTimeTimeField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "startTime:time"#--elapsedTimeField :: Field-elapsedTimeField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "elapsedTime"#--categoryField :: Field-categoryField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "category"#--sequenceNumberField :: Field-sequenceNumberField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "sequenceNumber"#--actionFlagsField :: Field-actionFlagsField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "actionFlags"#--sourceCountryField :: Field-sourceCountryField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "sourceCountry"#--destinationCountryField :: Field-destinationCountryField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "destinationCountry"#--packetsSentField :: Field-packetsSentField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "packetsSent"#--packetsReceivedField :: Field-packetsReceivedField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "packetsReceived"#--sessionEndReasonField :: Field-sessionEndReasonField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "sessionEndReason"#--deviceGroupHierarchyLevel1Field :: Field-deviceGroupHierarchyLevel1Field = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "deviceGroupHierarchyLevel1"#--deviceGroupHierarchyLevel2Field :: Field-deviceGroupHierarchyLevel2Field = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "deviceGroupHierarchyLevel2"#--deviceGroupHierarchyLevel3Field :: Field-deviceGroupHierarchyLevel3Field = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "deviceGroupHierarchyLevel3"#--deviceGroupHierarchyLevel4Field :: Field-deviceGroupHierarchyLevel4Field = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "deviceGroupHierarchyLevel4"#--virtualSystemNameField :: Field-virtualSystemNameField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "virtualSystemName"#--payloadProtocolField :: Field-payloadProtocolField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "field:payloadProtocol"#--senderField :: Field-senderField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "field:sender"#--recipientField :: Field-recipientField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "field:recipient"#--refererField :: Field-refererField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "field:referer"#--pcapIdField :: Field-pcapIdField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "field:pcapId"#--directionField :: Field-directionField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "field:direction"#--contentTypeField :: Field-contentTypeField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "field:contentType"#--severityField :: Field-severityField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "field:severity"#--cloudField :: Field-cloudField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "field:cloud"#--threatCategoryField :: Field-threatCategoryField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "field:threatCategory"#--urlIndexField :: Field-urlIndexField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "field:urlIndex"#--fileDigestField :: Field-fileDigestField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "field:fileDigest"#--fileTypeField :: Field-fileTypeField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "field:fileType"#--forwardedForField :: Field-forwardedForField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "field:forwardedFor"#--userAgentField :: Field-userAgentField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "field:userAgent"#--subjectField :: Field-subjectField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "field:subject"#--contentVersionField :: Field-contentVersionField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "field:contentVersion"#--httpMethodField :: Field-httpMethodField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "field:httpMethod"#--httpHeadersField :: Field-httpHeadersField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "field:httpHeaders"#--reportIdField :: Field-reportIdField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "field:reportId"#--miscellaneousField :: Field-miscellaneousField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "field:miscellaneous"#--threatIdField :: Field-threatIdField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "field:threatId"#--deviceNameField :: Field-deviceNameField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "deviceName"#--actionSourceField :: Field-actionSourceField = Field (UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "actionSource"#--futureUseAField :: Field-futureUseAField = Field (UnmanagedBytes (Addr x#) (I# (cstringLen# x#)))- where !x# = "futureUse:A"#--futureUseBField :: Field-futureUseBField = Field (UnmanagedBytes (Addr x#) (I# (cstringLen# x#)))- where !x# = "futureUse:B"#--futureUseCField :: Field-futureUseCField = Field (UnmanagedBytes (Addr x#) (I# (cstringLen# x#)))- where !x# = "futureUse:C"#--futureUseDField :: Field-futureUseDField = Field (UnmanagedBytes (Addr x#) (I# (cstringLen# x#)))- where !x# = "futureUse:D"#--futureUseEField :: Field-futureUseEField = Field (UnmanagedBytes (Addr x#) (I# (cstringLen# x#)))- where !x# = "futureUse:E"#--futureUseFField :: Field-futureUseFField = Field (UnmanagedBytes (Addr x#) (I# (cstringLen# x#)))- where !x# = "futureUse:F"#--futureUseGField :: Field-futureUseGField = Field (UnmanagedBytes (Addr x#) (I# (cstringLen# x#)))- where !x# = "futureUse:G"#--leftoversField :: Field-leftoversField = Field (UnmanagedBytes (Addr x#) (I# (cstringLen# x#)))- where !x# = "framing:leftovers"#--sourceVmUuidField :: Field-sourceVmUuidField = Field (UnmanagedBytes (Addr x#) (I# (cstringLen# x#)))- where !x# = "field:source_uuid"#--destinationVmUuidField :: Field-destinationVmUuidField = Field (UnmanagedBytes (Addr x#) (I# (cstringLen# x#)))- where !x# = "field:dst_uuid"#--tunnelIdField :: Field-tunnelIdField = Field (UnmanagedBytes (Addr x#) (I# (cstringLen# x#)))- where !x# = "field:tunnelid"#--monitorTagField :: Field-monitorTagField = Field (UnmanagedBytes (Addr x#) (I# (cstringLen# x#)))- where !x# = "field:monitortag"#--parentSessionIdField :: Field-parentSessionIdField = Field (UnmanagedBytes (Addr x#) (I# (cstringLen# x#)))- where !x# = "field:parent_session_id"#--parentStartTimeField :: Field-parentStartTimeField = Field (UnmanagedBytes (Addr x#) (I# (cstringLen# x#)))- where !x# = "field:parent_start_time"#--tunnelTypeField :: Field-tunnelTypeField = Field (UnmanagedBytes (Addr x#) (I# (cstringLen# x#)))- where !x# = "field:tunnel"#--sctpAssociationIdField :: Field-sctpAssociationIdField = Field (UnmanagedBytes (Addr x#) (I# (cstringLen# x#)))- where !x# = "field:assoc_id"#--sctpChunksField :: Field-sctpChunksField = Field (UnmanagedBytes (Addr x#) (I# (cstringLen# x#)))- where !x# = "field:chunks"#--sctpChunksSentField :: Field-sctpChunksSentField = Field (UnmanagedBytes (Addr x#) (I# (cstringLen# x#)))- where !x# = "field:chunks_sent"#--sctpChunksReceivedField :: Field-sctpChunksReceivedField = Field (UnmanagedBytes (Addr x#) (I# (cstringLen# x#)))- where !x# = "field:chunks_received"#--moduleField :: Field-moduleField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "field:module"#--descriptionField :: Field-descriptionField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "field:description"#--eventIdField :: Field-eventIdField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "field:eventId"#--objectField :: Field-objectField = Field ( UnmanagedBytes (Addr x#) (I# ( cstringLen# x#)))- where !x# = "field:object"#---untilSpace :: e -> Parser e s Bounds-{-# inline untilSpace #-}-untilSpace e = do- start <- Unsafe.cursor- Latin.skipTrailedBy e ' '- endSucc <- Unsafe.cursor- let end = endSucc - 1- pure (Bounds start (end - start))--untilComma :: e -> Parser e s Bounds-{-# inline untilComma #-}-untilComma e = do- start <- Unsafe.cursor- Latin.skipTrailedBy e ','- endSucc <- Unsafe.cursor- let end = endSucc - 1- pure (Bounds start (end - start))--skipThroughComma :: e -> Parser e s ()-{-# inline skipThroughComma #-}-skipThroughComma e = Latin.skipTrailedBy e ','---- There should not be any more commas left in the input.--- This takes until it finds a comma or until end of input--- is reached.-finalField :: Parser e s Bounds-{-# inline finalField #-}-finalField = do- start <- Unsafe.cursor- Latin.skipUntil ','- end <- Unsafe.cursor- pure (Bounds start (end - start))---- This does not require that any digits are--- actually present.-skipDigitsThroughComma :: e -> Parser e s ()-{-# inline skipDigitsThroughComma #-}-skipDigitsThroughComma e =- Latin.skipDigits *> Latin.char e ','--w64Comma :: e -> Parser e s Word64-{-# inline w64Comma #-}-w64Comma e = do- w <- Latin.decWord64 e- Latin.char e ','- pure w--w16Comma :: e -> Parser e s Word16-{-# inline w16Comma #-}-w16Comma e = Latin.decWord16 e <* Latin.char e ','---- Returns the receive time and the serial number. There is a--- little subtlety here. The PANOS guide says that logs should--- start with something like:--- 1,2019/07/14 10:26:22,005923187997--- The leading field is reserved for future use. However, there--- is typically an additional prefix consisting of a syslog priority,--- another datetime (in a different format), and a hostname:--- <14> Jul 14 11:26:23 MY-HOST.example.com 1,...--- The datetime is within typically within a second of the other one.--- Additionally, it's missing the year. So, we discard it. The--- syslog priority is worthless, so we throw it out as well. The--- host name, however, does provide useful information that does--- not exist elsewhere in the log. We should be as flexible--- as possible with this somewhat fragile part of the log.-parserPrefix :: Parser Field s (Bounds,Datetime,Bounds)-{-# inline parserPrefix #-}-parserPrefix = do- Latin.skipChar ' '- -- We allow the syslog priority (the number in angle brackets)- -- to be absent.- Latin.trySatisfy (== '<') >>= \case- True -> do- Latin.skipTrailedBy syslogPriorityField '>'- Latin.skipChar ' '- False -> pure ()- Ascii.skipAlpha1 syslogDatetimeField -- Month- Latin.skipChar1 syslogDatetimeField ' '- Latin.skipDigits1 syslogDatetimeField -- Day- Latin.skipChar1 syslogDatetimeField ' '- Latin.skipDigits1 syslogDatetimeField -- Hour- Latin.char syslogDatetimeField ':'- Latin.skipDigits1 syslogDatetimeField -- Minute- Latin.char syslogDatetimeField ':'- Latin.skipDigits1 syslogDatetimeField -- Second- Latin.skipChar1 syslogDatetimeField ' '- hostBounds <- untilSpace syslogHostField- Latin.skipChar ' '- skipThroughComma futureUseDField- !recv <- parserDatetime receiveTimeDateField receiveTimeTimeField- !ser <- untilComma serialNumberField- pure (hostBounds,recv,ser)---- | Decode a PAN-OS syslog message of an unknown type.-decode :: Bytes -> Either Field Log-decode b = case P.parseBytes parserLog b of- P.Failure e -> Left e- P.Success (P.Slice _ len r) -> case len of- 0 -> Right r- _ -> Left leftoversField--parserLog :: Parser Field s Log-parserLog = do- (!hostBounds,!receiveTime,!serialNumber) <- parserPrefix- Latin.any typeField >>= \case- 'S' -> do- Latin.char6 typeField 'Y' 'S' 'T' 'E' 'M' ','- !x <- parserSystem hostBounds receiveTime serialNumber- pure (LogSystem x)- 'T' -> Latin.any typeField >>= \case- 'R' -> do- Latin.char6 typeField 'A' 'F' 'F' 'I' 'C' ','- !x <- parserTraffic hostBounds receiveTime serialNumber- pure (LogTraffic x)- 'H' -> do- Latin.char5 typeField 'R' 'E' 'A' 'T' ','- !x <- parserThreat hostBounds receiveTime serialNumber- pure (LogThreat x)- _ -> P.fail typeField- _ -> P.fail typeField--parserTraffic :: Bounds -> Datetime -> Bounds -> Parser Field s Traffic-parserTraffic !syslogHost receiveTime !serialNumber = do- subtype <- untilComma subtypeField- skipThroughComma futureUseAField- -- The datetime parser consumes the trailing comma- timeGenerated <- parserDatetime timeGeneratedDateField timeGeneratedTimeField- sourceAddress <- IP.fromIPv4 <$> IPv4.parserUtf8Bytes sourceAddressField- Latin.char sourceAddressField ','- destinationAddress <- IP.fromIPv4 <$> IPv4.parserUtf8Bytes destinationAddressField- Latin.char destinationAddressField ','- natSourceIp <- IP.fromIPv4 <$> IPv4.parserUtf8Bytes natSourceIpField- Latin.char natSourceIpField ','- natDestinationIp <- IP.fromIPv4 <$> IPv4.parserUtf8Bytes natDestinationIpField- Latin.char natDestinationIpField ','- ruleName <- untilComma ruleNameField- sourceUser <- untilComma sourceUserField- destinationUser <- untilComma destinationUserField- application <- untilComma applicationField- virtualSystem <- untilComma virtualSystemField- sourceZone <- untilComma sourceZoneField- destinationZone <- untilComma destinationZoneField- inboundInterface <- untilComma inboundInterfaceField- outboundInterface <- untilComma outboundInterfaceField- logAction <- untilComma logActionField- skipThroughComma futureUseBField- sessionId <- w64Comma sessionIdField- repeatCount <- w64Comma repeatCountField- sourcePort <- w16Comma sourcePortField- destinationPort <- w16Comma destinationPortField- natSourcePort <- w16Comma natSourcePortField- natDestinationPort <- w16Comma natDestinationPortField- -- TODO: handle the flags- Latin.char actionFlagsField '0'- Latin.char actionFlagsField 'x'- _ <- untilComma flagsField- let flags = 0- ipProtocol <- untilComma ipProtocolField- action <- untilComma actionField- bytes <- w64Comma bytesField- bytesSent <- w64Comma bytesSentField- bytesReceived <- w64Comma bytesReceivedField- packets <- w64Comma packetsField- startTime <- parserDatetime startTimeDateField startTimeTimeField- elapsedTime <- w64Comma elapsedTimeField- category <- untilComma categoryField- skipThroughComma futureUseCField- sequenceNumber <- w64Comma sequenceNumberField- -- TODO: handle action flags- Latin.char actionFlagsField '0'- Latin.char actionFlagsField 'x'- _ <- untilComma actionFlagsField- let actionFlags = 0- sourceCountry <- untilComma sourceCountryField- destinationCountry <- untilComma destinationCountryField- skipThroughComma futureUseEField- packetsSent <- w64Comma packetsSentField- packetsReceived <- w64Comma packetsReceivedField- sessionEndReason <- untilComma sessionEndReasonField- deviceGroupHierarchyLevel1 <- w64Comma deviceGroupHierarchyLevel1Field- deviceGroupHierarchyLevel2 <- w64Comma deviceGroupHierarchyLevel2Field- deviceGroupHierarchyLevel3 <- w64Comma deviceGroupHierarchyLevel3Field- deviceGroupHierarchyLevel4 <- w64Comma deviceGroupHierarchyLevel4Field- virtualSystemName <- untilComma virtualSystemNameField- deviceName <- untilComma deviceNameField- actionSource <- untilComma actionSourceField- skipThroughComma sourceVmUuidField- skipThroughComma destinationVmUuidField- skipThroughComma tunnelIdField- skipThroughComma monitorTagField- skipThroughComma parentSessionIdField- skipThroughComma parentStartTimeField- skipThroughComma tunnelTypeField- skipThroughComma sctpAssociationIdField- skipDigitsThroughComma sctpChunksField- skipDigitsThroughComma sctpChunksSentField- Latin.skipDigits1 sctpChunksReceivedField- message <- Unsafe.expose- pure Traffic- { subtype , timeGenerated , sourceAddress , destinationAddress - , natSourceIp , natDestinationIp , ruleName , sourceUser - , destinationUser , application , virtualSystem , sourceZone - , destinationZone , inboundInterface , outboundInterface , logAction - , sessionId , repeatCount , sourcePort , destinationPort - , natSourcePort , natDestinationPort , ipProtocol - , action , bytes , bytesSent , bytesReceived - , packets , startTime , elapsedTime , category - , sequenceNumber , sourceCountry , destinationCountry - , packetsSent , sessionEndReason - , deviceGroupHierarchyLevel1 , deviceGroupHierarchyLevel2 - , deviceGroupHierarchyLevel3 , deviceGroupHierarchyLevel4 - , virtualSystemName , deviceName , actionSource , receiveTime- , serialNumber, packetsReceived, actionFlags, flags, message- , syslogHost- }--parserSystem :: Bounds -> Datetime -> Bounds -> Parser Field s System-parserSystem syslogHost receiveTime serialNumber = do- subtype <- untilComma subtypeField- skipThroughComma futureUseAField- -- The datetime parser consumes the trailing comma- timeGenerated <- parserDatetime timeGeneratedDateField timeGeneratedTimeField- virtualSystem <- untilComma virtualSystemField- eventId <- untilComma eventIdField- object <- untilComma objectField- skipThroughComma futureUseBField- skipThroughComma futureUseCField- module_ <- untilComma moduleField- severity <- untilComma severityField- Bytes{array=descriptionByteArray,offset=descrOff,length=descrLen} <-- parserOptionallyQuoted descriptionField- let descriptionBounds = Bounds descrOff descrLen- sequenceNumber <- w64Comma sequenceNumberField- -- TODO: handle action flags- Latin.char actionFlagsField '0'- Latin.char actionFlagsField 'x'- _ <- untilComma actionFlagsField- let actionFlags = 0- deviceGroupHierarchyLevel1 <- w64Comma deviceGroupHierarchyLevel1Field- deviceGroupHierarchyLevel2 <- w64Comma deviceGroupHierarchyLevel2Field- deviceGroupHierarchyLevel3 <- w64Comma deviceGroupHierarchyLevel3Field- deviceGroupHierarchyLevel4 <- w64Comma deviceGroupHierarchyLevel4Field- virtualSystemName <- untilComma virtualSystemNameField- deviceName <- finalField- message <- Unsafe.expose- pure System- { subtype , timeGenerated- , sequenceNumber - , deviceGroupHierarchyLevel1 , deviceGroupHierarchyLevel2 - , deviceGroupHierarchyLevel3 , deviceGroupHierarchyLevel4 - , virtualSystemName , deviceName , receiveTime- , serialNumber, actionFlags, message- , syslogHost, virtualSystem, eventId, object, module_- , severity, descriptionBounds, descriptionByteArray- }--parserThreat :: Bounds -> Datetime -> Bounds -> Parser Field s Threat-parserThreat !syslogHost receiveTime !serialNumber = do- subtype <- untilComma subtypeField- skipThroughComma futureUseAField- -- the datetime parser also grabs the trailing comma- timeGenerated <- parserDatetime timeGeneratedDateField timeGeneratedTimeField- sourceAddress <- IP.fromIPv4 <$> IPv4.parserUtf8Bytes sourceAddressField- Latin.char sourceAddressField ','- destinationAddress <- IP.fromIPv4 <$> IPv4.parserUtf8Bytes destinationAddressField- Latin.char destinationAddressField ','- natSourceIp <- IP.fromIPv4 <$> IPv4.parserUtf8Bytes natSourceIpField- Latin.char natSourceIpField ','- natDestinationIp <- IP.fromIPv4 <$> IPv4.parserUtf8Bytes natDestinationIpField- Latin.char natDestinationIpField ','- ruleName <- untilComma ruleNameField- sourceUser <- untilComma sourceUserField- destinationUser <- untilComma destinationUserField- application <- untilComma applicationField- virtualSystem <- untilComma virtualSystemField- sourceZone <- untilComma sourceZoneField- destinationZone <- untilComma destinationZoneField- inboundInterface <- untilComma inboundInterfaceField- outboundInterface <- untilComma outboundInterfaceField- logAction <- untilComma logActionField- skipThroughComma futureUseBField- sessionId <- w64Comma sessionIdField- repeatCount <- w64Comma repeatCountField- sourcePort <- w16Comma sourcePortField- destinationPort <- w16Comma destinationPortField- natSourcePort <- w16Comma natSourcePortField- natDestinationPort <- w16Comma natDestinationPortField- -- TODO: handle the flags- Latin.char actionFlagsField '0'- Latin.char actionFlagsField 'x'- _ <- untilComma flagsField- let flags = 0- ipProtocol <- untilComma ipProtocolField- action <- untilComma actionField- Bytes{array=miscellaneousByteArray,offset=miscOff,length=miscLen} <-- parserOptionallyQuoted miscellaneousField- let miscellaneousBounds = Bounds miscOff miscLen- (threatName,threatId) <- parserThreatId- category <- untilComma categoryField- severity <- untilComma severityField- direction <- untilComma directionField- sequenceNumber <- w64Comma sequenceNumberField- -- TODO: handle action flags- Latin.char actionFlagsField '0'- Latin.char actionFlagsField 'x'- _ <- untilComma actionFlagsField- let actionFlags = 0- sourceCountry <- untilComma sourceCountryField- destinationCountry <- untilComma destinationCountryField- skipThroughComma futureUseEField- contentType <- untilComma contentTypeField- pcapId <- w64Comma pcapIdField- fileDigest <- untilComma fileDigestField- cloud <- untilComma cloudField- urlIndex <- w64Comma urlIndexField- Bytes{array=userAgentByteArray,offset=uaOff,length=uaLen} <-- parserOptionallyQuoted userAgentField- let userAgentBounds = Bounds uaOff uaLen- fileType <- untilComma fileTypeField- forwardedFor <- untilComma forwardedForField- referer <- parserOptionallyQuoted refererField- sender <- parserOptionallyQuoted senderField- subject <- parserOptionallyQuoted subjectField- recipient <- parserOptionallyQuoted recipientField- reportId <- untilComma reportIdField- deviceGroupHierarchyLevel1 <- w64Comma deviceGroupHierarchyLevel1Field- deviceGroupHierarchyLevel2 <- w64Comma deviceGroupHierarchyLevel2Field- deviceGroupHierarchyLevel3 <- w64Comma deviceGroupHierarchyLevel3Field- deviceGroupHierarchyLevel4 <- w64Comma deviceGroupHierarchyLevel4Field- virtualSystemName <- untilComma virtualSystemNameField- deviceName <- untilComma deviceNameField- parserOptionallyQuoted_ futureUseFField- skipThroughComma sourceVmUuidField- skipThroughComma destinationVmUuidField- httpMethod <- untilComma httpMethodField- tunnelId <- w64Comma tunnelIdField- skipThroughComma monitorTagField- parentSessionId <- w64Comma parentSessionIdField- skipThroughComma parentStartTimeField- skipThroughComma tunnelTypeField- threatCategory <- untilComma threatCategoryField- contentVersion <- untilComma contentVersionField- skipThroughComma futureUseGField- sctpAssociationId <- w64Comma sctpAssociationIdField- payloadProtocolId <- w64Comma payloadProtocolField- -- TODO: Handle HTTP Headers correctly- httpHeaders <- finalOptionallyQuoted httpHeadersField- message <- Unsafe.expose- pure Threat- { subtype , timeGenerated , sourceAddress , destinationAddress - , natSourceIp , natDestinationIp , ruleName , sourceUser - , destinationUser , application , virtualSystem , sourceZone - , destinationZone , inboundInterface , outboundInterface , logAction - , sessionId , repeatCount , sourcePort , destinationPort - , natSourcePort , natDestinationPort , ipProtocol - , action , category - , sequenceNumber , sourceCountry , destinationCountry - , deviceGroupHierarchyLevel1 , deviceGroupHierarchyLevel2 - , deviceGroupHierarchyLevel3 , deviceGroupHierarchyLevel4 - , virtualSystemName , deviceName , receiveTime- , serialNumber, actionFlags, flags, message- , syslogHost, threatId, severity, direction, threatName- , contentType, pcapId- , fileDigest, cloud, urlIndex- , userAgentBounds, sctpAssociationId- , userAgentByteArray, fileType- , forwardedFor, referer- , sender, subject, recipient- , reportId, httpMethod, contentVersion- , threatCategory, miscellaneousBounds, miscellaneousByteArray- , payloadProtocolId, parentSessionId, tunnelId- , httpHeaders- }---- Threat IDs are weird. There are three different kinds of--- strings that can show up here:------ * (9999)--- * Microsoft RPC Endpoint Mapper Detection(30845)--- * Windows Executable (EXE)(52020)------ URL logs have a threat id of 9999, and there is no description.--- Everything else has a human-readable description. Sometimes,--- this description is suffixed by a space and a parenthesized--- acronym (EXE, DLL, etc.).-parserThreatId :: Parser Field s (Bounds,Word64)-parserThreatId = Latin.any threatIdField >>= \case- '(' -> do- theId <- Latin.decWord64 threatIdField- Latin.char threatIdField ')'- Latin.char threatIdField ','- pure (Bounds 0 0, theId)- _ -> do- startSucc <- Unsafe.cursor- Latin.skipTrailedBy threatIdField '('- end <- Latin.trySatisfy (\c -> isAsciiUpper c || isAsciiLower c) >>= \case- True -> do- endSuccSucc <- Unsafe.cursor- Latin.skipTrailedBy threatIdField '('- arr <- Unsafe.expose- -- We go back an extra character to remove the trailing- -- space. I do not believe this can lead to negative-length- -- slices, but the line of reasoning is muddy.- case indexCharArray arr (endSuccSucc - 3) of- ' ' -> pure (endSuccSucc - 3)- _ -> P.fail threatIdField- False -> do- endSucc <- Unsafe.cursor- pure (endSucc - 1)- theId <- Latin.decWord64 threatIdField- Latin.char threatIdField ')'- Latin.char threatIdField ','- let start = startSucc - 1- pure (Bounds start (end - start), theId)--parserOptionallyQuoted_ :: e -> Parser e s ()-parserOptionallyQuoted_ e = Latin.any e >>= \case- '"' -> do- _ <- consumeQuoted e 0- pure ()- ',' -> pure ()- _ -> Latin.skipTrailedBy e ','---- Precondition: the cursor is placed at the beginning of the--- possibly-quoted content. That is, the comma preceeding has--- already been consumed. This is very similar to parserOptionallyQuoted,--- but it differs slightly because there is no trailing comma. -finalOptionallyQuoted :: e -> Parser e s Bytes-finalOptionallyQuoted e = Latin.opt >>= \case- Nothing -> do- !array <- Unsafe.expose- pure $! Bytes{array,offset=0,length=0}- Just c -> case c of- '"' -> do- -- First, we do a run through just to see if anything- -- actually needs to be escaped.- start <- Unsafe.cursor- !n <- consumeFinalQuoted e 0- !array <- Unsafe.expose- !endSucc <- Unsafe.cursor- let end = endSucc - 1- if n == 0- then pure Bytes{array,offset=start,length=(end - start)}- else do- let !r = escapeQuotes Bytes{array,offset=start,length=(end - start)}- pure $! Bytes{array=r,offset=0,length=PM.sizeofByteArray r}- _ -> do- !startSucc <- Unsafe.cursor- Latin.skipUntil ','- !end <- Unsafe.cursor- !arr <- Unsafe.expose- let start = startSucc - 1- pure $! Bytes arr start (end - start)---- Precondition: the cursor is placed at the beginning of the--- possibly-quoted content. That is, the comma preceeding has--- already been consumed.-parserOptionallyQuoted :: e -> Parser e s Bytes-parserOptionallyQuoted e = Latin.any e >>= \case- '"' -> do- -- First, we do a run through just to see if anything- -- actually needs to be escaped.- start <- Unsafe.cursor- !n <- consumeQuoted e 0- !array <- Unsafe.expose- !endSuccSucc <- Unsafe.cursor- let end = endSuccSucc - 2- if n == 0- then pure Bytes{array,offset=start,length=(end - start)}- else do- let !r = escapeQuotes Bytes{array,offset=start,length=(end - start)}- pure $! Bytes{array=r,offset=0,length=PM.sizeofByteArray r}- ',' -> do- !array <- Unsafe.expose- pure $! Bytes{array,offset=0,length=0}- _ -> do- !startSucc <- Unsafe.cursor- Latin.skipTrailedBy e ','- !endSucc <- Unsafe.cursor- !arr <- Unsafe.expose- let start = startSucc - 1- let end = endSucc - 1- pure $! (Bytes arr start (end - start))---- Precondition: the input is a valid CSV-style quoted-escaped--- string. That is, any double quote character is guaranteed to--- be followed by another one.-escapeQuotes :: Bytes -> ByteArray-escapeQuotes (Bytes arr off0 len0) = runByteArrayST $ do- marr <- PM.newByteArray len0- let go !soff !doff !len = if len > 0- then do- let w :: Word8 = PM.indexByteArray arr soff- PM.writeByteArray marr doff w- if w /= c2w '"'- then go (soff + 1) (doff + 1) (len - 1)- else go (soff + 2) (doff + 1) (len - 2)- else pure doff- finalSz <- go off0 0 len0- marr' <- PM.resizeMutableByteArray marr finalSz- PM.unsafeFreezeByteArray marr'---- When this parser completed, the position in the input will be--- just after the comma that followed the quoted field.--- This is defined recursively.-consumeQuoted ::- e- -> Int -- the number of escaped quotes we have encountered- -> Parser e s Int-consumeQuoted e !n = do- Latin.skipTrailedBy e '"'- Latin.any e >>= \case- ',' -> pure n- '"' -> consumeQuoted e (n + 1)- _ -> P.fail e---- Like consumeQuoted except that we are expected end-of-input--- instead of a comma at the end.-consumeFinalQuoted ::- e- -> Int -- the number of escaped quotes we have encountered- -> Parser e s Int-consumeFinalQuoted e !n = do- Latin.skipTrailedBy e '"'- Latin.opt >>= \case- Nothing -> pure n- Just c -> case c of- '"' -> consumeFinalQuoted e (n + 1)- _ -> P.fail e--parserDatetime :: e -> e -> Parser e s Datetime-{-# noinline parserDatetime #-}-parserDatetime edate etime = do- year <- Latin.decWord edate- Latin.char edate '/'- monthPlusOne <- Latin.decWord edate- let month = monthPlusOne - 1- if month > 11- then P.fail edate- else pure ()- Latin.char edate '/'- day <- Latin.decWord edate- Latin.char etime ' '- hour <- Latin.decWord etime- Latin.char etime ':'- minute <- Latin.decWord etime- Latin.char etime ':'- second <- Latin.decWord etime- Latin.char etime ','- pure $ Datetime- (Date- (Year (fromIntegral year))- (Month (fromIntegral month))- (DayOfMonth (fromIntegral day))- )- (TimeOfDay- (fromIntegral hour)- (fromIntegral minute)- (1_000_000_000 * fromIntegral second)- )--cstringLen# :: Addr# -> Int#-{-# noinline cstringLen# #-}-cstringLen# ptr = go 0 where- go !ix@(I# ix#) = if PM.indexOffPtr (Ptr ptr) ix == (0 :: Word8)- then ix#- else go (ix + 1)--c2w :: Char -> Word8-c2w = fromIntegral . ord--indexCharArray :: ByteArray -> Int -> Char-indexCharArray (PM.ByteArray x) (I# i) =- Exts.C# (Exts.indexCharArray# x i)+ , User(..)+ , Correlation(..)+ , Field(..)+ , Bounds(..)+ -- * Decoding+ , decode+ ) where++import Panos.Syslog.Internal.Common++import Chronos (Datetime,Offset(Offset),OffsetDatetime(OffsetDatetime))+import Data.Bytes.Parser (Parser)+import Data.Bytes.Types (Bytes(..))+import GHC.Exts (Ptr(Ptr))+import Panos.Syslog.Internal.Traffic (Traffic(..),parserTraffic)+import Panos.Syslog.Internal.Threat (Threat(..),parserThreat)+import Panos.Syslog.Internal.System (System(..),parserSystem)+import Panos.Syslog.Internal.User (User(..),parserUser)+import Panos.Syslog.Internal.Correlation (Correlation(..),parserCorrelation)++import qualified Chronos+import qualified Data.Bytes.Parser as P+import qualified Data.Bytes.Parser.Ascii as Ascii+import qualified Data.Bytes.Parser.Latin as Latin+import qualified Data.Bytes.Parser.Unsafe as Unsafe++-- | Sum that represents all known PAN-OS syslog types. Use 'decode'+-- to parse a byte sequence into a structured log.+data Log+ = LogTraffic !Traffic+ | LogThreat !Threat+ | LogSystem !System+ | LogUser !User+ | LogCorrelation !Correlation+ | LogOther++untilSpace :: e -> Parser e s Bounds+{-# inline untilSpace #-}+untilSpace e = do+ start <- Unsafe.cursor+ Latin.skipTrailedBy e ' '+ endSucc <- Unsafe.cursor+ let end = endSucc - 1+ pure (Bounds start (end - start))++-- Returns the receive time and the serial number. There is a+-- little subtlety here. The PANOS guide says that logs should+-- start with something like:+-- 1,2019/07/14 10:26:22,005923187997+-- The leading field is reserved for future use. However, there+-- is typically an additional prefix consisting of a syslog priority,+-- another datetime (in a different format), and a hostname:+-- <14> Jul 14 11:26:23 MY-HOST.example.com 1,...+-- The datetime is within typically within a second of the other one.+-- Additionally, it's missing the year. So, we discard it. The+-- syslog priority is worthless, so we throw it out as well. The+-- host name, however, does provide useful information that does+-- not exist elsewhere in the log. We should be as flexible+-- as possible with this somewhat fragile part of the log.+--+-- Prisma logs add another wrinkle. These begin with:+-- <14>1 2021-10-27T19:21:00.034Z stream-logfwd20-example logforwarder - panwlogs - 2021-10-27T19:20:59.000000Z,no-serial,...+-- Notice that the timestamps are now ISO-8601 encoded. The hostname is+-- in Prisma logs is worthless, but the device_name from the PAN log+-- inside gives the end user the information they will want.+parserPrefix :: Parser Field s (Bounds,Datetime,Bounds)+{-# inline parserPrefix #-}+parserPrefix = do+ Latin.skipChar ' '+ -- We allow the syslog priority (the number in angle brackets)+ -- to be absent.+ Latin.trySatisfy (== '<') >>= \case+ True -> do+ Latin.skipTrailedBy syslogPriorityField '>'+ Latin.skipChar ' '+ False -> pure ()+ Latin.trySatisfy (== '1') >>= \case+ True -> do+ Latin.any futureUseDField >>= \case+ ',' -> do+ !recv <- parserDatetime receiveTimeDateField receiveTimeTimeField+ !ser <- untilComma serialNumberField+ pure (Bounds 0 0,recv,ser)+ ' ' -> do -- Prisma logs+ -- TODO: In chronos, add a datetime parser that discards the+ -- datetime instead of constructing it.+ _ <- P.orElse Chronos.parserUtf8BytesIso8601 (P.fail syslogDatetimeField)+ Latin.char syslogDatetimeField ' '+ hostBounds <- untilSpace syslogHostField+ P.cstring prismaDataField (Ptr "logforwarder - panwlogs - "# )+ OffsetDatetime recv (Offset off) <- P.orElse Chronos.parserUtf8BytesIso8601 (P.fail receiveTimeDateField)+ Latin.char syslogDatetimeField ','+ case off of+ 0 -> pure ()+ _ -> P.fail syslogDatetimeField+ !ser <- untilComma serialNumberField+ pure (hostBounds,recv,ser)+ _ -> P.fail futureUseDField+ False -> do+ Ascii.skipAlpha1 syslogDatetimeField -- Month+ Latin.skipChar1 syslogDatetimeField ' '+ Latin.skipDigits1 syslogDatetimeField -- Day+ Latin.skipChar1 syslogDatetimeField ' '+ Latin.skipDigits1 syslogDatetimeField -- Hour+ Latin.char syslogDatetimeField ':'+ Latin.skipDigits1 syslogDatetimeField -- Minute+ Latin.char syslogDatetimeField ':'+ Latin.skipDigits1 syslogDatetimeField -- Second+ Latin.skipChar1 syslogDatetimeField ' '+ hostBounds <- untilSpace syslogHostField+ Latin.skipChar ' '+ skipThroughComma futureUseDField+ !recv <- parserDatetime receiveTimeDateField receiveTimeTimeField+ !ser <- untilComma serialNumberField+ pure (hostBounds,recv,ser)++-- | Decode a PAN-OS syslog message of an unknown type. If there are+-- leftovers, we still succeed. We do this because every release of PAN-OS+-- adds a few more fields to the end, and it\'s good to have this library+-- be able to parse these logs even if it means ignoring the new fields.+decode :: Bytes -> Either Field Log+decode b = case P.parseBytes parserLog b of+ P.Failure e -> Left e+ P.Success (P.Slice _ _ r) -> Right r++parserLog :: Parser Field s Log+parserLog = do+ (!hostBounds,!receiveTime,!serialNumber) <- parserPrefix+ Latin.any typeField >>= \case+ 'C' -> do+ Latin.char11 typeField 'O' 'R' 'R' 'E' 'L' 'A' 'T' 'I' 'O' 'N' ','+ !x <- parserCorrelation hostBounds receiveTime serialNumber+ pure (LogCorrelation x)+ 'U' -> do+ Latin.char6 typeField 'S' 'E' 'R' 'I' 'D' ','+ !x <- parserUser hostBounds receiveTime serialNumber+ pure (LogUser x)+ 'S' -> do+ Latin.char6 typeField 'Y' 'S' 'T' 'E' 'M' ','+ !x <- parserSystem hostBounds receiveTime serialNumber+ pure (LogSystem x)+ 'T' -> Latin.any typeField >>= \case+ 'R' -> do+ Latin.char6 typeField 'A' 'F' 'F' 'I' 'C' ','+ !x <- parserTraffic hostBounds receiveTime serialNumber+ pure (LogTraffic x)+ 'H' -> do+ Latin.char5 typeField 'R' 'E' 'A' 'T' ','+ !x <- parserThreat hostBounds receiveTime serialNumber+ pure (LogThreat x)+ _ -> P.fail typeField+ _ -> P.fail typeField++
+ src/Panos/Syslog/User.hs view
@@ -0,0 +1,129 @@+{-# language BangPatterns #-}+{-# language MagicHash #-}+{-# language NamedFieldPuns #-}+{-# language DuplicateRecordFields #-}+{-# language NumericUnderscores #-}+{-# language DerivingStrategies #-}+{-# language GeneralizedNewtypeDeriving #-}+{-# language OverloadedRecordDot #-}++-- | Fields for user logs.+module Panos.Syslog.User+ ( dataSource+ , dataSourceName+ , deviceGroupHierarchyLevel1+ , deviceGroupHierarchyLevel2+ , deviceGroupHierarchyLevel3+ , deviceGroupHierarchyLevel4+ , deviceName+ , sourceIp+ , repeatCount+ , sequenceNumber+ , serialNumber+ , subtype+ , syslogHost+ , timeGenerated+ , user+ , virtualSystem+ , virtualSystemName+ ) where++import Chronos (Datetime)+import Data.Bytes.Types (Bytes(..))+import Data.Word (Word64)+import Net.Types (IP)+import Panos.Syslog.Unsafe (User(User),Bounds(Bounds))++import qualified Panos.Syslog.Unsafe as U++-- | Subtype of the system log; refers to the system daemon+-- generating the log; values are @crypto@, @dhcp@, @dnsproxy@,+-- @dos@, @general@, @global-protect@, @ha@, @hw@, @nat@, @ntpd@,+-- @pbf@, @port@, @pppoe@, @ras@, @routing@, @satd@, @sslmgr@,+-- @sslvpn@, @userid@, @url-filtering@, @vpn@.+subtype :: User -> Bytes+subtype (User{subtype=Bounds off len,message=msg}) =+ Bytes{offset=off,length=len,array=msg}++-- | The hostname of the firewall on which the session was logged.+deviceName :: User -> Bytes+deviceName (User{deviceName=Bounds off len,message=msg}) =+ Bytes{offset=off,length=len,array=msg}++-- | Identifies the end user.+user :: User -> Bytes+user (User{user=Bounds off len,message=msg}) =+ Bytes{offset=off,length=len,array=msg}++-- | Source from which mapping information is collected.+dataSource :: User -> Bytes+dataSource (User{dataSource=Bounds off len,message=msg}) =+ Bytes{offset=off,length=len,array=msg}++-- | User-ID source that sends the IP (Port)-User Mapping.+dataSourceName :: User -> Bytes+dataSourceName (User{dataSourceName=Bounds off len,message=msg}) =+ Bytes{offset=off,length=len,array=msg}++-- | Time the log was generated on the dataplane.+timeGenerated :: User -> Datetime+{-# inline timeGenerated #-}+timeGenerated u = u.timeGenerated++-- | A 64-bit log entry identifier incremented sequentially;+-- each log type has a unique number space.+sequenceNumber :: User -> Word64+{-# inline sequenceNumber #-}+sequenceNumber u = u.sequenceNumber++-- | Serial number of the firewall that generated the log. These+-- occassionally contain non-numeric characters, so do not attempt+-- to parse this as a decimal number.+serialNumber :: User -> Bytes+serialNumber (User{serialNumber=Bounds off len,message=msg}) =+ Bytes{offset=off,length=len,array=msg}++-- | Virtual System associated with the session.+virtualSystem :: User -> Bytes+virtualSystem (User{virtualSystem=Bounds off len,message=msg}) =+ Bytes{offset=off,length=len,array=msg}++-- | The name of the virtual system associated with the session; only valid+-- on firewalls enabled for multiple virtual systems.+virtualSystemName :: User -> Bytes+virtualSystemName (User{virtualSystemName=Bounds off len,message=msg}) =+ Bytes{offset=off,length=len,array=msg}++-- | The hostname from the syslog header appended to the PAN-OS log.+-- This field is not documented by Palo Alto Network and technically+-- is not part of the log, but in practice, it is always present.+-- This is similar to @deviceName@.+syslogHost :: User -> Bytes+syslogHost (User{syslogHost=Bounds off len,message=msg}) =+ Bytes{offset=off,length=len,array=msg}++-- | Number of total bytes (transmit and receive) for the session.+repeatCount :: User -> Word64+{-# inline repeatCount #-}+repeatCount u = u.repeatCount++deviceGroupHierarchyLevel1 :: User -> Word64+{-# inline deviceGroupHierarchyLevel1 #-}+deviceGroupHierarchyLevel1 u = u.deviceGroupHierarchyLevel1++deviceGroupHierarchyLevel2 :: User -> Word64+{-# inline deviceGroupHierarchyLevel2 #-}+deviceGroupHierarchyLevel2 u = u.deviceGroupHierarchyLevel2++deviceGroupHierarchyLevel3 :: User -> Word64+{-# inline deviceGroupHierarchyLevel3 #-}+deviceGroupHierarchyLevel3 u = u.deviceGroupHierarchyLevel3++deviceGroupHierarchyLevel4 :: User -> Word64+{-# inline deviceGroupHierarchyLevel4 #-}+deviceGroupHierarchyLevel4 u = u.deviceGroupHierarchyLevel4++-- | Original session source IP address.+sourceIp :: User -> IP+sourceIp = U.sourceIp+
test/Main.hs view
@@ -11,11 +11,13 @@ import Data.Char (ord,chr) import Data.Bytes.Types (Bytes(Bytes)) -import qualified Panos.Syslog.Traffic as Traffic-import qualified Panos.Syslog.Threat as Threat-import qualified Panos.Syslog.System as System import qualified Data.Primitive as PM import qualified GHC.Exts as Exts+import qualified Panos.Syslog.Correlation as Correlation+import qualified Panos.Syslog.System as System+import qualified Panos.Syslog.Threat as Threat+import qualified Panos.Syslog.Traffic as Traffic+import qualified Panos.Syslog.User as User import qualified Sample as S main :: IO ()@@ -25,6 +27,10 @@ testA putStrLn "8.1-Traffic-B" testTrafficB+ putStrLn "9.0-Traffic-A"+ testTraffic_9_0_A+ putStrLn "Prisma-Traffic-A"+ testPrismaTrafficA putStrLn "8.1-Threat-A" testB putStrLn "8.1-Threat-B"@@ -43,8 +49,24 @@ testThreatH putStrLn "8.1-Threat-I" testThreatI+ putStrLn "9.0-Threat-A"+ testThreat_9_0_A+ putStrLn "9.1-Threat-A"+ testThreat_9_1_A+ putStrLn "9.1-Threat-B"+ testThreat_9_1_B+ putStrLn "Prisma-Threat-A"+ testPrismaThreatA+ putStrLn "Prisma-Threat-B"+ testPrismaThreatB+ putStrLn "Prisma-Threat-C"+ testPrismaThreatC putStrLn "8.1-System-A" testSystemA+ putStrLn "User-A"+ testUserA+ putStrLn "Correlation-A"+ testCorrelationA putStrLn "Finished" testA :: IO ()@@ -81,6 +103,38 @@ | otherwise -> pure () Right _ -> fail "wrong log type" +testTraffic_9_0_A :: IO ()+testTraffic_9_0_A = case decode S.traffic_9_0_A of+ Left err -> throwIO err+ Right (LogTraffic t) ->+ if | Traffic.deviceName t /= bytes "NY-PAN-FW-5" ->+ fail $+ "wrong device name:\nexpected: " +++ show (bytes "NY-PAN-FW-5") +++ "\nactually: " +++ show (Traffic.deviceName t)+ | Traffic.actionSource t /= bytes "my-policy" ->+ fail $+ "wrong action source:\nexpected: " +++ show (bytes "my-policy") +++ "\nactually: " +++ prettyBytes (Traffic.actionSource t)+ | otherwise -> pure ()+ Right _ -> fail "wrong log type"++testPrismaTrafficA :: IO ()+testPrismaTrafficA = case decode S.traffic_prisma_A of+ Left err -> throwIO err+ Right (LogTraffic t) ->+ if | Traffic.deviceName t /= bytes "The-Device-Name" ->+ fail $+ "wrong device name:\nexpected: " +++ show (bytes "NY-PAN-FW-5") +++ "\nactually: " +++ show (Traffic.deviceName t)+ | otherwise -> pure ()+ Right _ -> fail "wrong log type"+ testTrafficB :: IO () testTrafficB = case decode S.traffic_8_1_B of Left err -> throwIO err@@ -110,6 +164,79 @@ | otherwise -> pure () Right _ -> fail "wrong log type" +testThreat_9_0_A :: IO ()+testThreat_9_0_A = case decode S.threat_9_0_A of+ Left err -> throwIO err+ Right (LogThreat t) ->+ if | Threat.miscellaneous t /= bytes "dt.adsafeprotected.com/" -> fail $+ "wrong miscellaneous (URL):\nExpected: dt.adsafeprotected.com/\nActually: " +++ prettyBytes (Threat.miscellaneous t) ++ "\n"+ | otherwise -> pure ()+ Right _ -> fail "wrong log type"++testThreat_9_1_A :: IO ()+testThreat_9_1_A = case decode S.threat_9_1_A of+ Left err -> throwIO err+ Right (LogThreat t) ->+ if | Threat.miscellaneous t /= bytes "192.0.2.17_solarwinds_zero_configuration:5986/" -> fail $+ "wrong miscellaneous (URL):\nExpected: " +++ "192.0.2.17_solarwinds_zero_configuration:5986/\nActually: " +++ prettyBytes (Threat.miscellaneous t) ++ "\n"+ | otherwise -> pure ()+ Right _ -> fail "wrong log type"++testThreat_9_1_B :: IO ()+testThreat_9_1_B = case decode S.threat_9_1_B of+ Left err -> throwIO err+ Right (LogThreat t) ->+ if | Threat.urlCategoryList t /= Just (bytes "social-networking,low-risk") ->+ fail $+ "wrong url category list\nExpected:\n"+ +++ "social-networking,low-risk"+ +++ "\nActually:\n"+ +++ maybe "(empty)" prettyBytes (Threat.urlCategoryList t)+ +++ "\n"+ | otherwise -> pure ()+ Right _ -> fail "wrong log type"++testPrismaThreatA :: IO ()+testPrismaThreatA = case decode S.threat_prisma_A of+ Left err -> throwIO err+ Right (LogThreat t) ->+ if | Threat.miscellaneous t /= bytes "play.google.com/" -> fail $+ "wrong miscellaneous (URL):\nExpected: " +++ "play.google.com/\nActually: " +++ prettyBytes (Threat.miscellaneous t) ++ "\n"+ | otherwise -> pure ()+ Right _ -> fail "wrong log type"++testPrismaThreatB :: IO ()+testPrismaThreatB = case decode S.threat_prisma_B of+ Left err -> throwIO err+ Right (LogThreat t) ->+ if | Threat.miscellaneous t /= bytes "example.com" -> fail $+ "wrong miscellaneous (domain name):\nExpected: " +++ "example.com\nActually: " +++ prettyBytes (Threat.miscellaneous t) ++ "\n"+ | otherwise -> pure ()+ Right _ -> fail "wrong log type"++testPrismaThreatC :: IO ()+testPrismaThreatC = case decode S.threat_prisma_C of+ Left err -> throwIO err+ Right (LogThreat t) ->+ if | Threat.subtype t /= bytes "wildfire" -> fail "wrong subtype"+ | Threat.miscellaneous t /= bytes "Exchange.asmx" -> fail $+ "wrong miscellaneous:\nExpected: " +++ "Exchange.asmx\nActually: " +++ prettyBytes (Threat.miscellaneous t) ++ "\n"+ | otherwise -> pure ()+ Right _ -> fail "wrong log type"+ testD :: IO () testD = case decode S.threat_8_1_C of Left err -> throwIO err@@ -201,6 +328,24 @@ | otherwise -> pure () Right _ -> fail "wrong log type" +testUserA :: IO ()+testUserA = case decode S.user_A of+ Left err -> throwIO err+ Right (LogUser t) ->+ if | User.subtype t /= bytes "login" -> fail $+ "wrong subtype name:\nexpected: login\nactually: " +++ show (User.subtype t)+ | User.user t /= bytes "BIGDAWG10$@EXAMPLE.COM" -> fail "wrong user"+ | otherwise -> pure ()+ Right _ -> fail "wrong log type"++testCorrelationA :: IO ()+testCorrelationA = case decode S.correlation_A of+ Left err -> throwIO err+ Right (LogCorrelation t) ->+ if | Correlation.objectId t /= 6005 -> fail "wrong object id"+ | otherwise -> pure ()+ Right _ -> fail "wrong log type" bytes :: String -> Bytes bytes s = let b = pack s in Bytes b 0 (PM.sizeofByteArray b)