Home / Blog / SIEM and Tools

Intermediate SIEM and Tools

How to Use Microsoft Sentinel Watchlists to Cut False Positives

Use Sentinel watchlists to suppress known scanners, flag VIP accounts and keep exclusions out of your rule logic, with ready KQL for each pattern.

Most noisy analytics rules in Microsoft Sentinel are noisy for a boring reason: the same scanners, service accounts and admin hosts trip them every day. Watchlists let you keep those lists in one managed place and reference them from any query, so you can tune rules without hard coding IP addresses into KQL. This guide is for analysts and detection engineers who want fewer repeat alerts without losing coverage.

Time to set upAbout 30 minutes for a first watchlist
You needSentinel Contributor rights and a CSV file
Main function_GetWatchlist('alias')
>

Scenario: Every Monday at 02:00 your "Multiple failed logons from one source" rule fires 40 times. Each incident points at 10.0.4.22 and 10.0.4.23, the vulnerability management team's authenticated scanners. The night shift closes them as benign, but the rule has an exclusion list typed directly into the KQL that nobody has updated since a third scanner was added. You want the exclusion to live somewhere the owning team can maintain, with an expiry date, and you want the rule to stay sharp for everything else.

What is a watchlist and when should you use one?

A watchlist is a small table of reference data that you upload to your Sentinel workspace, usually from a CSV file. Sentinel stores the rows in the Watchlist table as name and value pairs, and you read them back with the _GetWatchlist function. You can then use that data in hunting queries, workbooks, analytics rules and playbooks.

When you create a watchlist you pick one column as the SearchKey. This is the column you expect to join on or search most often, for example an IP address or a user principal name. Microsoft's documentation recommends using SearchKey in joins and in filters for the best performance, and it appears in query results as its own column.

Watchlists work in both the Defender portal (Microsoft Sentinel, then Configuration, then Watchlist) and the Azure portal. Microsoft has announced that Sentinel will only be available in the Defender portal after 31 March 2027, so it is worth learning the Defender path now.

Approved scannersIP addresses of vulnerability and asset discovery tools that legitimately probe many hosts.
VIP and high value usersExecutives, finance approvers and domain admins whose alerts deserve a higher priority.
Admin jump hostsPrivileged access workstations where admin tools and remote sessions are expected.
Service accountsNon human accounts with a known owner, purpose and set of hosts they should log on to.
Known egress and VPN rangesCorporate NAT and VPN addresses, so sign ins from them are not treated as unfamiliar.
Terminated usersLeavers whose accounts should never authenticate again, turning any sign in into a high signal alert.
i

Note: Watchlists are built for reference data, not bulk telemetry. Local CSV uploads are limited to 3.8 MB, uploads from Azure Storage (in preview at the time of writing) go up to 500 MB, and a workspace can hold up to 10 million active watchlist items in total. If you need to load threat intelligence feeds or millions of indicators, use the threat intelligence features or a custom table instead.

Build the CSV so it stays maintainable

The quality of a watchlist depends on its columns. A bare list of IP addresses works on day one but becomes a mystery six months later. Add columns that answer the questions an analyst will ask when they see an exclusion: who owns it, why it exists and when it should be reviewed.

approved_scanners.csvCSV
IPAddress,Owner,Purpose,Ticket,ExpiryDate
10.0.4.22,Vulnerability Management,Authenticated Nessus scanner,CHG-20418,2027-03-31
10.0.4.23,Vulnerability Management,Authenticated Nessus scanner,CHG-20418,2027-03-31
10.0.4.31,IT Asset Team,Network discovery appliance,CHG-20977,2027-01-15

When you create the watchlist in the portal, give it a clear name and alias such as ApprovedScanners, set the number of header lines to skip (usually 0 if the first row is the header), and choose IPAddress as the SearchKey. Aliases must be 3 to 64 characters, start and end with a letter or number, and can contain spaces, hyphens and underscores in between. Avoid spaces anyway: they make queries harder to read.

  • One watchlist per purpose. Do not mix scanners, VIPs and service accounts in a single file.
  • Always include an owner and a change or ticket reference.
  • Include an expiry or review date in ISO format (YYYY-MM-DD) so KQL can parse it.
  • Normalise case for user principal names before upload, or handle it in the query.
  • Keep the source CSV in version control so you have a history of who added what.

Exclude known scanners from a detection rule

Start by checking that the watchlist returns what you expect. Run these in Logs (Azure portal) or Advanced hunting (Defender portal):

Microsoft SentinelKQL
// List every watchlist alias in the workspace
_GetWatchlistAlias

// Preview the scanner list
_GetWatchlist('ApprovedScanners')
| project SearchKey, Owner, Purpose, ExpiryDate

Now rewrite the failed logon rule. Instead of a hard coded !in ("10.0.4.22", "10.0.4.23"), load the SearchKey column into a variable and filter on it. Only exclude entries that have not expired, so a forgotten exclusion stops protecting a scanner address once its review date passes.

Microsoft Sentinel analytics ruleKQL
let ActiveScanners = _GetWatchlist('ApprovedScanners')
    | where todatetime(ExpiryDate) > now()
    | project SearchKey;
