Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions .github/skills/crowdstrike-cql-data-movement/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
---
name: crowdstrike-cql-data-movement
description: "Use for CrowdStrike CQL investigations of files written to removable media, external-storage exfiltration, Outlook links, and Outlook attachments."
user-invocable: true
---

# Email and Data-Movement Hunting

Use these query bodies as starting points. Validate event names, fields, time scope, function support, and telemetry availability in the target tenant. These queries are investigative leads, not verdicts.

## Files Written to Removable Media

Source file: `Files_Written_to_Removable_Media.yml`

```cql
#event_simpleName=/Written/ IsOnRemovableDisk=1
| FileSizeMB:=unit:convert(Size, to=M)
| groupBy([ComputerName], function=([sum(Size, as=SizeBytes), sum(FileSizeMB, as=FileSizeMB), count(TargetFileName, as="File Count"), collect([TargetFileName])]))
```

## Detect Data Exfiltration via external storage devices

Source file: `data_exfiltration_external_storage.yml`

```cql
#event_simpleName=/FileWritten/i and IsOnRemovableDisk = 1
| VolumeSessionUUID=*
| "Size (MB)" := Size/1024/1024
| format(format="%.2f", field=["Size (MB)"], as="Size (MB)")
| join(query={#event_simpleName=DcUsbDeviceConnected | rename(DeviceInstanceId, as="DiskParentDeviceInstanceId")}, mode=left, field=[DiskParentDeviceInstanceId], include=[DeviceManufacturer, DeviceProduct])
| groupBy([ComputerName, UserName, DeviceManufacturer, DeviceProduct], function=[min(field=@timestamp, as=firstTime),max(field=@timestamp, as=lastTime),sum(Size, as="Size")])
| "Size (MB)" := Size/1024/1024
| format(format="%.2f", field=["Size (MB)"], as="Size (MB)")
```

## Phishing - List of links opened from Outlook

Source file: `Hunt_links_opened_from_Outlook.yml`

```cql
#event_simpleName=ProcessRollup2
| aid=?aid ImageFileName=/\\outlook\.exe/i
| regex("(?<FileName>[^\\/|\\\\]*)$", field=ImageFileName, strict=false)
| join(
{
#event_simpleName=ProcessRollup2 ImageFileName=/(chrome|firefox|iexplore)\.exe/i
| MD5:=MD5HashData | ImageFileName=/(\/|\\)(?<ChildFileName>\w*\.?\w*)$/
| ChildCLI:=CommandLine
},
key=ParentProcessId, field=TargetProcessId, include=[MD5, ChildFileName, ChildCLI]
)
| groupBy([aid, FileName, CommandLine, ChildFileName, ChildCLI, MD5], limit=max)
```

## List of attachments sent from Outlook

Source file: `attachments_send_by_outlook.yml`

```cql
#event_simpleName=ProcessRollup2
| CommandLine=/content.outlook/i
| aid=?aid
| ImageFileName=/(\/|\\)(?<FileName>\w*\.?\w*)$/
| FileName=/(winword|excel|powerpnt)\.exe/i
| CommandLine=/Outlook\\(?<ShortFile>\w*\\.*)$/i
| table([@timestamp, aid, TargetProcessId, ShortFile, CommandLine], limit=1000)
```

## Additional Query Patterns

The following CQL bodies are embedded directly for reuse. Validate event names, field availability, query-surface support, and telemetry in the target environment.
The SMB file-copy query uses Microsoft Defender for Identity data fields, not Falcon `#event_simpleName` events; use it only against a compatible dataset.

### High Volume SMB File Copy (Data Exfiltration / Ransomware) – Microsoft Defender for Identity

Source YAML: `high_volume_smb_file_copy_data_exfiltration_ransomware_microsoft_defender_for_identity.yml`

