Detection rules › Elastic

Newly Observed FortiGate Alert

Status
production
Severity
critical
Time window
7205m
Group by
event.category, event.outcome, message
Author
Elastic
Source
github.com/elastic/detection-rules

This rule detects FortiGate alerts that are observed for the first time in the previous 5 days of alert history. Analysts can use this to prioritize triage and response.

Rule body elastic

[metadata]
creation_date = "2026/01/21"
integration = ["fortinet_fortigate"]
maturity = "production"
updated_date = "2026/02/18"

[rule]
author = ["Elastic"]
description = """
This rule detects FortiGate alerts that are observed for the first time in the previous 5 days of alert history.
Analysts can use this to prioritize triage and response.
"""
from = "now-7205m"
interval = "5m"
language = "esql"
license = "Elastic License v2"
name = "Newly Observed FortiGate Alert"
risk_score = 99
rule_id = "2c40dfe2-c13e-48a8-8eff-fb9bfb2a7854"
severity = "critical"
tags = ["Use Case: Threat Detection", "Rule Type: Higher-Order Rule", "Resources: Investigation Guide", "Domain: Network", "Data Source: Fortinet"]
timestamp_override = "event.ingested"
type = "esql"

query = '''
FROM logs-fortinet_fortigate.*, filebeat-* metadata _id

| WHERE event.module == "fortinet_fortigate" and event.action in ("signature", "ssl-anomaly") and
  message is not null and event.category != "authentication" and
  message != "Connection Failed" and not message like "Web.Client: *" and
  not message like "Network.Service: *" and not message like "General.Interest*" and not message like "Update: *" and
  not message like "tcp_reassembler*" and not message like "a-ipdf*" and not message like "Video*" and not message like "nbss_decode*" and
  not message like "name_server*" and not message like "misc*" and not message like "Collaboration*" and not message like "Business*" and
  not message like "Cloud.IT*" and not message like "Mobile*"

| STATS Esql.alerts_count = count(*),
        Esql.first_time_seen = MIN(@timestamp),
        Esql.distinct_count_src_ip = COUNT_DISTINCT(source.ip),
        Esql.distinct_count_dst_ip = COUNT_DISTINCT(destination.ip),
        src_ip = VALUES(source.ip),
        dst_ip = VALUES(destination.ip),
        url_domain = VALUES(url.domain),
        url_path = VALUES(url.path) by message, event.category, event.outcome

// first time seen is within 10m of the rule execution time
| eval Esql.recent = DATE_DIFF("minute", Esql.first_time_seen, now())
| where Esql.recent <= 10 and Esql.alerts_count <= 5 and Esql.distinct_count_src_ip <= 2 and Esql.distinct_count_dst_ip <= 2

// move dynamic fields to ECS equivalent for rule exceptions
| eval source.ip = MV_FIRST(src_ip),
       destination.ip = MV_FIRST(dst_ip),
       url.domain = MV_FIRST(url_domain),
       url.path = MV_FIRST(url_path)

| keep message, event.category, event.outcome, Esql.*, source.ip, destination.ip, url.domain, url.path
'''
note = """## Triage and analysis

### Investigating Newly Observed Fortigate Alert

This rule surfaces newly observed, low-frequency high severity FortiGate alerts within the last 5 days.

Because the alert has not been seen previously, it should be prioritized for validation to determine whether it represents a true compromise or rare benign activity.

### Investigation Steps

- Identify the source address, affected host and review the associated message to understand the alert.
- Validate the source address under which the activity occurred and assess whether it aligns with normal behavior.
- Refer to the specific alert details like event.original to get more context.

### False Positive Considerations

- Vulnerability scanners and pentesting.
- Administrative scripts or automation tools can trigger detections when first introduced.
- Development or testing environments may produce one-off behaviors that resemble malicious techniques.

### Response and Remediation

- If the activity is confirmed malicious, isolate the affected host to prevent further execution or lateral movement.
- Terminate malicious processes and remove any dropped files or persistence mechanisms.
- Collect forensic artifacts to understand initial access and execution flow.
- Patch or remediate any vulnerabilities or misconfigurations that enabled the behavior.
- If benign, document the finding and consider tuning or exception handling to reduce future noise.
- Continue monitoring the host and environment for recurrence of the behavior or related alerts."""
references = ["https://www.elastic.co/docs/reference/integrations/fortinet_fortigate"]

