packages feed

pan-os-syslog (empty) → 0.1.0.0

raw patch · 12 files changed

+2442/−0 lines, 12 filesdep +basedep +byteslicedep +bytesmithsetup-changed

Dependencies added: base, byteslice, bytesmith, chronos, gauge, ip, pan-os-syslog, primitive, primitive-addr, run-st

Files

+ CHANGELOG.md view
@@ -0,0 +1,5 @@+# Revision history for palo-alto-syslog++## 0.1.0.0 -- 2020-01-15++* Initial release.
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2019, Andrew Martin++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Andrew Martin nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ bench/Main.hs view
@@ -0,0 +1,13 @@+import Gauge (bench,whnf,defaultMain)+import Panos.Syslog (decode)++import qualified Sample as S++main :: IO ()+main = defaultMain+  [ bench "8-1-Traffic-A" (whnf decode S.traffic_8_1_A)+  , bench "8-1-Threat-A" (whnf decode S.threat_8_1_A)+  , bench "8-1-Threat-B" (whnf decode S.threat_8_1_B)+  , bench "8-1-Threat-C" (whnf decode S.threat_8_1_C)+  ]+
+ common/Sample.hs view
@@ -0,0 +1,226 @@+{-# language TypeApplications #-}++module Sample+  ( traffic_8_1_A+  , traffic_8_1_B+  , threat_8_1_A+  , threat_8_1_B+  , threat_8_1_C+  , threat_8_1_D+  , threat_8_1_E+  , threat_8_1_F+  , threat_8_1_G+  , threat_8_1_H+  , threat_8_1_I+  , system_8_1_A+  ) where++import Data.Bytes (Bytes)+import Data.Word (Word8)+import Data.Char (ord)+import qualified Data.Bytes as Bytes+import qualified GHC.Exts as Exts++-- Sample Logs. If you add a sample log to this file, please+-- replace all information in the log that could possibly be+-- meaningful. At a bare minimum, this means:+--+-- * Replace any IP addresses with non-routable addresses +--   from the TEST-NET-1 block (192.0.2.0/24).+-- * Replace any domain names with the reserved domain+--   name example.com.+-- * Replace any hostnames with something like MY-HOST+--   or NY-APP or SAMPLE-HOST.+-- * Replace rule names.++pack :: String -> Bytes+pack = Bytes.fromByteArray . Exts.fromList . map (fromIntegral @Int @Word8 . ord)++-- Traffic Log +traffic_8_1_A :: Bytes+traffic_8_1_A = pack $ concat+  [ "<12> Jul 14 11:22:29 MY-HOST.example.com 1,2019/07/14 10:26:22,"+  , "003924147953,TRAFFIC,end,2057,2019/07/14 10:20:24,192.0.2.235,"+  , "192.0.2.251,0.0.0.0,0.0.0.0,Example-Rule-Name,,,incomplete,"+  , "vsys45,My-Source-Zone,My-Dest-Zone,eth-inbound.42,eth-outbound.43,"+  , "Example-Forwarding-Profile,2019/07/14 10:19:25,33651,1,59061,177,"+  , "0,0,0x19,tcp,deny,135,101,34,1,2019/07/14 10:18:15,0,my-category,"+  , "0,5384825641,0x8000000000000000,10.0.0.0-10.255.255.255,"+  , "10.0.0.0-10.255.255.255,0,1,0,policy-deny,174,0,0,0,MY-VSYS-NAME,"+  , "MY-DEVICE-NAME,from-policy,,,0,,0,,N/A,0,0,0,0"+  ]++traffic_8_1_B :: Bytes+traffic_8_1_B = pack $ concat+  [ "<13> Nov 1 11:26:30 TX-PAN-FW-1.example.org 1,2019/10/01 11:29:30,001922410172,"+  , "TRAFFIC,end,2067,2019/11/01 11:26:30,192.0.2.165,192.0.2.100,"+  , "192.0.2.130,192.0.2.100,Some-Rule,example\\jdoe,,google-base,"+  , "vsys13,ORG-TX-Private,ORG-TX-Public,ethernet9/4.57,ethernet7/3,"+  , "Forward-Logs,2019/10/01 11:33:34,267134,1,59173,443,5532,443,0x400053,"+  , "tcp,allow,19421,2151,17270,39,2019/10/01 11:29:17,0,search-engines,0,"+  , "2176523185,0x8000000000000000,10.0.0.0-10.255.255.255,United States,0,"+  , "18,21,tcp-fin,134,0,0,0,Some-Name,TX-PAN-FW-1,my-policy,,,0,,0,,N/A,"+  , "0,0,0,0"+  ]++-- Threat log for web browsing+threat_8_1_A :: Bytes+threat_8_1_A = pack $ concat+  [ "<13> Jul 23 15:45:11 NY-APP-8.local 1,2019/07/23 15:45:11,"+  , "028191718331,THREAT,url,2056,2019/07/23 15:45:12,192.0.2.240,"+  , "192.0.2.243,192.0.2.54,192.0.2.13,MY-RULE-NAME,,,ssl,vsys79,"+  , "src-zone,dst-zone,eth3/2.7,eth6/4,Example-Forwarding-Policy,"+  , "2019/07/23 15:45:17,95963,2,59457,443,3984,443,0x400000,tcp,"+  , "alert,\"www.example.com/string/\"\"hello\"\"\",(9999),"+  , "the-category-name,informational,client-to-server,"+  , "97089310,0xa000000000000000,10.0.0.0-10.255.255.255,United States,"+  , "0,,0,,,0,,,,,,,,0,225,0,0,0,,NY-APP-8,,,,,0,,0,,N/A,unknown,"+  , "AppThreat-1-6,0x0,0,3176142484,"+  ]++-- Threat log for web browsing+threat_8_1_B :: Bytes+threat_8_1_B = pack $ concat+  [ "<13> Jul 24 08:38:34 THE-FW-3.local 1,2019/07/24 08:38:34,"+  , "293471355489,THREAT,url,3179,2019/07/24 08:38:35,192.0.2.99,"+  , "192.0.2.57,192.0.2.53,192.0.2.11,MY-WEB-TRAFFIC-RULE,,,"+  , "web-browsing,vsys46,the-src-zone,the-dst-zone,ethernet13/6.721,"+  , "ethernet12/5,the-policy-name,2019/07/24 08:38:36,140720,1,39537,80,"+  , "55176,80,0x40b000,tcp,alert,\"www.example.com/\",(9999),"+  , "internet-communications-and-telephony,informational,client-to-server,"+  , "787958469,0xa000000000000000,10.0.0.0-10.255.255.255,United States,"+  , "0,text/html,0,,,107,"+  , "\"Mozilla/8.2 (Macintosh; Intel Mac OS X 10_19_7) AppleWebKit/943.87 "+  , "(KHTML, like Gecko) Chrome/95.17.6820.145 Safari/922.12\","+  , ",,,,,,"+  , "0,134,0,0,0,the-vsys-name,THE-FW-HOST,,,,get,0,,0,,N/A,unknown,"+  , "AppThreat-3-7,0x0,0,5323721019,"+  ]++-- Threat log for endpoint mapper detection, omitting syslog priority+-- from the header.+threat_8_1_C :: Bytes+threat_8_1_C = pack $ concat+  [ "<14> Jul 24 12:12:23 THE-FW-4.local 1,2019/07/24 12:12:22,"+  , "012001011326,THREAT,vulnerability,2049,2019/07/24 12:12:35,"+  , "192.0.2.3,192.0.2.3,0.0.0.0,0.0.0.0,SOME-RULE,,,"+  , "msrpc-base,vsys413,my-src-zone,my-dst-zone,eth5/4.2,tunnel.2341,"+  , "Example-Forward-Policy,2019/07/24 12:12:39,60519,1,61242,142,0,"+  , "0,0x2000,tcp,alert,\"\",Microsoft RPC Endpoint Mapper Detection(30845),"+  , "any,informational,client-to-server,89814684,0xa000000000000000,"+  , "10.0.0.0-10.255.255.255,10.0.0.0-10.255.255.255,0,,0,,,0,,,,,,,,"+  , "0,225,0,0,0,,THE-FW-4-HOST,,,,,0,,0,,N/A,info-leak,AppThreat-8174-5569,"+  , "0x0,0,6279943187,"+  ]++-- Threat log for email+threat_8_1_D :: Bytes+threat_8_1_D = pack $ concat+  [ "<13>Nov  3 07:01:51 BIG-OL-PAN.example.com 1,2019/11/03 07:01:51,"+  , "008724449461,THREAT,file,2049,2019/11/03 07:01:51,192.0.2.105,"+  , "192.0.2.106,192.0.2.107,192.0.2.108,My-Email-Rule,,,smtp,vsys17,"+  , "MY-ZN,YOUR-ZN,ae2,ae13.5,Syslog,2019/11/03 07:01:51,206756,1,31532,"+  , "25,42813,25,0x406000,tcp,alert,\"\",Email Link(52143),any,low,"+  , "client-to-server,87562570,0xa000000000000000,United States,"+  , "United States,0,,0,,,1,,,,"+  , ",\"From: \"\"John Doe\"\" <jdoe@example.com>\",\"Sub: Hello, Worlds\","+  , "To: <foo@bar.org>;  ,0,11,0,0,0,,BIG-OL-PAN,,,,,0,,0,,N/A,unknown,"+  , "AppThreat-8194-5693,0x0,0,5382217271,"+  ]+++-- Threat log for windows executable+threat_8_1_E :: Bytes+threat_8_1_E = pack $ concat+  [ "<13>Nov  3 07:28:05 bigbox.example.com 1,2019/11/03 07:28:04,"+  , "003942130261,THREAT,file,2049,2019/11/03 07:28:04,192.0.2.11,"+  , "192.0.2.12,0.0.0.0,0.0.0.0,Alpha-Rule,,,ms-ds-smbv2,vsys13,"+  , "Alpha-Zone,Beta-Zone,ae4.133,ae3,Syslog,2019/11/03 07:28:04,"+  , "187912,2,53181,445,0,0,0x6000,tcp,alert,\"SylinkDrop.exe\","+  , "Windows Executable (EXE)(52020),any,low,server-to-client,"+  , "96402817,0xa000000000000000,10.0.0.0-10.255.255.255,"+  , "10.0.0.0-10.255.255.255,0,,0,,,0,,,,,,,,0,11,0,0,0,,bigbox,"+  , ",,,,0,,0,,N/A,unknown,AppThreat-8217-3140,0x0,0,4794968105,"+  ]++-- Threat log for tmp file+threat_8_1_F :: Bytes+threat_8_1_F = pack $ concat+  [ "<13>Nov  3 07:37:10 fw-3.example.com 1,2019/11/03 07:37:09,001561170391,"+  , "THREAT,file,2049,2019/11/03 07:37:09,192.0.2.55,192.0.2.58,192.0.2.57,"+  , "192.0.2.56,Some-Rule,,,web-browsing,vsys51,ZONE-A,ZONE-B,ae5.67,ae8,"+  , "Syslog,2019/11/03 07:37:09,48536,2,49619,80,24617,80,0x406000,tcp,"+  , "alert,\"temp.tmp\",Temporary TMP File(52228),computer-and-internet-info,"+  , "low,server-to-client,78561201,0xa000000000000000,10.0.0.0-10.255.255.255,"+  , "United States,0,,0,,,1,,,,,,,,0,11,0,0,0,,fw-3,"+  , "download.garmin.com/garmindlm/temp.tmp,,,,0,,0,,N/A,unknown,"+  , "AppThreat-9311-6102,0x0,0,2694915680,"+  , "download.garmin.com/garmindlm/temp.tmp"+  ]++-- Threat log for PHP file+threat_8_1_G :: Bytes+threat_8_1_G = pack $ concat+  [ "<13>Nov  7 13:44:45 fw-4.example.com 1,2019/11/07 13:44:44,012301030273,"+  , "THREAT,file,2049,2019/11/07 13:44:44,192.0.2.3,192.0.2.4,192.0.2.5,"+  , "192.0.2.6,My-Rule,example\\jdoe,,web-browsing,vsys34,A-Zone,B-Zone,"+  , "ethernet1/1.201,ethernet1/6,Forward-Logs,2019/11/07 13:44:44,257969,"+  , "6,57863,80,29036,80,0x402000,tcp,alert,\"checksync.php\","+  , "Hypertext Preprocessor PHP File(52256),web-advertisements,low,"+  , "server-to-client,241048149,0xa000000000000000,10.0.0.0-10.255.255.255,"+  , "United States,0,,0,,,2,,,,,,,,0,134,0,0,0,Staging,fw-4,\"contextual."+  , "media.net/checksync.php?&vsSync=1&cs=1&hb=1&cv=37&ndec=1&cid=8HBJW752U"+  , "&prvid=41,108,141,181,192,3007,3008&refUrl=http://www.msn.com&"+  , "rtime=4\",,,,0,,0,,N/A,unknown,AppThreat-8307-9861,0x0,0,"+  , "4294967295,\"contextual.media.net/\""+  ]++-- URL threat log. Regression test. Also, the serial number in this+-- log has non-numeric characters.+threat_8_1_H :: Bytes+threat_8_1_H = pack $ concat+  [ "<14>Dec 10 19:11:09 fw-4.example.com 1,2019/12/10 19:11:08,002926C74589,"+  , "THREAT,url,2049,2019/12/10 19:11:08,192.0.2.123,192.0.2.125,192.0.2.126,"+  , "192.0.2.127,Some-Rule,example\\mbjordan,,web-browsing,vsys1,"+  , "X-Zone,Y-Zone,ethernet2/4.234,ethernet6/7,Forward-Logs,"+  , "2019/12/10 19:11:08,241982,1,59055,80,2039,80,0x403000,tcp,"+  , "alert,\"i.nflcdn.com/static/site/7.5/img/fonts/endzone-sans/"+  , "medium-cond.woff\",(9999),content-delivery-networks,informational,"+  , "client-to-server,257316637,0xa000000000000000,10.0.0.0-10.255.255.255,"+  , "United States,0,,0,,,1,\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "+  , "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/"+  , "537.36\",,,\"http://combine.nflcdn.com/yui/min2/index.php?7.5"+  , "hotfix-7.5.146&b=yui3%2Fstatic%2F7.5%2Fscripts%2Fmodules&f=font-"+  , "endzonesans-condmedium/font-endzonesans-condmedium.css,font-"+  , "endzoneslab-medium/font-endzoneslab-medium.css,font-endzoneslab-"+  , "bold/font-endzoneslab-bold.css,font-endzonetech-medium/font-"+  , "endzonetech-medium.css,font-endzonetech-bold/font-endzonetech-"+  , "bold.css,font-endzonesans-bold/font-endzonesans-bold.css\",,,,"+  , "0,134,0,0,0,Production,GA-PA-FW-1,,,,get,0,,0,,N/A,unknown,"+  , "AppThreat-0-0,0x0,0,4380241866,"+  ]++-- Spyware threat log.+threat_8_1_I :: Bytes+threat_8_1_I = pack $ concat+  [ "<12>Jan 10 01:56:39 NY-PA-FW-1.example 1,2020/01/10 01:56:38,"+  , "012862319694,THREAT,spyware,2049,2020/01/10 01:56:38,192.0.2.15,"+  , "192.0.2.30,192.0.2.31,192.0.2.32,Some-Rule,,,dns,vsys1,Src-Zone,"+  , "Dst-Zone,ethernet1/9.255,ethernet1/1,Forward-Logs,"+  , "2020/01/10 01:56:38,146414,1,45476,53,15021,53,0x80402000,udp,"+  , "sinkhole,\"\",Suspicious DNS Query (generic:comparetvs.net)(320511141),"+  , "any,medium,client-to-server,267287993,0xa000000000000000,"+  , "10.0.0.0-10.255.255.255,United States,0,,1315498975425810792,,,"+  , "0,,,,,,,,0,134,0,0,0,Production,GA-PA-FW-1,,,,,0,,0,,N/A,dns,"+  , "AppThreat-3218-3729,0x0,0,4294967295,"+  ]++-- System log (IKE delete)+system_8_1_A :: Bytes+system_8_1_A = pack $ concat+  [ "<14>Nov  9 19:53:08 NY-DC-FW-2.example.com 1,2019/10/06 15:46:26,"+  , "009732949126,SYSTEM,vpn,0,2019/10/19 15:39:29,,ike-send-p2-delete,"+  , "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"+  ]
+ pan-os-syslog.cabal view
@@ -0,0 +1,96 @@+cabal-version: 2.2+name: pan-os-syslog+version: 0.1.0.0+synopsis: Parse syslog traffic from PAN-OS+description:+  Parse syslog traffic from PAN-OS. The data types in this library are+  optimized for decoding logs, not for creating them. On consumer-grade+  hardware, the benchmark suite demonstrates that 500-byte traffic logs are+  parsed in under one microsecond. Contributions are welcome. This project's+  goals are:+  .+  * Support as many PAN-OS syslog types as possible: traffic,+    threat, hip-match, etc.+  .+  * Support as many versions of PAN-OS as possible: 8.0, 8.1, 9.0, etc.+  .+  * High performance. This library strives to avoid unneeded allocations.+    Some allocations cannot be avoided. For example, it is necessary to+    allocate space for the results. +  .+  * Do a minimum amount of useful work on each field. The reasoning is+    that users will typically discard most of the fields, so there is+    no point wasting clock cycles doing unneeded work. Its hard to define+    what this is precisely. Roughly, the rule this library follows is that+    integral fields are parsed as @Word64@, and non-integral fields are+    @Bytes@. This library does not attempt to validate hostnames, URIs, etc.+  .+  A good way to think about this library is that it is kind of like+  a tokenizer. It is the first step when parsing PAN-OS logs into+  some application-specific data type. There almost certainly needs+  to be a second step to decodes fields that are actually of interest+  to an application. This second step may involve validating URIs,+  splitting the user domain and user name, etc.+homepage: https://github.com/layer-3-communications/palo-alto-syslog+bug-reports: https://github.com/layer-3-communications/palo-alto-syslog/issues+license: BSD-3-Clause+license-file: LICENSE+author: Andrew Martin+maintainer: amartin@layer3com.com+copyright: 2019 Layer 3 Communications+category: Network+extra-source-files: CHANGELOG.md++library+  exposed-modules:+    Panos.Syslog+    Panos.Syslog.System+    Panos.Syslog.Traffic+    Panos.Syslog.Threat+    Panos.Syslog.Unsafe+  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+    , ip >=1.6 && <1.8+    , primitive >=0.7 && <0.8+    , primitive-addr >=0.1.0.2 && <2+    , run-st >=0.1 && <0.2+  hs-source-dirs: src+  ghc-options: -Wall -O2+  default-language: Haskell2010++test-suite test+  type: exitcode-stdio-1.0+  hs-source-dirs: test, common+  main-is: Main.hs+  other-modules:+    Sample+  build-depends:+    , base+    , pan-os-syslog+    , primitive+    , byteslice+  ghc-options: -Wall -O2+  default-language: Haskell2010++benchmark bench+  type: exitcode-stdio-1.0+  main-is: Main.hs+  other-modules: Sample+  hs-source-dirs:+      bench+    , common+  build-depends:+    , base+    , byteslice+    , gauge+    , primitive+    , pan-os-syslog+  ghc-options: -O2 -Wall+  default-language: Haskell2010++source-repository head+  type: git+  location: git://github.com/layer-3-communications/pan-os-syslog.git
+ src/Panos/Syslog.hs view
@@ -0,0 +1,12 @@+module Panos.Syslog+  ( -- * Types+    U.Log(..)+  , U.Traffic+  , U.Threat+  , U.System+  , U.Field+    -- * Decoding+  , U.decode+  ) where++import qualified Panos.Syslog.Unsafe as U
+ src/Panos/Syslog/System.hs view
@@ -0,0 +1,87 @@+{-# language BangPatterns #-}+{-# language MagicHash #-}+{-# language NamedFieldPuns #-}+{-# language DuplicateRecordFields #-}+{-# language NumericUnderscores #-}+{-# language DerivingStrategies #-}+{-# language GeneralizedNewtypeDeriving #-}++-- | Fields for system logs.+module Panos.Syslog.System+  ( description+  , deviceName+  , eventId+  , module_+  , object+  , sequenceNumber+  , serialNumber+  , severity+  , subtype+  , timeGenerated+  ) where++import Data.Bytes.Types (Bytes(..))+import Data.Word (Word64)+import Panos.Syslog.Unsafe (System(System),Bounds(Bounds))+import Chronos (Datetime)+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 :: System -> Bytes+subtype (System{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 :: System -> Bytes+deviceName (System{deviceName=Bounds off len,message=msg}) =+  Bytes{offset=off,length=len,array=msg}++-- | Name of the object associated with the system event.+object :: System -> Bytes+object (System{object=Bounds off len,message=msg}) =+  Bytes{offset=off,length=len,array=msg}++-- | This field is valid only when the value of the @subtype@+-- field is @general@. It provides additional information about+-- the sub-system generating the log; values are @general@,+-- @management@, @auth@, @ha@, @upgrade@, @chassis@.+module_ :: System -> Bytes+module_ (System{module_=Bounds off len,message=msg}) =+  Bytes{offset=off,length=len,array=msg}++-- | Severity associated with the event; values are @informational@,+-- @low@, @medium@, @high@, @critical@.+severity :: System -> Bytes+severity (System{severity=Bounds off len,message=msg}) =+  Bytes{offset=off,length=len,array=msg}++-- | String showing the name of the event.+eventId :: System -> Bytes+eventId (System{eventId=Bounds off len,message=msg}) =+  Bytes{offset=off,length=len,array=msg}++-- | Detailed description of the event, up to a maximum of 512 bytes.+description :: System -> Bytes+description (System{descriptionBounds=Bounds off len,descriptionByteArray=m}) =+  Bytes{offset=off,length=len,array=m}++-- | Time the log was generated on the dataplane.+timeGenerated :: System -> Datetime+timeGenerated = U.timeGenerated++-- | A 64-bit log entry identifier incremented sequentially;+-- each log type has a unique number space.+sequenceNumber :: System -> Word64+sequenceNumber = 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 :: System -> Bytes+serialNumber (System{serialNumber=Bounds off len,message=msg}) =+  Bytes{offset=off,length=len,array=msg}+
+ src/Panos/Syslog/Threat.hs view
@@ -0,0 +1,240 @@+{-# language BangPatterns #-}+{-# language MagicHash #-}+{-# language NamedFieldPuns #-}+{-# language DuplicateRecordFields #-}+{-# language NumericUnderscores #-}+{-# language DerivingStrategies #-}+{-# language GeneralizedNewtypeDeriving #-}++-- | Fields for threat logs.+module Panos.Syslog.Threat+  ( action+  , application+  , category+  , contentVersion+  , destinationAddress+  , destinationCountry+  , destinationPort+  , destinationUser+  , destinationZone+  , deviceName+  , httpHeaders+  , httpMethod+  , inboundInterface+  , miscellaneous+  , natDestinationIp+  , natDestinationPort+  , natSourceIp+  , natSourcePort+  , outboundInterface+  , recipient+  , referer+  , ruleName+  , sender+  , sequenceNumber+  , serialNumber+  , severity+  , sourceAddress+  , sourceCountry+  , sourcePort+  , sourceUser+  , sourceZone+  , subject+  , subtype+  , threatCategory+  , threatId+  , threatName+  , timeGenerated+  , virtualSystemName+  ) where++import Data.Bytes.Types (Bytes(..))+import Panos.Syslog.Unsafe (Threat(Threat),Bounds(Bounds))+import Data.Word (Word64,Word16)+import Chronos (Datetime)+import Net.Types (IP)+import qualified Panos.Syslog.Unsafe as U++-- | Subtype of threat log. Values include: @data@, @file@, @flood@,+-- @packet@, @scan@, @spyware@, @url@, @virus@, @vulnerability@,+-- @wildfire@, @wildfire-virus@.+subtype :: Threat -> Bytes+subtype (Threat{subtype=Bounds off len,message=msg}) =+  Bytes{offset=fromIntegral off,length=fromIntegral len,array=msg}++-- | Username of the user who initiated the session.+sourceUser :: Threat -> Bytes+sourceUser (Threat{sourceUser=Bounds off len,message=msg}) =+  Bytes{offset=fromIntegral off,length=fromIntegral len,array=msg}++-- | Username of the user to which the session was destined.+destinationUser :: Threat -> Bytes+destinationUser (Threat{destinationUser=Bounds off len,message=msg}) =+  Bytes{offset=fromIntegral off,length=fromIntegral len,array=msg}++-- | Interface that the session was sourced from.+inboundInterface :: Threat -> Bytes+inboundInterface (Threat{inboundInterface=Bounds off len,message=msg}) =+  Bytes{offset=fromIntegral off,length=fromIntegral len,array=msg}++-- | Interface that the session was destined to.+outboundInterface :: Threat -> Bytes+outboundInterface (Threat{outboundInterface=Bounds off len,message=msg}) =+  Bytes{offset=fromIntegral off,length=fromIntegral len,array=msg}++-- | Zone the session was sourced from.+sourceZone :: Threat -> Bytes+sourceZone (Threat{sourceZone=Bounds off len,message=msg}) =+  Bytes{offset=fromIntegral off,length=fromIntegral len,array=msg}++-- | Zone the session was destined to.+destinationZone :: Threat -> Bytes+destinationZone (Threat{destinationZone=Bounds off len,message=msg}) =+  Bytes{offset=fromIntegral off,length=fromIntegral len,array=msg}++-- | The hostname of the firewall on which the session was logged.+deviceName :: Threat -> Bytes+deviceName (Threat{deviceName=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.+--+-- This field is just the description string. The numerical identifier+-- goes can be accessed with 'threatId'.+threatName :: Threat -> Bytes+threatName (Threat{threatName=Bounds off len,message=msg}) =+  Bytes{offset=fromIntegral off,length=fromIntegral len,array=msg}++-- | The numerical identifier for a threat. See 'threatName'.+threatId :: Threat -> Word64+threatId = U.threatId++-- | Time the log was generated on the dataplane.+timeGenerated :: Threat -> Datetime+timeGenerated = 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@,+-- or @benign@; For other subtypes, the value is @any@.+category :: Threat -> Bytes+category (Threat{category=Bounds off len,message=msg}) =+  Bytes{offset=fromIntegral off,length=fromIntegral len,array=msg}++contentVersion :: Threat -> Bytes+contentVersion (Threat{contentVersion=Bounds off len,message=msg}) =+  Bytes{offset=fromIntegral off,length=fromIntegral len,array=msg}++-- | The name of the virtual system associated with the session;+-- only valid on firewalls enabled for multiple virtual systems.+virtualSystemName :: Threat -> Bytes+virtualSystemName (Threat{virtualSystemName=Bounds off len,message=msg}) =+  Bytes{offset=fromIntegral off,length=fromIntegral len,array=msg}++threatCategory :: Threat -> Bytes+threatCategory (Threat{threatCategory=Bounds off len,message=msg}) =+  Bytes{offset=fromIntegral off,length=fromIntegral len,array=msg}++httpHeaders :: Threat -> Bytes+httpHeaders = U.httpHeaders++miscellaneous :: Threat -> Bytes+miscellaneous (Threat{miscellaneousBounds=Bounds off len,miscellaneousByteArray=m}) =+  Bytes{offset=fromIntegral off,length=fromIntegral len,array=m}++-- | A 64-bit log entry identifier incremented sequentially. Each log+-- type has a unique number space. This field is not supported on+-- PA-7000 Series firewalls.+sequenceNumber :: Threat -> Word64+sequenceNumber = 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 :: Threat -> Bytes+serialNumber (Threat{serialNumber=Bounds off len,message=msg}) =+  Bytes{offset=off,length=len,array=msg}++-- | Severity associated with the threat; values are informational,+-- low, medium, high, critical.+severity :: Threat -> Bytes+severity (Threat{severity=Bounds off len,message=msg}) =+  Bytes{offset=fromIntegral off,length=fromIntegral len,array=msg}++referer :: Threat -> Bytes+referer = U.referer++httpMethod :: Threat -> Bytes+httpMethod (Threat{httpMethod=Bounds off len,message=msg}) =+  Bytes{offset=fromIntegral off,length=fromIntegral len,array=msg}++-- | Action taken for the session; values are @alert@, @allow@,+-- @deny@, @drop@, @drop-all-packets@, @reset-client@, @reset-server@,+-- @reset-both@, @block-url@.+action :: Threat -> Bytes+action (Threat{action=Bounds off len,message=msg}) =+  Bytes{offset=fromIntegral off,length=fromIntegral len,array=msg}++-- | Application associated with the session.+application :: Threat -> Bytes+application (Threat{application=Bounds off len,message=msg}) =+  Bytes{offset=fromIntegral off,length=fromIntegral len,array=msg}++-- | Name of the rule that the session matched.+ruleName :: Threat -> Bytes+ruleName (Threat{ruleName=Bounds off len,message=msg}) =+  Bytes{offset=fromIntegral off,length=fromIntegral len,array=msg}++-- | Original session destination IP address.+destinationAddress :: Threat -> IP+destinationAddress = U.destinationAddress++-- | Original session source IP address.+sourceAddress :: Threat -> IP+sourceAddress = U.sourceAddress++-- | Source port utilized by the session.+sourcePort :: Threat -> Word16+sourcePort = U.sourcePort++-- | Destination port utilized by the session.+destinationPort :: Threat -> Word16+destinationPort = U.destinationPort++sender :: Threat -> Bytes+sender = U.sender++subject :: Threat -> Bytes+subject = U.subject++recipient :: Threat -> Bytes+recipient = U.recipient++-- | Post-NAT destination port.+natDestinationPort :: Threat -> Word16+natDestinationPort = U.natDestinationPort++-- | If Source NAT performed, the post-NAT Source IP address.+natSourceIp :: Threat -> IP+natSourceIp = U.natSourceIp++-- | If Destination NAT performed, the post-NAT Destination IP address.+natDestinationIp :: Threat -> IP+natDestinationIp = U.natDestinationIp++-- | Post-NAT source port.+natSourcePort :: Threat -> Word16+natSourcePort = U.natSourcePort++-- | Source country or Internal region for private addresses;+-- maximum length is 32 bytes.+sourceCountry :: Threat -> Bytes+sourceCountry (Threat{sourceCountry=Bounds off len,message=msg}) =+  Bytes{offset=off,length=len,array=msg}++-- | Destination country or Internal region for private addresses.+-- Maximum length is 32 bytes.+destinationCountry :: Threat -> Bytes+destinationCountry (Threat{destinationCountry=Bounds off len,message=msg}) =+  Bytes{offset=off,length=len,array=msg}
+ src/Panos/Syslog/Traffic.hs view
@@ -0,0 +1,267 @@+{-# language BangPatterns #-}+{-# language MagicHash #-}+{-# language NamedFieldPuns #-}+{-# language DuplicateRecordFields #-}+{-# language NumericUnderscores #-}+{-# language DerivingStrategies #-}+{-# language GeneralizedNewtypeDeriving #-}++-- | Fields for traffic logs.+module Panos.Syslog.Traffic+  ( -- * Fields+    action+  , application+  , bytes+  , bytesReceived+  , bytesSent+  , destinationAddress+  , destinationCountry+  , destinationPort+  , destinationUser+  , destinationZone+  , deviceName+  , elapsedTime+  , inboundInterface+  , ipProtocol+  , logAction+  , natDestinationIp+  , natDestinationPort+  , natSourceIp+  , natSourcePort+  , outboundInterface+  , packets+  , packetsReceived+  , packetsSent+  , ruleName+  , sequenceNumber+  , serialNumber+  , sessionEndReason+  , sourceAddress+  , sourceCountry+  , sourcePort+  , sourceUser+  , sourceZone+  , subtype+  , syslogHost+  , timeGenerated+  , virtualSystem+  , virtualSystemName+  -- * Device Group Hierarchy+  , deviceGroupHierarchyLevel1+  , deviceGroupHierarchyLevel2+  , deviceGroupHierarchyLevel3+  , deviceGroupHierarchyLevel4+  ) where++import Chronos (Datetime)+import Data.Bytes.Types (Bytes(..))+import Panos.Syslog.Unsafe (Traffic(Traffic),Bounds(Bounds))+import Data.Word (Word64,Word16)+import Net.Types (IP)+import qualified Panos.Syslog.Unsafe as U++-- | IP protocol associated with the session.+ipProtocol :: Traffic -> Bytes+ipProtocol (Traffic{ipProtocol=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 :: Traffic -> Bytes+syslogHost (Traffic{syslogHost=Bounds off len,message=msg}) =+  Bytes{offset=off,length=len,array=msg}++-- | Subtype of traffic log; values are @start@, @end@, @drop@, and @deny@.+--+-- * Start: session started+-- * End: session ended+-- * Drop: session dropped before the application is identified and+--   there is no rule that allows the session.+-- * Deny: session dropped after the application is identified and+--   there is a rule to block or no rule that allows the session.+subtype :: Traffic -> Bytes+subtype (Traffic{subtype=Bounds off len,message=msg}) =+  Bytes{offset=off,length=len,array=msg}++-- | Name of the rule that the session matched.+ruleName :: Traffic -> Bytes+ruleName (Traffic{ruleName=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}) =+  Bytes{offset=off,length=len,array=msg}++-- | Interface that the session was destined to.+outboundInterface :: Traffic -> Bytes+outboundInterface (Traffic{outboundInterface=Bounds off len,message=msg}) =+  Bytes{offset=off,length=len,array=msg}++-- | Log Forwarding Profile that was applied to the session.+logAction :: Traffic -> Bytes+logAction (Traffic{logAction=Bounds off len,message=msg}) =+  Bytes{offset=off,length=len,array=msg}++-- | Action taken for the session; possible values are:+--+-- * allow: session was allowed by policy+-- * deny: session was denied by policy+-- * drop: session was dropped silently+-- * drop ICMP: session was silently dropped with an ICMP unreachable+--   message to the host or application+-- * reset both: session was terminated and a TCP reset is sent to+--   both the sides of the connection+-- * reset client: session was terminated and a TCP reset is sent to the client+-- * reset server: session was terminated and a TCP reset is sent to the server+action :: Traffic -> Bytes+action (Traffic{action=Bounds off len,message=msg}) =+  Bytes{offset=off,length=len,array=msg}++-- | Application associated with the session.+application :: Traffic -> Bytes+application (Traffic{application=Bounds off len,message=msg}) =+  Bytes{offset=off,length=len,array=msg}++-- | The hostname of the firewall on which the session was logged.+deviceName :: Traffic -> Bytes+deviceName (Traffic{deviceName=Bounds off len,message=msg}) =+  Bytes{offset=off,length=len,array=msg}++-- | Elapsed time of the session.+elapsedTime :: Traffic -> Word64+elapsedTime = U.elapsedTime++-- | Username of the user who initiated the session.+sourceUser :: Traffic -> Bytes+sourceUser (Traffic{sourceUser=Bounds off len,message=msg}) =+  Bytes{offset=off,length=len,array=msg}++-- | Username of the user to which the session was destined.+destinationUser :: Traffic -> Bytes+destinationUser (Traffic{destinationUser=Bounds off len,message=msg}) =+  Bytes{offset=off,length=len,array=msg}++-- | Number of server-to-client packets for the session.+packetsReceived :: Traffic -> Word64+packetsReceived = U.packetsReceived++-- | Number of client-to-server packets for the session.+packetsSent :: Traffic -> Word64+packetsSent = U.packetsSent++-- | Source port utilized by the session.+sourcePort :: Traffic -> Word16+sourcePort = U.sourcePort++-- | Time the log was generated on the dataplane.+timeGenerated :: Traffic -> Datetime+timeGenerated = U.timeGenerated++-- | A 64-bit log entry identifier incremented sequentially;+-- each log type has a unique number space.+sequenceNumber :: Traffic -> Word64+sequenceNumber = U.sequenceNumber++-- | Post-NAT source port.+natSourcePort :: Traffic -> Word16+natSourcePort = U.natSourcePort++-- | Destination port utilized by the session.+destinationPort :: Traffic -> Word16+destinationPort = U.destinationPort++-- | Post-NAT destination port.+natDestinationPort :: Traffic -> Word16+natDestinationPort = U.natDestinationPort++-- | If Source NAT performed, the post-NAT Source IP address.+natSourceIp :: Traffic -> IP+natSourceIp = U.natSourceIp++-- | If Destination NAT performed, the post-NAT Destination IP address.+natDestinationIp :: Traffic -> IP+natDestinationIp = U.natDestinationIp++-- | Original session source IP address.+sourceAddress :: Traffic -> IP+sourceAddress = U.sourceAddress++-- | Original session destination IP address.+destinationAddress :: Traffic -> IP+destinationAddress = U.destinationAddress++-- | Number of total packets (transmit and receive) for the session.+packets :: Traffic -> Word64+packets = U.packets++-- | Number of bytes in the server-to-client direction of the session.+bytesReceived :: Traffic -> Word64+bytesReceived = U.bytesReceived++-- | Number of bytes in the client-to-server direction of the session.+bytesSent :: Traffic -> Word64+bytesSent = U.bytesSent++-- | Number of total bytes (transmit and receive) for the session.+bytes :: Traffic -> Word64+bytes = U.bytes++-- | 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 :: Traffic -> Bytes+serialNumber (Traffic{serialNumber=Bounds off len,message=msg}) =+  Bytes{offset=off,length=len,array=msg}++deviceGroupHierarchyLevel1 :: Traffic -> Word64+deviceGroupHierarchyLevel1 = U.deviceGroupHierarchyLevel1++deviceGroupHierarchyLevel2 :: Traffic -> Word64+deviceGroupHierarchyLevel2 = U.deviceGroupHierarchyLevel2++deviceGroupHierarchyLevel3 :: Traffic -> Word64+deviceGroupHierarchyLevel3 = U.deviceGroupHierarchyLevel3++deviceGroupHierarchyLevel4 :: Traffic -> Word64+deviceGroupHierarchyLevel4 = U.deviceGroupHierarchyLevel4++-- | The reason a session terminated.+sessionEndReason :: Traffic -> Bytes+sessionEndReason (Traffic{sessionEndReason=Bounds off len,message=msg}) =+  Bytes{offset=off,length=len,array=msg}++-- | Virtual System associated with the session.+virtualSystem :: Traffic -> Bytes+virtualSystem (Traffic{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 :: Traffic -> Bytes+virtualSystemName (Traffic{virtualSystemName=Bounds off len,message=msg}) =+  Bytes{offset=off,length=len,array=msg}++-- | Zone the session was sourced from.+sourceZone :: Traffic -> Bytes+sourceZone (Traffic{sourceZone=Bounds off len,message=msg}) =+  Bytes{offset=off,length=len,array=msg}++-- | Zone the session was destined to.+destinationZone :: Traffic -> Bytes+destinationZone (Traffic{destinationZone=Bounds off len,message=msg}) =+  Bytes{offset=off,length=len,array=msg}++-- | Source country or Internal region for private addresses;+-- maximum length is 32 bytes.+sourceCountry :: Traffic -> Bytes+sourceCountry (Traffic{sourceCountry=Bounds off len,message=msg}) =+  Bytes{offset=off,length=len,array=msg}++-- | Destination country or Internal region for private addresses.+-- Maximum length is 32 bytes.+destinationCountry :: Traffic -> Bytes+destinationCountry (Traffic{destinationCountry=Bounds off len,message=msg}) =+  Bytes{offset=off,length=len,array=msg}
+ src/Panos/Syslog/Unsafe.hs view
@@ -0,0 +1,1245 @@+{-# 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(..)+  , 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)
+ test/Main.hs view
@@ -0,0 +1,219 @@+{-# language BangPatterns #-}+{-# language MultiWayIf #-}+{-# language ScopedTypeVariables #-}+{-# language TypeApplications #-}++import Panos.Syslog (Log(..),decode)++import Control.Exception (throwIO)+import Data.Primitive (ByteArray)+import Data.Word (Word8)+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 Sample as S++main :: IO ()+main = do+  putStrLn "Start"+  putStrLn "8.1-Traffic-A"+  testA+  putStrLn "8.1-Traffic-B"+  testTrafficB+  putStrLn "8.1-Threat-A"+  testB+  putStrLn "8.1-Threat-B"+  testC+  putStrLn "8.1-Threat-C"+  testD+  putStrLn "8.1-Threat-D"+  testThreatD+  putStrLn "8.1-Threat-E"+  testThreatE+  putStrLn "8.1-Threat-F"+  testThreatF+  putStrLn "8.1-Threat-G"+  testThreatG+  putStrLn "8.1-Threat-H"+  testThreatH+  putStrLn "8.1-Threat-I"+  testThreatI+  putStrLn "8.1-System-A"+  testSystemA+  putStrLn "Finished"++testA :: IO ()+testA = case decode S.traffic_8_1_A of+  Left err -> throwIO err+  Right (LogTraffic t) ->+    if | Traffic.deviceName t /= bytes "MY-DEVICE-NAME" ->+           fail $+             "wrong device name:\nexpected: " +++             show (bytes "MY-DEVICE-NAME") +++             "\nactually: " +++             show (Traffic.deviceName t)+       | Traffic.packetsReceived t /= 0 ->+           fail $+             "wrong packets received: expected 0 but got " +++             show (Traffic.packetsReceived t)+       | Traffic.bytesReceived t /= 34 ->+           fail $+             "wrong bytes received: expected 34 but got " +++             show (Traffic.bytesReceived t)+       | otherwise -> pure ()+  Right _ -> fail "wrong log type"++testB :: IO ()+testB = case decode S.threat_8_1_A of+  Left err -> throwIO err+  Right (LogThreat t) ->+    if | Threat.contentVersion t /= bytes "AppThreat-1-6" -> fail $+           "wrong content version:\nExpected: AppThreat-1-6\nActually: " +++           prettyBytes (Threat.contentVersion t)+       | Threat.miscellaneous t /= bytes "www.example.com/string/\"hello\"" -> fail $+           "wrong miscellaneous (URL):\nExpected: www.example.com\nActually: " +++           prettyBytes (Threat.miscellaneous t)+       | otherwise -> pure ()+  Right _ -> fail "wrong log type"++testTrafficB :: IO ()+testTrafficB = case decode S.traffic_8_1_B of+  Left err -> throwIO err+  Right (LogTraffic t) ->+    if | Traffic.sourceUser t /= bytes "example\\jdoe" ->+           fail $+             "wrong source user:\nexpected: " +++             show (bytes "MY-DEVICE-NAME") +++             "\nactually: " +++             show (Traffic.deviceName t)+       | otherwise -> pure ()+  Right _ -> fail "wrong log type"++testC :: IO ()+testC = case decode S.threat_8_1_B of+  Left err -> throwIO err+  Right (LogThreat t) ->+    if | Threat.miscellaneous t /= bytes "www.example.com/" -> fail $+           "wrong miscellaneous (URL):\nExpected: www.example.com/\nActually: " +++           prettyBytes (Threat.miscellaneous t) ++ "\n"+       | Threat.virtualSystemName t /= bytes "the-vsys-name" -> fail $+           "wrong vsys name:\nExpected: the-vsys-name\nActually: " +++           prettyBytes (Threat.virtualSystemName t) ++ "\n"+       | Threat.contentVersion t /= bytes "AppThreat-3-7" -> fail $+           "wrong content version:\nExpected: AppThreat-3-7\nActually: " +++           prettyBytes (Threat.contentVersion t) ++ "\n"+       | otherwise -> pure ()+  Right _ -> fail "wrong log type"++testD :: IO ()+testD = case decode S.threat_8_1_C of+  Left err -> throwIO err+  Right (LogThreat t) ->+    if | Threat.threatId t /= 30845 -> fail $+           "wrong threat id:\nExpected: 30845\nActually: " +++           show (Threat.threatId t) ++ "\n"+       | Threat.threatName t /= bytes "Microsoft RPC Endpoint Mapper Detection" -> fail $+           "wrong threat name:\nExpected: Microsoft RPC Endpoint Mapper Detection\nActually: " +++           prettyBytes (Threat.threatName t) ++ "\n"+       | otherwise -> pure ()+  Right _ -> fail "wrong log type"++testThreatD :: IO ()+testThreatD = case decode S.threat_8_1_D of+  Left err -> throwIO err+  Right (LogThreat t) ->+    if | Threat.threatName t /= bytes "Email Link" -> fail $+           "wrong threat name:\nExpected: Email Link\nActually: " +++           prettyBytes (Threat.threatName t) ++ "\n"+       | Threat.sender t /= bytes "From: \"John Doe\" <jdoe@example.com>" ->+           fail $ "wrong sender"+       | otherwise -> pure ()+  Right _ -> fail "wrong log type"++testThreatE :: IO ()+testThreatE = case decode S.threat_8_1_E of+  Left err -> throwIO err+  Right (LogThreat t) ->+    if | Threat.threatName t /= bytes "Windows Executable" -> fail $+           "wrong threat name:\nExpected: Windows Executable\nActually: " +++           prettyBytes (Threat.threatName t) ++ "\n"+       | otherwise -> pure ()+  Right _ -> fail "wrong log type"++testThreatF :: IO ()+testThreatF = case decode S.threat_8_1_F of+  Left err -> throwIO err+  Right (LogThreat t) ->+    if | Threat.threatName t /= bytes "Temporary TMP File" ->+           fail "wrong threat name"+       | otherwise -> pure ()+  Right _ -> fail "wrong log type"++testThreatG :: IO ()+testThreatG = case decode S.threat_8_1_G of+  Left err -> throwIO err+  Right (LogThreat t) ->+    if | Threat.threatName t /= bytes "Hypertext Preprocessor PHP File" ->+           fail "wrong threat name"+       | Threat.httpHeaders t /= bytes "contextual.media.net/" ->+           fail "wrong http headers"+       | otherwise -> pure ()+  Right _ -> fail "wrong log type"++testThreatH :: IO ()+testThreatH = case decode S.threat_8_1_H of+  Left err -> throwIO err+  Right (LogThreat t) ->+    if | Threat.threatId t /= 9999 ->+           fail "wrong threat id"+       | otherwise -> pure ()+  Right _ -> fail "wrong log type"++testThreatI :: IO ()+testThreatI = case decode S.threat_8_1_I of+  Left err -> throwIO err+  Right (LogThreat t) ->+    if | Threat.threatId t /= 320511141 ->+           fail "wrong threat id"+       | otherwise -> pure ()+  Right _ -> fail "wrong log type"++testSystemA :: IO ()+testSystemA = case decode S.system_8_1_A of+  Left err -> throwIO err+  Right (LogSystem t) ->+    if | System.deviceName t /= bytes "NY-DC-FW-2" -> fail $+           "wrong device name:\nexpected: NY-DC-FW-2\nactually: " +++           show (System.deviceName t)+       | System.description t /= bytes+           ( concat+             [ "IKE protocol IPSec SA delete message "+             , "sent to peer. SPI:0xA1CD910F."+             ]+           ) -> fail $+             "wrong description:\nexpected something about IKE\nactually: " +++             show (System.description t)+       | otherwise -> pure ()+  Right _ -> fail "wrong log type"+++bytes :: String -> Bytes+bytes s = let b = pack s in Bytes b 0 (PM.sizeofByteArray b)++pack :: String -> ByteArray+pack = Exts.fromList . map (fromIntegral @Int @Word8 . ord)++prettyBytes :: Bytes -> String+prettyBytes (Bytes arr off len) = if len > 0+  then+    let w = PM.indexByteArray arr off :: Word8+        c = if w > 31 && w < 127+              then chr (fromIntegral w)+              else 'X'+     in c : prettyBytes (Bytes arr (off + 1) (len - 1))+  else []