Detection rules › Panther

Panther rules: microsoft

Microsoft Exchange External Forwarding

#
Severity
high
Log types
Microsoft365.Audit.Exchange
Tags
Microsoft365, Exchange, Data Exfiltration, Email Security
Reference
learn.microsoft.com
Source
github.com/panther-labs/panther-analysis

Detects when a user creates email forwarding rules to external organizations in Microsoft Exchange Online. This can indicate data exfiltration attempts, where an attacker sets up forwarding to collect emails outside the organization. The rule detects both mailbox forwarding (Set-Mailbox) and inbox rules (New-InboxRule). The detection includes: 1. External organization forwarding based on domain comparison 2. Suspicious forwarding patterns like: - Forwarding without keeping a copy - Deleting messages after forwarding - Stopping rule processing after forwarding 3. Multiple forwarding destinations 4. Various forwarding methods (SMTP, redirect, forward as attachment)

MITRE ATT&CK coverage

Detection logic

from panther_msft_helpers import is_external_address, m365_alert_context

FORWARDING_PARAMETERS = {
    "ForwardingSmtpAddress",
    "ForwardTo",
    "ForwardingAddress",
    "RedirectTo",
    "ForwardAsAttachmentTo",
}

SUSPICIOUS_PATTERNS = {
    "DeliverToMailboxAndForward": "False",  # Only forward, don't keep copy
    "DeleteMessage": "True",  # Delete after forwarding
    "StopProcessingRules": "True",  # Stop processing other rules
}


def rule(event):
    """Alert on suspicious or external email forwarding configurations."""
    # Skip non-forwarding related operations
    if event.get("operation") not in ("Set-Mailbox", "New-InboxRule"):
        return False

    # Get organization domains from userid and organizationname
    onmicrosoft_domain = event.get("organizationname", "").lower()
    userid = event.get("userid", "").lower()
    try:
        primary_domain = userid.split("@")[1]
    except (IndexError, AttributeError):
        primary_domain = onmicrosoft_domain if onmicrosoft_domain else None

    if not primary_domain:
        return True  # Alert if we can't determine organization

    # Check each parameter
    for param in event.get("parameters", []):
        param_name = param.get("Name", "")
        param_value = param.get("Value", "")

        # Check for external forwarding
        if param_name in FORWARDING_PARAMETERS and param_value:
            if is_external_address(param_value, primary_domain, onmicrosoft_domain):
                return True

    return False


def title(event):
    parameters = event.get("parameters", [])
    forwarding_addresses = []
    suspicious_configs = []

    for param in parameters:
        param_name = param.get("Name", "")
        param_value = param.get("Value", "")

        if param_name in FORWARDING_PARAMETERS and param_value:
            # Handle smtp: prefix
            if param_value.lower().startswith("smtp:"):
                param_value = param_value[5:]
            # Handle multiple addresses
            addresses = param_value.split(";")
            forwarding_addresses.extend(addr.strip() for addr in addresses if addr.strip())
        if param_name in SUSPICIOUS_PATTERNS and param_value == SUSPICIOUS_PATTERNS[param_name]:
            suspicious_configs.append(f"{param_name}={param_value}")

    to_emails = ", ".join(forwarding_addresses) if forwarding_addresses else "<no-recipient-found>"
    suspicious_str = f" [Suspicious: {', '.join(suspicious_configs)}]" if suspicious_configs else ""

    return (
        f"Microsoft365: External Forwarding Created From [{event.get('userid', '')}] "
        f"to [{to_emails}]{suspicious_str}"
    )


def severity(event):
    if not is_suspicious_pattern(event):
        return "LOW"
    return "DEFAULT"


def alert_context(event):
    return m365_alert_context(event)


def is_suspicious_pattern(event):
    parameters = event.get("parameters", [])
    for param in parameters:
        param_name = param.get("Name", "")
        param_value = param.get("Value", "")
        if param_name in SUSPICIOUS_PATTERNS and param_value == SUSPICIOUS_PATTERNS[param_name]:
            return True
    return False

