###                      __                                  ___ 
###         .-----.--.--|  .-----.-----.  .----.-----.-----.'  _|
###         |     |_   _|  |  _  |  _  |__|  __|  _  |     |   _|
###         |__|__|__.__|__|_____|___  |__|____|_____|__|__|__|  
###                              |_____|                         
###                                                          
### "TGVzcyBjcmFzaCwgbGVzcyBzdGFzaCwgbW9yZSBjbGFzcyB0aGFuIExvZ3N0YXNoLg=="
###                                                          
### Description: Multi-source syslog relay configuration. Receives logs from:
###
###              SOURCE 1: FortiGate — TLS (port 6515), native KVP format,
###                        mapped to CEF.
###              SOURCE 2: Cisco ASA — UDP/TCP (port 6514), standard syslog
###                        (RFC 3164 / RFC 5424 / EMBLEM), forwarded to
###                        SentinelOne Singularity Data Lake.
###              SOURCE 3: F5 AFM (Advanced Firewall Manager) — UDP/TCP
###                        (port 6514), syslog KVP format, forwarded to
###                        SentinelOne Singularity Data Lake.
###              SOURCE 4: F5 ASM (Application Security Manager) — UDP/TCP
###                        (port 6514), syslog KVP format, forwarded to
###                        SentinelOne Singularity Data Lake.
###
###              OUTPUT 1: Local flat file for debugging/validation (FortiGate).
###              OUTPUT 2: UDP syslog to logcollector.oneeuronet.com:6514
###                        (Elasticsearch cluster ingestion — FortiGate).
###              OUTPUT 3: HTTPS POST to SentinelOne Singularity Data Lake
###                        addEvents API (FortiGate, Cisco ASA, F5 AFM, F5 ASM).
###
###              FortiGate outputs use disk-backed persistent queues (20 GB max)
###              with rate-limited drain to prevent flooding the destination
###              when connectivity is restored after an outage.
###
###              SentinelOne output uses disk-backed persistent queues (20 GB
###              max) with rate-limited drain for the same resilience guarantee.
###
###              INPUT 1: TLS (im_ssl) on port 6515 — FortiGate (validated).
###              INPUT 2: UDP (im_udp) on port 6514 — Cisco ASA / F5 AFM / F5 ASM.
###              INPUT 3: TCP (im_tcp) on port 6514 — Cisco ASA / F5 AFM / F5 ASM.
### Author(s): Rajib Gorkhali Pradhanang
###            Marco Belmonte
### Version: 1.3b1
###

# ------------------------------------------------------------------------------
# Global Directives & Constants
# ------------------------------------------------------------------------------
define CONFDIR  /opt/nxlog/etc
define CERTDIR  /opt/nxlog/var/lib/nxlog/cert
define LOGDIR   /var/log/nxlog
define QUEUEDIR /var/spool/nxlog

# Log settings for the NXLog agent itself
LogLevel info
LogFile  %LOGDIR%/nxlog.log

# Run as a non-privileged user after binding to privileged ports (Linux only)
#User  nxlog
#Group nxlog

# ------------------------------------------------------------------------------
# Extension Modules (Parsers & Formatters)
# ------------------------------------------------------------------------------
# Syslog extension for parsing incoming headers and formatting outgoing syslog.
# Also provides the Syslog_TLS InputType (octet-counting reader per RFC 5425/6587).
<Extension syslog>
    Module  xm_syslog
</Extension>

# Key-Value Pair (KVP) extension for parsing FortiGate's native log format
# and F5 ASM/AFM key-value pair log format.
<Extension kvp>
    Module         xm_kvp
    # FortiGate and F5 logs use '=' to separate keys from values
    KVDelimiter    =
    # FortiGate and F5 logs use spaces to separate different key-value pairs
    KVPDelimiter   ' '
    # Allow single, double, or unquoted values (handles fields like devname="FG")
    KeyQuoteChar   '"'
    ValueQuoteChar '"'
</Extension>

# Common Event Format (CEF) extension for formatting FortiGate output
<Extension cef>
    Module              xm_cef
    # Include hidden/internal fields starting with underscores in CEF extensions
    IncludeHiddenFields TRUE
</Extension>

# JSON extension for constructing the SentinelOne addEvents API payload
<Extension json>
    Module  xm_json
</Extension>

