Skip to main content

The Universal Phishing Containment Architecture: SecOps & SOAR

  • April 22, 2026
  • 2 replies
  • 674 views

dnehoda
Staff
Forum|alt.badge.img+16

Author: David Nehoda, Technical Solutions Consultant

 

What We're Solving

Phishing has historically been an initial access vector for ransomware, business email compromise, and credential theft. Despite billions spent on email gateways and security awareness training, phishing attacks continue to succeed because containment has historically operated at human speed. When an employee reports a phishing email or a threat intelligence feed flags a malicious domain, the SOC enters a race against time. Every minute the malicious email sits in employee inboxes is a minute someone can click the link, enter their credentials, and hand the attacker a foothold inside your environment.

The traditional SOC workflow for phishing is fundamentally weak:

  1. Alert lands in the queue (5–15 min polling delay)

  2. Tier-1 analyst picks it up (0–60 min depending on queue depth)

  3. Analyst manually extracts IOCs, checks VirusTotal, reads the email headers (15–30 min)

  4. Analyst decides to escalate or purge (5–10 min)

  5. Analyst executes the O365 purge manually or submits a ticket to IT (10–30 min)

Total: 35 minutes to 2+ hours. During that window, dozens or hundreds of employees may click the link.

 

What We're Delivering

This architecture eliminates human latency from the phishing containment lifecycle entirely. We build a fully autonomous pipeline that:

  • Detects inbound phishing emails by correlating embedded URLs against live threat intelligence feeds using a YARA-L 2.0 rule in Google SecOps
  • Enriches the alert with VirusTotal reputation scoring and forensic evidence (URL screenshots, sender reputation)
  • Contains the threat by executing a Microsoft Graph API Hard Purge - surgically removing the malicious email from every employee inbox enterprise-wide - in under 60 seconds
  • Revokes compromised sessions by suspending the user's Okta/Azure AD account and clearing all active tokens if click-through is detected
  • Closes the case autonomously with full forensic documentation on the SOAR Case Wall

How It Works (Architecture in 30 Seconds)
 

Inbound Email → SecOps YARA-L Detection (TI correlation)

SOAR Alert Created (< 1 second)

┌───────────────┼───────────────┐
↓ ↓ ↓
VT Enrichment VIP Check Extract Message-ID
↓ (Data Table) ↓
Score > threshold? ↓ Graph API Hard Purge
↓ VIP → Page IC (all inboxes, < 10s)
Yes → Continue Non-VIP → Auto ↓
↓ Okta Session Kill
↓ ↓
Case Auto-Closed
(full forensic trail preserved)

 

The Numbers
 

Metric

Before (Manual)

After (Autonomous)

Mean Time to Contain

35 min – 2+ hours

< 60 seconds

SOC Hours/Year on Phishing

~1,200 hours

~50 hours (exception handling only)

"Patient Zero" Click Risk

High — email sits in inbox during triage

Near-zero — email purged before most users check inbox

Campaign Coverage

One email at a time, manually

Enterprise-wide purge of every copy in one API call

VIP Protection

Same queue position as everyone else

Instant escalation to Incident Commander via Data Table lookup

Forensic Trail

Scattered across email, tickets, chat

Complete Case Wall: VT report, purge confirmation, session kill log

 

 

Who This Is For

This guide is written for Detection Engineers building YARA-L rules, SOAR Engineers wiring playbooks, and SOC Managers who need to understand the architecture they're approving. Every code block is production-ready. Every design decision is explained with the tradeoff rationale.
 

📈 Impact & ROI Summary
 

Dimension

Manual SOC Triage

Autonomous Containment Pipeline

MTTR

30+ minutes per incident. A campaign hitting 5,000 users paralyzes Tier-1 entirely.

< 60 seconds. Detection → enrichment → O365 Hard Purge → session revocation, zero human touch.

Coverage

Analyst-dependent. "Patient zero" clicks the link before triage begins.

Mathematical guarantee: malicious emails eradicated from all inboxes before any user interaction.

ROI

~1,200 SOC hours/year consumed by phishing triage.

Those hours reclaimed. Analysts shift to proactive threat hunting.

 

🔍 Phase 1: The Detection Engine (YARA-L 2.0)

