EDR-Based Containment
CrowdStrike host isolation, SentinelOne network quarantine, and MDE isolation — what each platform blocks versus allows, how to contain at scale, and how to roll back isolation when you're done.
It's 2 AM and you have 47 confirmed compromised hosts. Your EDR is CrowdStrike Falcon. You need to contain all 47 simultaneously in under 2 minutes before the attacker notices their beacon on host 1 just went quiet. The Falcon console lets you isolate one host at a time through the UI — which would take 15+ minutes. The API lets you do all 47 in a single call. This chapter covers both approaches: the console for single-host response, and the API/PowerShell for scaled containment in a real P1.
What EDR Isolation Actually Does
EDR network containment is not the same as powering off the machine or blocking it at the firewall. It's a WFP (Windows Filtering Platform) or kernel-level driver action that the EDR agent applies locally on the endpoint.
EDR Isolation: What's Blocked vs Allowed
═══════════════════════════════════════════════════════════════════
BLOCKED (for all platforms):
├── All inbound TCP/UDP except EDR management channel
├── All outbound TCP/UDP except EDR management channel
├── SMB (445) — stops lateral movement from contained host
├── RDP (3389) — blocks new remote sessions
└── Any C2 connection (blocked along with everything else)
ALLOWED (varies by platform):
├── EDR cloud backend (Falcon/SentinelOne/MDE cloud)
│ → Console can still push commands to isolated host
│ → Can still collect files, run live response shell
│ → Can still acquire memory via EDR built-in
└── (Some platforms) DNS resolution for the EDR domain
What this means operationally:
→ Attacker loses beacon immediately
→ Analyst retains full remote access for evidence collection
→ Machine is still running — volatile evidence preserved
→ Containment takes effect in seconds
→ No network engineer needed, no firewall change
→ Rollback is instant (single button/API call)
CrowdStrike Falcon
CrowdStrike calls its isolation feature "Network Containment" in the console and "contain" via the API. The action is available from the Investigate → Hosts view.
"""
Bulk host containment via CrowdStrike Falcon API (FalconPy SDK)
Install: pip install crowdstrike-falconpy
"""
from falconpy import Hosts
import json
# Initialize client
falcon = Hosts(
client_id=CLIENT_ID,
client_secret=CLIENT_SECRET,
base_url="https://api.crowdstrike.com"
)
def get_device_ids(hostnames: list[str]) -> list[str]:
"""Resolve hostnames to CrowdStrike device IDs."""
device_ids = []
for hostname in hostnames:
response = falcon.query_devices_by_filter(filter=f"hostname:'{hostname}'")
if response["status_code"] == 200 and response["body"]["resources"]:
device_ids.extend(response["body"]["resources"])
return device_ids
def contain_hosts(device_ids: list[str]) -> dict:
"""Network-contain all specified device IDs simultaneously."""
response = falcon.perform_action(
action_name="contain",
body={"ids": device_ids}
)
return response
def lift_containment(device_ids: list[str]) -> dict:
"""Lift containment when IR is complete."""
response = falcon.perform_action(
action_name="lift_containment",
body={"ids": device_ids}
)
return response
# Example: contain 47 hosts by hostname list
hostnames = ["LAPTOP-01", "LAPTOP-02", "SERVER-FILES01"] # extend to full list
device_ids = get_device_ids(hostnames)
print(f"Found {len(device_ids)} device IDs")
result = contain_hosts(device_ids)
if result["status_code"] == 202:
print(f"Containment initiated for {len(device_ids)} hosts")
else:
print(f"Error: {result['body']}")
# CrowdStrike containment via REST API directly (no SDK required)
# Requires: OAuth2 client_id and client_secret with Hosts: WRITE scope
$clientId = $env:CS_CLIENT_ID
$clientSecret = $env:CS_CLIENT_SECRET
$baseUrl = "https://api.crowdstrike.com"
# Get OAuth2 token
$tokenBody = "client_id=$clientId&client_secret=$clientSecret"
$token = (Invoke-RestMethod -Uri "$baseUrl/oauth2/token" `
-Method POST -Body $tokenBody `
-ContentType "application/x-www-form-urlencoded").access_token
$headers = @{ Authorization = "Bearer $token" }
# Resolve hostname(s) to device_id(s)
$hostnames = @("LAPTOP-01","LAPTOP-02","SERVER-FILES01")
$deviceIds = @()
foreach ($h in $hostnames) {
$filter = [Uri]::EscapeDataString("hostname:'$h'")
$resp = Invoke-RestMethod -Uri "$baseUrl/devices/queries/devices/v1?filter=$filter" `
-Headers $headers
$deviceIds += $resp.resources
}
Write-Host "Targeting $($deviceIds.Count) devices"
# Contain all hosts simultaneously
$body = @{ ids = $deviceIds } | ConvertTo-Json
Invoke-RestMethod -Uri "$baseUrl/devices/entities/devices-actions/v2?action_name=contain" `
-Method POST -Headers $headers -Body $body -ContentType "application/json"
Write-Host "Containment command sent"
SentinelOne
SentinelOne calls the feature "Network Quarantine." It operates at the same kernel driver level as CrowdStrike. The management console allows multi-select and bulk quarantine.
"""SentinelOne bulk quarantine via REST API."""
import requests
S1_URL = "https://usea1.sentinelone.net"
API_TOKEN = "YOUR_API_TOKEN" # from SentinelOne console (Admin → API)
headers = {
"Authorization": f"ApiToken {API_TOKEN}",
"Content-Type": "application/json"
}
def get_agent_ids(hostnames: list[str]) -> list[str]:
ids = []
for hostname in hostnames:
r = requests.get(
f"{S1_URL}/web/api/v2.1/agents",
headers=headers,
params={"computerName": hostname, "isActive": True}
)
for agent in r.json().get("data", []):
ids.append(agent["id"])
return ids
def quarantine_agents(agent_ids: list[str]):
payload = {"filter": {"ids": agent_ids}}
r = requests.post(
f"{S1_URL}/web/api/v2.1/agents/actions/disconnect",
headers=headers,
json=payload
)
print(f"Quarantine response: {r.status_code} — {r.json().get('data', {})}")
def reconnect_agents(agent_ids: list[str]):
payload = {"filter": {"ids": agent_ids}}
r = requests.post(
f"{S1_URL}/web/api/v2.1/agents/actions/connect",
headers=headers,
json=payload
)
print(f"Reconnect response: {r.status_code}")
# Contain all compromised hosts
hostnames = ["LAPTOP-01", "SERVER-FILES01"]
ids = get_agent_ids(hostnames)
quarantine_agents(ids)
Microsoft Defender for Endpoint (MDE)
MDE calls this "Isolate machine." It's available from the Device page in the Defender portal and via the Microsoft Graph Security API.
# MDE machine isolation via Microsoft Graph Security API
# Requires: Entra ID app registration with Machine.Isolate permission
$tenantId = $env:AZURE_TENANT_ID
$clientId = $env:AZURE_CLIENT_ID
$clientSec = $env:AZURE_CLIENT_SECRET
# Get access token
$tokenBody = @{
grant_type = "client_credentials"
client_id = $clientId
client_secret = $clientSec
scope = "https://api.securitycenter.microsoft.com/.default"
}
$token = (Invoke-RestMethod -Uri `
"https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token" `
-Method POST -Body $tokenBody).access_token
$headers = @{ Authorization = "Bearer $token"; "Content-Type" = "application/json" }
$baseUrl = "https://api.securitycenter.microsoft.com/api"
# Isolate a single machine by machine ID (from MDE device page URL)
function Isolate-MDEMachine ($machineId, [string]$comment = "IR containment") {
$body = @{
Comment = $comment
IsolationType = "Full" # Full or Selective (Selective allows Outlook)
} | ConvertTo-Json
Invoke-RestMethod -Uri "$baseUrl/machines/$machineId/isolate" `
-Method POST -Headers $headers -Body $body
}
# Look up machine IDs from computer names
function Get-MDEMachineId ($computerName) {
$filter = [Uri]::EscapeDataString("computerDnsName eq '$computerName'")
$r = Invoke-RestMethod -Uri "$baseUrl/machines?`$filter=$filter" `
-Headers $headers
return $r.value | Select-Object -ExpandProperty id
}
# Bulk isolate
$targets = @("LAPTOP-01","SERVER-FILES01")
foreach ($t in $targets) {
$id = Get-MDEMachineId -computerName $t
if ($id) {
Isolate-MDEMachine -machineId $id -comment "IR-2026-023 containment"
Write-Host "Isolated: $t ($id)"
} else {
Write-Warning "Machine not found in MDE: $t"
}
}
Verifying Containment
Don't assume containment worked. Verify it — both from the EDR console (status should show "contained/isolated") and from the endpoint itself if you still have console access.
# Run via EDR live response shell on isolated host to verify network is cut
# Should show no external connections
Get-NetTCPConnection -State Established |
Where-Object { $_.RemoteAddress -notmatch "^(127\.|::1|0\.0\.0)" } |
ForEach-Object {
$p = Get-Process -Id $_.OwningProcess -EA 0
[PSCustomObject]@{
Remote = "$($_.RemoteAddress):$($_.RemotePort)"
Process = if ($p) { $p.Name } else { "?" }
PID = $_.OwningProcess
}
} | Format-Table
# Expected after full isolation: only EDR cloud IP(s) remain
# If other IPs remain: isolation may not have applied correctly — investigate
Many analysts treat EDR isolation like a reboot — something that destroys volatile evidence. It doesn't. The machine stays running. Memory is intact. The running process list is unchanged. The EDR management channel remains active. You can collect memory, pull files, run live response commands, and continue forensic investigation after isolation with no loss of volatile data. Isolation only cuts network — the machine continues to run in a network-isolated state indefinitely until you lift containment. This is the correct first response for most incidents: isolate immediately, then investigate from the isolated host at whatever pace is needed.
Q & A
Q: The EDR agent isn't installed on 10 of the 47 compromised hosts. How do you contain those?
For machines without EDR, you have four options in order of preference: (1) Network-level isolation — block the machine's MAC address at the switch port or put it in a quarantine VLAN via VLAN reconfiguration (requires network team coordination). (2) Firewall rule — block all traffic to/from the host's IP at the nearest network firewall segment. (3) Physical isolation — disconnect the network cable or disable the wireless adapter (requires physical or console access). (4) Windows Firewall via remote PowerShell (if WinRM is available): Invoke-Command -ComputerName HOST-05 -ScriptBlock { Set-NetFirewallProfile -All -DefaultInboundAction Block -DefaultOutboundAction Block }. For EDR-less hosts, add them to the post-incident EDR deployment backlog — this is a preparation gap that was exploited.
Q: You isolated a host via EDR. Two hours later someone complains their work machine is unreachable. How do you lift containment?
Before lifting containment, verify: (1) Has the host been through eradication — persistence removed, malware cleared, or full reimage? (2) Are all affected credentials rotated? (3) Is the re-introduction of this host to the network safe given current incident status? If yes to all: lift containment via the EDR console (CrowdStrike: Actions → Lift Network Containment; SentinelOne: Reconnect; MDE: Release from Isolation). If the host has not been through eradication, do not lift containment regardless of business pressure. Escalate the conflict to the IR lead and CISO — the decision to release an uncleared host must be documented with explicit authorization.
Q: MDE isolation is slower than CrowdStrike — sometimes taking 5-10 minutes to apply. Why?
MDE isolation relies on the cloud command reaching the Defender agent on the endpoint. If the endpoint has intermittent connectivity or the Defender cloud pipeline is under load, there can be delays. CrowdStrike's isolation applies faster partly because the Falcon agent polls the cloud backend more frequently. For time-critical bulk containment in MDE, monitor the isolation status in the portal rather than assuming it applied immediately. The machine page in Defender Security Center shows isolation status — wait for it to show "Isolated" before treating the machine as contained. For very time-critical scenarios, use VLAN isolation at the network layer in parallel with EDR isolation rather than waiting for EDR to confirm.