| |

Microsoft Sentinel Watchlists: The Complete Guide to Dynamic Detection Lists

Microsoft Sentinel Watchlists: The Complete Operational Guide

📅 May 2026⏱ 11 min read 🏷 Microsoft Sentinel · Watchlists · Detection

Watchlists are one of Microsoft Sentinel’s most versatile and underutilised features. They act as a queryable reference store — bring in any CSV data (IP blocklists, asset inventories, VIP user lists, privileged account registers, network CIDR ranges) and reference it directly in KQL analytic rules, hunting queries, and playbooks.

Unlike static lookup tables baked into KQL queries, Watchlists are dynamic: update the CSV and every query that references it automatically uses the new data. No rule edits, no redeployments.

1. What Are Watchlists?

A Watchlist is a named dataset stored in your Sentinel workspace, created from a CSV file. Each row in the CSV becomes a queryable record. Sentinel makes the data available via the _GetWatchlist() function in KQL.

  • Maximum size: up to 10 million rows per watchlist (using Log Analytics ingestion)
  • Update frequency: can be updated manually or via API/Logic App automation
  • Access: via _GetWatchlist('WatchlistAlias') — returns a table with all columns from the original CSV
  • Cost: data stored in watchlists does not incur standard Log Analytics ingestion charges (stored separately)

Max Rows

10M rows

Update Method

CSV / API

Access in KQL

_GetWatchlist()

Ingestion Cost

Separate (no LA charge)

2. Core Use Cases

Watchlist Name (example)ContentDetection Use
BlockedIPsMalicious IPs / IOC IPsAlert when traffic seen from blocked IPs
CorporateRangesInternal CIDR rangesDetect external sign-ins (not in corporate IP ranges)
VIPUsersExecutive / privileged user UPNsPriority alerting for VIP account anomalies
ServiceAccountsService account namesAlert on interactive logons by service accounts
AssetInventoryComputer names, owners, criticalityEnrich alerts with asset context
AllowedCountriesISO country codes allowed for sign-inAlert on sign-ins from unexpected countries
TrustedDomainsApproved external email domainsDetect email from non-whitelisted domains

3. Creating and Managing Watchlists

Watchlists are created in the Sentinel portal under Configuration → Watchlists → New. You upload a CSV file and designate a Search Key — the primary lookup field (must be unique per row).

CSV format requirements

  • First row must be the header row with column names
  • Use UTF-8 encoding (no BOM)
  • The Search Key column must contain unique values — it is indexed for fast lookups
  • Maximum column name length: 500 characters

Example CSV: CorporateRanges watchlist

// CSV content (save as corporate_ranges.csv):
// IPRange,Location,Owner
// 10.0.0.0/8,HQ Data Centre,IT Operations
// 172.16.0.0/12,Branch Offices,IT Operations
// 192.168.0.0/16,VPN Pool,IT Operations
// 203.0.113.0/24,Azure Egress London,Cloud Team

4. Querying Watchlists — All Correct Patterns

Pattern 1: Exact set membership (most common)

// Project only the SearchKey column, then use 'in' operator
let BlockedIPs = (_GetWatchlist('BlockedIPs') | project SearchKey);
CommonSecurityLog
| where TimeGenerated > ago(24h)
| where SourceIP in (BlockedIPs)
| project TimeGenerated, SourceIP, DestinationIP, Activity, DeviceVendor, DeviceProduct

Pattern 2: Multi-column lookup with join

// Join watchlist to enrich events with additional context columns
let VIPUsers = _GetWatchlist('VIPUsers')
    | project SearchKey, DisplayName = Column1, Department = Column2;
SecurityAlert
| where TimeGenerated > ago(24h)
| extend AlertUser = tostring(parse_json(Entities)[0].AadUserId)
| join kind=leftouter (VIPUsers) on $left.AlertUser == $right.SearchKey
| extend IsVIP = isnotempty(DisplayName)
| project TimeGenerated, AlertName, AlertSeverity, AlertUser, IsVIP, DisplayName, Department

Pattern 3: CIDR range matching (critical — use correct syntax)

