Initial commit: KQL ↔ SDL PowerQuery proof of equivalence

This commit is contained in:
marc
2026-06-01 09:57:14 +02:00
commit 23cbaa9c08
91 changed files with 5966 additions and 0 deletions
@@ -0,0 +1,32 @@
// Rule: 01_anomalous_signin_location_increase
// Users showing a spike in distinct signin locations vs baseline
//
// Source KQL: see ../kql/01_anomalous_signin_location_increase.kql
//
// HOW TO RUN
// curl POST {sdl}/api/powerQuery with this body, OR paste in
// the SDL console. Set startTime = '2h' (or wider) so the API
// scans the freshly-ingested epochs that contain the events.
//
// Time anchor at export: NOW = 2026-05-31T20:10:05+00:00
// Recent-window cutoff: 2026-05-31T18:10:05+00:00
// (`ts_epoch_ms` below is that cutoff expressed in ms.
// Re-run harness/export_rules.py to refresh after regenerating
// sample_data/events.jsonl.)
//
// Fields referenced: AppDisplayName, Location, LocationCount, LocationList, LogonCount, RECENT_MS, SigninLogs, UserPrincipalName
//
// EDITING NOTE
// Every line that starts with `|` is a pipeline stage. Each `|`
// is REQUIRED. If you delete one (e.g. while changing a literal
// on the same line as a stage), SDL re-parses the keyword that
// follows as a search term and rejects the query with errors
// like `'estimate_distinct' is a grouping function`.
event_type='SigninLogs'
| filter ts_epoch_ms >= 1780251005000
| group LocationCount = estimate_distinct(Location),
LocationList = array_agg_distinct(Location),
LogonCount = count()
by UserPrincipalName, AppDisplayName
| filter LocationCount >= 3
+30
View File
@@ -0,0 +1,30 @@
// Rule: 02_rare_audit_activity_by_app
// AuditLogs OperationName seen in last 24h but not in 14d baseline
//
// Source KQL: see ../kql/02_rare_audit_activity_by_app.kql
//
// HOW TO RUN
// curl POST {sdl}/api/powerQuery with this body, OR paste in
// the SDL console. Set startTime = '2h' (or wider) so the API
// scans the freshly-ingested epochs that contain the events.
//
// Time anchor at export: NOW = 2026-05-31T20:10:05+00:00
// Recent-window cutoff: 2026-05-31T18:10:05+00:00
// (`ts_epoch_ms` below is that cutoff expressed in ms.
// Re-run harness/export_rules.py to refresh after regenerating
// sample_data/events.jsonl.)
//
// Fields referenced: Add, AuditLogs, Consent, OperationName, RECENT_MS
//
// EDITING NOTE
// Every line that starts with `|` is a pipeline stage. Each `|`
// is REQUIRED. If you delete one (e.g. while changing a literal
// on the same line as a stage), SDL re-parses the keyword that
// follows as a search term and rejects the query with errors
// like `'estimate_distinct' is a grouping function`.
event_type='AuditLogs'
| filter ts_epoch_ms >= 1780251005000
| filter OperationName in ('Add service principal', 'Consent to application')
| group n = count()
by OperationName
+31
View File
@@ -0,0 +1,31 @@
// Rule: 03_azure_rare_subscription_ops
// High-volume sensitive Azure subscription operations from a caller
//
// Source KQL: see ../kql/03_azure_rare_subscription_ops.kql
//
// HOW TO RUN
// curl POST {sdl}/api/powerQuery with this body, OR paste in
// the SDL console. Set startTime = '2h' (or wider) so the API
// scans the freshly-ingested epochs that contain the events.
//
// Time anchor at export: NOW = 2026-05-31T20:10:05+00:00
// Recent-window cutoff: 2026-05-31T18:10:05+00:00
// (`ts_epoch_ms` below is that cutoff expressed in ms.
// Re-run harness/export_rules.py to refresh after regenerating
// sample_data/events.jsonl.)
//
// Fields referenced: ActivityCount, ActivityStatusValue, AzureActivity, Caller, CallerIpAddress, OperationNameValue, Success
//
// EDITING NOTE
// Every line that starts with `|` is a pipeline stage. Each `|`
// is REQUIRED. If you delete one (e.g. while changing a literal
// on the same line as a stage), SDL re-parses the keyword that
// follows as a search term and rejects the query with errors
// like `'estimate_distinct' is a grouping function`.
event_type='AzureActivity'
| filter ActivityStatusValue = 'Success'
| filter OperationNameValue in ('microsoft.compute/snapshots/write', 'microsoft.network/networksecuritygroups/write', 'microsoft.storage/storageaccounts/listkeys/action')
| group ActivityCount = count()
by CallerIpAddress, Caller, OperationNameValue
| filter ActivityCount >= 5
+31
View File
@@ -0,0 +1,31 @@
// Rule: 04_daily_signin_location_trend
// Daily baseline of signin locations / IPs per user+app
//
// Source KQL: see ../kql/04_daily_signin_location_trend.kql
//
// HOW TO RUN
// curl POST {sdl}/api/powerQuery with this body, OR paste in
// the SDL console. Set startTime = '2h' (or wider) so the API
// scans the freshly-ingested epochs that contain the events.
//
// Time anchor at export: NOW = 2026-05-31T20:10:05+00:00
// Recent-window cutoff: 2026-05-31T18:10:05+00:00
// (`ts_epoch_ms` below is that cutoff expressed in ms.
// Re-run harness/export_rules.py to refresh after regenerating
// sample_data/events.jsonl.)
//
// Fields referenced: AppDisplayName, DistinctSourceIp, IPAddress, Location, LocationCount, LogonCount, RECENT_MS, SigninLogs, UserPrincipalName
//
// EDITING NOTE
// Every line that starts with `|` is a pipeline stage. Each `|`
// is REQUIRED. If you delete one (e.g. while changing a literal
// on the same line as a stage), SDL re-parses the keyword that
// follows as a search term and rejects the query with errors
// like `'estimate_distinct' is a grouping function`.
event_type='SigninLogs'
| filter ts_epoch_ms >= 1780251005000
| group LocationCount = estimate_distinct(Location),
DistinctSourceIp = estimate_distinct(IPAddress),
LogonCount = count()
by AppDisplayName, UserPrincipalName
+32
View File
@@ -0,0 +1,32 @@
// Rule: 05_daily_network_traffic_per_source
// Daily baseline of bytes & peers per source IP
//
// Source KQL: see ../kql/05_daily_network_traffic_per_source.kql
//
// HOW TO RUN
// curl POST {sdl}/api/powerQuery with this body, OR paste in
// the SDL console. Set startTime = '2h' (or wider) so the API
// scans the freshly-ingested epochs that contain the events.
//
// Time anchor at export: NOW = 2026-05-31T20:10:05+00:00
// Recent-window cutoff: 2026-05-31T18:10:05+00:00
// (`ts_epoch_ms` below is that cutoff expressed in ms.
// Re-run harness/export_rules.py to refresh after regenerating
// sample_data/events.jsonl.)
//
// Fields referenced: CommonSecurityLog, Count, DestinationIP, DeviceVendor, DistinctDestinationIps, NoofBytesReceived, NoofBytesTransferred, RECENT_MS, ReceivedBytes, SentBytes…
//
// EDITING NOTE
// Every line that starts with `|` is a pipeline stage. Each `|`
// is REQUIRED. If you delete one (e.g. while changing a literal
// on the same line as a stage), SDL re-parses the keyword that
// follows as a search term and rejects the query with errors
// like `'estimate_distinct' is a grouping function`.
event_type='CommonSecurityLog'
| filter ts_epoch_ms >= 1780251005000
| group Count = count(),
DistinctDestinationIps = estimate_distinct(DestinationIP),
NoofBytesTransferred = sum(SentBytes),
NoofBytesReceived = sum(ReceivedBytes)
by SourceIP, DeviceVendor
+34
View File
@@ -0,0 +1,34 @@
// Rule: 06_daily_process_execution_trend
// Daily baseline of process executions (4688)
//
// Source KQL: see ../kql/06_daily_process_execution_trend.kql
//
// HOW TO RUN
// curl POST {sdl}/api/powerQuery with this body, OR paste in
// the SDL console. Set startTime = '2h' (or wider) so the API
// scans the freshly-ingested epochs that contain the events.
//
// Time anchor at export: NOW = 2026-05-31T20:10:05+00:00
// Recent-window cutoff: 2026-05-31T18:10:05+00:00
// (`ts_epoch_ms` below is that cutoff expressed in ms.
// Re-run harness/export_rules.py to refresh after regenerating
// sample_data/events.jsonl.)
//
// Fields referenced: Account, CommandLine, Computer, Count, DistinctAccounts, DistinctComputers, DistinctParent, EventID, NewProcessName, NoofCommandLines…
//
// EDITING NOTE
// Every line that starts with `|` is a pipeline stage. Each `|`
// is REQUIRED. If you delete one (e.g. while changing a literal
// on the same line as a stage), SDL re-parses the keyword that
// follows as a search term and rejects the query with errors
// like `'estimate_distinct' is a grouping function`.
event_type='SecurityEvent'
| filter ts_epoch_ms >= 1780251005000
| filter EventID = 4688
| group Count = count(),
DistinctComputers = estimate_distinct(Computer),
DistinctAccounts = estimate_distinct(Account),
DistinctParent = estimate_distinct(ParentProcessName),
NoofCommandLines = estimate_distinct(CommandLine)
by NewProcessName
+31
View File
@@ -0,0 +1,31 @@
// Rule: 07_rare_user_agent_by_app
// UserAgent seen in last 24h not present in 7d baseline for that app
//
// Source KQL: see ../kql/07_rare_user_agent_by_app.kql
//
// HOW TO RUN
// curl POST {sdl}/api/powerQuery with this body, OR paste in
// the SDL console. Set startTime = '2h' (or wider) so the API
// scans the freshly-ingested epochs that contain the events.
//
// Time anchor at export: NOW = 2026-05-31T20:10:05+00:00
// Recent-window cutoff: 2026-05-31T18:10:05+00:00
// (`ts_epoch_ms` below is that cutoff expressed in ms.
// Re-run harness/export_rules.py to refresh after regenerating
// sample_data/events.jsonl.)
//
// Fields referenced: AppDisplayName, RECENT_MS, ResultType, SigninLogs, UserAgent, UserPrincipalName
//
// EDITING NOTE
// Every line that starts with `|` is a pipeline stage. Each `|`
// is REQUIRED. If you delete one (e.g. while changing a literal
// on the same line as a stage), SDL re-parses the keyword that
// follows as a search term and rejects the query with errors
// like `'estimate_distinct' is a grouping function`.
event_type='SigninLogs'
| filter ResultType = 0
| filter ts_epoch_ms >= 1780251005000
| group n = count()
by UserPrincipalName, AppDisplayName, UserAgent
| filter UserAgent contains 'curl' OR UserAgent contains 'python-requests'
+30
View File
@@ -0,0 +1,30 @@
// Rule: 08_network_ioc_match
// Traffic to IPs present in ThreatIntelIndicators
//
// Source KQL: see ../kql/08_network_ioc_match.kql
//
// HOW TO RUN
// curl POST {sdl}/api/powerQuery with this body, OR paste in
// the SDL console. Set startTime = '2h' (or wider) so the API
// scans the freshly-ingested epochs that contain the events.
//
// Time anchor at export: NOW = 2026-05-31T20:10:05+00:00
// Recent-window cutoff: 2026-05-31T18:10:05+00:00
// (`ts_epoch_ms` below is that cutoff expressed in ms.
// Re-run harness/export_rules.py to refresh after regenerating
// sample_data/events.jsonl.)
//
// Fields referenced: CommonSecurityLog, DestinationIP, DeviceVendor, RECENT_MS, SourceIP
//
// EDITING NOTE
// Every line that starts with `|` is a pipeline stage. Each `|`
// is REQUIRED. If you delete one (e.g. while changing a literal
// on the same line as a stage), SDL re-parses the keyword that
// follows as a search term and rejects the query with errors
// like `'estimate_distinct' is a grouping function`.
event_type='CommonSecurityLog'
| filter ts_epoch_ms >= 1780251005000
| filter DestinationIP in ('185.220.101.7')
| group hits = count()
by SourceIP, DestinationIP, DeviceVendor
+31
View File
@@ -0,0 +1,31 @@
// Rule: 09_new_processes_24h
// Process filenames seen today but never in the 14d baseline
//
// Source KQL: see ../kql/09_new_processes_24h.kql
//
// HOW TO RUN
// curl POST {sdl}/api/powerQuery with this body, OR paste in
// the SDL console. Set startTime = '2h' (or wider) so the API
// scans the freshly-ingested epochs that contain the events.
//
// Time anchor at export: NOW = 2026-05-31T20:10:05+00:00
// Recent-window cutoff: 2026-05-31T18:10:05+00:00
// (`ts_epoch_ms` below is that cutoff expressed in ms.
// Re-run harness/export_rules.py to refresh after regenerating
// sample_data/events.jsonl.)
//
// Fields referenced: Account, Computer, EventID, NewProcessName, RECENT_MS, SecurityEvent
//
// EDITING NOTE
// Every line that starts with `|` is a pipeline stage. Each `|`
// is REQUIRED. If you delete one (e.g. while changing a literal
// on the same line as a stage), SDL re-parses the keyword that
// follows as a search term and rejects the query with errors
// like `'estimate_distinct' is a grouping function`.
event_type='SecurityEvent'
| filter EventID = 4688
| filter ts_epoch_ms >= 1780251005000
| filter NewProcessName contains 'mimikatz'
| group n = count()
by NewProcessName, Account, Computer
+32
View File
@@ -0,0 +1,32 @@
// Rule: 10_sharepoint_anomaly
// SharePoint downloads/uploads deviating >25x from baseline
//
// Source KQL: see ../kql/10_sharepoint_anomaly.kql
//
// HOW TO RUN
// curl POST {sdl}/api/powerQuery with this body, OR paste in
// the SDL console. Set startTime = '2h' (or wider) so the API
// scans the freshly-ingested epochs that contain the events.
//
// Time anchor at export: NOW = 2026-05-31T20:10:05+00:00
// Recent-window cutoff: 2026-05-31T18:10:05+00:00
// (`ts_epoch_ms` below is that cutoff expressed in ms.
// Re-run harness/export_rules.py to refresh after regenerating
// sample_data/events.jsonl.)
//
// Fields referenced: ClientIP, FileDownloaded, FileUploaded, OfficeActivity, Operation, RECENT_MS, RecentCount, RecordType, SharePointFileOperation, Site_Url
//
// EDITING NOTE
// Every line that starts with `|` is a pipeline stage. Each `|`
// is REQUIRED. If you delete one (e.g. while changing a literal
// on the same line as a stage), SDL re-parses the keyword that
// follows as a search term and rejects the query with errors
// like `'estimate_distinct' is a grouping function`.
event_type='OfficeActivity'
| filter RecordType = 'SharePointFileOperation'
| filter Operation in ('FileDownloaded', 'FileUploaded')
| filter ts_epoch_ms >= 1780251005000
| group RecentCount = count()
by UserId, Operation, Site_Url, ClientIP
| filter RecentCount > 50
+31
View File
@@ -0,0 +1,31 @@
// Rule: 11_palo_alto_beacon
// Periodic Palo Alto traffic patterns matching C2 beacon profile
//
// Source KQL: see ../kql/11_palo_alto_beacon.kql
//
// HOW TO RUN
// curl POST {sdl}/api/powerQuery with this body, OR paste in
// the SDL console. Set startTime = '2h' (or wider) so the API
// scans the freshly-ingested epochs that contain the events.
//
// Time anchor at export: NOW = 2026-05-31T20:10:05+00:00
// Recent-window cutoff: 2026-05-31T18:10:05+00:00
// (`ts_epoch_ms` below is that cutoff expressed in ms.
// Re-run harness/export_rules.py to refresh after regenerating
// sample_data/events.jsonl.)
//
// Fields referenced: Activity, Alto, CommonSecurityLog, DestinationIP, DestinationPort, DeviceVendor, Networks, Palo, RECENT_MS, SourceIP
//
// EDITING NOTE
// Every line that starts with `|` is a pipeline stage. Each `|`
// is REQUIRED. If you delete one (e.g. while changing a literal
// on the same line as a stage), SDL re-parses the keyword that
// follows as a search term and rejects the query with errors
// like `'estimate_distinct' is a grouping function`.
event_type='CommonSecurityLog'
| filter DeviceVendor = 'Palo Alto Networks' AND Activity = 'TRAFFIC'
| filter ts_epoch_ms >= 1780251005000
| group TotalEvents = count()
by SourceIP, DestinationIP, DestinationPort
| filter TotalEvents > 30
@@ -0,0 +1,32 @@
// Rule: 12_suspicious_windows_logon_off_hours
// Logon outside that user's historical hour-range
//
// Source KQL: see ../kql/12_suspicious_windows_logon_off_hours.kql
//
// HOW TO RUN
// curl POST {sdl}/api/powerQuery with this body, OR paste in
// the SDL console. Set startTime = '2h' (or wider) so the API
// scans the freshly-ingested epochs that contain the events.
//
// Time anchor at export: NOW = 2026-05-31T20:10:05+00:00
// Recent-window cutoff: 2026-05-31T18:10:05+00:00
// (`ts_epoch_ms` below is that cutoff expressed in ms.
// Re-run harness/export_rules.py to refresh after regenerating
// sample_data/events.jsonl.)
//
// Fields referenced: EventID, Interactive, IpAddress, LogonTypeName, RECENT_MS, RemoteInteractive, SecurityEvent, TargetUserName
//
// EDITING NOTE
// Every line that starts with `|` is a pipeline stage. Each `|`
// is REQUIRED. If you delete one (e.g. while changing a literal
// on the same line as a stage), SDL re-parses the keyword that
// follows as a search term and rejects the query with errors
// like `'estimate_distinct' is a grouping function`.
event_type='SecurityEvent'
| filter EventID = 4624 OR EventID = 4625
| filter LogonTypeName = '2 - Interactive' OR LogonTypeName = '10 - RemoteInteractive'
| filter is_off_hours = 'true'
| filter ts_epoch_ms >= 1780251005000
| group n = count()
by TargetUserName, IpAddress
+31
View File
@@ -0,0 +1,31 @@
// Rule: 13_insider_threat_sensitive_files
// Sensitive file access within confidential folders
//
// Source KQL: see ../kql/13_insider_threat_sensitive_files.kql
//
// HOW TO RUN
// curl POST {sdl}/api/powerQuery with this body, OR paste in
// the SDL console. Set startTime = '2h' (or wider) so the API
// scans the freshly-ingested epochs that contain the events.
//
// Time anchor at export: NOW = 2026-05-31T20:10:05+00:00
// Recent-window cutoff: 2026-05-31T18:10:05+00:00
// (`ts_epoch_ms` below is that cutoff expressed in ms.
// Re-run harness/export_rules.py to refresh after regenerating
// sample_data/events.jsonl.)
//
// Fields referenced: AccessCount, ActionType, Confidential, DeviceFileEvents, FileAccessed, FileCopied, FileModified, FileMoved, FileName, FileRead
//
// EDITING NOTE
// Every line that starts with `|` is a pipeline stage. Each `|`
// is REQUIRED. If you delete one (e.g. while changing a literal
// on the same line as a stage), SDL re-parses the keyword that
// follows as a search term and rejects the query with errors
// like `'estimate_distinct' is a grouping function`.
event_type='DeviceFileEvents'
| filter ts_epoch_ms >= 1780251005000
| filter FolderPath contains 'Confidential' OR FolderPath contains 'Sensitive' OR FolderPath contains 'Restricted'
| filter ActionType in ('FileAccessed','FileRead','FileModified','FileCopied','FileMoved')
| group AccessCount = count()
by FileName, InitiatingProcessAccountName
+30
View File
@@ -0,0 +1,30 @@
// Rule: 14_priv_escalation
// Sensitive Entra operations joined to successful signin context
//
// Source KQL: see ../kql/14_priv_escalation.kql
//
// HOW TO RUN
// curl POST {sdl}/api/powerQuery with this body, OR paste in
// the SDL console. Set startTime = '2h' (or wider) so the API
// scans the freshly-ingested epochs that contain the events.
//
// Time anchor at export: NOW = 2026-05-31T20:10:05+00:00
// Recent-window cutoff: 2026-05-31T18:10:05+00:00
// (`ts_epoch_ms` below is that cutoff expressed in ms.
// Re-run harness/export_rules.py to refresh after regenerating
// sample_data/events.jsonl.)
//
// Fields referenced: Add, AuditLogs, Certificates, OperationName, RECENT_MS
//
// EDITING NOTE
// Every line that starts with `|` is a pipeline stage. Each `|`
// is REQUIRED. If you delete one (e.g. while changing a literal
// on the same line as a stage), SDL re-parses the keyword that
// follows as a search term and rejects the query with errors
// like `'estimate_distinct' is a grouping function`.
event_type='AuditLogs'
| filter OperationName in ('Add service principal', 'Certificates and secrets management')
| filter ts_epoch_ms >= 1780251005000
| group ops = count()
by OperationName
+32
View File
@@ -0,0 +1,32 @@
// Rule: 15_slow_brute_force
// High volume of failed signins from one IP across many users
//
// Source KQL: see ../kql/15_slow_brute_force.kql
//
// HOW TO RUN
// curl POST {sdl}/api/powerQuery with this body, OR paste in
// the SDL console. Set startTime = '2h' (or wider) so the API
// scans the freshly-ingested epochs that contain the events.
//
// Time anchor at export: NOW = 2026-05-31T20:10:05+00:00
// Recent-window cutoff: 2026-05-31T18:10:05+00:00
// (`ts_epoch_ms` below is that cutoff expressed in ms.
// Re-run harness/export_rules.py to refresh after regenerating
// sample_data/events.jsonl.)
//
// Fields referenced: FailedAttempts, IPAddress, RECENT_MS, ResultType, SigninLogs, UniqueUsers, UserPrincipalName
//
// EDITING NOTE
// Every line that starts with `|` is a pipeline stage. Each `|`
// is REQUIRED. If you delete one (e.g. while changing a literal
// on the same line as a stage), SDL re-parses the keyword that
// follows as a search term and rejects the query with errors
// like `'estimate_distinct' is a grouping function`.
event_type='SigninLogs'
| filter ts_epoch_ms >= 1780251005000
| filter ResultType in (50053,50126,50055,50057,50155,50105,50133,50005,50076,50079,50173,50158,50072,50074,53003,53000,53001,50129)
| group FailedAttempts = count(),
UniqueUsers = estimate_distinct(UserPrincipalName)
by IPAddress
| filter FailedAttempts > 5 AND UniqueUsers > 5
+32
View File
@@ -0,0 +1,32 @@
// Rule: 16_suspicious_travel
// User signed in from >3 distinct countries in 24h
//
// Source KQL: see ../kql/16_suspicious_travel.kql
//
// HOW TO RUN
// curl POST {sdl}/api/powerQuery with this body, OR paste in
// the SDL console. Set startTime = '2h' (or wider) so the API
// scans the freshly-ingested epochs that contain the events.
//
// Time anchor at export: NOW = 2026-05-31T20:10:05+00:00
// Recent-window cutoff: 2026-05-31T18:10:05+00:00
// (`ts_epoch_ms` below is that cutoff expressed in ms.
// Re-run harness/export_rules.py to refresh after regenerating
// sample_data/events.jsonl.)
//
// Fields referenced: CountriesAccessed, Location, RECENT_MS, ResultType, SigninLogs, UserPrincipalName
//
// EDITING NOTE
// Every line that starts with `|` is a pipeline stage. Each `|`
// is REQUIRED. If you delete one (e.g. while changing a literal
// on the same line as a stage), SDL re-parses the keyword that
// follows as a search term and rejects the query with errors
// like `'estimate_distinct' is a grouping function`.
event_type='SigninLogs'
| filter ResultType = 0
| filter ts_epoch_ms >= 1780251005000
| group CountriesAccessed = array_agg_distinct(Location),
n = estimate_distinct(Location)
by UserPrincipalName
| filter n >= 4
+32
View File
@@ -0,0 +1,32 @@
// Rule: 17_daily_baseline_new_locations
// User signing in today from a country never seen in 14d baseline
//
// Source KQL: see ../kql/17_daily_baseline_new_locations.kql
//
// HOW TO RUN
// curl POST {sdl}/api/powerQuery with this body, OR paste in
// the SDL console. Set startTime = '2h' (or wider) so the API
// scans the freshly-ingested epochs that contain the events.
//
// Time anchor at export: NOW = 2026-05-31T20:10:05+00:00
// Recent-window cutoff: 2026-05-31T18:10:05+00:00
// (`ts_epoch_ms` below is that cutoff expressed in ms.
// Re-run harness/export_rules.py to refresh after regenerating
// sample_data/events.jsonl.)
//
// Fields referenced: Location, RECENT_MS, ResultType, SigninLogs, TodayCountries, UserPrincipalName
//
// EDITING NOTE
// Every line that starts with `|` is a pipeline stage. Each `|`
// is REQUIRED. If you delete one (e.g. while changing a literal
// on the same line as a stage), SDL re-parses the keyword that
// follows as a search term and rejects the query with errors
// like `'estimate_distinct' is a grouping function`.
event_type='SigninLogs'
| filter ResultType = 0
| filter ts_epoch_ms >= 1780251005000
| group TodayCountries = array_agg_distinct(Location),
nLocs = estimate_distinct(Location)
by UserPrincipalName
| filter nLocs >= 1