Before SOAR can act, the SIEM must generate a high-fidelity alert. This rule correlates inbound email URLs against active Threat Intelligence feeds—producing the exact variables the downstream playbook requires.

The Rule: Line-by-Line Technical Breakdown
 

rule Inbound_Phishing_Threat_Intel_Match {
meta:
author = "Detection Engineering"
description = "Correlates inbound email URLs against active TI domains. Extracts Message-ID and sender for downstream SOAR containment."
severity = "HIGH"
priority = "HIGH"
mitre_attack = "T1566.002" // Phishing: Spearphishing Link

events:
// --- Email event binding ---
// Isolate inbound email transactions from O365 MessageTrace or Google Workspace logs.
// The UDM event_type EMAIL_TRANSACTION is populated by the parser when ingesting
// Exchange Online MessageTrace exports or Gmail log exports.
$email.metadata.event_type = "EMAIL_TRANSACTION"

// --- Domain extraction from embedded URLs ---
// UDM normalizes embedded body links into network.http.parsed_url.hostname.
// This captures the domain portion of any URL found in the email body or headers.
// Example: if the email contains "https://evil-login.example.com/harvest",
// this field resolves to "evil-login.example.com".
$email.network.http.parsed_url.hostname = $suspect_domain

// --- Threat Intelligence correlation ---
// The SecOps Entity Graph maintains a live index of all ingested TI feeds
// (STIX/TAXII, MISP, Google Threat Intelligence, custom CSV uploads).
// Binding $ti.graph.entity.hostname to $suspect_domain creates an implicit JOIN:
// only events where the embedded domain exists in the TI graph will match.
$ti.graph.entity.hostname = $suspect_domain

// Filter to MALWARE category TI entries. Other categories (PHISHING, C2, etc.)
// can be added with OR logic if your TI feeds categorize differently.
$ti.graph.metadata.threat.category = "MALWARE"

// Confidence threshold: TI feeds assign confidence scores (0-100).
// Setting >= 80 filters out low-fidelity IOCs that would generate false positives.
// Tune this threshold based on your feed quality:
// - Google Threat Intelligence: 80+ is reliable
// - Open-source feeds (abuse.ch, etc.): consider 90+ due to higher noise
$ti.graph.metadata.threat.confidence >= 80

match:
// Group by recipient email address over a 1-hour window.
// This means: one alert per recipient per hour, even if multiple malicious
// emails arrive. Prevents alert flooding during mass phishing campaigns.
// The 1h window is a balance between timely detection and alert deduplication.
$recipient = $email.principal.user.email_addresses over 1h

outcome:
// --- Variables passed directly to SOAR ---
// These outcome variables populate the alert entity fields that the
// SOAR playbook's trigger configuration reads.

// Risk score: hardcoded to 90 because any TI match with confidence >= 80
// is high-confidence malicious. SOAR uses this for priority routing.
$risk_score = max(90)

// Message-ID: the RFC 2822 Message-ID header. This is the UNIQUE identifier
// required by the Microsoft Graph API to locate and purge the specific email.
// Without this, the Graph API cannot target the correct message.
$message_id = array_distinct($email.network.email.message_id)

// Sender: used for campaign correlation and sender-based blocking rules.
$sender_email = array_distinct($email.network.email.from)

// The malicious domain itself: passed to VirusTotal enrichment in the playbook.
$malicious_url = array_distinct($suspect_domain)

// Recipient list: for campaign-scale containment, we need every targeted user.
$targeted_recipients = array_distinct($email.principal.user.email_addresses)

condition:
// Both event streams must have at least one matching record.
// The implicit JOIN on $suspect_domain guarantees correlation.
$email and $ti
}

 

Key design decisions explained:
 

Decision

Rationale

confidence >= 80 threshold

Eliminates low-fidelity TI noise that would trigger false containment. Tune per feed.

over 1h match window

Deduplicates alerts during mass campaigns. A 5,000-recipient blast generates 1 alert per user, not 1 per email.

array_distinct on outcomes

Prevents duplicate Message-IDs when the same email appears in multiple log sources (MessageTrace + MailItemsAccessed).

$email.principal.user.email_addresses as recipient

In UDM, the principal of an EMAIL_TRANSACTION is the recipient (the entity performing the "receive" action).

MITRE ATT&CK tagging

T1566.002 (Spearphishing Link) maps this rule to the framework, enabling coverage gap analysis dashboards.

 

