If you’ve ever looked at syslogs generated by a Fortinet Firewall, you know they are difficult to read. I was unable to find an easy way to make them human readable so I decided to do it myself with PowerShell and a little help from AI with the Regular Expressions (regex) needed to extract each key-value pair from the data.
Here’s an example to show what I mean.
Your Fortigate Syslog data looks like this:
time=22:59:23 devname="FGT40F-A" devid="FGT100FXXX" eventtime=1751259563122277579 tz="-0600" logid="0000000013" type="traffic" subtype="forward" level="notice" vd="root" srcip=188.117.57.162 srcport=44498 srcintf="wan" srcintfrole="wan" dstip=1.2.3.4 dstport=443 dstintf="lan" dstintfrole="lan" srccountry="United States" dstcountry="Canada" sessionid=36305090 proto=6 action="deny" policyid=17 policytype="policy" poluuid="528bd556-f7ad-51ef-dc1f-395084d39886" policyname="Block1" service="HTTPS" trandisp="dnat" tranip=192.168.1.49 tranport=443 duration=0 sentbyte=0 rcvdbyte=0 sentpkt=0 rcvdpkt=0 appcat="unscanned" crscore=30 craction=131072 crlevel="high"
time=23:07:56 devname="FGT40F-A" devid="FGT100FXXX" eventtime=1751260076522569039 tz="-0600" logid="0000000013" type="traffic" subtype="forward" level="notice" vd="root" srcip=201.163.2.188 srcport=38294 srcintf="wan" srcintfrole="wan" dstip=1.2.3.4 dstport=443 dstintf="lan" dstintfrole="lan" srccountry="United States" dstcountry="Canada" sessionid=36307786 proto=6 action="deny" policyid=17 policytype="policy" poluuid="528bd556-f7ad-51ef-dc1f-395084d39886" policyname="Block2" service="HTTPS" trandisp="dnat" tranip=192.168.1.249 tranport=443 duration=0 sentbyte=0 rcvdbyte=0 sentpkt=0 rcvdpkt=0 appcat="unscanned" crscore=30 craction=131072 crlevel="high"
time=23:11:09 devname="FGT40F-A" devid="FGT100FXXX" eventtime=1751260269071401579 tz="-0600" logid="0000000013" type="traffic" subtype="forward" level="notice" vd="root" srcip=133.118.195.68 srcport=11089 srcintf="wan" srcintfrole="wan" dstip=1.2.3.4 dstport=80 dstintf="VLAN2" dstintfrole="lan" srccountry="Brazil" dstcountry="Canada" sessionid=36308833 proto=6 action="deny" policyid=17 policytype="policy" poluuid="528bd556-f7ad-51ef-dc1f-395084d39886" policyname="Block2" service="tcp/82" trandisp="dnat" tranip=172.16.1.32 tranport=80 duration=0 sentbyte=0 rcvdbyte=0 sentpkt=0 rcvdpkt=0 appcat="unscanned" crscore=30 craction=131072 crlevel="high"
Unreadable, right? To fix that, run your syslog data through the script below and you end up with this:

So much easier to read, right? Not to mention now you can filter and sort to your hearts content.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 |
# Example logs - Use Get-Content or extract from a Syslog database or whatever you have to do to get the raw syslog data $Logs = @' time=22:59:23 devname="FGT100F-A" devid="FGT100FXXX" eventtime=1751259563122277579 tz="-0600" logid="0000000013" type="traffic" subtype="forward" level="notice" vd="root" srcip=188.117.57.162 srcport=44498 srcintf="wan" srcintfrole="wan" dstip=1.2.3.4 dstport=443 dstintf="lan" dstintfrole="lan" srccountry="United States" dstcountry="Canada" sessionid=36305090 proto=6 action="deny" policyid=17 policytype="policy" poluuid="528bd556-f7ad-51ef-dc1f-395084d39886" policyname="Block1" service="HTTPS" trandisp="dnat" tranip=192.168.1.49 tranport=443 duration=0 sentbyte=0 rcvdbyte=0 sentpkt=0 rcvdpkt=0 appcat="unscanned" crscore=30 craction=131072 crlevel="high" time=23:07:56 devname="FGT100F-A" devid="FGT100FXXXL" eventtime=1751260076522569039 tz="-0600" logid="0000000013" type="traffic" subtype="forward" level="notice" vd="root" srcip=201.163.2.188 srcport=38294 srcintf="wan" srcintfrole="wan" dstip=1.2.3.4 dstport=443 dstintf="lan" dstintfrole="lan" srccountry="United States" dstcountry="Canada" sessionid=36307786 proto=6 action="deny" policyid=17 policytype="policy" poluuid="528bd556-f7ad-51ef-dc1f-395084d39886" policyname="Block2" service="HTTPS" trandisp="dnat" tranip=192.168.1.249 tranport=443 duration=0 sentbyte=0 rcvdbyte=0 sentpkt=0 rcvdpkt=0 appcat="unscanned" crscore=30 craction=131072 crlevel="high" time=23:11:09 devname="FGT100F-A" devid="FGT100FXXX" eventtime=1751260269071401579 tz="-0600" logid="0000000013" type="traffic" subtype="forward" level="notice" vd="root" srcip=133.118.195.68 srcport=11089 srcintf="wan" srcintfrole="wan" dstip=1.2.3.4 dstport=80 dstintf="VLAN2" dstintfrole="lan" srccountry="Brazil" dstcountry="Canada" sessionid=36308833 proto=6 action="deny" policyid=17 policytype="policy" poluuid="528bd556-f7ad-51ef-dc1f-395084d39886" policyname="Block2" service="tcp/82" trandisp="dnat" tranip=172.16.1.32 tranport=80 duration=0 sentbyte=0 rcvdbyte=0 sentpkt=0 rcvdpkt=0 appcat="unscanned" crscore=30 craction=131072 crlevel="high" '@ -split "rn" Function Convert-UnixTimeToDateTime($inputUnixTime){ # Convert nanoseconds to ticks (1 tick = 100ns) $Ticks = [math]::Floor($inputUnixTime / 100) # Unix epoch as DateTime (in UTC) $Epoch = Get-Date -Date "1970-01-01 00:00:00Z" # Add ticks to epoch $ReadableTime = $Epoch.AddTicks($Ticks) # Show result in local time $ReadableTime.ToLocalTime() } Function Parse-FortiGateSyslogMsg { param ( [string]$msg ) $result = @{} # Pattern: key="value with spaces" OR key=value (no quotes) $pattern = '\b(?<key>\w+)=(".*?"|\S+)' <# Regex Character | Purpose -----------------|---------------------------------------------------------- \b | Asserts a word boundary. ( | Opens a capturing group. ? | Marks the following group as a named capturing group. <key> | Names the capturing group as "key". \w | Matches any word character (alphanumeric and underscore). + | Matches one or more of the preceding character or group. ) | Closes the named capturing group. = | Matches the literal equals sign character. ( | Opens a capturing group (for the value alternatives). " | Matches a literal double quote. . | Matches any character (except newline by default). * | Matches zero or more of the preceding character or group. ? | Makes the preceding quantifier lazy (matches as few as possible). " | Matches a literal double quote. | | Acts as an OR operator, allowing either the left or right pattern to match. \S | Matches any non-whitespace character. + | Matches one or more of the preceding character or group. ) | Closes the capturing group for the value alternatives. #> foreach ($match in [regex]::Matches($msg, $pattern)) { $key = $match.Groups['key'].Value $value = $match.Value -replace "^\w+=", '' # Strip quotes if present if ($value.StartsWith('"') -and $value.EndsWith('"')) { $value = $value.Substring(1, $value.Length - 2) } # Special case for Fortinet-style FILETIME timestamps if ($key -ieq 'eventtime' -and $value -match '^\d+$') { try { $value = Convert-UnixTimeToDateTime $value } catch { # If parsing fails, keep original } } $result[$key] = $value } return $result } $Results = @() foreach ($Entry in $logs) { $parsed = Parse-FortiGateSyslogMsg -msg $Entry $Results += [pscustomobject]$parsed } $Results | Out-GridView |
I’ve found this incredibly helpful and used it a lot since I wrote it so I figured I’d share.