mirror of
https://github.com/marcredhat/kql
synced 2026-06-08 13:23:58 +00:00
Initial commit: KQL ↔ SDL PowerQuery proof of equivalence
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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'
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user