# ------------------------------------------------------------------------------
# Input 1: FortiGate — TLS on port 6515 (WORKING — validated)
# ------------------------------------------------------------------------------
<Input fortigate_tls_in>
    Module      im_ssl
    ListenAddr  0.0.0.0:6515

    # FortiGate uses RFC 6587 octet-counting framing even over TLS
    InputType   Syslog_TLS

    # TLS Configuration
    # Server leaf certificate and private key
    CertFile    %CERTDIR%/nxlog-server.crt
    CertKeyFile %CERTDIR%/nxlog-server.key

    # Set to TRUE to require FortiGate to present a valid client certificate (mTLS)
    RequireCert FALSE

    # Set to TRUE for testing with self-signed certs; set FALSE in production
    AllowUntrusted TRUE

    # Restrict to modern TLS only
    SSLProtocol TLSv1.2, TLSv1.3

    <Exec>
        parse_syslog();

        if defined($Message) {
            parse_kvp($Message);
        } else {
            parse_kvp($raw_event);
        }

        $CEFVersion       = 0;
        $CEFDeviceVendor  = "Fortinet";
        $CEFDeviceProduct = "FortiGate";

        if defined($devver) $CEFDeviceVersion = $devver;
        else $CEFDeviceVersion = "FortiOS";

        if defined($logid) $CEFSignatureID = $logid;
        else $CEFSignatureID = "0000000000";

        if defined($subtype) $CEFName = "FortiGate Log: " + $subtype;
        else $CEFName = "FortiGate Log";

        if not defined($level) {
            $CEFSeverity = 3;
        } else {
            if      $level == "emergency"   $CEFSeverity = 10;
            else if $level == "alert"       $CEFSeverity = 9;
            else if $level == "critical"    $CEFSeverity = 8;
            else if $level == "error"       $CEFSeverity = 7;
            else if $level == "warning"     $CEFSeverity = 5;
            else if $level == "notice"      $CEFSeverity = 3;
            else if $level == "information" $CEFSeverity = 1;
            else if $level == "debug"       $CEFSeverity = 0;
            else                            $CEFSeverity = 3;
        }

        # Source & Destination IPs
        if defined($srcip) $src = $srcip;
        if defined($dstip) $dst = $dstip;

        # Ports — convert to string first for safe regex check, then to integer for CEF
        if defined($srcport) and string($srcport) =~ /^\d+$/ $spt = integer($srcport);
        if defined($dstport) and string($dstport) =~ /^\d+$/ $dpt = integer($dstport);

        # Network Protocol — convert to string for comparison since xm_kvp may parse as integer
        if defined($proto) {
            if      string($proto) == "6"  $proto = "TCP";
            else if string($proto) == "17" $proto = "UDP";
            else if string($proto) == "1"  $proto = "ICMP";
        }

        if defined($action) $act = $action;

        if defined($srcintf) $deviceInboundInterface  = $srcintf;
        if defined($dstintf) $deviceOutboundInterface = $dstintf;

        # Policy and Session IDs — convert to string for regex, then to integer
        if defined($policyid)  and string($policyid)  =~ /^\d+$/ { $cn1 = integer($policyid);  $cn1Label = "PolicyID";  }
        if defined($sessionid) and string($sessionid) =~ /^\d+$/ { $cn2 = integer($sessionid); $cn2Label = "SessionID"; }

        # Bytes Sent and Received
        if defined($sentbyte) and string($sentbyte) =~ /^\d+$/ $out = integer($sentbyte);
        if defined($rcvdbyte) and string($rcvdbyte) =~ /^\d+$/ $in  = integer($rcvdbyte);

        # Duration
        if defined($duration) and string($duration) =~ /^\d+$/ { $cn3 = integer($duration); $cn3Label = "Duration"; }
    </Exec>
</Input>

# ------------------------------------------------------------------------------
# Input 2: Cisco ASA / F5 AFM / F5 ASM — UDP on port 6514
# ------------------------------------------------------------------------------
# Receives standard syslog datagrams from Cisco ASA (RFC 3164 / RFC 5424 /
# EMBLEM format) and F5 BIG-IP HSL syslog output (AFM and ASM).
# ------------------------------------------------------------------------------
<Input cisco_f5_udp_in>
    Module      im_udp
    ListenAddr  0.0.0.0:6514

    <Exec>
        # Parse the syslog header to extract $Hostname, $EventTime,
        # $SyslogSeverityValue, and $Message from the incoming datagram.
        parse_syslog();
    </Exec>
</Input>