Stages and Predicates

Stage 1: from

FROM logs-fortinet_fortigate.*, filebeat-* metadata _id

Stage 2: where

| WHERE event.module == "fortinet_fortigate" and event.action in ("signature", "ssl-anomaly") and
  message is not null and event.category != "authentication" and
  message != "Connection Failed" and not message like "Web.Client: *" and
  not message like "Network.Service: *" and not message like "General.Interest*" and not message like "Update: *" and
  not message like "tcp_reassembler*" and not message like "a-ipdf*" and not message like "Video*" and not message like "nbss_decode*" and
  not message like "name_server*" and not message like "misc*" and not message like "Collaboration*" and not message like "Business*" and
  not message like "Cloud.IT*" and not message like "Mobile*"

Stage 3: stats

| STATS Esql.alerts_count = count(*),
        Esql.first_time_seen = MIN(@timestamp),
        Esql.distinct_count_src_ip = COUNT_DISTINCT(source.ip),
        Esql.distinct_count_dst_ip = COUNT_DISTINCT(destination.ip),
        src_ip = VALUES(source.ip),
        dst_ip = VALUES(destination.ip),
        url_domain = VALUES(url.domain),
        url_path = VALUES(url.path) by message, event.category, event.outcome

Stage 4: eval

| eval Esql.recent = DATE_DIFF("minute", Esql.first_time_seen, now())

Stage 5: where

| where Esql.recent <= 10 and Esql.alerts_count <= 5 and Esql.distinct_count_src_ip <= 2 and Esql.distinct_count_dst_ip <= 2

Stage 6: eval

| eval source.ip = MV_FIRST(src_ip),
       destination.ip = MV_FIRST(dst_ip),
       url.domain = MV_FIRST(url_domain),
       url.path = MV_FIRST(url_path)

Stage 7: keep

| keep message, event.category, event.outcome, Esql.*, source.ip, destination.ip, url.domain, url.path

Exclusions

Top-level NOT(...) conjuncts: predicates this rule actively suppresses.

FieldKindExcluded values
messagestarts_withBusiness
messagestarts_withCloud.IT
messagestarts_withCollaboration
messagestarts_withGeneral.Interest
messagestarts_withMobile
messagestarts_withNetwork.Service:
messagestarts_withUpdate:
messagestarts_withVideo
messagestarts_withWeb.Client:
messagestarts_witha-ipdf
messagestarts_withmisc
messagestarts_withname_server
messagestarts_withnbss_decode
messagestarts_withtcp_reassembler

Indicators

Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.

FieldKindValues
Esql.alerts_countle
  • 5
Esql.distinct_count_dst_iple
  • 2
Esql.distinct_count_src_iple
  • 2
Esql.recentle
  • 10
event.actionin
  • signature
  • ssl-anomaly
event.categoryne
  • authentication
event.moduleeq
  • fortinet_fortigate
messageis_not_null
  • (no value, null check)
messagene
  • Connection Failed

Output fields

Fields the rule emits when it matches. Chronicle authors list these in the outcome block; they appear on the detection and $risk_score drives alerting. Sentinel / Defender XDR rules build them up through project / summarize / extend stages. Sentinel maps these into alert fields via entityMappings and customDetails; Defender XDR custom detections surface them as alert fields directly.

FieldSource
messageKEEP message
event.categoryKEEP event.category
event.outcomeKEEP event.outcome
Esql.*KEEP Esql.*
source.ipKEEP source.ip
destination.ipKEEP destination.ip
url.domainKEEP url.domain
url.pathKEEP url.path