packages feed

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 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)