# ------------------------------------------------------------------------------
# Input 3: Cisco ASA / F5 AFM / F5 ASM — TCP on port 6514
# ------------------------------------------------------------------------------
# Optional TCP listener for sources configured to use reliable TCP delivery.
# Uses RFC 6587 octet-counting framing to correctly delimit multi-line messages.
# ------------------------------------------------------------------------------
<Input cisco_f5_tcp_in>
    Module      im_tcp
    ListenAddr  0.0.0.0:6514

    # RFC 6587 octet-counting framing for reliable TCP syslog delivery
    InputType   Syslog_TLS

    <Exec>
        parse_syslog();
    </Exec>
</Input>

# ------------------------------------------------------------------------------
# Output 1: Local file (debug/validation — FortiGate CEF-formatted)
# ------------------------------------------------------------------------------
<Output file_out>
    Module  om_file
    # Output file path — one CEF-formatted log line per event
    File    '%LOGDIR%/fortigate-cef-test.log'

    ## Disk-backed persistent queue (20 GB max)
    ## Survives NXLog restarts and protects against data loss during outages
    #LogqueueDir     %QUEUEDIR%
    #LogqueueSize    21474836480
    #PersistLogqueue TRUE
    #SyncLogqueue    FALSE

    # Format the event as a CEF string before writing to disk
    Exec    $logtypeuid = "fwa2mk5SezxQsG848yerRi6Q"; to_cef();
    Exec    $Message = to_cef();
</Output>

# ------------------------------------------------------------------------------
# Output 2: UDP syslog to Elasticsearch cluster via logcollector.oneeuronet.com
# ------------------------------------------------------------------------------
<Output logcollector_udp_out>
    Module  om_udp
    Host    logcollector.oneeuronet.com:6514

    # Disk-backed persistent queue (20 GB max)
    # Buffers events to disk when the destination is unreachable
    LogqueueDir     %QUEUEDIR%
    LogqueueSize    21474836480
    PersistLogqueue TRUE
    SyncLogqueue    FALSE

    # Rate limiting: sleep 200 microseconds per event = max ~5000 events/sec
    # At ~500 bytes/event average, this caps throughput at roughly 2.5 MB/s (~20 Mbps).
    # This prevents NXLog from flooding the destination when draining a large queue
    # after an outage, while still being fast enough to clear a full 20 GB queue
    # in approximately 2-3 hours under sustained drain conditions.
    Exec    $logtypeuid = "fwa2mk5SezxQsG848yerRi6Q"; to_kvp();
    Exec    $Message = to_cef(); sleep(100);
</Output>