```cql
#Vendor = "microsoft"
| #event.module = "defender-identity"
| Vendor.category = "AdvancedHunting-IdentityDirectoryEvents"
| Vendor.properties.ActionType = "SMB file copy"
| groupBy([user.name, source.address], function=[count(as=file_copies),collect(fields=Vendor.properties.DestinationDeviceName),collect(fields=Vendor.properties.DeviceName),min(@timestamp, as=start_time),max(@timestamp, as=end_time)])
| file_copies > 50
| time_diff_min := (end_time - start_time) / 60000
| time_diff_min <= 10
| start_time_fmt := formatTime("%Y-%m-%d %H:%M:%S", field=start_time, timezone="UTC")
| end_time_fmt := formatTime("%Y-%m-%d %H:%M:%S", field=end_time, timezone="UTC")
| drop([start_time, end_time])
| sort([file_copies], order=desc)
```
387 changes: 387 additions & 0 deletions .github/skills/crowdstrike-cql-host-integrity/SKILL.md

Large diffs are not rendered by default.

252 changes: 252 additions & 0 deletions .github/skills/crowdstrike-cql-identity/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
---
name: crowdstrike-cql-identity
description: "Use for CrowdStrike CQL hunts covering failed or successful logons, brute-force follow-up, account usage, and local user creation or deletion."
user-invocable: true
---

# Identity and Logon Hunting

Use these query bodies as starting points. Validate event names, fields, time scope, function support, and telemetry availability in the target tenant. These queries are investigative leads, not verdicts.

## Failed User Logon Thresholding

Source file: `Failed_User_Logon_Thresholding.yml`

```cql
// Get Windows UserLogonFailed events
event_platform=Win #event_simpleName=UserLogonFailed2

// This line is completely optional, but converts SubStatus to hex
| SubStatus_hex:=format(field=SubStatus, "%x") | SubStatus_hex:=upper(SubStatus_hex) | SubStatus_hex:=format(format="0x%s", field=[SubStatus_hex])

// Aggregate results
| groupBy([aid, ComputerName, UserName, LogonType, SubStatus_hex, SubStatus], function=([count(aid, as=FailCount), min(ContextTimeStamp, as=FirstLogonAttempt), max(ContextTimeStamp, as=LastLogonAttempt), collect([LocalAddressIP4, aip])]))

// Perform rate calculations
| firstLastDeltaHours:=((LastLogonAttempt-FirstLogonAttempt)/60/60) | round("firstLastDeltaHours")
| logonAttemptsPerHour:=(failCount/firstLastDeltaHours) | round("logonAttemptsPerHour")

// Convert timestamps from epoch to human
| FirstLogonAttempt:=formatTime(format="%F %T.%L", field="FirstLogonAttempt")
| LastLogonAttempt:=formatTime(format="%F %T.%L", field="LastLogonAttempt")

// Optional: set threshold for failed logins
| FailCount> 5

// Sort descending
| sort(FailCount, order=desc, limit=2000)

// Convert fields from decimal to human readable
| $falcon/helper:enrich(field=LogonType)
| $falcon/helper:enrich(field=SubStatus)
```

## Failed and Successful User Logon Events

Source file: `Failed_and_Successful_User_Logon_Events.yml`

```cql
#event_simpleName=/UserLogon/
| case{
#event_simpleName=UserLogon | SuccessLogonTime:=ContextTimeStamp;
#event_simpleName=UserLogonFailed2 | FailedLogonTime:=ContextTimeStamp;
}
| groupBy([UserSid, UserName], function=([min(FailedLogonTime, as=FirstFailedLogon), max(FailedLogonTime, as=LastFailedLogon), max(SuccessLogonTime, as=LastSuccessfulLogin), count(SuccessLogonTime, as=TotalSuccessfulLogins), count(FailedLogonTime, as=TotalFailedLogins), selectFromMax(field="@timestamp", include=[PasswordLastSet]), {#event_simpleName=UserLogon | selectFromMax(field="@timestamp", include=[ComputerName]) | rename(field="ComputerName", as="LastLoggedOnHost")}]))
| TotalFailedLogins>3
| $falcon/helper:enrich(field=UserLogonFlags)
| formatTime(format="%F %T", field=FirstFailedLogon, as="FirstFailedLogon", timezone="EST")
| formatTime(format="%F %T", field=LastFailedLogon, as="LastFailedLogon", timezone="EST")
| formatTime(format="%F %T", field=LastSuccessfulLogin, as="LastSuccessfulLogin", timezone="EST")
| PasswordLastSet:=PasswordLastSet*1000 | formatTime(format="%F %T", field=PasswordLastSet, as="PasswordLastSet", timezone="EST")
| default(value="-", field=[FirstFailedLogon, LastFailedLogon, LastSuccessfulLogin, TotalSuccessfulLogins, TotalFailedLogins, PasswordLastSet, LastLoggedOnHost])
| sort(order=desc, TotalFailedLogins, limit=20000)
```

