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 +5/−0
- LICENSE +30/−0
- Setup.hs +2/−0
- bench/Main.hs +13/−0
- common/Sample.hs +226/−0
- pan-os-syslog.cabal +96/−0
- src/Panos/Syslog.hs +12/−0
- src/Panos/Syslog/System.hs +87/−0
- src/Panos/Syslog/Threat.hs +240/−0
- src/Panos/Syslog/Traffic.hs +267/−0
- src/Panos/Syslog/Unsafe.hs +1245/−0
- test/Main.hs +219/−0
+ 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 []