Rule specification

AnalysisType: rule
Description: >
  Detects when a user creates email forwarding rules to external organizations in Microsoft Exchange Online.
  This can indicate data exfiltration attempts, where an attacker sets up forwarding to collect emails outside
  the organization. The rule detects both mailbox forwarding (Set-Mailbox) and inbox rules (New-InboxRule).
  
  The detection includes:
  1. External organization forwarding based on domain comparison
  2. Suspicious forwarding patterns like:
     - Forwarding without keeping a copy
     - Deleting messages after forwarding
     - Stopping rule processing after forwarding
  3. Multiple forwarding destinations
  4. Various forwarding methods (SMTP, redirect, forward as attachment)
DisplayName: "Microsoft Exchange External Forwarding"
Enabled: true
Filename: microsoft_exchange_external_forwarding.py
Reports:
  MITRE ATT&CK:
    - TA0003:T1137.005 # Persistence - Office Application Startup: Outlook Rules
    - TA0009:T1114.003 # Collection - Email Collection: Email Forwarding Rule
    - TA0010:T1020 # Exfiltration - Automated Exfiltration
Reference: https://learn.microsoft.com/en-us/microsoft-365/security/office-365-security/outbound-spam-policies-external-email-forwarding?view=o365-worldwide
Severity: High
Tags:
  - Microsoft365
  - Exchange
  - Data Exfiltration
  - Email Security
DedupPeriodMinutes: 60
LogTypes:
  - Microsoft365.Audit.Exchange
RuleID: "Microsoft365.Exchange.External.Forwarding"
Threshold: 1
SummaryAttributes:
  - userid
  - parameters
  - organizationname
Runbook: |
  1. Investigate the forwarding configuration:
     - Check if the forwarding is legitimate and approved
     - Verify the destination addresses
     - Review any suspicious patterns (deletion, no copy kept)
  2. If unauthorized:
     - Remove the forwarding rule
     - Check for any data that may have been forwarded
     - Review the user's recent activity
  3. If authorized:
     - Document the business justification
     - Ensure it complies with security policies
     - Monitor for any changes to the forwarding configuration

Stages and Predicates

Fires on Microsoft365.Audit.Exchange events when the condition below holds.

Condition

  • operation is one of Set-Mailbox, New-InboxRule

This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
operationin
  • New-InboxRule
  • Set-Mailbox
field:"operation" kind:in

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

FieldSource
operationOperation
organization_idOrganizationId
client_ipClientIp
extended_propertiesExtendedProperties
modified_propertiesModifiedProperties
applicationApplication
actorActor
userid

Response runbook

1. Investigate the forwarding configuration:

- Check if the forwarding is legitimate and approved

- Verify the destination addresses

- Review any suspicious patterns (deletion, no copy kept)

2. If unauthorized:

- Remove the forwarding rule

- Check for any data that may have been forwarded

- Review the user's recent activity

3. If authorized:

- Document the business justification

- Ensure it complies with security policies

- Monitor for any changes to the forwarding configuration

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "clientip": "1.2.3.4",
  "creationtime": "2022-12-12 22:19:00",
  "externalaccess": false,
  "id": "111-22-33",
  "objectid": "homer.simpson",
  "operation": "Set-Mailbox",
  "organizationid": "11-aa-bb",
  "organizationname": "simpsons.onmicrosoft.com",
  "originatingserver": "QWERTY (1.2.3.4)",
  "parameters": [
    {
      "Name": "Identity",
      "Value": "homer.simpson@simpsons.onmicrosoft.com"
    },
    {
      "Name": "ForwardingSmtpAddress",
      "Value": "smtp:peter.griffin@familyguy.com"
    },
    {
      "Name": "DeliverToMailboxAndForward",
      "Value": "False"
    }
  ],
  "recordtype": 1,
  "resultstatus": "True",
  "userid": "homer.simpson@simpsons.onmicrosoft.com",
  "userkey": "12345",
  "usertype": 2,
  "workload": "Exchange"
}