SecurityEvent
| where TimeGenerated > ago(1h)
| where EventID == 4625
| where IpAddress !in (ActiveScanners)
| summarize FailedLogons = count(),
            TargetAccounts = dcount(TargetUserName),
            Hosts = make_set(Computer, 20)
    by IpAddress
| where FailedLogons > 25
+

Tip: Filtering on an expiry date turns your watchlist into a self cleaning exclusion list. Pair it with a weekly hunting query that lists entries expiring in the next 14 days, and send that to the owning team before the alerts return.

!

Watch out: An exclusion is a blind spot. If an attacker compromises 10.0.4.22, this rule will never fire for it. Keep a separate, lower severity rule or hunting query that watches the scanner addresses for activity outside their normal schedule or against hosts they never touch.

Enrich alerts with a VIP watchlist

Suppression is only half the value. Watchlists can also raise priority. Create a VIPUsers watchlist with columns such as UserPrincipalName, Role and Department, with UserPrincipalName as the SearchKey. Then join it to risky sign ins so the alert already tells the analyst who the user is.

Microsoft SentinelKQL
let VIPs = _GetWatchlist('VIPUsers')
    | project UserPrincipalName = tolower(tostring(SearchKey)),
              VipRole = tostring(Role),
              VipDepartment = tostring(Department);
SigninLogs
| where TimeGenerated > ago(1d)
| where RiskLevelDuringSignIn in ("medium", "high")
| extend UserPrincipalName = tolower(UserPrincipalName)
| lookup kind=inner VIPs on UserPrincipalName
| project TimeGenerated, UserPrincipalName, VipRole, VipDepartment,
          IPAddress, Location, AppDisplayName, RiskLevelDuringSignIn

The lookup operator is a good fit here because the watchlist is small and the sign in table is large. Using kind=inner means only VIP sign ins come back. Change it to kind=leftouter if you want every risky sign in with the VIP columns filled in where they match, which is useful for a workbook.

Lower casing both sides matters. A UPN uploaded as J.Smith@contoso.com will not match j.smith@contoso.com in a string equality join, and the result is a silent miss rather than an error.

Avoid the common watchlist mistakes

Most watchlist problems show up as a rule that quietly stops working. These are the ones worth checking first when an alert you expected never arrives.

Stale exclusions

High
What it looks like

Decommissioned scanner IPs stay in the list, then get reassigned by DHCP to an ordinary workstation that is now invisible to the rule.

Control

Mandatory expiry column, filtered in KQL, plus a monthly review with the owner.

Over broad entries

High
What it looks like

Someone adds a whole subnet or a shared admin account to silence one incident, removing coverage for dozens of systems.

Control

Peer review for watchlist changes and a rule that entries must be single hosts or accounts.

Case and format mismatches

Medium
What it looks like

Joins return nothing because of upper case UPNs, trailing spaces or IPv6 formatting differences.

Control

Normalise with tolower() and trim() on both sides of the comparison.

Empty results after creation

Medium
What it looks like

A new watchlist returns zero rows, often because the workspace hit its daily ingestion cap or the query time range is too narrow.

Control

Check the daily cap, widen the time range when testing and confirm row counts before enabling the rule.

i

Note: The Watchlist table in Log Analytics has a 28 day retention, but Sentinel refreshes watchlists every 12 days, updating TimeGenerated. That is why a watchlist stays queryable for as long as it exists. You do not need to re upload it to keep it alive.

Grow from lists to a tuning process

Watchlists are most useful when the whole team treats them as the standard place for reference data. This ladder shows how teams typically mature.

  1. L0
    Hard coded exclusionsIP addresses and account names typed straight into rule KQL, edited by whoever closed the last false positive.
    Fragile
  2. L1
    Shared watchlistsExclusions moved into named watchlists with an owner and purpose column, referenced with _GetWatchlist.
    Start here
  3. L2
    Expiring and reviewedEvery entry has an expiry date filtered in KQL, with a scheduled query that reports upcoming expiries.
    Recommended
  4. L3
    Managed as codeCSV files live in a repository, changes go through pull requests and a pipeline or API call updates the watchlist.
    Mature

A simple hunting query helps you move to L2. Schedule it weekly and share the output with each owner.

Microsoft SentinelKQL
_GetWatchlist('ApprovedScanners')
| extend Expiry = todatetime(ExpiryDate)
| where isnull(Expiry) or Expiry < now() + 14d
| project SearchKey, Owner, Purpose, Ticket, Expiry
| order by Expiry asc

The isnull(Expiry) clause catches entries where someone left the date blank or typed it in a format KQL cannot parse, which is exactly the kind of row that turns into a permanent blind spot.

Key takeaways

  1. Use watchlists for small, slowly changing reference data such as scanners, VIPs, jump hosts and service accounts.
  2. Choose the SearchKey carefully and use it in joins and in filters for the best performance.
  3. Give every entry an owner, a ticket and an expiry date, and filter expired rows in the query itself.
  4. Normalise case and whitespace on both sides of a comparison to avoid silent misses.
  5. Treat every exclusion as a blind spot and keep a separate check on the excluded assets.

Pick the noisiest rule in your workspace this week, move its exclusions into a watchlist with expiry dates, and compare the incident count after a fortnight. You will usually find that the rule becomes quieter and easier to trust at the same time.