## Failed logon attempt group by userName and unique Endpoint involved

Source file: `Failed_logon_attempt.yml`

```cql
#event_simpleName = UserLogonFailed
| groupBy(UserName, function=([count(timestamp, distinct=true, as=uniqueFailedLogons), (count(aid, distinct=true, as=uniqueEP)), collect(fields = [ComputerName, aid], limit =10000)]))
| default(field = "UserName", value="-", replaceEmpty=true)
| uniqueFailedLogons >= 5
| uniqueEP >= 10
| sort(uniqueEP)
```

## Public IP Successfully Authenticated Following Brute Force Activity

Source file: `Public_IP_Successfully_Authenticated_Following_Brute_Force_Activity.yml`

```cql
#Vendor ="crowdstrike"
|"#event_simpleName" ="RemoteBruteForceDetectInfo"
| DetectDescription=~/^A public IP successfully brute forced an account on this system/
|table([@timestamp,ComputerName,user.name,RemoteIP])
```

## Detection of Generic User Account Usage

Source file: `detection_of_generic_user_account_usage.yml`

```cql
"#event_simpleName" = UserLogon | user.name := lower("user.name") | groupBy(user.name,ComputerName) | match(file="generic-usernames.csv", field=[user.name], column=[username])
| table([user.name, ComputerName, _count])
| User := rename(user.name)
| Host := rename(ComputerName)
| LogonCount := rename(_count)
```

## Created Local User Accounts

Source file: `created_local_user_accounts.yml`

```cql
#event_simpleName=UserAccountCreated
| table([@timestamp, UserName, aid, aip, ComputerName, event_platform, LocalIP, name], limit=20000)
| sort(@timestamp)
```

## Deleted Local User Accounts

Source file: `deleted_local_user_accounts.yml`

```cql
#event_simpleName=UserAccountDeleted
| groupBy([UserName, aid, aip, ComputerName, event_platform, LocalIP, name], function=selectLast([@timestamp]))
| table([@timestamp, UserName, ComputerName, aid, aip, event_platform, LocalIP, name])
| sort(@timestamp)
```

## Additional Query Patterns

The following CQL bodies are embedded directly for reuse. Validate event names, field availability, query-surface support, and telemetry in the target environment.

### Find events triggered at logon

Source YAML: `logon_events.yml`

```cql
#event_simpleName=ScheduledTaskRegistered
| parseXml(TaskXml)
| Trigger:=rename(Task.Triggers.LogonTrigger.Enabled)
| Trigger=* // Remove this line if you don't care if it's empty
| table([aid, Trigger, TaskXml], limit=1000)
```

### NTLM authentication where Kerberos is expected (Baseline)

Source YAML: `ntlm_authentication_where_kerberos_is_expected_baseline.yml`

