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.
# 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 "`r`n"
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.