Detection rules › Elastic
Potential NFS Destructive Operation Burst
Identifies a burst of successful NFS write activity combined with destructive REMOVE or RENAME operations from a single client to one export server within a one-minute window. Ransomware and destructive actors often encrypt, delete, or rename large numbers of files on mounted NFS shares; this aggregation surfaces that behavior using NFS opcode telemetry when file paths are not available on the wire.
Known false positives
- Backup deduplication engines, migration utilities, and filesystem sync tools can generate high volumes of legitimate NFS writes. Validate the source against known backup or storage-management hosts before escalating.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Impact |
Rule body
[metadata]
creation_date = "2026/07/31"
integration = ["network_traffic"]
maturity = "production"
min_stack_comments = "Requires ES|QL JSON_EXTRACT on _source to read NFS opcode and status fields."
min_stack_version = "9.4.0"
updated_date = "2026/07/31"
[rule]
author = ["Elastic"]
description = """
Identifies a burst of successful NFS write activity combined with destructive REMOVE or RENAME operations from a single
client to one export server within a one-minute window. Ransomware and destructive actors often encrypt, delete, or
rename large numbers of files on mounted NFS shares; this aggregation surfaces that behavior using NFS opcode telemetry
when file paths are not available on the wire.
"""
false_positives = [
"""
Backup deduplication engines, migration utilities, and filesystem sync tools can generate high volumes of legitimate
NFS writes. Validate the source against known backup or storage-management hosts before escalating.
""",
]
from = "now-9m"
language = "esql"
license = "Elastic License v2"
max_signals = 5
name = "Potential NFS Destructive Operation Burst"
note = """## Triage and analysis
### Investigating Potential NFS Destructive Operation Burst
Remote encryption over NFS typically manifests as sustained successful WRITE activity paired with REMOVE/RENAME
operations from one client IP against a single export. This rule aggregates mutating NFS opcodes because Packetbeat
does not publish individual file paths in the NFS data stream.
### Possible investigation steps
- Review `Esql.write_ops`, `Esql.destructive_ops`, and `Esql.values_opcodes`, and compare `source.ip` to approved backup
or admin hosts.
- Pivot to endpoint telemetry on the source for ransomware families, suspicious processes, or recent `mount.nfs` activity.
- Inspect the export for ransom notes, mass extension changes, or abnormal file timestamps following the alert window.
- Correlate with preceding AUTH_SYS root UID or READDIR-heavy activity from the same client.
### False positive analysis
- Scheduled backup, rsync-style sync, or VM storage migration from known infrastructure is the most common benign cause.
Exception stable backup clients after validating opcode mix and timing against maintenance windows.
### Response and remediation
- Isolate the source host from the export if ransomware is suspected and snapshot the export where possible.
- Revoke export access for the client IP and audit `/etc/exports` for overly permissive entries.
- Restore affected data from immutable backups after confirming the encryption stage has ended.
"""
references = ["https://attack.mitre.org/techniques/T1486/", "https://attack.mitre.org/techniques/T1039/"]
risk_score = 47
rule_id = "8e78b1a5-9674-4a96-8ce7-76ef54566a85"
setup = """## Setup
This rule requires the Elastic **network_traffic** integration with the **NFS** protocol module enabled on a sensor that
observes NFS traffic to monitored export servers.
"""
severity = "medium"
tags = [
"Domain: Network",
"Use Case: Threat Detection",
"Use Case: Network Security Monitoring",
"Tactic: Impact",
"Data Source: Network Packet Capture",
"Resources: Investigation Guide",
]
timestamp_override = "event.ingested"
type = "esql"
query = '''
from logs-network_traffic.nfs-*, packetbeat-* metadata _source
| eval
Esql.opcode = TO_UPPER(COALESCE(
JSON_EXTRACT(_source, "network_traffic.nfs.opcode"),
JSON_EXTRACT(_source, "nfs.opcode")
)),
Esql.status = TO_UPPER(COALESCE(
JSON_EXTRACT(_source, "network_traffic.nfs.status"),
JSON_EXTRACT(_source, "nfs.status")
))
| where Esql.opcode in ("WRITE", "REMOVE", "RENAME") and Esql.status == "NFS_OK" and
source.ip is not null and destination.ip is not null
| eval Esql.time_window = DATE_TRUNC(1 minutes, @timestamp)
| eval Esql.is_write = CASE(Esql.opcode == "WRITE", 1, 0),
Esql.is_destructive = CASE(Esql.opcode == "REMOVE" or Esql.opcode == "RENAME", 1, 0)
| stats
Esql.mutating_ops = COUNT(*),
Esql.write_ops = SUM(Esql.is_write),
Esql.destructive_ops = SUM(Esql.is_destructive),
Esql.values_opcodes = VALUES(Esql.opcode)
by Esql.time_window, source.ip, destination.ip
| where Esql.mutating_ops >= 100 and Esql.write_ops > 0 and Esql.destructive_ops >= 20
| keep source.ip, destination.ip, Esql.*
'''
[[rule.threat]]
framework = "MITRE ATT&CK"
[[rule.threat.technique]]
id = "T1486"
name = "Data Encrypted for Impact"
reference = "https://attack.mitre.org/techniques/T1486/"
[rule.threat.tactic]
id = "TA0040"
name = "Impact"
reference = "https://attack.mitre.org/tactics/TA0040/"
[rule.alert_suppression]
group_by = ["source.ip", "destination.ip"]
missing_fields_strategy = "suppress"
[rule.alert_suppression.duration]
unit = "h"
value = 1
Stages and Predicates
Stage 1: from
from logs-network_traffic.nfs-*, packetbeat-* metadata _source
Stage 2: eval
| eval
Esql.opcode = TO_UPPER(COALESCE(
JSON_EXTRACT(_source, "network_traffic.nfs.opcode"),
JSON_EXTRACT(_source, "nfs.opcode")
)),
Esql.status = TO_UPPER(COALESCE(
JSON_EXTRACT(_source, "network_traffic.nfs.status"),
JSON_EXTRACT(_source, "nfs.status")
))
Stage 3: where
| where Esql.opcode in ("WRITE", "REMOVE", "RENAME") and Esql.status == "NFS_OK" and
source.ip is not null and destination.ip is not null
Stage 4: eval
| eval Esql.time_window = DATE_TRUNC(1 minutes, @timestamp)
Stage 5: eval
| eval Esql.is_write = CASE(Esql.opcode == "WRITE", 1, 0),
Esql.is_destructive = CASE(Esql.opcode == "REMOVE" or Esql.opcode == "RENAME", 1, 0)
Esql.is_destructive =if
Esql.opcode == "REMOVE" or Esql.opcode == "RENAME"1else
0Esql.is_write =if
Esql.opcode == "WRITE"1else
0Stage 6: stats
| stats
Esql.mutating_ops = COUNT(*),
Esql.write_ops = SUM(Esql.is_write),
Esql.destructive_ops = SUM(Esql.is_destructive),
Esql.values_opcodes = VALUES(Esql.opcode)
by Esql.time_window, source.ip, destination.ip
Stage 7: where
| where Esql.mutating_ops >= 100 and Esql.write_ops > 0 and Esql.destructive_ops >= 20
Stage 8: keep
| keep source.ip, destination.ip, Esql.*
Indicators
These rows show field, operator, and value matches.
Output fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
source.ip | KEEP source.ip |
destination.ip | KEEP destination.ip |
Esql.* | KEEP Esql.* |