```cql
// Hunt for NTLM authentications in scenarios where Kerberos would normally be expected
#event_simpleName=ActiveDirectoryAuthentication

// Keep only NTLM authentications
| in(field=ActiveDirectoryAuthenticationMethod, values=[1, 2, 5])

// Focus on service-based access (SPN/service context),
// where Kerberos should normally be available
| TargetServiceAccessIdentifier=*

// Optional: suppress machine accounts if you want a user-only view
// | SourceAccountSamAccountName!=/$/

// Map NTLM authentication method values to readable names
| case {
ActiveDirectoryAuthenticationMethod = 1 | AuthMethod := "NTLM_V1";
ActiveDirectoryAuthenticationMethod = 2 | AuthMethod := "NTLM_V2";
ActiveDirectoryAuthenticationMethod = 5 | AuthMethod := "UNKNOWN_NTLM";
* | AuthMethod := "OTHER";
}

// Map AD protocol values for easier triage
| case {
ActiveDirectoryDataProtocol = 0 | DataProtocol := "LDAP";
ActiveDirectoryDataProtocol = 1 | DataProtocol := "DCE_RPC";
ActiveDirectoryDataProtocol = 2 | DataProtocol := "RDP";
ActiveDirectoryDataProtocol = 3 | DataProtocol := "SMB";
* | DataProtocol := format(format="PROTO_%s", field=[ActiveDirectoryDataProtocol]);
}

// Summarize NTLM fallback activity
| groupBy([
SourceEndpointHostName,
SourceEndpointAddressIP4,
SourceAccountDomain,
SourceAccountSamAccountName,
TargetServiceAccessIdentifier,
TargetServerHostName,
TargetServerAddressIP4,
DataProtocol,
AuthMethod
], function=[
sum(AggregationActivityCount, as="ntlm_auth_count"),
min(AggregationEarliestTimestamp, as="first_seen"),
max(AggregationLatestTimestamp, as="last_seen")
])

// Show highest NTLM usage first
| sort(field=ntlm_auth_count, order=desc)
```

### User Logoff Activity

Source YAML: `user_logoff_activity.yml`

```cql
#event_simpleName=UserLogoff
| groupBy([UserName, name, aid, aip, ComputerName, event_platform, LocalIP, LogonDomain, LogonServer, LogonType], function=[count(@timestamp), selectLast([@timestamp])])
| table([@timestamp, UserName, ComputerName, aid, aip, event_platform, LocalIP, LogonDomain, LogonType], limit=20000)
```

### User Logon Activity

Source YAML: `user_logon_activity.yml`

```cql
#event_simpleName=UserLogon
| groupBy([UserName, name, aid, aip, ComputerName, event_platform, LocalIP, LogonDomain, LogonServer, LogonType], function=[count(@timestamp), selectLast([@timestamp])])
| table([@timestamp, UserName, ComputerName, aid, aip, event_platform, LocalIP, LogonDomain, LogonType], limit=20000)
```

### User Logon Details (Time, Type, Location, Last Password Change)

Source YAML: `user_logon_details__time__type__location__last_password_change_.yml`

```cql
#event_simpleName=UserLogon UserSid=S-1-5-21-*
| in(LogonType, values=["2","10"])
| ipLocation(aip)
| case {UserIsAdmin = "1" | UserIsAdmin := "Yes" ;
UserIsAdmin = "0" | UserIsAdmin := "No" ;
* }
| case {
LogonType = "2" | LogonType := "Interactive" ;
LogonType = "3" | LogonType := "Network" ;
LogonType = "4" | LogonType := "Batch" ;
LogonType = "5" | LogonType := "Service" ;
LogonType = "7" | LogonType := "Unlock" ;
LogonType = "8" | LogonType := "Network Cleartext" ;
LogonType = "9" | LogonType := "New Credentials" ;
LogonType = "10" | LogonType := "Remote Interactive" ;
LogonType = "11" | LogonType := "Cached Interactive" ;
* }
| PasswordLastSet := PasswordLastSet*1000
| LogonTime := LogonTime*1000
| PasswordLastSet := formatTime("%Y-%m-%d %H:%M:%S", field=PasswordLastSet, locale=en_US, timezone=Z)
| LogonTime := formatTime("%Y-%m-%d %H:%M:%S", field=LogonTime, locale=en_US, timezone=Z)
| table(["LogonTime", "aid", "UserName", "UserSid", "LogonType", "UserIsAdmin", "PasswordLastSet", "aip.city", "aip.state", "aip.country"])
```

### Windows authentication traffic metrics

Source YAML: `windows_authentication_traffic_metrics.yml`

```cql
#repo=base_sensor #event_simpleName="IdpDcPerfReport"
| aid=?SelectedAid
| IdpPerfCounterAvg:= IdpPerfCounterSum / IdpPerfSampleCount
| timeChart(span=15m, function=[avg("IdpPerfCounterAvg")], series=IdpPerfCounterPath)
```
Loading