// Build a scalar dynamic array first — REQUIRED for ipv4_is_in_any_range
let CorpRanges = toscalar(
    _GetWatchlist('CorporateRanges')
    | summarize make_list(SearchKey)   // SearchKey = IPRange column
);
SigninLogs
| where TimeGenerated > ago(24h)
| extend IsInternal = ipv4_is_in_any_range(IPAddress, CorpRanges)
| where not(IsInternal)               // External sign-ins only
| summarize
    ExternalCount = count(),
    Countries     = make_set(tostring(LocationDetails.countryOrRegion))
  by UserPrincipalName, IPAddress
| order by ExternalCount desc
❌ WRONG — Will Error or Give Incorrect Results | where ipv4_is_in_any_range(IPAddress, VPNRanges) — where VPNRanges is a column from a joined table. This does not work. The second argument to ipv4_is_in_any_range must be a scalar dynamic array, not a column reference. Always use toscalar(... | summarize make_list(...)).

Pattern 4: Country allowlist (geo-blocking detection)

let AllowedCountries = (_GetWatchlist('AllowedCountries') | project SearchKey);
SigninLogs
| where TimeGenerated > ago(24h)
| where ResultType == 0
| extend Country = tostring(LocationDetails.countryOrRegion)
| where Country !in (AllowedCountries)
| where isnotempty(Country)
| summarize Count = count() by UserPrincipalName, IPAddress, Country
| order by Count desc

Pattern 5: Service account interactive logon detection

let ServiceAccounts = (_GetWatchlist('ServiceAccounts') | project SearchKey);
SecurityEvent
| where TimeGenerated > ago(24h)
| where EventID == 4624
| where LogonType in (2, 10)          // Interactive (2) and RemoteInteractive (10)
| where Account in~ (ServiceAccounts)
| project TimeGenerated, Account, Computer, IpAddress, LogonType

5. Automating Watchlist Updates

Static watchlists quickly become stale. For IOC-based watchlists especially, you need automated refresh from your threat intel feeds. Two common automation approaches:

Logic App automation

  • Trigger: Recurrence (daily or on-demand)
  • Action 1: HTTP call to your threat intel feed API to fetch new IOC list
  • Action 2: Convert to CSV format
  • Action 3: Call Microsoft Sentinel → Watchlists → Update action to replace the watchlist content

API-based update (Python)

// Watchlist update endpoint (Azure Management API):
// PUT https://management.azure.com/subscriptions/{sub}/resourceGroups/{rg}
//     /providers/Microsoft.OperationalInsights/workspaces/{ws}
//     /providers/Microsoft.SecurityInsights/watchlists/{alias}
//     /watchlistItems/{itemId}?api-version=2022-11-01
//
// Body: { "properties": { "itemsKeyValue": { "SearchKey": "1.2.3.4", "Source": "ThreatFeed" } } }

6. Production-Ready Detection Examples

TOR exit node detection

// Assumes a watchlist 'TORExitNodes' updated daily from dan.me.uk/torlist
let TORNodes = (_GetWatchlist('TORExitNodes') | project SearchKey);
SigninLogs
| where TimeGenerated > ago(24h)
| where ResultType == 0
| where IPAddress in (TORNodes)
| project TimeGenerated, UserPrincipalName, IPAddress, AppDisplayName,
          Country = tostring(LocationDetails.countryOrRegion)

Privileged account baseline deviation

let PrivAccounts = (_GetWatchlist('PrivilegedAccounts') | project SearchKey);
SecurityEvent
| where TimeGenerated > ago(7d)
| where EventID == 4624
| where Account in~ (PrivAccounts)
| extend HourOfDay = hourofday(TimeGenerated)
| where HourOfDay !between (7 .. 19)     // After hours
| summarize
    AfterHoursLogons = count(),
    Computers        = make_set(Computer)
  by Account, IpAddress
| order by AfterHoursLogons desc
✅ Watchlist Governance Checklist Document every watchlist: purpose, owner, update frequency, and which analytic rules depend on it. Review each watchlist quarterly. Set a Logic App alert if a watchlist has not been updated within its expected refresh window — stale watchlists silently degrade detection coverage.
S
Sujit Mahakhud
Microsoft Sentinel Specialist · SecByte Founder
5+ years in cybersecurity · Sentinel · Threat Hunting · Cloud Security

Similar Posts

Leave a Reply