Make Fortinet SysLogs Human Readable with PowerShell

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.

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.