Microsoft Graph Passthrough

#

This is a third-party alert feed, not a detection over modeled telemetry. Another security product raised the finding; this rule forwards or reshapes it into the SIEM. It is searchable for reference but is excluded from the detection-rule browse and the ATT&CK coverage matrix.

Severity
medium
Group by
id
Log types
MicrosoftGraph.SecurityAlertV2
Reference
learn.microsoft.com
Source
github.com/panther-labs/panther-analysis

The Microsoft Graph security API federates queries to all onboarded security providers, including Azure AD Identity Protection, Microsoft 365, Microsoft Defender (Cloud, Endpoint, Identity) and Microsoft Sentinel

Detection logic

from panther_msft_helpers import msft_graph_alert_context

SEVERITY_MAP = {
    "informational": "INFO",
    "low": "LOW",
    "medium": "MEDIUM",
    "high": "HIGH",
}


def rule(event):
    return event.get("status") == "new" and event.get("severity", "").lower() != "informational"


def title(event):
    return f"Microsoft Graph Alert ({event.get('title')})"


def dedup(event):
    return event.get("id")


def severity(event):
    return SEVERITY_MAP.get(event.get("severity", "").lower(), "INFO")


def alert_context(event):
    return msft_graph_alert_context(event)

Rule specification

AnalysisType: rule
Description: The Microsoft Graph security API federates queries to all onboarded security providers, including Azure AD Identity Protection, Microsoft 365, Microsoft Defender (Cloud, Endpoint, Identity) and Microsoft Sentinel
Reference: https://learn.microsoft.com/en-us/graph/api/resources/security-alert?view=graph-rest-1.0
DisplayName: "Microsoft Graph Passthrough"
Enabled: true
Filename: microsoft_graph_passthrough.py
Severity: Medium
DedupPeriodMinutes: 60
LogTypes:
  - MicrosoftGraph.SecurityAlertV2
RuleID: "Microsoft.Graph.Passthrough"
Threshold: 1

Stages and Predicates

Fires on MicrosoftGraph.SecurityAlertV2 events when all of the conditions below hold.

Condition

  • status is new
  • severity is not informational (case-insensitive)

Indicators

These rows show field, operator, and value matches.

Output fields

Fields the rule emits when it matches, drawn from the rule's alert_context.

Field
category
description
evidence
serviceSource
productName
incidentId
alertWebUrl
title

Worked example

A sample event from the rule's unit tests that triggers a match.

Sample Test Event
{
  "category": "AnonymousLogin",
  "createdDateTime": "2026-04-01T14:31:48.438Z",
  "description": "Sign-in from an anonymous IP address (e.g. Tor browser, anonymizer VPNs)",
  "evidence": [
    {
      "at_sign_odata_type": "#microsoft.graph.security.userEvidence",
      "userAccount": {
        "accountName": "homer.simpson",
        "azureAdUserId": "011d5ede-0faa-4946-a25e-b2cd0c47a52c",
        "domainName": "corporation.onmicrosoft.com",
        "userPrincipalName": "homer.simpson@corporation.onmicrosoft.com"
      }
    },
    {
      "at_sign_odata_type": "#microsoft.graph.security.ipEvidence",
      "countryLetterCode": "US",
      "ipAddress": "185.220.103.6"
    }
  ],
  "firstActivityDateTime": "2026-04-01T14:31:48.438Z",
  "id": "abcd12345efghijk6789",
  "lastActivityDateTime": "2026-04-01T14:31:48.438Z",
  "lastUpdateDateTime": "2026-04-01T14:34:56.229Z",
  "productName": "Microsoft Defender for Identity",
  "serviceSource": "microsoftDefenderForIdentity",
  "severity": "medium",
  "status": "new",
  "tenantId": "12345-abcde-a1b2k3",
  "title": "Anonymous IP address"
}