Detection rules › Elastic

Potential NFS Destructive Operation Burst

Status
production
Severity
medium
Time window
9m
Group by
Esql.time_window, destination.ip, source.ip
Author
Elastic
Source
github.com/elastic/detection-rules

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

TacticTechniques
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 =
ifEsql.opcode == "REMOVE" or Esql.opcode == "RENAME"1
else0
Esql.is_write =
ifEsql.opcode == "WRITE"1
else0

Stage 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.

FieldSource
source.ipKEEP source.ip
destination.ipKEEP destination.ip
Esql.*KEEP Esql.*