Extending the Rule: Multi-Signal Correlation

For higher-fidelity detection, you can extend the rule to correlate email delivery with subsequent user behavior, catching cases where the user actually clicked the link:
 

rule Phishing_Click_Through_Correlation {
meta:
author = "Detection Engineering"
description = "Correlates phishing email delivery with subsequent HTTP navigation to the malicious domain, indicating the user clicked the link."
severity = "CRITICAL"
priority = "CRITICAL"

events:
// Email delivery event
$email.metadata.event_type = "EMAIL_TRANSACTION"
$email.network.http.parsed_url.hostname = $suspect_domain
$email.principal.user.email_addresses = $user

// Subsequent web navigation to the same domain
$click.metadata.event_type = "NETWORK_HTTP"
$click.principal.user.email_addresses = $user
$click.target.hostname = $suspect_domain

// TI match
$ti.graph.entity.hostname = $suspect_domain
$ti.graph.metadata.threat.category = "MALWARE"

// Time ordering: click must happen after email delivery
$click.metadata.event_timestamp.seconds >
$email.metadata.event_timestamp.seconds

match:
$user over 4h

outcome:
$risk_score = max(99)
$message_id = array_distinct($email.network.email.message_id)
$sender_email = array_distinct($email.network.email.from)
$clicked_url = array_distinct($click.target.url)

condition:
$email and $click and $ti
}


This rule fires only when a user both received a malicious email AND subsequently navigated to the embedded domain, which is a much stronger signal that warrants immediate session revocation.


🤖 Phase 2: The SOAR Playbook — Complete Build Guide

When the YARA-L rule fires, it pushes an Alert into Google SecOps SOAR. Below is the exact step-by-step construction of the autonomous containment playbook, including every configuration detail.

Step 1: Trigger Configuration

  1. Navigate to SOAR > Playbooks > Create New Playbook.

  2. Name: Universal_Phishing_Containment

  3. Description: Autonomous phishing containment: email purge, session revocation, campaign-scale response.

  4. Trigger Type: Alert Trigger

  5. Alert Filter: Rule Name equals Inbound_Phishing_Threat_Intel_Match

  6. Execution Mode: Set to Run Automatically — no analyst approval required for initial execution.

  7. Priority Override: Set playbook priority to Critical so it preempts lower-priority playbooks in the execution queue.

Step 2: Entity Extraction and Mapping

The playbook's first action block maps YARA-L outcome variables to SOAR entities that downstream actions consume.

  1. Add Action: Extract Entities

  • Map $message_id → SOAR entity type Email Message ID

  • Map $sender_email → SOAR entity type Email Address (mark as Sender)

  • Map $malicious_url → SOAR entity type URL

  • Map $targeted_recipients → SOAR entity type Email Address (mark as Recipient)

  1. Add Action: VirusTotal V3 — Get URL Report

  • Input: The extracted URL entity

  • Purpose: Enrich the case wall with forensic data: detection ratio, community votes, WHOIS history, SSL certificate details

  • Timeout: 30 seconds (VT API can be slow under load)

  • On failure: Continue execution (VT enrichment is informational, not blocking)

  1. Add Action: Google Threat Intelligence — Get Domain Report (if available)

  • Input: The extracted domain from $malicious_url

  • Purpose: Pull Google-specific threat context, campaign attribution, and related IOCs


Step 3: VIP Gate (Conditional Branch)

You may wish to avoid automatically suspending accounts of executive leadership (like CEOs).  This conditional node implements identity-aware routing.

Add Condition Node: VIP Check

  • Data Source: Query against a SOAR Data Table named vip_users containing executive email addresses and their escalation contacts.

  • Condition Logic:

IF [Recipient.EmailAddress] EXISTS IN DataTable("vip_users", "email")
→ Branch: MANUAL APPROVAL
- Create a high-priority case
- Send Slack/Teams notification to the SOC manager
- Wait for manual approval (timeout: 15 minutes)
- If approved → proceed to containment
- If timeout → escalate to CISO and proceed to containment

ELSE
→ Branch: AUTOMATED CONTAINMENT
- Proceed directly to kill actions


Building the VIP Data Table:

In SIEM, navigate to Investigations > Data Tables > Create:

  • Table Name: vip_users

  • Columns: email (string), name (string), title (string), escalation_contact (string)

  • Populate with C-Suite, VP, and board member email addresses

  • Schedule quarterly reviews of this table with HR


Step 4: The Kill — Office 365 Hard Purge

The core containment action. Uses the Microsoft Graph API to remove the malicious email from the target inbox, bypassing the trash folder entirely.

Action Configuration:

  • Integration: Microsoft 365 Defender

  • Action: Soft/Hard Delete Email

  • Target: [Recipient.EmailAddress]

  • Message-ID: [Alert.MessageID]

  • Purge Type: HardDelete (bypasses Deleted Items—user cannot recover the email)

The underlying Python implementation (Siemplify SDK):
 

from SiemplifyAction import SiemplifyAction
from SiemplifyUtils import output_handler
import requests
import time

MAX_RETRIES = 3
RETRY_DELAY = 5 # seconds


@output_handler
def main():
siemplify = SiemplifyAction()

target_mailbox = siemplify.extract_action_param("Target Mailbox", print_value=True)
message_id = siemplify.extract_action_param("Message ID", print_value=True)
access_token = siemplify.extract_action_param("Graph API Token", print_value=False)

graph_base = "https://graph.microsoft.com/v1.0"
headers = {"Authorization": f"Bearer {access_token}"}

# ---- Step 1: Locate the email by Internet Message-ID ----
# The internetMessageId is the RFC 2822 Message-ID from the email headers.
# Graph API requires URL-encoding of special characters in the filter.
search_url = f"{graph_base}/users/{target_mailbox}/messages"
params = {
"$filter": f"internetMessageId eq '{message_id}'",
"$select": "id,subject,receivedDateTime,from",
}

try:
search_resp = requests.get(search_url, headers=headers, params=params, timeout=30)
search_resp.raise_for_status()
search_data = search_resp.json()

if not search_data.get("value"):
# Email may have been purged by another playbook instance or manually deleted
siemplify.LOGGER.info(f"Email with Message-ID {message_id} not found in {target_mailbox}")
siemplify.end("Email not found in mailbox (may already be purged).", "false")
return

internal_id = search_data["value"][0]["id"]
subject = search_data["value"][0].get("subject", "N/A")
siemplify.LOGGER.info(f"Located email: '{subject}' (internal ID: {internal_id})")

# ---- Step 2: Execute Hard Delete with retry logic ----
delete_url = f"{graph_base}/users/{target_mailbox}/messages/{internal_id}"

for attempt in range(MAX_RETRIES):
resp = requests.delete(delete_url, headers=headers, timeout=30)

if resp.status_code == 204:
result_msg = (
f"Email Hard Purged from {target_mailbox}. "
f"Subject: '{subject}', Message-ID: {message_id}"
)
siemplify.LOGGER.info(result_msg)
siemplify.end(result_msg, "true")
return

elif resp.status_code == 429:
# Graph API rate limiting — respect Retry-After
retry_after = int(resp.headers.get("Retry-After", RETRY_DELAY))
siemplify.LOGGER.warn(f"Rate limited by Graph API. Waiting {retry_after}s...")
time.sleep(retry_after)
continue

elif resp.status_code >= 500:
# Transient server error — retry
siemplify.LOGGER.warn(f"Graph API returned {resp.status_code}. Retry {attempt + 1}/{MAX_RETRIES}")
time.sleep(RETRY_DELAY)
continue

else:
siemplify.end(
f"Delete failed: HTTP {resp.status_code} — {resp.text}", "false"
)
return

siemplify.end(f"Delete failed after {MAX_RETRIES} retries.", "false")

except requests.exceptions.Timeout:
siemplify.end("Graph API request timed out after 30 seconds.", "false")
except Exception as e:
siemplify.end(f"API exception: {e}", "false")


if __name__ == "__main__":
main()


Graph API permission requirements:
 

Permission

Type

Purpose

Mail.ReadWrite

Application

Search and delete emails in any mailbox

Mail.Read

Application

Locate emails by Message-ID filter

User.Read.All

Application

Resolve recipient identities


These must be granted as Application permissions (not Delegated) with admin consent, since the playbook operates without a logged-in user context.

Step 5: Identity Containment — Okta Session Kill

