pan-os-syslog-0.2.0.0: src/Panos/Syslog/Unsafe.hs
{-# language BangPatterns #-}
{-# language DerivingStrategies #-}
{-# language DuplicateRecordFields #-}
{-# language GeneralizedNewtypeDeriving #-}
{-# language LambdaCase #-}
{-# language MagicHash #-}
{-# language NamedFieldPuns #-}
{-# language NumericUnderscores #-}
{-# language ScopedTypeVariables #-}
module Panos.Syslog.Unsafe
( -- * Types
Log(..)
, Traffic(..)
, Threat(..)
, System(..)
, 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