# ------------------------------------------------------------------------------
# Output 3: HTTPS POST to SentinelOne Singularity Data Lake (addEvents API)
# ------------------------------------------------------------------------------
# Receives events from cisco_f5_udp_in and cisco_f5_tcp_in, constructs the
# JSON payload required by the SentinelOne / DataSet addEvents REST API, and
# delivers it over HTTPS with Bearer token authentication.
#
# Replace <YOUR_SDL_API_TOKEN> with your Log Write Access key generated from:
#   Singularity Data Lake > (user menu) > API Keys > Log Access Key > Add Write Key
#
# Replace <YOUR_SDL_ENDPOINT_URL> with your regional addEvents endpoint, e.g.:
#   https://app.scalyr.com/api/addEvents        (US)
#   https://eu.scalyr.com/api/addEvents         (EU)
# ------------------------------------------------------------------------------
<Output sentinelone_sdl_out>
    Module      om_http

    # SentinelOne / DataSet addEvents endpoint — update for your region
    URL         https://<YOUR_SDL_ENDPOINT_URL>/api/addEvents

    # Payload must be JSON
    ContentType application/json

    # Disk-backed persistent queue (20 GB max)
    # Buffers events to disk when the SDL endpoint is unreachable
    LogqueueDir     %QUEUEDIR%
    LogqueueSize    21474836480
    PersistLogqueue TRUE
    SyncLogqueue    FALSE

    <Exec>
        # ------------------------------------------------------------------
        # Step 1 — Identify the log source type for sessionInfo.serverType.
        #          This value surfaces in the SDL UI for easy filtering.
        # ------------------------------------------------------------------
        $serverType = "unknown_syslog";

        if $raw_event =~ /%ASA-/ {
            # Cisco ASA: EMBLEM or standard syslog with %ASA-<sev>-<msgid>
            $serverType = "cisco_asa";
        } else if $raw_event =~ /attack_type=/ or $raw_event =~ /policy_name=/ {
            # F5 ASM: key-value pairs always include attack_type or policy_name
            $serverType = "f5_asm";
        } else if $raw_event =~ /acl_policy_name=/ or $raw_event =~ /acl_rule_name=/ {
            # F5 AFM: key-value pairs always include acl_policy_name or acl_rule_name
            $serverType = "f5_afm";
        } else if $raw_event =~ /devtype=/ or $raw_event =~ /CEFDeviceVendor=Fortinet/ or defined($CEFDeviceVendor) {
            # FortiGate: KVP logs always carry devtype= and the CEF pipeline sets
            # $CEFDeviceVendor; either marker reliably identifies this source.
            $serverType = "fortigate";
        }

        # ------------------------------------------------------------------
        # Step 2 — Convert the parsed syslog timestamp to nanoseconds since
        #          the Unix epoch (string type required by the addEvents API).
        # ------------------------------------------------------------------
        if defined($EventTime) {
            $ts = string(integer($EventTime) * 1000000000);
        } else {
            $ts = string(integer(now()) * 1000000000);
        }

        # ------------------------------------------------------------------
        # Step 3 — Map syslog severity (0–7) to SDL severity scale (0–6).
        #          SDL scale: 0=finest, 1=finer/trace, 2=fine/debug,
        #          3=info, 4=warn, 5=error, 6=fatal/critical
        # ------------------------------------------------------------------
        $sev = 3;
        if defined($SyslogSeverityValue) {
            if      $SyslogSeverityValue == 0 $sev = 6;
            else if $SyslogSeverityValue == 1 $sev = 6;
            else if $SyslogSeverityValue == 2 $sev = 5;
            else if $SyslogSeverityValue == 3 $sev = 5;
            else if $SyslogSeverityValue == 4 $sev = 4;
            else if $SyslogSeverityValue == 5 $sev = 3;
            else if $SyslogSeverityValue == 6 $sev = 3;
            else if $SyslogSeverityValue == 7 $sev = 2;
        }

        # ------------------------------------------------------------------
        # Step 4 — Construct the addEvents JSON payload.
        #          The session ID is derived from the source hostname and the
        #          epoch timestamp, producing a stable per-source identifier
        #          without relying on uuid() which is not available in all
        #          NXLog Enterprise builds.
        # ------------------------------------------------------------------
        $session_id = string($Hostname) + "-" + $ts;

        $raw_event = '{'
            + '"token":"<YOUR_SDL_API_TOKEN>",'
            + '"session":"' + $session_id + '",'
            + '"sessionInfo":{'
            +     '"serverType":"' + $serverType + '",'
            +     '"serverHost":"' + string($Hostname) + '"'
            + '},'
            + '"events":[{'
            +     '"ts":"'  + $ts  + '",'
            +     '"sev":'  + string($sev) + ','
            +     '"attrs":{"message":' + to_json($raw_event) + '}'
            + '}]'
            + '}';

        # ------------------------------------------------------------------
        # Step 5 — Inject the Bearer token authentication header.
        #          add_http_header() is an NXLog Enterprise Edition function.
        # ------------------------------------------------------------------
        add_http_header("Authorization", "Bearer <YOUR_SDL_API_TOKEN>");

        # Rate limiting: sleep 200 microseconds per event = max ~5000 events/sec
        # Mirrors the drain-rate policy used on logcollector_udp_out to prevent
        # flooding the SDL endpoint when recovering from a connectivity outage.
        sleep(200);
    </Exec>
</Output>

# ------------------------------------------------------------------------------
# Routing
# ------------------------------------------------------------------------------
# FortiGate TLS input feeds the local debug file, the Elasticsearch cluster,
# AND the SentinelOne SDL — preserving the original dual-output behaviour while
# adding SDL as a third destination.
# Cisco ASA and F5 inputs (UDP and TCP) feed exclusively to SentinelOne SDL.
# ------------------------------------------------------------------------------
<Route fortigate_to_file>
    Path    fortigate_tls_in => file_out
</Route>

<Route fortigate_to_logcollector>
    Path    fortigate_tls_in => logcollector_udp_out
</Route>

<Route fortigate_to_sentinelone>
    Path    fortigate_tls_in => sentinelone_sdl_out
</Route>

<Route cisco_f5_to_sentinelone>
    Path    cisco_f5_udp_in, cisco_f5_tcp_in => sentinelone_sdl_out
</Route>