If telemetry indicates the user actually clicked the malicious link (detected by the Phishing_Click_Through_Correlation rule or proxy logs showing HTTP 200 to the malicious domain), we can to introduce additional logic to account for this:

  1. Add Condition Node: Click Detection Check

  • Check if the alert source is Phishing_Click_Through_Correlation (the multi-signal rule)

  • OR check if proxy logs show the user navigated to $malicious_url

  1. Add Action: Okta — Suspend User

  • Target: [Recipient.EmailAddress]

  • Purpose: Prevent the compromised account from accessing any SSO-protected application

  1. Add Action: Okta — Clear User Sessions

  • Target: [Recipient.EmailAddress]

  • Purpose: Invalidate ALL active access tokens and refresh tokens immediately

  • This kills sessions across every application the user was logged into via Okta SSO

  1. Add Action: Send Notification

  • Send a templated message to the affected user via Slack/Teams/email explaining their account was suspended due to a security incident and providing instructions to contact the help desk for re-enablement

Okta API integration detail:
 

import requests

OKTA_DOMAIN = "https://your-org.okta.com"
OKTA_API_TOKEN = "your-api-token" # Store in SOAR credential vault, never hardcode

headers = {
"Authorization": f"SSWS {OKTA_API_TOKEN}",
"Content-Type": "application/json",
}


def suspend_user(email: str) -> bool:
"""Suspend an Okta user by email address."""
# First, find the user ID by email
search_url = f"{OKTA_DOMAIN}/api/v1/users?search=profile.email eq \"{email}\""
resp = requests.get(search_url, headers=headers, timeout=15)
users = resp.json()

if not users:
return False

user_id = users[0]["id"]

# Suspend the user
suspend_url = f"{OKTA_DOMAIN}/api/v1/users/{user_id}/lifecycle/suspend"
resp = requests.post(suspend_url, headers=headers, timeout=15)
return resp.status_code == 200


def clear_sessions(email: str) -> bool:
"""Clear all active sessions for an Okta user."""
search_url = f"{OKTA_DOMAIN}/api/v1/users?search=profile.email eq \"{email}\""
resp = requests.get(search_url, headers=headers, timeout=15)
users = resp.json()

if not users:
return False

user_id = users[0]["id"]

# Clear all sessions
sessions_url = f"{OKTA_DOMAIN}/api/v1/users/{user_id}/sessions"
resp = requests.delete(sessions_url, headers=headers, timeout=15)
return resp.status_code == 204


Step 6: Automated Case Closure

After all containment actions execute:

  • Add Action: Change Case Status

    • Status: Closed

    • Root Cause: Phishing — Contained

Resolution Note: Auto-generated summary:
 

Automated playbook executed successfully.
- Email [Alert.MessageID] hard-purged via Microsoft Graph API
- Sender: [Alert.SenderEmail]
- Malicious domain: [Alert.MaliciousURL]
- VT detection ratio: [VT.DetectionRatio]
- User sessions terminated: [Yes/No based on click detection]
- Execution time: [Playbook.Duration]
- Zero human touch required.


🌊 Phase 3: Campaign-Scale Containment

A single phishing email is a containment exercise. A campaign targeting 5,000 mailboxes is a crisis. The architecture above handles individual alerts, but campaign-scale events require additional orchestration.

Detecting Campaigns

Add a YARA-L rule that identifies campaign patterns—multiple recipients receiving emails from the same sender with the same malicious domain within a short window:
 

rule Phishing_Campaign_Detection {
meta:
author = "Detection Engineering"
description = "Detects phishing campaigns: same sender + same malicious domain targeting 10+ recipients within 1 hour."
severity = "CRITICAL"

events:
$email.metadata.event_type = "EMAIL_TRANSACTION"
$email.network.http.parsed_url.hostname = $suspect_domain
$email.network.email.from = $sender

$ti.graph.entity.hostname = $suspect_domain
$ti.graph.metadata.threat.category = "MALWARE"
$ti.graph.metadata.threat.confidence >= 80

match:
$sender, $suspect_domain over 1h

outcome:
$risk_score = max(99)
$campaign_size = count_distinct($email.principal.user.email_addresses)
$all_recipients = array_distinct($email.principal.user.email_addresses)
$all_message_ids = array_distinct($email.network.email.message_id)

condition:
$email and $ti and $campaign_size >= 10
}

 

