bokamba / logforge / parse / FortiGate firewall

$ logforge parse fortigate

Parse FortiGate firewall logs → regex, Grok, Wazuh & rsyslog

FortiGate firewalls (Fortinet's FortiOS appliances) emit logs as a single line of space-separated key=value pairs, one event per line. Depending on how the appliance is configured they arrive over syslog to a collector, get pulled by FortiAnalyzer/FortiManager, or are shipped to a SIEM; the on-the-wire text is the same key=value blob regardless of transport. Every line leads with date= and time= as two separate fields (date=2026-07-03 time=14:22:15) rather than one combined timestamp, and carries device identity in devname= and devid=, a numeric logid=, and a type/subtype pair — the most common being type="traffic" subtype="forward" for through-traffic sessions, alongside type="utm", "event", and others.

The structure looks trivial until you try to tokenize it. Values are sometimes quoted and sometimes bare: devname="FGT60F" and action="accept" are quoted, but srcip=192.0.2.10 and dstport=443 are not, and a naive split on spaces breaks the moment a quoted value contains a space (a msg="..." or a URL). The key set is not fixed — it varies by log type, by FortiOS major version, and by which UTM features are licensed and enabled — so two lines from the same box can carry different keys, and a parser that assumes a fixed column order will fail. This is the canonical case for order-independent, key-anchored parsing rather than a rigid left-to-right template: when your real traffic reorders or omits keys between events, you want a pattern that matches each key=value pair wherever it appears (LogForge switches to lookahead-based captures once its sample lines disagree on key order).

For firewall analytics and detection the load-bearing fields are the five-tuple and the verdict: srcip, srcport, dstip, dstport, the service ("HTTPS", "SSH", …), and action, which is the field everyone alerts on — action="accept" versus action="deny" is the difference between allowed and blocked traffic. sentbyte and rcvdbyte drive volume and exfiltration analysis (a deny with both at 0 is a blocked connection attempt; a huge sentbyte to an external dstip is worth a look). level= carries the Fortinet severity (notice, warning, alert). Because FortiGate is one of the most widely deployed firewalls, mature decoders exist — Wazuh ships a FortiGate decoder using exactly the parent/child prematch pattern LogForge generates — but hand-writing one that survives version drift and the quoting rules is where the time goes.

Open this in LogForge →

What a FortiGate firewall line looks like

The key=value sample below is fed verbatim into the engine to produce every parser on this page.

date=2026-07-03 time=14:22:15 devname="FGT60F" devid="FGT60FTK20012345" logid="0000000013" type="traffic" subtype="forward" level="notice" srcip=192.0.2.10 srcport=51234 dstip=198.51.100.20 dstport=443 action="accept" service="HTTPS" sentbyte=15320 rcvdbyte=88210
date=2026-07-03 time=14:22:44 devname="FGT60F" devid="FGT60FTK20012345" logid="0000000013" type="traffic" subtype="forward" level="notice" srcip=192.0.2.31 srcport=61002 dstip=203.0.113.80 dstport=22 action="deny" service="SSH" sentbyte=0 rcvdbyte=0

Detected fields

The engine classified this sample as kv and consolidated 16 fields across 2 lines. Fields marked literal were identical on every sample line, so they are baked into the pattern as anchors rather than captured.

  • date : timestamp · literal
  • time : timestamp
  • devname : hostname · literal
  • devid : quoted_string · literal
  • logid : number · literal
  • type : quoted_string · literal
  • subtype : quoted_string · literal
  • level : severity · literal
  • srcip : ipv4
  • srcport : port
  • dstip : ipv4
  • dstport : port
  • action : quoted_string
  • service : quoted_string
  • sentbyte : number
  • rcvdbyte : number

Regex (named capture groups)

# sample: date=2026-07-03 time=14:22:15 devname="FGT60F" devid="FGT60FTK20012345" logid="0000000013" type="traffic" subtype="forward" level="notice" srcip=192.0.2.10 srcport=51234 dstip=198.51.100.20 dstport=443 action="accept" service="HTTPS" sentbyte=15320 rcvdbyte=88210
# groups: time=14:22:15, srcip=192.0.2.10, srcport=51234, dstip=198.51.100.20, dstport=443, action=accept, service=HTTPS, sentbyte=15320, rcvdbyte=88210
^date=2026-07-03 time=(?<time>\d+:\d+:\d+) devname="FGT60F" devid="FGT60FTK20012345" logid="0000000013" type="traffic" subtype="forward" level="notice" srcip=(?<srcip>\d{1,3}(?:\.\d{1,3}){3}) srcport=(?<srcport>\d{1,5}) dstip=(?<dstip>\d{1,3}(?:\.\d{1,3}){3}) dstport=(?<dstport>\d{1,5}) action="(?<action>[^"]*)" service="(?<service>[^"]*)" sentbyte=(?<sentbyte>-?\d+(?:\.\d+)?) rcvdbyte=(?<rcvdbyte>-?\d+(?:\.\d+)?)$

Grok pattern (Logstash / Elastic)

# custom patterns
FORTIGATE_NOTDQUOTE [^"]*

date=2026-07-03 time=%{TIME:time} devname="FGT60F" devid="FGT60FTK20012345" logid="0000000013" type="traffic" subtype="forward" level="notice" srcip=%{IPV4:srcip} srcport=%{INT:srcport} dstip=%{IPV4:dstip} dstport=%{INT:dstport} action="%{FORTIGATE_NOTDQUOTE:action}" service="%{FORTIGATE_NOTDQUOTE:service}" sentbyte=%{NUMBER:sentbyte} rcvdbyte=%{NUMBER:rcvdbyte}
  • note kv-structured input — consider the Logstash kv filter instead of (or after) grok
  • note constant field "date" embedded as literal anchor "2026-07-03" (varying=false)
  • note constant field "devname" embedded as literal anchor "FGT60F" (varying=false)
  • note constant field "devid" embedded as literal anchor "FGT60FTK20012345" (varying=false)
  • note constant field "logid" embedded as literal anchor "0000000013" (varying=false)
  • note constant field "type" embedded as literal anchor "traffic" (varying=false)
  • note constant field "subtype" embedded as literal anchor "forward" (varying=false)
  • note constant field "level" embedded as literal anchor "notice" (varying=false)
  • note custom patterns emitted — save the '# custom patterns' block to a file in your patterns_dir

Wazuh decoder (OS_Regex XML)

<!--
  Generated by LogForge - Wazuh decoder (OS_Regex dialect, not PCRE)
  sample: date=2026-07-03 time=14:22:15 devname="FGT60F" devid="FGT60FTK20012345" logid="0000000013" type="traffic" subtype="forward" level="notice" srcip=192.0.2.10 srcp
  test with: /var/ossec/bin/wazuh-logtest
-->

<decoder name="fortigate-kv">
  <prematch>^date=\d+-\d+-\d+ time=</prematch>
</decoder>

<decoder name="fortigate-kv">
  <parent>fortigate-kv</parent>
  <regex offset="after_parent">^(\d+:\d+:\d+) devname="\w+" devid="\w+" logid="\d+" type="\w+" subtype="\w+" level="\w+" srcip=(\d+.\d+.\d+.\d+) srcport=(\d+) dstip=(\d+.\d+.\d+.\d+) dstport=(\d+) action="(\w+)" service="(\w+)" sentbyte=(\d+)</regex>
  <order>time, srcip, srcport, dstip, dstport, action, service, sentbyte</order>
</decoder>

<decoder name="fortigate-kv">
  <parent>fortigate-kv</parent>
  <regex offset="after_parent"> rcvdbyte=(\d+)</regex>
  <order>rcvdbyte</order>
</decoder>

<!-- ============================================================
     ALERT RULE (starter) — put this in a RULES file, e.g.
     /var/ossec/etc/rules/local_rules.xml. Decoders and rules live
     in SEPARATE files. The rule matches the decoder above through
     <decoded_as>; set <level> and add <field>/<match> conditions so
     it alerts only on the events you care about. Rule ids 100000+
     are the user range — change them if they collide with yours.
     ============================================================ -->
<group name="fortigate,">
  <rule id="100000" level="3">
    <decoded_as>fortigate-kv</decoded_as>
    <description>fortigate: srcip=$(srcip) dstip=$(dstip) action=$(action)</description>
  </rule>

  <!-- Example — a higher-level alert gated on one field (uncomment and edit):
  <rule id="100001" level="10">
    <if_sid>100000</if_sid>
    <field name="action">^deny$</field>
    <description>fortigate: a denied action from $(action)</description>
  </rule>
  -->
</group>
  • note constant field "date" skipped (identical in every line)
  • note kv fields are extracted by same-named sibling decoders (offset="after_parent"), so per-line field order/absence is tolerated — the shared name is what makes Wazuh evaluate every sibling
  • note added a starter alert <rule> (level 3, matched to the decoder via <decoded_as>) — put it in a RULES file (not the decoders file), set the level, and add <field>/<match> conditions; the commented example child rule shows the pattern
  • note decoder order and prematch specificity may need site-specific tuning (other decoders in your ruleset can shadow these) — validate with /var/ossec/bin/wazuh-logtest

Wazuh's OS_Regex is not PCRE — a bare . is a literal dot and \. matches any character. Test Wazuh OS_Regex patterns →

rsyslog template / liblognorm rulebase

version=2
# fortigate — liblognorm v2 rulebase (generated by LogForge)
# Usage with rsyslog (mmnormalize runs liblognorm):
#   module(load="mmnormalize")
#   action(type="mmnormalize" rulebase="/etc/rsyslog.d/fortigate.rb" useRawMsg="on")
# Literal "%" is escaped as "%%"; raw tabs are written as \x09.
rule=fortigate:date=2026-07-03 time=%time:time-24hr% devname="FGT60F" devid="FGT60FTK20012345" logid="0000000013" type="traffic" subtype="forward" level="notice" srcip=%srcip:ipv4% srcport=%srcport:number% dstip=%dstip:ipv4% dstport=%dstport:number% action="%action:char-to{"extradata":"\""}%" service="%service:char-to{"extradata":"\""}%" sentbyte=%sentbyte:number% rcvdbyte=%rcvdbyte:number%
  • note kv structure: rsyslog offers mmfields (fast, fixed single-char separator, untyped) and mmnormalize (this rulebase, typed fields + literal anchors); mmnormalize was chosen for typed extraction
  • note chosen parser types: time=time-24hr, srcip=ipv4, srcport=number, dstip=ipv4, dstport=number, action=char-to("), service=char-to("), sentbyte=number, rcvdbyte=number

Splunk

# props.conf  (search-time extraction)
[<REPLACE_WITH_SOURCETYPE>]
EXTRACT-logforge = date=2026-07-03 time=(?<time>\d+:\d+:\d+) devname="FGT60F" devid="FGT60FTK20012345" logid="0000000013" type="traffic" subtype="forward" level="notice" srcip=(?<srcip>\d{1,3}(?:\.\d{1,3}){3}) srcport=(?<srcport>\d{1,5}) dstip=(?<dstip>\d{1,3}(?:\.\d{1,3}){3}) dstport=(?<dstport>\d{1,5}) action="(?<action>[^"]*)" service="(?<service>[^"]*)" sentbyte=(?<sentbyte>-?\d+(?:\.\d+)?) rcvdbyte=(?<rcvdbyte>-?\d+(?:\.\d+)?)

# Quick search-time test in SPL:
# | rex field=_raw "date=2026-07-03 time=(?<time>\\d+:\\d+:\\d+) devname=\"FGT60F\" devid=\"FGT60FTK20012345\" logid=\"0000000013\" type=\"traffic\" subtype=\"forward\" level=\"notice\" srcip=(?<srcip>\\d{1,3}(?:\\.\\d{1,3}){3}) srcport=(?<srcport>\\d{1,5}) dstip=(?<dstip>\\d{1,3}(?:\\.\\d{1,3}){3}) dstport=(?<dstport>\\d{1,5}) action=\"(?<action>[^\"]*)\" service=\"(?<service>[^\"]*)\" sentbyte=(?<sentbyte>-?\\d+(?:\\.\\d+)?) rcvdbyte=(?<rcvdbyte>-?\\d+(?:\\.\\d+)?)"
  • note EXTRACT-<class> names must be unique within a sourcetype stanza — rename EXTRACT-logforge if you already use that class for this sourcetype
  • note a timestamp field was detected: this EXTRACT only makes it a searchable field. To set the event _time at index time, add TIME_PREFIX and TIME_FORMAT to this props.conf stanza (TIME_FORMAT uses Splunk strptime, e.g. %Y-%m-%dT%H:%M:%S) — this generator does not guess the strptime format.

ES ingest

PUT _ingest/pipeline/fortigate
{
  "description": "LogForge-generated ingest pipeline for fortigate",
  "processors": [
    {
      "kv": {
        "field": "message",
        "field_split": " ",
        "value_split": "="
      }
    }
  ]
}
  • note grok: kv-structured input — consider the Logstash kv filter instead of (or after) grok
  • note grok: constant field "date" embedded as literal anchor "2026-07-03" (varying=false)
  • note grok: constant field "devname" embedded as literal anchor "FGT60F" (varying=false)
  • note grok: constant field "devid" embedded as literal anchor "FGT60FTK20012345" (varying=false)
  • note grok: constant field "logid" embedded as literal anchor "0000000013" (varying=false)
  • note grok: constant field "type" embedded as literal anchor "traffic" (varying=false)
  • note grok: constant field "subtype" embedded as literal anchor "forward" (varying=false)
  • note grok: constant field "level" embedded as literal anchor "notice" (varying=false)
  • note grok: custom patterns emitted — save the '# custom patterns' block to a file in your patterns_dir
  • note kv structure: emitted a { kv: { field: "message", field_split: " ", value_split: "=" } } processor — it extracts key=value pairs natively; adjust field_split/value_split if your delimiter differs
  • note a grok alternative is available too (the reused grok pattern is shown in the notes above); prefer the kv processor unless you need typed/anchored extraction
  • note test in Kibana Dev Tools with: POST _ingest/pipeline/fortigate/_simulate (supply a docs[] array whose _source.message holds a sample line)

Graylog

# Grok patterns to add under System > Grok Patterns:
# (Graylog needs these custom patterns installed globally BEFORE the rule/extractor below will work.)
#   FORTIGATE_NOTDQUOTE [^"]*

# --- Graylog processing pipeline rule (primary) ---
# Paste under System > Pipelines > Manage rules, then attach the rule to a pipeline stage.
rule "fortigate-parse"
when
  has_field("message")
then
  let gp = grok(pattern: "date=2026-07-03 time=%{TIME:time} devname=\"FGT60F\" devid=\"FGT60FTK20012345\" logid=\"0000000013\" type=\"traffic\" subtype=\"forward\" level=\"notice\" srcip=%{IPV4:srcip} srcport=%{INT:srcport} dstip=%{IPV4:dstip} dstport=%{INT:dstport} action=\"%{FORTIGATE_NOTDQUOTE:action}\" service=\"%{FORTIGATE_NOTDQUOTE:service}\" sentbyte=%{NUMBER:sentbyte} rcvdbyte=%{NUMBER:rcvdbyte}", value: to_string($message.message), only_named_captures: true);
  set_fields(gp);
end

# --- Graylog import-ready extractor JSON (secondary) ---
# Save as a .json file and import under System > Inputs > (input) > Manage extractors > Actions > Import extractors.
{
  "extractors": [
    {
      "title": "fortigate",
      "extractor_type": "grok",
      "converters": [],
      "order": 0,
      "cursor_strategy": "copy",
      "source_field": "message",
      "target_field": "",
      "extractor_config": {
        "grok_pattern": "date=2026-07-03 time=%{TIME:time} devname=\"FGT60F\" devid=\"FGT60FTK20012345\" logid=\"0000000013\" type=\"traffic\" subtype=\"forward\" level=\"notice\" srcip=%{IPV4:srcip} srcport=%{INT:srcport} dstip=%{IPV4:dstip} dstport=%{INT:dstport} action=\"%{FORTIGATE_NOTDQUOTE:action}\" service=\"%{FORTIGATE_NOTDQUOTE:service}\" sentbyte=%{NUMBER:sentbyte} rcvdbyte=%{NUMBER:rcvdbyte}",
        "named_captures_only": true
      },
      "condition_type": "none",
      "condition_value": ""
    }
  ],
  "version": "5.0.0"
}
  • note grok: kv-structured input — consider the Logstash kv filter instead of (or after) grok
  • note grok: constant field "date" embedded as literal anchor "2026-07-03" (varying=false)
  • note grok: constant field "devname" embedded as literal anchor "FGT60F" (varying=false)
  • note grok: constant field "devid" embedded as literal anchor "FGT60FTK20012345" (varying=false)
  • note grok: constant field "logid" embedded as literal anchor "0000000013" (varying=false)
  • note grok: constant field "type" embedded as literal anchor "traffic" (varying=false)
  • note grok: constant field "subtype" embedded as literal anchor "forward" (varying=false)
  • note grok: constant field "level" embedded as literal anchor "notice" (varying=false)
  • note grok: custom patterns emitted — save the '# custom patterns' block to a file in your patterns_dir
  • note 1 custom grok pattern(s) (FORTIGATE_NOTDQUOTE) must be installed globally first under System > Grok Patterns — see the block at the top of the output
  • note primary artifact is the processing-pipeline rule; the extractor JSON is an equivalent import-ready alternative for the classic extractor UI

Datadog

logforge_rule date=2026-07-03 time=%{data:time} devname="FGT60F" devid="FGT60FTK20012345" logid="0000000013" type="traffic" subtype="forward" level="notice" srcip=%{ipv4:srcip} srcport=%{integer:srcport} dstip=%{ipv4:dstip} dstport=%{integer:dstport} action="%{quotedString:action}" service="%{quotedString:service}" sentbyte=%{number:sentbyte} rcvdbyte=%{number:rcvdbyte}
  • note emitted rule name is "logforge_rule"; rename it to match your "fortigate" convention if desired
  • note kv input — consider Datadog's key-value/`keyvalue()` filter in the Grok Parser instead of anchoring each key by hand
  • note constant field "date" embedded as literal anchor "2026-07-03" (varying=false)
  • note field "time" (timestamp): could not derive a Joda/Java date format from the sample shape; using %{data} — add a date("…") format by hand if you need a parsed timestamp
  • note constant field "devname" embedded as literal anchor "FGT60F" (varying=false)
  • note constant field "devid" embedded as literal anchor "FGT60FTK20012345" (varying=false)
  • note constant field "logid" embedded as literal anchor "0000000013" (varying=false)
  • note constant field "type" embedded as literal anchor "traffic" (varying=false)
  • note constant field "subtype" embedded as literal anchor "forward" (varying=false)
  • note constant field "level" embedded as literal anchor "notice" (varying=false)
  • note paste this line into a Grok Parser processor in a Datadog Log Pipeline; matchers are anchored left-to-right and rule whitespace matches log whitespace. Complex or multi-shape logs may need Helper Rules.

Fluent Bit

[PARSER]
    Name        fortigate
    Format      regex
    Regex       ^date=2026-07-03 time=(?<time>\d+:\d+:\d+) devname="FGT60F" devid="FGT60FTK20012345" logid="0000000013" type="traffic" subtype="forward" level="notice" srcip=(?<srcip>\d{1,3}(?:\.\d{1,3}){3}) srcport=(?<srcport>\d{1,5}) dstip=(?<dstip>\d{1,3}(?:\.\d{1,3}){3}) dstport=(?<dstport>\d{1,5}) action="(?<action>[^"]*)" service="(?<service>[^"]*)" sentbyte=(?<sentbyte>-?\d+(?:\.\d+)?) rcvdbyte=(?<rcvdbyte>-?\d+(?:\.\d+)?)$
    Time_Key    time
    Time_Format %H:%M:%S
# Fluentd <parse> block:
#   <parse>
#     @type regexp
#     expression /^date=2026-07-03 time=(?<time>\d+:\d+:\d+) devname="FGT60F" devid="FGT60FTK20012345" logid="0000000013" type="traffic" subtype="forward" level="notice" srcip=(?<srcip>\d{1,3}(?:\.\d{1,3}){3}) srcport=(?<srcport>\d{1,5}) dstip=(?<dstip>\d{1,3}(?:\.\d{1,3}){3}) dstport=(?<dstport>\d{1,5}) action="(?<action>[^"]*)" service="(?<service>[^"]*)" sentbyte=(?<sentbyte>-?\d+(?:\.\d+)?) rcvdbyte=(?<rcvdbyte>-?\d+(?:\.\d+)?)$/
#     time_key time
#     time_format %H:%M:%S
#   </parse>
  • note Time_Key set to "time"; Time_Format "%H:%M:%S" is a best-effort strptime derived from the sample shape — verify it against your data (Fluent Bit uses %L for fractional seconds and %z for numeric offsets)

Vector

[transforms.fortigate_parse]
type = "remap"
inputs = ["REPLACE_WITH_SOURCE"]
source = '''
. |= parse_regex!(.message, r'date=2026-07-03 time=(?P<time>\d+:\d+:\d+) devname="FGT60F" devid="FGT60FTK20012345" logid="0000000013" type="traffic" subtype="forward" level="notice" srcip=(?P<srcip>\d{1,3}(?:\.\d{1,3}){3}) srcport=(?P<srcport>\d{1,5}) dstip=(?P<dstip>\d{1,3}(?:\.\d{1,3}){3}) dstport=(?P<dstport>\d{1,5}) action="(?P<action>[^"]*)" service="(?P<service>[^"]*)" sentbyte=(?P<sentbyte>-?\d+(?:\.\d+)?) rcvdbyte=(?P<rcvdbyte>-?\d+(?:\.\d+)?)')
'''
  • note kv input — parse_key_value!(.message) is the idiomatic Vector parser and is usually preferable to a regex

Loki

# promtail pipeline for "fortigate" (generated by LogForge)
# Add these stages under a scrape_config in your promtail config:
#   scrape_configs:
#     - job_name: fortigate
#       pipeline_stages:
# (the stages below are indented to sit under pipeline_stages)
pipeline_stages:
  - regex:
      expression: '^date=2026-07-03 time=(?P<time>\d+:\d+:\d+) devname="FGT60F" devid="FGT60FTK20012345" logid="0000000013" type="traffic" subtype="forward" level="notice" srcip=(?P<srcip>\d{1,3}(?:\.\d{1,3}){3}) srcport=(?P<srcport>\d{1,5}) dstip=(?P<dstip>\d{1,3}(?:\.\d{1,3}){3}) dstport=(?P<dstport>\d{1,5}) action="(?P<action>[^"]*)" service="(?P<service>[^"]*)" sentbyte=(?P<sentbyte>-?\d+(?:\.\d+)?) rcvdbyte=(?P<rcvdbyte>-?\d+(?:\.\d+)?)$'
  • note no low-cardinality field found to promote to a Loki label — omitted the `- labels:` stage; every captured field stays in the extracted map for later stages
  • note left in the extracted map (NOT promoted to labels — high cardinality would explode Loki streams): time, srcip, srcport, dstip, dstport, action, service, sentbyte, rcvdbyte

syslog-ng

parser p_fortigate {
    regexp-parser(
        prefix(".fortigate.")
        patterns("date=2026-07-03 time=(?<time>\\d+:\\d+:\\d+) devname=\"FGT60F\" devid=\"FGT60FTK20012345\" logid=\"0000000013\" type=\"traffic\" subtype=\"forward\" level=\"notice\" srcip=(?<srcip>\\d{1,3}(?:\\.\\d{1,3}){3}) srcport=(?<srcport>\\d{1,5}) dstip=(?<dstip>\\d{1,3}(?:\\.\\d{1,3}){3}) dstport=(?<dstport>\\d{1,5}) action=\"(?<action>[^\"]*)\" service=\"(?<service>[^\"]*)\" sentbyte=(?<sentbyte>-?\\d+(?:\\.\\d+)?) rcvdbyte=(?<rcvdbyte>-?\\d+(?:\\.\\d+)?)")
    );
};
  • note captured fields are stored as name-value pairs under the prefix ".fortigate." (e.g. a group (?<srcip>…) becomes ".fortigate.srcip")
  • note kv structure: syslog-ng has a dedicated kv-parser() that is simpler and more robust than regexp-parser for key=value logs — consider kv-parser(prefix(".logforge.")) instead of the emitted regexp-parser

FAQ

What fields does a FortiGate traffic log contain?
A traffic (type="traffic", subtype="forward") log carries date and time as separate fields, device identity (devname, devid), a numeric logid, the connection five-tuple (srcip, srcport, dstip, dstport), the service name, the action (accept/deny), byte counters (sentbyte, rcvdbyte), and a severity level. UTM and event logs add their own keys — the exact set depends on FortiOS version and enabled features.
Why are some FortiGate values quoted and others not?
FortiOS quotes values that may contain spaces or are string-typed (devname="FGT60F", action="accept", msg="…") and leaves numeric or token values bare (srcip=192.0.2.10, dstport=443). A parser must handle both forms and must not split on spaces inside a quoted value, which is the number-one cause of broken FortiGate parsing.
Why do two FortiGate lines have different keys?
Because the key set depends on the log type, subtype, FortiOS version, and which UTM inspection profiles are active. A traffic log and a UTM/threat log share the header keys but diverge in the body. Use an order-independent, key-anchored parser (match key=value pairs wherever they appear) rather than a fixed-column pattern.
How do I tell allowed traffic from blocked traffic in a FortiGate log?
The action field is the verdict: action="accept" is allowed, action="deny" is blocked (other values like "close", "timeout", and "server-rst" describe session teardown). A deny with sentbyte=0 and rcvdbyte=0 is a connection that was refused outright — a common signal when hunting for scanning or blocked lateral movement.

Try it on your own FortiGate firewall lines

Paste a few real lines, review the detected fields, and copy whichever format your stack needs. Free, no account, nothing uploaded.

Open this sample in LogForge →