Campaign SOAR Playbook Additions

When the campaign rule fires, the SOAR playbook needs additional steps:

  1. Batch Purge: Instead of purging one mailbox, iterate over $all_recipients and purge from each. The Graph API supports batch requests (/$batch endpoint) for up to 20 operations per call:
def batch_purge_emails(
recipients: list[str],
message_id: str,
access_token: str,
batch_size: int = 20,
) -> dict:
"""
Batch purge a malicious email from multiple mailboxes using
the Microsoft Graph batch API.
"""
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
}
results = {"purged": [], "not_found": [], "failed": []}

for i in range(0, len(recipients), batch_size):
batch = recipients[i : i + batch_size]
requests_payload = []

for idx, recipient in enumerate(batch):
requests_payload.append({
"id": str(idx),
"method": "GET",
"url": f"/users/{recipient}/messages?$filter=internetMessageId eq '{message_id}'&$select=id",
})

batch_resp = requests.post(
"https://graph.microsoft.com/v1.0/$batch",
headers=headers,
json={"requests": requests_payload},
timeout=60,
)

# Process search results and issue delete requests
delete_requests = []
for response in batch_resp.json().get("responses", []):
recipient = batch[int(response["id"])]
messages = response.get("body", {}).get("value", [])

if messages:
internal_id = messages[0]["id"]
delete_requests.append({
"id": response["id"],
"method": "DELETE",
"url": f"/users/{recipient}/messages/{internal_id}",
})
else:
results["not_found"].append(recipient)

if delete_requests:
del_resp = requests.post(
"https://graph.microsoft.com/v1.0/$batch",
headers=headers,
json={"requests": delete_requests},
timeout=60,
)
for response in del_resp.json().get("responses", []):
recipient = batch[int(response["id"])]
if response.get("status") == 204:
results["purged"].append(recipient)
else:
results["failed"].append(recipient)

# Respect Graph API rate limits between batches
time.sleep(1)

return results

 

  1.  Sender Block: Automatically add the sender domain to the Exchange Online transport rule blocklist or the email gateway's deny list.

  2. Domain Block at Proxy: Push $suspect_domain to the web proxy (Zscaler, Palo Alto URL Filtering) to prevent any user from navigating to the phishing landing page.

  3. Executive Notification: For campaigns exceeding 100 recipients, auto-generate a Slack/Teams message to the CISO with a campaign summary dashboard link.

  4. Post-Incident IOC Extraction: Feed all campaign IOCs (sender IPs, domains, URLs, file hashes from attachments) back into the automated IOC pipeline (see the "Automating SOC IOCs" article) to update Data Tables for ongoing detectio


Conclusion

This pipeline transforms phishing containment from a 30-minute manual scramble into a deterministic, API-driven kill chain that executes in under a minute. The YARA-L rule extracts the precise $message_id, the SOAR playbook feeds it directly into the Graph API Hard Delete, and Okta session revocation seals the blast radius.  This architecture is vendor-agnostic; you can adapt it to various email vendors and SSO providers.  The provided Python scripts demonstrate SOAR integration, but you can also repurpose them for standalone mass deletion of known phishing emails before they escalate into incidents..  

At campaign scale, the batch purge architecture ensures that even a 5,000-recipient phishing blast is fully contained within minutes every malicious email purged, every compromised session killed, every malicious domain blocked at the proxy layer.

The key architectural principle: Detection and containment are not separate workflows. They are a single, continuous pipeline where YARA-L rules produce the exact variables that SOAR playbooks consume, and SOAR actions produce the telemetry that feeds the next detection cycle.

Next step: Your SOAR automation is only as reliable as the telemetry feeding it. If your Microsoft logs are incomplete or not  parsed, the playbook's entity extraction will fail silently. Proceed to the "Decrypting Microsoft: Telemetry to UDM Mapping" guide to ensure O365, Defender, and AD logs are perfectly structured for autonomous consumption.

 

2 replies

_eo
Forum|alt.badge.img+5
  • Bronze 4
  • May 5, 2026

Does this require Applied Threat Intel? These detection rules don’t seem to work with standard SecOps license.


dnehoda
Staff
Forum|alt.badge.img+16
  • Author
  • Staff
  • May 5, 2026

I have this fixed but will need to apply in the morning.