From 52ba288187e4f846c12e67714b7e682e75f93a54 Mon Sep 17 00:00:00 2001 From: yayalingo <74298875+yayalingo@users.noreply.github.com> Date: Tue, 29 Sep 2026 04:23:18 +0000 Subject: [PATCH] Add focused CrowdStrike CQL agent skills --- .../crowdstrike-cql-data-movement/SKILL.md | 91 ++++ .../crowdstrike-cql-host-integrity/SKILL.md | 387 +++++++++++++++++ .../skills/crowdstrike-cql-identity/SKILL.md | 252 +++++++++++ .../skills/crowdstrike-cql-inventory/SKILL.md | 252 +++++++++++ .../skills/crowdstrike-cql-network/SKILL.md | 362 ++++++++++++++++ .../crowdstrike-cql-process-extended/SKILL.md | 196 +++++++++ .../skills/crowdstrike-cql-process/SKILL.md | 405 ++++++++++++++++++ .../crowdstrike-cql-scheduled-tasks/SKILL.md | 185 ++++++++ .github/skills/crowdstrike-cql/SKILL.md | 72 ++++ 9 files changed, 2202 insertions(+) create mode 100644 .github/skills/crowdstrike-cql-data-movement/SKILL.md create mode 100644 .github/skills/crowdstrike-cql-host-integrity/SKILL.md create mode 100644 .github/skills/crowdstrike-cql-identity/SKILL.md create mode 100644 .github/skills/crowdstrike-cql-inventory/SKILL.md create mode 100644 .github/skills/crowdstrike-cql-network/SKILL.md create mode 100644 .github/skills/crowdstrike-cql-process-extended/SKILL.md create mode 100644 .github/skills/crowdstrike-cql-process/SKILL.md create mode 100644 .github/skills/crowdstrike-cql-scheduled-tasks/SKILL.md create mode 100644 .github/skills/crowdstrike-cql/SKILL.md diff --git a/.github/skills/crowdstrike-cql-data-movement/SKILL.md b/.github/skills/crowdstrike-cql-data-movement/SKILL.md new file mode 100644 index 0000000..4097085 --- /dev/null +++ b/.github/skills/crowdstrike-cql-data-movement/SKILL.md @@ -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("(?[^\\/|\\\\]*)$", field=ImageFileName, strict=false) +| join( + { + #event_simpleName=ProcessRollup2 ImageFileName=/(chrome|firefox|iexplore)\.exe/i + | MD5:=MD5HashData | ImageFileName=/(\/|\\)(?\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=/(\/|\\)(?\w*\.?\w*)$/ +| FileName=/(winword|excel|powerpnt)\.exe/i +| CommandLine=/Outlook\\(?\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) +``` diff --git a/.github/skills/crowdstrike-cql-host-integrity/SKILL.md b/.github/skills/crowdstrike-cql-host-integrity/SKILL.md new file mode 100644 index 0000000..98a79f1 --- /dev/null +++ b/.github/skills/crowdstrike-cql-host-integrity/SKILL.md @@ -0,0 +1,387 @@ +--- +name: crowdstrike-cql-host-integrity +description: "Use for CrowdStrike CQL investigations of DLL side-loading, vulnerable driver abuse, firewall changes, registry modifications, and locally disabled response tooling." +user-invocable: true +--- + +# Host Integrity and Configuration 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. + +## Dll-Side Loading Detection Query + +Source file: `Dll-Side_Loading_Detection_Query.yml` + +```cql +//Tracing the ProcessId of a Process / File which is writting atleast 1 each EXE and DLL to same Path, Doing the Process Original name masquarading and atleast 1 File Author name is Microsoft in "DLL-Filewrite", tracking throughtout as SusProcessID +defineTable(query={#event_simpleName=/(PeFileWritten)/iF +|lowercase("FileName") +|lowercase("OriginalFilename") +|(FileName="*" and OriginalFilename="*") +| regex("(?^.*)\.dll", field=FileName, strict=false) +| regex("(?^.*)\.exe", field=FileName, strict=false) +| MasquraeCheck:=if(FileName==OriginalFilename, then="Normal", else="Masquarade") |MasquraeCheck!="Normal" +|SusProcessID:=format(format="%s%s", field=[aid,ContextProcessId]) +|rename(field="SHA256HashData", as="SusHash") +|rename(field="FileName", as="FileWritten") +// Exclusions FOr Edge Browser +|OriginalFilename!=microsoftedgeupdate.exe OriginalFilename!=msedgeupdate.dll +|groupBy([SusProcessID,FilePath],function=([collect([DllFileName,EXEFileName,SusHash,FileWritten,OriginalFilename,CompanyName]),count(DllFileName,as=DllC),count(EXEFileName,as=EXEC)]),limit=max) +|DllC>=1 EXEC>=1 CompanyName=/Microsoft/iF +}, include=[FilePath,FileWritten,OriginalFilename,SusHash,DllFileName,EXEFileName,CompanyName,SusProcessID,ComputerName,UserName], name="DLL-Filewrite") + +// Then tracing the Parent File for files written operation in "DLL-Filewrite" getting FileWriteParent, tracked as "DLL-Parent" +|defineTable(query={#event_simpleName=/(ProcessRollup2)/iF +|TargetProcessId:=format(format="%s%s", field=[aid,TargetProcessId]) +|ParentProcessId:=format(format="%s%s", field=[aid,ParentProcessId]) +|match(file="DLL-Filewrite", field=[TargetProcessId],column=[SusProcessID],strict=true,include=[FilePath,FileWritten,OriginalFilename,SusHash,CompanyName,SusProcessID,ComputerName,UserName]) +|rename(field="ParentBaseFileName", as="FileWriteParent") +|case{ +CommandLine=* |regex("\"[^\"]+\"\\s+\"(?P[^\"]*\\\\)?", field=CommandLine)| regex(".*\\\\(?[^\\\\\"]+?)\"?$", field=CommandLine); +* +} +|case{ + FullPath="*" or FileNamey="*" | FileWriteFileSource:=format(format="%s\n\t└-> %s", field=[FileNamey,FullPath]); + FullPath!="*" FileNamey!="*" | FileWriteFileSource:=format(format="%s", field=[FileName]); + * +} +| coalesce([FileNamey,FileName],as=FileWriteFile,ignoreEmpty=false) +}, include=[FileWriteFile,FileWriteFileSource,FileWriteParent,FilePath,FileWritten,SusHash,OriginalFilename,CompanyName,SusProcessID,ComputerName,UserName], name="DLL-Parent") + +// Then Tracing the DLL-side-Loading Process startup for "DLL-Parent", getting DLLSideLoadProcess, tracked as "DLLSideLoadProcess" +|defineTable(query={#event_simpleName=/(ProcessRollup2)/iF |DLLSideLoadProcess:=format(format="%s\n\t└-> %s", field=[ParentBaseFileName,FileName]) +|TargetProcessId:=format(format="%s%s", field=[aid,TargetProcessId]) +|ParentProcessId:=format(format="%s%s", field=[aid,ParentProcessId]) +|match(file="DLL-Parent", field=[ParentProcessId],column=[SusProcessID],strict=true,include=[FileWriteFile,FileWriteFileSource,FileWriteParent,FilePath,FileWritten,SusHash,OriginalFilename,CompanyName,SusProcessID,ComputerName,UserName]) +|rename(field="TargetProcessId", as="ModuleLoadId") +| rename(field="ProcessStartTime", as="ProcessStartTime") +}, include=[FileWriteFile,FileWriteFileSource,ProcessStartTime,DLLSideLoadProcess,FileWriteParent,FilePath,FileWritten,SusHash,OriginalFilename,CompanyName,ModuleLoadId,SusProcessID,ComputerName,UserName], name="DLLSideLoadProcess") + +// Then tracing the DLL/EXE side loaded for DLLSideLoadProcess from "DLLSideLoadProcess", tracked as "DllLoading" +|defineTable(query={#event_simpleName=/(ClassifiedModuleLoad)/iF |rename(field="FileName", as="DllLoad") +|TargetProcessId:=format(format="%s%s", field=[aid,TargetProcessId]) +|ParentProcessId:=format(format="%s%s", field=[aid,ParentProcessId]) +|ContextProcessId:=format(format="%s%s", field=[aid,ContextProcessId]) +| "DllLoaded Files":= format(format="%s\n\t└-> %s", field=[DllLoad,FilePath]) +|match(file="DLLSideLoadProcess", field=[ContextProcessId],column=[ModuleLoadId],strict=true,include=[FileWriteFile,FileWriteFileSource,ProcessStartTime,DLLSideLoadProcess,FileWriteParent,FilePath,FileWritten,SusHash,OriginalFilename,CompanyName,SusProcessID,ComputerName,UserName]) +|rename(field="TargetProcessId", as="ModuleLoadId") + +|case { + ModuleLoadTelemetryClassification = 1 +| ModuleLoadTelemetryClassification := "FIRST_LOAD\n\t\t└->This is the first time this module has been loaded into a process on the host"; + ModuleLoadTelemetryClassification = 2 +| ModuleLoadTelemetryClassification := "RUNDLL32_TARGET\n\t\t└->This module is the target of a rundll32.exe invocation"; + ModuleLoadTelemetryClassification = 4 +| ModuleLoadTelemetryClassification := "DETECT_TREE\n\t\t└->The module was loaded into a process that is in an active detect tree"; + ModuleLoadTelemetryClassification = 8 +| ModuleLoadTelemetryClassification := "MAPPED_FROM_KERNEL_MODE\n\t\t└->The module was loaded into kernel mode address space"; + ModuleLoadTelemetryClassification = 16 +| ModuleLoadTelemetryClassification := "UNUSUAL_EXTENSION\n\t\t└->The module has an unexpected, unusual or rare extension"; + ModuleLoadTelemetryClassification = 32 +| ModuleLoadTelemetryClassification := "MOTW\n\t\t└->The module has the Mark of the Web zone identifier"; + ModuleLoadTelemetryClassification = 64 +| ModuleLoadTelemetryClassification := "SIGN_INFO_CONTINUITY\n\t\t└->The module does not have a valid signature and it was loaded into a process with a primary module that does have a valid signature"; + ModuleLoadTelemetryClassification = 256 +| ModuleLoadTelemetryClassification := "ORIGINAL_FILENAME_MISMATCH\n\t\t└->Module's ImageFileName doesn't match OriginalFileName"; + ModuleLoadTelemetryClassification = 512 +| ModuleLoadTelemetryClassification := "REMOVABLE_MEDIA\n\t\t└->The module was loaded from removable media (ISO/IMG)"; + ModuleLoadTelemetryClassification = 1024 +| ModuleLoadTelemetryClassification := "DATA_EXTENSION\n\t\t└->The module has a data type extension"; + ModuleLoadTelemetryClassification = 257 +| ModuleLoadTelemetryClassification := "FIRST_LOAD_AND_FILENAME_MISMATCH\n\t\t└->This is the first time this module has been loaded into a process on the host and its ImageFileName doesnt match OriginalFileName"; + * +| ModuleLoadTelemetryClassification := format(format="Value=%s\n\t\t└->Multiple module load telemetry flags are set, Check ModuleLoadTelemetryClassification documentation", field=[ModuleLoadTelemetryClassification]) +} + +}, include=[FileWriteFile,FileWriteFileSource,ProcessStartTime,DLLSideLoadProcess,"DllLoaded Files",ModuleLoadTelemetryClassification,FileWriteParent,FilePath,FileWritten,SusHash,OriginalFilename,CompanyName,SusProcessID,ComputerName,UserName], name="DllLoading") + +//Performing the aggregation in the presentable format + to prepare for matchup for MOTW URLS in next table +|defineTable(query={readFile([DllLoading]) +|groupBy([ProcessStartTime,SusProcessID,ComputerName,UserName],function=([collect([FileWriteFile,FileWriteFileSource,FileWriteParent,FilePath,FileWritten,OriginalFilename,CompanyName,DLLSideLoadProcess,"DllLoaded Files",ModuleLoadTelemetryClassification,SusHash]),count("DllLoaded Files",distinct=true,as="DllLoaded Files Count")]),limit=max)},include=[ProcessStartTime,SusProcessID,ComputerName,FileWriteFile,UserName,FileWriteFileSource,FileWriteParent,FilePath,FileWritten,OriginalFilename,CompanyName,DLLSideLoadProcess,"DllLoaded Files",ModuleLoadTelemetryClassification,SusHash,"DllLoaded Files Count"], name="Aggregation") + +//Fetching MOTW URLS +|defineTable(query={#event_simpleName=MotwWritten +|match(file="Aggregation", field=[ComputerName,FileName],column=[ComputerName,FileWriteFile],strict=true,ignoreCase=true, include=[FileWriteFile,FileWriteFileSource,ProcessStartTime,DLLSideLoadProcess,"DllLoaded Files",ModuleLoadTelemetryClassification,FileWriteParent,FilePath,FileWritten,SusHash,OriginalFilename,CompanyName,SusProcessID,ComputerName,UserName,"DllLoaded Files Count"]) +|case{ + HostUrl!="" ReferrerUrl="" |FileWriteFileSourceURL:=format(format="Download URL= %s", field=[HostUrl]); + HostUrl="" ReferrerUrl!="" |FileWriteFileSourceURL:=format(format="Referrer URL= %s", field=[ReferrerUrl]); + HostUrl!="" OR ReferrerUrl!="" |FileWriteFileSourceURL:=format(format="Download URL= %s\nReferrer URL= %s", field=[HostUrl,ReferrerUrl]); + * +} +}, include=[FileWriteFile,FileWriteFileSourceURL,FileWriteFileSource,ProcessStartTime,DLLSideLoadProcess,"DllLoaded Files",ModuleLoadTelemetryClassification,FileWriteParent,FilePath,FileWritten,SusHash,OriginalFilename,CompanyName,SusProcessID,ComputerName,UserName,"DllLoaded Files Count"], name="MOTW") +|readFile(["Aggregation","MOTW"]) +|case{ + FileWriteFileSourceURL!="*" |FileWriteFileSourceURL:=format(format="No URL Found", field=[]); + * +} +|groupBy([ProcessStartTime,SusProcessID,ComputerName,UserName],function=([collect([FileWriteFileSourceURL,FileWriteFileSource,FileWriteParent,FilePath,FileWritten,OriginalFilename,CompanyName,DLLSideLoadProcess,"DllLoaded Files",ModuleLoadTelemetryClassification,SusHash,"DllLoaded Files Count"])])) +| ProcessStartTime:=ProcessStartTime*1000 |ProcessStartTime := formatTime("%e %b %Y %r", field=ProcessStartTime, locale=en_UAE, timezone="Asia/Dubai") +| rename([[FilePath,FileWrittenPath],[CompanyName,"ExeAuthorCompanyName"],[ModuleLoadTelemetryClassification,"DllLoaded Files Signature"]]) +|drop([SusProcessID]) +``` + +## BYOVD Driver Load with EDR/AV Process Termination (Medusa Ransomware) + +Source file: `byovd_driver_load_with_edr_av_process_termination_medusa_ransomware.yml` + +```cql +/* Phase 1 — Detect BYOVD: known-vulnerable or out-of-place signed drivers */ +#event_simpleName = DriverLoad OR #event_simpleName = ClassifiedModuleLoad +| case { + in(field=FileName, values=[ + "gdrv.sys", "msio64.sys", "ntiolib.sys", "kprocesshacker.sys", + "physmem.sys", "dbk64.sys", "procexp152.sys", "NSSM.sys", + "wantd.sys", "AsrDrv104.sys", "mhyprot2.sys" + ]) | BYOVDIndicator := "Known vulnerable driver loaded"; + FilePath = /AppData|Temp|ProgramData|Users\\.*\\Desktop/i + FileName = /\.sys$/i + | BYOVDIndicator := "Driver loaded from suspicious user-writable path"; + * | BYOVDIndicator := "none"; + } +| BYOVDIndicator != "none" +| join( + { + #event_simpleName = TerminateProcess + | ImageFileName = /(MsMpEng|CsAgent|CsFalconService|csshell|SentinelAgent|cbdefense|MBAMService|avp\.exe|fmon|avgnt|bdservicehost|mcshield|ekrn)\.exe$/i + | rename(field=ImageFileName, as=TerminatedSecurity) + }, + field=aid, key=aid + ) +| TerminatedSecurity = * +``` + +## Firewall Rule Additions + +Source file: `Firewall_Rule_Additions.yml` + +```cql +#event_simpleName=ProcessRollup2 +| join({#event_simpleName=FirewallSetRule}, key=ContextProcessId, field=TargetProcessId, include=[FirewallRule, FirewallRuleId]) +| ImageFileName=/.*\\(?.*\..*)/ +| table([aid, UserSid, fileName, FirewallRuleId, FirewallRule, ImageFileName, CommandLine]) +``` + +## Suspicious Registry Modifications + +Source file: `Suspicious_Registry_Modifications.yml` + +```cql +#event_simpleName=RegGenericValue +| RegObjectName=/\\(Run|RunOnce|Winlogon|AppInit_DLLs|Image File Execution Options)/i +| RegValueName!=/^(ctfmon|SecurityHealth|OneDrive)$/i +| join({#event_simpleName=UserIdentity}, field=AuthenticationID, include=[UserName]) +| table([aid, UserName, RegObjectName, RegValueName, RegStringValue, ProcessImageFileName]) +``` + +## Detect locally disabled RTR + +Source file: `detect_locally_disabled_rtr.yml` + +```cql +#event_simpleName=SensorHeartbeat +| groupBy([aid], function=selectLast([@timestamp, ComputerName, SensorStateBitMap]), limit=max) +| bitfield:extractFlags( +field=SensorStateBitMap, + output=[ + [2, RTR_Locally_Disabled] +]) +| RTR_Locally_Disabled="true" +``` + +## 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. + +### EDRCHOKER - QoS Policy Abuse Targeting EDR/AV Processes + +Source YAML: `edrchoker_qos_policy_abuse_targeting_edr_av_processes.yml` + +```cql +// // EDRCHOKER - QoS Policy abuse targeting EDR/AV processes (T1562 – Impair Defenses) +// // https://www.zerosalarium.com/2026/06/edrchoker-choking-telemetry-stream-block-edr.html +// Author: Aamir Muhammad +|case{ +#event_simpleName=/ProcessRollup2|WmiCreateProcess/iF +CommandLine=/New-NetQosPolicy|Set-NetQosPolicy/iF +CommandLine=/ThrottleRateActionBitsPerSecond|AppPathNameMatchCondition/iF +CommandLine=/(SenseIR\.exe|MsSense\.exe|MsMpEng\.exe|WinDefend\.exe|falcon-sensor\.exe|CSFalconService\.exe|SentinelService\.exe|SentinelAgent\.exe|CortexXDR\.exe|cyvera\.exe|pmsu\.exe|cb\.exe|carbonblack\.exe|edragent\.exe|HarfangLab\.exe|elastic-agent\.exe)/iF +| SuspectActivity := format( "QoS Policy Creation via Process Command Line: %s", field=[#event_simpleNam]); +#event_simpleName=/RegGenericValueUpdate|AsepValueUpdate|RegSystemConfigValueUpdate|RegistryHiveFileWritten|reg/iF +RegObjectName=/\\SOFTWARE\\Policies\\Microsoft\\Windows\\QOS/i +RegStringValue=/(SenseIR\.exe|MsSense\.exe|MsMpEng\.exe|WinDefend\.exe|falcon-sensor\.exe|CSFalconService\.exe|SentinelService\.exe|SentinelAgent\.exe|CortexXDR\.exe|cyvera\.exe|pmsu\.exe|cb\.exe|carbonblack\.exe|edragent\.exe|HarfangLab\.exe|elastic-agent\.exe)/i +| SuspectActivity := format("QoS Registry Manipulation targeting: %s", field=[RegStringValue]) +} +|groupBy([@timestamp,ComputerName,FileName,ParentBaseFileName,CommandLine,#event_simpleName]) +``` + +### Hunting EDR Freeze + +Source YAML: `hunting_edr_freeze.yml` + +```cql +// Look for process handles opening Falcon +#event_simpleName=FalconProcessHandleOpDetectInfo FileName="WerFaultSecure.exe" + +// Check for command line switching signal +| GrandparentCommandLine=/\.exe"?\s+\d+\s+\d+$/ OR ParentCommandLine=/\.exe"?\s+\d+\s+\d+$/ OR CommandLine=/\.exe"?\s+\d+\s+\d+$/ + +// Create process lineage tree for easier reading +| ProcessLineage:=format(format="%s (%s)\n └ %s (%s)\n └ %s (%s)", field=[GrandparentImageFileName, GrandparentCommandLine, ParentImageFileName, ParentCommandLine, ImageFileName, CommandLine]) + +// Output deatils to table +| table([@timestamp, aid, ComputerName, ContextProcessId, ProcessLineage]) + +// Create direct link to Process Explorer - Uncomment the rootURL value that matches your cloud +| rootURL := "https://falcon.crowdstrike.com/" /* US-1 */ +//| rootURL := "https://falcon.us-2.crowdstrike.com/" /* US-2 */ +//| rootURL := "https://falcon.laggar.gcw.crowdstrike.com/" /* Gov */ +//| rootURL := "https://falcon.eu-1.crowdstrike.com/" /* EU */ +| format("[Responsible Process](%sgraphs/process-explorer/tree?id=pid:%s:%s)", field=["rootURL", "aid", "ContextProcessId"], as="Process Explorer") + +// Remove unnecessary fields +| drop([rootURL, ContextProcessId]) +``` + +### IOC search | PTC Windchill & FlexPLM vulnerability + +Source YAML: `ioc_search_ptc_windchill_flexplm_vulnerability.yml` + +```cql +case{ + #event_simpleName = /.*FileWritten/i + | FileName = /GW\.class/i or FileName = /Gen\.class/i or FileName = /dpr_.*\.jsp/i; + #event_simpleName = /.*FileWritten/i + | in(field="FileName",values=["Gen.java","GW.java","HTTPRequest.java","HTTPResponse.java","IXBCommonStreamer.java","IXBStreamer.java","MethodFeedback.java","MethodResult.java","WTContextUpdate.java"]); +} +| table(@timestamp,ComputerName,FileName,ContextBaseFileName) +``` + +### Packed Binary Detected + +Source YAML: `packed_binary_detected.yml` + +```cql +| #Vendor = crowdstrike +| #repo = "base_sensor" +| "#event_simpleName" = "PackedExecutableWritten" +| aid = ?aid +//| ComputerName ="XXXX" //Put your hostname here to check it for specfic host. + +| case { + wildcard(field=FilePath, pattern="*\\Temp\\*") | location_risk := "High - Temp Directory" ; + wildcard(field=FilePath, pattern="*\\AppData\\*") | location_risk := "High - AppData" ; + wildcard(field=FilePath, pattern="*\\Windows\\*") | location_risk := "Critical - Windows Directory" ; + wildcard(field=FilePath, pattern="*\\System32\\*") | location_risk := "Critical - System32" ; + wildcard(field=FilePath, pattern="*\\Startup\\*") | location_risk := "Critical - Startup Folder" ; + wildcard(field=FilePath, pattern="*\\Downloads\\*") | location_risk := "Medium - Downloads" ; + * | location_risk := "Low - Standard Path" + } + +| groupBy([ComputerName], function=[collect(FileName),collect(FilePath),collect(TargetFileName),collect(SHA256HashData),collect(location_risk),count(as=total_packed_writes)]) +| sort(total_packed_writes, order=desc) +``` + +### Ransomware Precursors + +Source YAML: `ransomware_precursors.yml` + +```cql +#event_simpleName=ProcessRollup2 event_platform=Win +// Optional scoping for testing on a single host +| ComputerName=?ComputerName +// Normalise the command line once for all subsequent matching +| CmdLower := lower("CommandLine") +// --- Recovery-inhibition classification --------------------------------- +| case { + // Shadow copy deletion via vssadmin + ImageFileName=/\\vssadmin\.exe$/i + AND CmdLower=/delete\s+shadows/ + | Hypothesis := "H1_VSSADMIN_SHADOW_DELETE" | Confidence := "High"; + + // Shadow storage resize to force silent shadow deletion (401 KB trick) + ImageFileName=/\\vssadmin\.exe$/i + AND CmdLower=/resize\s+shadowstorage/ + | Hypothesis := "H2_VSSADMIN_SHADOWSTORAGE_RESIZE" | Confidence := "Medium"; + + // Shadow copy deletion via WMIC + ImageFileName=/\\wmic\.exe$/i + AND CmdLower=/shadowcopy/ AND CmdLower=/delete/ + | Hypothesis := "H3_WMIC_SHADOW_DELETE" | Confidence := "High"; + + // Shadow copy deletion via PowerShell WMI/CIM + ImageFileName=/\\(powershell|powershell_ise|pwsh)\.exe$/i + AND CmdLower=/win32_shadowcopy|get-wmiobject.{0,40}shadowcopy|get-ciminstance.{0,40}shadowcopy/ + AND CmdLower=/delete|remove/ + | Hypothesis := "H4_POWERSHELL_SHADOW_DELETE" | Confidence := "High"; + + // Backup catalog / system state backup destruction + ImageFileName=/\\wbadmin\.exe$/i + AND CmdLower=/delete\s+(catalog|systemstatebackup|backup)/ + | Hypothesis := "H5_WBADMIN_BACKUP_DELETE" | Confidence := "High"; + + // Disable Windows Recovery Environment / automatic repair + ImageFileName=/\\bcdedit\.exe$/i + AND CmdLower=/recoveryenabled\s+(no|off)|bootstatuspolicy\s+ignoreallfailures/ + | Hypothesis := "H6_BCDEDIT_RECOVERY_TAMPER" | Confidence := "High"; + + // USN change journal deletion (anti-forensics, common in ransomware playbooks) + ImageFileName=/\\fsutil\.exe$/i + AND CmdLower=/usn\s+deletejournal/ + | Hypothesis := "H7_FSUTIL_USN_DELETE" | Confidence := "Medium"; + + * | Hypothesis := "NO_MATCH"; +} +| Hypothesis != "NO_MATCH" +// --- Output -------------------------------------------------------------- +| groupBy([aid, ComputerName], function=[ + count(as=PrecursorEvents), + count(Hypothesis, distinct=true, as=DistinctTechniques), + min(@timestamp, as=FirstSeen), + max(@timestamp, as=LastSeen), + collect([Hypothesis, Confidence, UserName, ImageFileName, CommandLine, ParentBaseFileName]) + ], limit=10000) +// Multiple distinct recovery-inhibition techniques on one host is near-certain ransomware staging +| case { + DistinctTechniques >= 2 | Priority := "CRITICAL - multiple recovery-inhibition techniques"; + PrecursorEvents >= 3 | Priority := "HIGH - repeated recovery-inhibition activity"; + * | Priority := "MEDIUM - single event, validate context"; +} +| FirstSeen := formatTime("%F %T %Z", field=FirstSeen) +| LastSeen := formatTime("%F %T %Z", field=LastSeen) +| sort(DistinctTechniques, order=desc) +``` + +### Recent RTR Sessions + +Source YAML: `recent_rtr_sessions.yml` + +```cql +// Get RTR Start events +#repo=detections #event_simpleName=Event_RemoteResponseSessionStartEvent + +// Rename Agent ID value +| rename(field="AgentIdString", as="aid") + +// Display results in table +| table([StartTimestamp, UserName, aid], limit=20000) + +// Bring in data from AID Master lookup file +| aid=~match(file="aid_master_main.csv", column=[aid], strict=false) + +// Convert timestamp to human-readable value +| formatTime(format="%F %T %Z", as=StartTimestamp, field=StartTimestamp) +``` + +### Suspicious DLL / Module loads + +Source YAML: `suspicious_dll_module_loads.yml` + +```cql +| #Vendor = crowdstrike +| #repo = "base_sensor" +| "#event_simpleName" = "ModuleLoadV3DetectInfo" +| aid=?aid +//| ComputerName="XXXXX"//Enter computer name to check for specific endpoint +| groupBy([ComputerName,aid], function=[collect(FileName),collect(FilePath),collect(ImageFileName),collect(ParentCommandLine),count(as=total_module_loads)]) +| sort(total_module_loads, order=desc) +``` diff --git a/.github/skills/crowdstrike-cql-identity/SKILL.md b/.github/skills/crowdstrike-cql-identity/SKILL.md new file mode 100644 index 0000000..550b774 --- /dev/null +++ b/.github/skills/crowdstrike-cql-identity/SKILL.md @@ -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) +``` diff --git a/.github/skills/crowdstrike-cql-inventory/SKILL.md b/.github/skills/crowdstrike-cql-inventory/SKILL.md new file mode 100644 index 0000000..1d43470 --- /dev/null +++ b/.github/skills/crowdstrike-cql-inventory/SKILL.md @@ -0,0 +1,252 @@ +--- +name: crowdstrike-cql-inventory +description: "Use for CrowdStrike CQL endpoint inventory, operating-system prevalence, browser extensions, driver, boot-time, and software clustering queries." +user-invocable: true +--- + +# Endpoint Inventory and Prevalence + +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. + +## Evaluate Operating System Prevalence + +Source file: `Evaluate_Operating_System_Prevalence.yml` + +```cql +#event_simpleName=OsVersionInfo event_platform=Win +| groupby(aid, function=selectLast([ProductName])) +| groupBy([ProductName], function=stats([count(aid, as="endpointCount")])) +``` + +## Enumerate Windows Driver Loads + +Source file: `Enumerate_Windows_Driver_Loads.yml` + +```cql +// Get all DriverLoad events and Event_ModuleSummaryInfoEvent events so certificate data can be merged in +(#event_simpleName=DriverLoad event_platform=Win) OR (#repo=detections ExternalApiType=Event_ModuleSummaryInfoEvent ) +// Shorten file path from DriverLoad event +| case{ + #event_simpleName=DriverLoad | FilePath=/Device\\HarddiskVolume\d+(?.+$)/; + *; +} +// Create selfJoinFilter +| selfJoinFilter(field=[SHA256HashData], where=[{#event_simpleName=DriverLoad}, {#repo=detections ExternalApiType=Event_ModuleSummaryInfoEvent}]) +// Aggregate +| groupBy([SHA256HashData], function=([collect([ShortFileParth, FileName, OriginalFilename, SubjectCN, IssuerCN])]), limit=max) +| FileName=* +// Set default values +| default(value="-", field=[SubjectCN, IssuerCN, OriginalFilename]) +``` + +## Frequency Analysis via Program Clustering + +Source file: `Frequency_Analysis_via_Program_Clustering.yml` + +```cql +// Get file names of interest +event_platform=Win #event_simpleName=ProcessRollup2 FileName=/(whoami|arp|cmd|net|net1|ipconfig|route|netstat|nslookup|nltest|systeminfo|wmic|tasklist|tracert|ping|adfind|nbtstat|find|ldifde|netsh|wbadmin)\.exe/i + +// Aggregate in 10 minute buckets; set search to 24 hours +| bucket(span=10min, field=[cid, aid, ComputerName,ParentBaseFileName,ParentProcessId], function=[count(FileName, distinct=true, as=fNameCount), collect([FileName, CommandLine])], limit=500) + +// Set threshold at three distinct file name values +| test(fNameCount>=3) +``` + +## Inventory of Installed Browser Extensions Across Endpoints + +Source file: `Installed_Browser_Extensions_Across_Endpoints.yml` + +```cql +#event_simpleName=InstalledBrowserExtension BrowserExtensionId!="no-extension-available" +| groupBy([event_platform, BrowserName, BrowserExtensionId, BrowserExtensionName], function=([count(aid, distinct=true, as=TotalEndpoints)])) +| format("[See Extension](https://chromewebstore.google.com/detail/%s)", field=[BrowserExtensionId], as="Chrome Store Link") +| sort(order=desc, TotalEndpoints, limit=1000) +| case{ + BrowserName="3" | BrowserName:="Chrome"; + BrowserName="4" | BrowserName:="Edge"; + *; +} +``` + +## Malicious Chrome Extension FreeVPN-One Detection + +Source file: `Malicious_Chrome_Extension_FreeVPN-One_Detection.yml` + +```cql +defineTable(query={#event_simpleName=InstalledBrowserExtension +|case{ + BrowserExtensionId=/jcbiifklmgnkppebelchllpdbnibihel/iF; + BrowserExtensionName=/FreeVPN/iF +} +| case{ + "BrowserExtensionStatusEnabled"="0" | BrowserExtensionStatusEnabled:="Disabled"; + "BrowserExtensionStatusEnabled"="1" | BrowserExtensionStatusEnabled:="Enabled"; + *; +} +| BrowserExtensionInstalledTimestamp := BrowserExtensionInstalledTimestamp * 1000 +| "Extension Installation date" := formatTime("%d-%m-%Y %H:%M:%S.%L", field=BrowserExtensionInstalledTimestamp, locale=en_UAE, timezone="Asia/Dubai") +| "Extension(s)":=format(format="Status=%s, Installation Date=%s", field=[BrowserExtensionStatusEnabled,"Extension Installation date"]) +| groupBy([event_platform, aid, UserName, BrowserProfileId, BrowserName,BrowserExtensionName], function=([collect([ComputerName,"Extension(s)",BrowserExtensionPath,BrowserExtensionRequestedPermissions])])) +| drop([_count,aid]) +| case{ + BrowserName ="0" | BrowserName := "UNKNOWN" ; + BrowserName="1" | BrowserName:="Firefox"; + BrowserName="2" | BrowserName:="Safari"; + BrowserName="3" | BrowserName:="Chrome"; + BrowserName="4" | BrowserName:="Edge"; + BrowserName="5" | BrowserName:="EDGE CHROMIUM"; + BrowserName="6" | BrowserName:="Internet Explorer"; + BrowserName="7" | BrowserName:="Edge Legacy"; + BrowserName="8" | BrowserName:="IE_TYPED_URL"; + BrowserName="9" | BrowserName:="FIREFOX_APP"; + *; +}}, include=[*], name="Extension") +|defineTable(query={#event_simpleName=DnsRequest | in(field="DomainName", values=["aitd.one","extrahefty.com","scan.aitd.one","freevpn.one"],ignoreCase=true)}, include=[*], name="ExtensionTraffic") +|readFile(["Extension","ExtensionTraffic"]) +|groupBy([ComputerName,DomainName], function=([collect([UserName, BrowserProfileId, BrowserName,BrowserExtensionName,"Extension(s)",BrowserExtensionPath,BrowserExtensionRequestedPermissions])])) +``` + +## Calculate Last Windows Boot Time + +Source file: `calculate_last_windows_boot_time.yml` + +```cql +#event_simpleName=AgentOnline event_platform=Win +| groupBy([aid], function=([selectLast([BaseTime])])) +| LastReboot_milli:=(BaseTime/1000*1024)+978307200 +| round("LastReboot_milli") +| LastRebootAgo:=now()-(LastReboot_milli*1000) +| formatDuration("LastRebootAgo", precision=2) +| LastReboot:=formatTime(format="%F %T %Z", field="LastReboot_milli") +``` + +## Check Domain Controller for NSX Driver + +Source file: `check_domain_controller_for_nsx_driver.yml` + +```cql +event_platform=/Win/i #event_simpleName=/DriverLoad/i +| in(field=FileName,values=["vnetwfp.sys", "vnetflt.sys"],ignoreCase=true) +| join({$falcon/investigate:aid_master()}, field=aid, key=aid, include=[ProductType]) +| ProductType=2 +| "Domain Controller":=ComputerName +| LocalIP:=LocalAddressIP4 +| Drivers:=FileName +| groupBy([aid,"Domain Controller",LocalIP,Drivers],function=[]) +``` + +## Chromium-Based Browser Hunting via DLL Load + +Source file: `chromium_based_browser_hunting_via_dll_load.yml` + +```cql +defineTable(query={#event_simpleName=ClassifiedModuleLoad +| ImageFileName=/chrome\.dll/i +| TargetImageFileName!=/chrome\.exe/i}, include=[ComputerName, TargetProcessId], name="DllLoads") +| #event_simpleName=ProcessRollup2 TargetProcessId=* +| match(table="DllLoads", field=[TargetProcessId]) +| table([@timestamp, aid, ComputerName, FileName, TargetProcessId, ImageFileName, TargetImageFileName]) +``` + +## 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. + +### Get USB Devices + +Source YAML: `get_usb_devices.yml` + +```cql +#event_simpleName=DcUsbDeviceConnected +| DeviceTimeStamp :=parseTimeStamp(field=DeviceTimeStamp,format=seconds) +| "Time Inserted" := formatTime("%Y-%m-%dT%H:%M:%S.%L", field=DeviceTimeStamp,timezone="Zulu") +| rename([[ComputerName,"Host Name"],[DevicePropertyClassName,"Connection Type"],[DeviceManufacturer,Manufacturer],[DeviceProduct,"Product Name"], [DevicePropertyDeviceDescription,Description], [DevicePropertyClassGuid,GUID],[DeviceInstanceId,"Device ID"]]) +| groupBy([aid, "Device ID"], function=([collect(["TimeInserted", ComputerName, "Connection Type",Manufacturer, "Product Name", Description, GUID])])) +``` + +### Installed Browser Extensions (Aggregate by Extension) + +Source YAML: `installed_browser_extensions__aggregate_by_extension_.yml` + +```cql +// Get browser extension event +#event_simpleName=InstalledBrowserExtension BrowserExtensionId!="no-extension-available" + +// Aggregate by event_platform, BrowserName, ExtensionID and ExtensionName +| groupBy([event_platform, BrowserName, BrowserExtensionId, BrowserExtensionName], function=([count(aid, distinct=true, as=TotalEndpoints)])) + +// Check to see if the extension is installed on fewer than 50 systems +| test(TotalEndpoints<50) + +// Create a link to the Chrome Extension Store +| format("[See Extension](https://chromewebstore.google.com/detail/%s)", field=[BrowserExtensionId], as="Chrome Store Link") + +// Sort in descending order +| sort(order=desc, TotalEndpoints, limit=1000) + +// Convert the browser name from decimal to human-readable +| case{ + BrowserName="3" | BrowserName:="Chrome"; + BrowserName="4" | BrowserName:="Edge"; + *; +} +``` + +### Installed Browser Extensions (Hunt Extension Name) + +Source YAML: `installed_browser_extensions__hunt_extension_name_.yml` + +```cql +// Get browser extension event +#event_simpleName=InstalledBrowserExtension BrowserExtensionId!="no-extension-available" + +// Look for string "vpn" in extension name +| BrowserExtensionName=/vpn/i + +// Make a new field that includes the extension ID and Name +| Extension:=format(format="%s (%s)", field=[BrowserExtensionId, BrowserExtensionName]) + +// Aggregate by endpoint and browser profile +| groupBy([event_platform, aid, ComputerName, UserName, BrowserProfileId, BrowserName], function=([collect([Extension])])) + +// Get unnecessary field +| drop([_count]) + +// Convert browser name from decimal to human readable +| case{ + BrowserName="3" | BrowserName:="Chrome"; + BrowserName="4" | BrowserName:="Edge"; + *; +} +``` + +### Packages in Container Images - Match Parameter + +Source YAML: `packages_in_container_images___match_parameter.yml` + +```cql +ImageScanEventType = ImageVulnerabilityEvent +| array:eval("CVEMapping[]", asArray="PackageName[]", function={PackageName := splitString(by="\|",field="CVEMapping",index=1)}) +| array:drop("CVEMapping[]") +| array:contains(array="PackageName[]", value=?Package) +| groupBy([ImageInfo.Registry,ImageInfo.Repository]) +``` + +### Windows Store Installs + +Source YAML: `windows_store_installs.yml` + +```cql +| regex("WindowsApps\\\\(?[^\\\\]+)\\\\", field=FilePath, strict=true) +| regex("^(?[^_]+)", field=PackageName, strict=false) +| ComputerName=~wildcard(?ComputerName, ignoreCase=true) +| PackageBase=~wildcard(?PackageBase, ignoreCase=true) +// Filter out good filepaths +//| !in(field=FilePath, values=[]) +// Filter out good Packages +//| !in(field=PackageBase, values=[]) +| groupBy([ComputerName, PackageBase]) +| sort(ComputerName, order=asc, limit=max) +``` diff --git a/.github/skills/crowdstrike-cql-network/SKILL.md b/.github/skills/crowdstrike-cql-network/SKILL.md new file mode 100644 index 0000000..49d6201 --- /dev/null +++ b/.github/skills/crowdstrike-cql-network/SKILL.md @@ -0,0 +1,362 @@ +--- +name: crowdstrike-cql-network +description: "Use for CrowdStrike CQL hunts covering endpoint network connections, DNS, public IPs, RDP exposure, lateral movement, C2 beaconing, Tor, or remote-management DNS." +user-invocable: true +--- + +# Network and Lateral-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. + +## External Connectons with Process + +Source file: `External_Connectons_with_Process.yml` + +```cql +#event_simpleName=NetworkConnectIP4 aid=?aid ComputerName=?Computername RemoteAddressIP4=?RemoteIP +| !cidr(RemoteAddressIP4, subnet=["10.0.0.0/8","192.168.0.0/16","172.16.0.0/12","127.0.0.0/8"]) +| join({#event_simpleName=ProcessRollup2 FileName=?Processname }, field=[ContextProcessId],key=TargetProcessId, include=[FileName, UserName,ImageFileName, RemoteAddressIP4, RemotePort,CommandLine], mode=left) +| groupBy(UserName, function=collect([FileName, UserName, ImageFileName, RemoteIP, RPort, CommandLine])) +| sort([_count], order=asc) +``` + +## Detection of External Direct IP Usage in CommandLine Windows and Mac + +Source file: `Detection_of_External_Direct_IP_Usage_in_CommandLine_Windows_and_Mac.yml` + +```cql +in(#event_simpleName, values=["ProcessRollup2","SyntheticProcessRollup2"]) +| CommandLine=*http* event_platform!="Lin" +// Basline to exclude legitimate process +//| !in(field="ParentBaseFileName", values=//["UmbrellaDiagnostic.exe","HPClickExe","Eagle" ,"HPClick.exe"]) +//| !in(field="FileName", values=["Google Chrome","chrome.exe"]) +//| !in(field="CommandLine", values=["Google Chrome.app"]) +| regex("(?\\bhttps?://\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}.*\\/\\b)", field=CommandLine) +| regex("(?\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})", field=Urlink) +| !cidr(Ipaddress, subnet=["224.0.0.0/4", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "127.0.0.0/8", "169.254.0.0/16", "168.63.0.0/16", "0.0.0.0/8"]) +// Basline to exclude legitimate url | !in(field="Urlink", values=[ +// Basline to exclude legitimate url "http://100.1.1.1" +// Basline to exclude legitimate url ]) +| default(field=GrandParentBaseFileName, value="Unknown") +| rootURL := "https://falcon.crowdstrike.com/" +| ProcessStartTime := round(ProcessStartTime) +| processStart:=formattime(field=ProcessStartTime, format="%m/%d %H:%M:%S") +// If Context Process ID is available utilize it, if not utilize Target Process ID +| case{ ContextProcessId ="*" +| ContextId:=ContextProcessId; TargetProcessId="*" +| ContextId:=TargetProcessId} +// Create URLs for Process and Graph Explorers +| format("[ProcessExplorer]%sinvestigate/process-explorer/%s/%s?_cid=%s", field=["rootURL", "aid", "ContextId", "cid"], as="ProcessExplorer") +| format("[GraphExplorer]%sgraphs/process-explorer/graph?id=pid:%s:%s", field=["rootURL", "aid", "TargetProcessId"], as="GraphExplorer") +// Format Execution Details for easy analysis +| format(format="%s\n\t↳ %s[ppid=%s]\n\t\t↳ %s [pid=%s|raw_pid=%s|start=%s]\n\t\t\t%,.100s[...TRIMMED]\n\t\t\t%s\n\t\t\t%s\n---", field=[GrandParentBaseFileName, ParentBaseFileName, ParentProcessId, ImageFileName, TargetProcessId, RawProcessId, processStart, CommandLine, ProcessExplorer, GraphExplorer], as="ExecutionSummary") +// Group by Source Host +| groupBy([ComputerName],function=([count(aid, as=executeCount), min(@timestamp, as=firstSeen), max(@timestamp, as=lastSeen), collect([UserName,ExecutionSummary,Ipaddress,ParentBaseFileName,ParentProcessId,ImageFileName,TargetProcessId], limit=1000)])) +| firstSeen:=formattime(field=firstSeen, format="%Y/%m/%d %H:%M:%S") +| lastSeen:=formattime(field=lastSeen, format="%Y/%m/%d %H:%M:%S") +``` + +## DNS Resolutions from Browser Processes + +Source file: `DNS_Resolutions_from_Browser_Processes.yml` + +```cql +// Get all process execution and DNS events on Windows +(#event_simpleName=ProcessRollup2 OR #event_simpleName=DnsRequest) event_platform=Win +| ComputerName=~wildcard(?ComputerName, ignoreCase=true) +// Normalize file name value across both events +| fileName:=concat([FileName, ContextBaseFileName]) +// Make sure responsible process is a web browser +| in(field="fileName", values=[chrome.exe, firefox.exe, msedge.exe], ignoreCase=true) +// Normalize Falcon UPID +| falconPID:=TargetProcessId | falconPID:=ContextProcessId +// Use selfJoinFilter to make sure execution and DNS resolution occured under the same UPID value +| selfJoinFilter(field=[aid, falconPID], where=[{#event_simpleName=ProcessRollup2}, {#event_simpleName=DnsRequest}]) +// Aggregate results +| groupBy([aid, falconPID], function=([collect([ComputerName, UserName, fileName, DomainName])])) +``` + +## Internet-Exposed RDP - Inbound Accepts from Public IPs + +Source file: `Internet_Exposed_RDP_Inbound_Accepts_from_Public_IPs.yml` + +```cql +#event_simpleName=NetworkReceiveAcceptIP4 event_platform=Win +| LocalPort=3389 +// Drop private, loopback, link-local and CGNAT source ranges +| !cidr(RemoteAddressIP4, subnet=["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "127.0.0.0/8", "169.254.0.0/16", "100.64.0.0/10"]) +| groupBy([ComputerName, aip], function=[count(as=TotalAccepts), count(RemoteAddressIP4, distinct=true, as=UniqueRemoteIPs), collect([RemoteAddressIP4], limit=20), max(@timestamp, as=LastSeen)]) +| formatTime("%Y-%m-%d %H:%M:%S", field=LastSeen, as=LastSeen) +| sort(UniqueRemoteIPs, order=desc, limit=200) +``` + +## Lateral Movement Detection + +Source file: `Lateral_Movement_Detection.yml` + +```cql +#event_simpleName=NetworkConnect +| (RemotePort=445 OR RemotePort=3389 OR RemotePort=5985) +| !cidr(RemoteAddressIP4, subnet=["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"]) +| join({#event_simpleName=ProcessRollup2}, field=[aid, RawProcessId], include=[ImageFileName, CommandLine]) +| join({#event_simpleName=UserIdentity}, field=AuthenticationID, include=[UserName]) +| table([aid, UserName, ImageFileName, RemoteAddressIP4, RemotePort, CommandLine]) +``` + +## C2 Beaconing Detection + +Source file: `c2_beaconing_detection.yml` + +```cql +#event_simpleName=NetworkConnectIP4 + +// Keep only egress to routable / external destinations +| !cidr(RemoteAddressIP4, subnet=[ + "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", + "127.0.0.0/8", "169.254.0.0/16", "224.0.0.0/4", + "0.0.0.0/8", "100.64.0.0/10" + ]) + +// Drop high-volume benign services that create artificial regularity +| RemotePort != 53 +| RemotePort != 123 +| RemotePort != 137 +| RemotePort != 138 + +// ===== STAGE 1: detect beacons per destination IP ===== +// Channel = host -> (remote IP + port). Keeping the IP here means two distinct +// beacons to the same provider are never merged before their cadence is measured. +| ConnKey := format(format="%s:%s", field=[RemoteAddressIP4, RemotePort]) +| ts := @timestamp +| sort(field=[aid, ConnKey, ts], order=[asc, asc, asc], limit=max) +| neighbor(include=[ts, aid, ConnKey], prefix=prev, direction=preceding) +| test(aid == prev.aid) +| test(ConnKey == prev.ConnKey) +| Delta := (ts - prev.ts) / 1000 +| Delta >= 1 +| groupBy([aid, ComputerName, RemoteAddressIP4, RemotePort], function=[ + count(as=Beacons), + avg(Delta, as=AvgInterval), + stdDev(field=Delta, as=JitterStdDev) + ], limit=max) +| CoV := JitterStdDev / AvgInterval + +// Beaconing profile (applied per IP so each real channel is judged on its own) +| Beacons >= 8 +| AvgInterval >= 10 +| AvgInterval <= 86400 +| CoV < 0.10 + +// ===== STAGE 2: de-duplicate anycast edges by (org + cadence) ===== +| asn(RemoteAddressIP4) +| Org := coalesce([RemoteAddressIP4.org, RemoteAddressIP4]) + +// --- OPTIONAL ALLOWLIST ------------------------------------------------ +// Populate with orgs already attributed to benign scheduled software. +// Do NOT blanket-trust Fastly / Cloudflare / Google - they are common C2 +// fronting providers; allowlist only AFTER confirming the process. +// | !in(field=Org, values=["EXAMPLE VENDOR ORG", "ANOTHER TRUSTED ORG"]) +// ----------------------------------------------------------------------- + +// Bucket the interval to the nearest minute so identical-cadence siblings merge, +// but channels with genuinely different intervals remain distinct rows. +| CadenceBucket := AvgInterval / 60 +| CadenceBucket := round(CadenceBucket) +| groupBy([aid, ComputerName, Org, RemotePort, CadenceBucket], function=[ + count(as=EdgeIPs), + collect([RemoteAddressIP4], limit=25), + avg(Beacons, as=Beacons), + avg(AvgInterval, as=AvgInterval), + avg(JitterStdDev, as=JitterStdDev), + avg(CoV, as=CoV) + ], limit=max) + +| Beacons := round(Beacons) +| AvgInterval := round(AvgInterval) +| JitterStdDev := round(JitterStdDev) +| sort(field=CoV, order=asc, limit=20000) +| format(format="%.4f", field=CoV, as=CoV) +| table([ComputerName, aid, Org, RemotePort, EdgeIPs, RemoteAddressIP4, Beacons, AvgInterval, JitterStdDev, CoV], limit=20000) +``` + +## Connections to Tor Exit Nodes + +Source file: `connections_to_tor_exit_nodes.yml` + +```cql +#event_simpleName=NetworkConnectIP4 +| match(file="tor-exit-nodes.csv", field=RemoteAddressIP4, column=ip, strict=true) +| groupBy( + [aid, ComputerName], + function=[ + count(aid, as=ConnectionCount), + count(aid, distinct=true, as=UniqueIPs), + collect([RemoteAddressIP4, RemotePort]), + min(@timestamp, as=FirstSeen), + max(@timestamp, as=LastSeen) + ] + ) +| FirstSeen := formatTime(format="%Y-%m-%d %H:%M:%S", field=FirstSeen) +| LastSeen := formatTime(format="%Y-%m-%d %H:%M:%S", field=LastSeen) +| sort(ConnectionCount, order=desc) +``` + +## Detect Remote Monitoring and Management (RMM) Tools over DNS + +Source file: `detect_rmm_dns.yml` + +```cql +#event_simpleName=DnsRequest +| DomainName=/anydesk\.com|action1\.com|beamyourscreen\.com|snapview\.de|rustdesk\.com|fleetdeck\.io|tailscale\.com|dwservice\.net|secure\.logmein\.com|teamviewer\.com|screenconnect\.com|fixme\.it|n-able\.com|domotz\.com|datto\.com|level\.io|itarian\.com|pulseway\.com|zoho\.com|manageengine\.com|bomgarcloud\.com|bomgar\.com|zabbix\.com/i +| groupBy([DomainName],function=[collect(ContextBaseFileName), count(aid,distinct=true,as=HostCount)]) +| sort(HostCount,order=asc) +``` + +## 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. + +### DNS Staging Detection: ClickFix-Inspired nslookup Execution + +Source YAML: `dns_staging_detection_clickfix_inspired_nslookup_execution.yml` + +```cql +// Start with process execution events for performance +#event_simpleName = ProcessRollup2 +// Filter for nslookup.exe +| ImageFileName = /\\nslookup\.exe$/i +// Looks for nslookup using specific record types (like TXT or ALL) and a piped parsing/execution chain associated with payload staging +| CommandLine = /(nslookup|n\^s\^l\^o\^o\^k\^u\^p).*(((-q|querytype|type)=?\s*)?(txt|all)).*\|.*findstr.*\|.*for \/f.*\|.*cmd/i +// Exclude common administrative noise if necessary +| ParentBaseFileName != /services\.exe|monitoring_agent\.exe/i +// Summarize the activity +| groupBy([ComputerName, UserName, ParentBaseFileName, CommandLine], limit=max) +``` + +### Overnight Post-RDP Activity Detection + +Source YAML: `overnight-post-rdp-activity.yml` + +```cql +#event_simpleName=ProcessRollup2 event_platform=Win +| ImageFileName=/\\(cmd|powershell|pwsh|wscript|cscript|mshta|rundll32|regsvr32|wmic|msbuild|installutil|regasm|regsvcs|certutil|bitsadmin|schtasks|sc|net|net1|nltest|whoami|quser|query|systeminfo|hostname|tasklist|netstat|ipconfig|curl|wget|rclone|scp|sftp|ftp|makecab|tar|7z|7za|rar|winrar|psexec|paexec|winrs|ssh|python|pythonw)\.exe$/i +// Parent/GrandParent: DENYLIST (fails open) — blank or unknown lineage still passes; only listed off-hours noise is dropped. +// To tune: add a process BASE name (no path, no .exe), case-insensitive, pipe-separated, inside the ( ) on BOTH lines as you confirm benign off-hours jobs. +// Example (parent): | ParentBaseFileName!=/^(AteraAgent|Syncro|Pulseway)\.exe$/i +// Example (grandparent): | GrandParentBaseFileName!=/^(MeshAgent|NinjaRMMAgent|CagService)\.exe$/i +| ParentBaseFileName!=/^()\.exe$/i +| GrandParentBaseFileName!=/^()\.exe$/i +| case { +CommandLine=/\b(whoami|quser|query\s+user|net1?(\.exe)?"?\s+(users?|group|localgroup|session)|nltest|ipconfig\s+\/all|systeminfo|hostname|tasklist|wmic|netstat)\b/i +| SignalType := "Enumeration"; +CommandLine=/\b(Get-Process|Get-NetTCPConnection|Get-SmbShare|Get-ADUser|Get-ADComputer|Get-DomainUser|Get-DomainComputer)\b/i +| SignalType := "PowerShell Enumeration"; +CommandLine=/\b(rclone|curl|wget|scp|sftp|ftp|bitsadmin|certutil|makecab|tar|7z|7za|rar|winrar)\b/i +| SignalType := "Transfer or Archive Utility"; +CommandLine=/\b(Invoke-WebRequest|Invoke-RestMethod|Start-BitsTransfer|Compress-Archive|System\.IO\.Compression)\b/i +| SignalType := "PowerShell Transfer or Archive"; +ImageFileName=/\\(cmd|powershell|pwsh)\.exe$/i AND CommandLine=/\s-(?:enc|encodedcommand)(?:\s|$|:)/i +| SignalType := "Windows Command Processor or PowerShell"; +* +| SignalType := "Other" +} +| SignalType!="Other" +| CommandTimestampMs := ProcessStartTime * 1000 +| join( +{ +#event_simpleName=UserLogon event_platform=Win LogonType=10 +| remoteHour := formatTime("%H", field=@timestamp, locale=en_US, timezone="America/Vancouver") // adjust timezone to where your clients/company operate (e.g. America/New_York for Eastern) +| in(field=remoteHour, values=["21","22","23","00","01","02","03"]) // adjust hours here as well if needed: Example: "02" will detect up to 02:59:99 +| LogonTimestampMs := LogonTime * 1000 +}, +field=[aid, AuthenticationId], +include=[LogonTimestampMs, UserPrincipal, RemoteAddressIP4, LogonType] +) +| TimeFromLogonMinutes := (CommandTimestampMs - LogonTimestampMs) / 60000 +| TimeFromLogonMinutes >= 0 +| TimeFromLogonMinutes <= 30 // adjust for a longer capture window from logon to command execution +| table([@timestamp, cid, LogonTimestampMs, CommandTimestampMs, TimeFromLogonMinutes, aid, ComputerName, UserName, UserPrincipal, LogonType, RemoteAddressIP4, SignalType, ImageFileName, FileName, FilePath, CommandLine, ParentBaseFileName, GrandParentBaseFileName, OriginalFilename, SHA256HashData], limit=1000) +| sort(@timestamp, order=desc) +``` + +### Process Execution directly from SMB share or SMB-mapped path + +Source YAML: `process_execution_directly_from_smb_share_or_smb_mapped_path.yml` + +```cql +| #Vendor = crowdstrike +| #repo = "base_sensor" +| "#event_simpleName"="ProcessExecOnSMBFile" +| table([@timestamp,UserName,ComputerName,ClientComputerName,LocalAddressIP4,RemoteAddressIP4]) +``` + +### Systems Initiating Connections to a High Number of Ports + +Source YAML: `systems_initiating_connections_to_a_high_number_of_ports.yml` + +```cql +#event_simpleName=/^(NetworkConnectIP4|ProcessRollup2)$/ +| falconPID:=TargetProcessId | falconPID:=ContextProcessId +| UserID:=UserSid | UserID:=UID +| selfJoinFilter(field=[aid, falconPID], where=[{#event_simpleName=NetworkConnectIP4}, {#event_simpleName=ProcessRollup2}]) +| groupBy([aid, ComputerName, falconPID], function=([ + collect([FileName, CommandLine, UserName, UserID]), + count(RemotePort, as=uniquePortCount), + collect([RemotePort], separator=", ", limit=25), + count(RemoteAddressIP4, distinct=true, as=remoteIPcount) + ]), limit=max) +| FileName=* RemotePort=* +| test(uniquePortCount>25) +``` + +### Unauthorized RMM Tool Usage + +Source YAML: `unauthorized_rmm_tool_usage.yml` + +```cql +#event_simpleName=ProcessRollup2 OR #event_simpleName=SyntheticProcessRollup2 +| ImageFileName=/(\\|\/)(?[^\\\/]+)$/ +| case { + FileName=/^anydesk(_custom)?(\.exe)?$/i | RMMTool:="AnyDesk"; + FileName=/^(teamviewer(_service|_desktop)?|tv_w32|tv_x64)(\.exe)?$/i | RMMTool:="TeamViewer"; + FileName=/^(screenconnect|connectwise)[\w.]*(\.exe)?$/i | RMMTool:="ScreenConnect / ConnectWise"; + FileName=/^(ateraagent|atera[\w.]*)(\.exe)?$/i | RMMTool:="Atera"; + FileName=/^(splashtop[\w.]*|srservice|strwinclt|srmanager)(\.exe)?$/i | RMMTool:="Splashtop"; + FileName=/^rustdesk(\.exe)?$/i | RMMTool:="RustDesk"; + FileName=/^supremo(helper|service)?(\.exe)?$/i | RMMTool:="Supremo"; + FileName=/^ammyy[\w.]*(\.exe)?$/i | RMMTool:="Ammyy Admin"; + FileName=/^ultraviewer[\w.]*(\.exe)?$/i | RMMTool:="UltraViewer"; + FileName=/^(dwagent|dwagsvc)(\.exe)?$/i | RMMTool:="DWService"; + FileName=/^meshagent(\.exe)?$/i | RMMTool:="MeshCentral / TacticalRMM"; + FileName=/^(logmein[\w.]*|lmiguardiansvc)(\.exe)?$/i | RMMTool:="LogMeIn"; + FileName=/^(gotoassist[\w.]*|gotohttp|g2comm|g2host)(\.exe)?$/i | RMMTool:="GoTo Assist"; + FileName=/^(rutserv|rfusclient|remoteutilities[\w.]*)(\.exe)?$/i | RMMTool:="Remote Utilities"; + FileName=/^radmin[\w.]*(\.exe)?$/i | RMMTool:="Radmin"; + FileName=/^(nomachine|nxservice|nxplayer|nxnode)(\.exe)?$/i | RMMTool:="NoMachine"; + FileName=/^(dwrcs|dameware[\w.]*)(\.exe)?$/i | RMMTool:="DameWare"; + FileName=/^(zohours|zohomeeting|zaservice|za_connect)(\.exe)?$/i | RMMTool:="Zoho Assist"; + FileName=/^(ngrok|frpc|frps)(\.exe)?$/i | RMMTool:="Tunneling (ngrok/frp)"; + * | RMMTool:="none"; +} +| RMMTool != "none" +| groupBy([RMMTool, aid, ComputerName, UserName], function=[ + count(as=Executions), + collect([ImageFileName, CommandLine], limit=10), + min(@timestamp, as=FirstSeen), + max(@timestamp, as=LastSeen) + ], limit=10000) +| formatTime(format="%F %T %Z", field=FirstSeen, as=FirstSeen) +| formatTime(format="%F %T %Z", field=LastSeen, as=LastSeen) +| sort(LastSeen, order=desc) +``` + +### Users creating Network Shares + +Source YAML: `users_creating_network_shares.yml` + +```cql +#event_simpleName="NetShareAdd" +| wildcard(field=UserName, pattern=?UserName, ignoreCase=true) +| wildcard(field=ComputerName, pattern=?ComputerName, ignoreCase=true) +| groupBy([ComputerName, UserName, ShareName, SharePath, ShareData, @timestamp]) +``` diff --git a/.github/skills/crowdstrike-cql-process-extended/SKILL.md b/.github/skills/crowdstrike-cql-process-extended/SKILL.md new file mode 100644 index 0000000..9ba492c --- /dev/null +++ b/.github/skills/crowdstrike-cql-process-extended/SKILL.md @@ -0,0 +1,196 @@ +--- +name: crowdstrike-cql-process-extended +description: "Use for additional CrowdStrike CQL process hunts covering file names, command lines, Bitsadmin, macOS InstallFix, JAR files, npm packages, host-specific prevalence, and runtime interpreters." +user-invocable: true +--- + +# Process Hunting: Additional Patterns + +Use these complete CQL bodies as starting points. Validate event names, field availability, functions, and telemetry against the target tenant. Queries are investigative leads, not verdicts. + +### Hunt for specific Command Line Activity + +Source YAML: `hunt_command_line.yml` + +```cql +#event_simpleName=ProcessRollup2 OR #event_simpleName=SyntheticProcessRollup2 +| aid=?aid +| CommandLine like ?CommandLine +| ImageFileName=/(\/|\\)(?\w*\.?\w*)$/ +| table([aid, FileName, ImageFileName, CommandLine], limit=1000) +``` + +### Hunt for a file name + +Source YAML: `hunt_filename.yml` + +```cql +#event_simpleName=ProcessRollup2 OR #event_simpleName=SyntheticProcessRollup2 +| aid=?aid +| ImageFileName like ?ImageFileName +| ImageFileName=/(\/|\\)(?\w*\.?\w*)$/ +| table([aid, FileName, ImageFileName, CommandLine], limit=1000) +``` + +### Hunting Bitsadmin usage + +Source YAML: `hunting_bitsadmin_usage.yml` + +```cql +| case { + #event_simpleName=ProcessRollup2 + AND (ImageFileName=/\\bitsadmin\.exe$/i OR OriginalFilename="bitsadmin.exe") + AND ( + CommandLine=/\/transfer/i + OR CommandLine=/\/addfile/i + OR CommandLine=/\/download/i + OR CommandLine=/\/SetNotifyCmdLine/i + OR CommandLine=/\/resume/i + OR CommandLine=/https?:\/\//i + OR CommandLine=/ftp:\/\//i + ) + AND NOT ( + ParentBaseFileName=svchost.exe + OR ParentBaseFileName=msiexec.exe + ) + | hunt_hypothesis := "H1_BITSADMIN_DIRECT_EXEC" ; + #event_simpleName=ScriptControlScanV2 OR #event_simpleName=CommandHistory + AND ( + ScriptContent=/Start-BitsTransfer/i + OR ScriptContent=/Import-Module\s+BitsTransfer/i + OR ScriptContent=/BITS\.IBackgroundCopyManager/i + ) + AND ( + ScriptContent=/https?:\/\//i + OR ScriptContent=/\-Source/i + OR ScriptContent=/\-Destination/i + ) + | hunt_hypothesis := "H2_POWERSHELL_BITSTRANSFER" ; + #event_simpleName=ProcessRollup2 + AND ( + CommandLine=/SetNotifyCmdLine/i + OR CommandLine=/SetMinRetryDelay/i + OR CommandLine=/SetNoProgressTimeout/i + ) + AND NOT CommandLine=/Windows.Update/i + | hunt_hypothesis := "H3_BITS_PERSISTENCE" ; + #event_simpleName=ProcessRollup2 + AND ImageFileName=/\\bitsadmin\.exe$/i + AND CommandLine=/getieproxy/i + | hunt_hypothesis := "H4_BITS_PROXY_RECON" ; + * | hunt_hypothesis := "NO_MATCH" ; +} +// Exclure les non-matchs +| hunt_hypothesis != "NO_MATCH" +| select([ + @timestamp, + hunt_hypothesis, + ComputerName, + UserName, + UserSid, + ImageFileName, + CommandLine, + ParentBaseFileName, + ParentCommandLine, + ScriptContent, + SHA256HashData +]) +| sort(@timestamp, order=desc) +``` + +### InstallFix on macOS + +Source YAML: `installfix_on_macos.yml` + +```cql +#repo="base_sensor" +| #event_simpleName="ProcessRollup2" +| event_platform="Mac" +| correlate( + Base64Decode: { + #event_simpleName="ProcessRollup2" + | CommandLine=/(?i)base64\s+-(d|D)/ + } include:[aid], + + SuspiciousCurl: { + #event_simpleName="ProcessRollup2" + | CommandLine=/(?i)curl\s+.*https?:\/\// + | CommandLine=/(?i)curl\s+-[a-z]*[ksfls]{4,}/ + | rootURL := "https://falcon.us-2.crowdstrike.com/" + | format("[Tree](%sgraphs/process-explorer/tree?id=pid:%s:%s)", field=["rootURL", "aid", "TargetProcessId"], as="URL") + } include:[ComputerName, UserName, aid, CommandLine, URL], + within=1m, + sequence=true, + globalConstraints=[aid], + includeMatchesOnceOnly=true +) +| ComputerName := SuspiciousCurl.ComputerName +| aid := SuspiciousCurl.aid +| @timestamp := SuspiciousCurl.@timestamp +| Tree := SuspiciousCurl.URL +| UserName := SuspiciousCurl.UserName +| Curl_CMD := SuspiciousCurl.CommandLine +| table([@timestamp, UserName, ComputerName, aid, Tree, Curl_CMD]) +``` + +### JAR files executed from %AppData% + +Source YAML: `jar_file_executed_from_appdata.yml` + +```cql +#event_simpleName=ProcessRollup2 +| ImageFileName=/javaw.exe/i CommandLine=/appdata/i +| table([aid, @timestamp, #event_simpleName, ImageFileName, SHA256HashData], limit=1000) +``` + +### JAR files written to %AppData% + +Source YAML: `jar_file_written_to_appdata.yaml` + +```cql +#event_simpleName=JarFileWritten +| TargetFileName=/\\AppData\\/i +| table([aid, @timestamp, TargetFileName, SHA256HashData], limit=1000) +``` + +### NPM Package Named Searches + +Source YAML: `npm_package_named_searches.yml` + +```cql +//Single package check +#event_simpleName=/written/i TargetFileName=*jscrambler@* +| groupBy([@timestamp, event_platform, #event_simpleName, ComputerName, TargetFileName, ParentBaseFileName, GrandParentBaseFileName, CommandLine]) + +//For multiple packages with versions +#event_simpleName=/written/i (TargetFileName=/chalk-5\.6\.1/i) OR (TargetFileName=/supports-hyperlinks-4\.1\.1/i) OR (TargetFileName=/chalk-template-1\.1\.1/i) OR (TargetFileName=/slice-ansi-7\.1\.1/i) OR (TargetFileName=/wrap-ansi-9\.0\.1/i) OR (TargetFileName=/has-ansi-6\.0\.1/i) OR (TargetFileName=/strip-ansi-7\.1\.1/i) OR (TargetFileName=/ansi-styles-6\.2\.2/i) OR (TargetFileName=/supports-color-10\.2\.1/i) OR (TargetFileName=/ansi-regex-6\.2\.1/i) OR (TargetFileName=/debug-4\.4\.2/i) OR (TargetFileName=/color-convert-3\.1\.1/i) OR (TargetFileName=/color-name-2\.0\.1/i) OR (TargetFileName=/is-arrayish-0\.3\.3/i) OR (TargetFileName=/color-5\.0\.1/i) OR (TargetFileName=/color-string-2\.1\.1/i) OR (TargetFileName=/simple-swizzle-0\.2\.3/i) OR (TargetFileName=/backslash-0\.2\.1/i) +| groupBy([@timestamp, event_platform, #event_simpleName, ComputerName, TargetFileName, ParentBaseFileName, GrandParentBaseFileName, CommandLine]) +``` + +### Find processes that only ran a few of times on a specific host + +Source YAML: `processes_specific_host.yml` + +```cql +#event_simpleName=ProcessRollup2 OR #event_simpleName=SyntheticProcessRollup2 +| aid=?aid +| groupBy([SHA256HashData, ImageFileName], limit=max) +| _count <5 +| sort(_count, limit=1000) +``` + +### Shadow MCP Server Activity via Common Runtime Interpreters + +Source YAML: `shadow_mcp_server_activity_via_common_runtime_interpreters.yml` + +```cql +#event_simpleName=ProcessRollup2 +| FileName = /(?i)^(node|node\.exe|npx|npx\.cmd|python|python\.exe|python3|uv|uvx|docker|docker\.exe)$/ +| CommandLine = /(?i)(server-filesystem|server-github|server-postgres|server-sqlite|server-puppeteer|server-brave|modelcontext|mcp)/ +| groupBy([aid, ComputerName, UserName, FileName, CommandLine], function=[ + count(as=Executions), + min(@timestamp, as=FirstSeen), + max(@timestamp, as=LastSeen) + ]) +| sort(LastSeen, order=desc) +``` diff --git a/.github/skills/crowdstrike-cql-process/SKILL.md b/.github/skills/crowdstrike-cql-process/SKILL.md new file mode 100644 index 0000000..928bcb0 --- /dev/null +++ b/.github/skills/crowdstrike-cql-process/SKILL.md @@ -0,0 +1,405 @@ +--- +name: crowdstrike-cql-process +description: "Use when writing CrowdStrike CQL for process execution, PowerShell, encoded commands, command lines, LOLBins, process trees, credential-dumping indicators, or DLL side-loading." +user-invocable: true +--- + +# Process and Script 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. + +## Command History with Process Tree + +Source file: `Command_History_with_Process_Tree.yml` + +```cql +#event_simpleName=/^(CommandHistory|ProcessRollup2)$/ +event_platform=Win +| selfJoinFilter( + field=[aid, TargetProcessId], + where=[ + { #event_simpleName=ProcessRollup2 }, + { #event_simpleName=CommandHistory } + ] + ) +| case { + #event_simpleName=CommandHistory + | CommandHistory=* + | splitString( + field=CommandHistory, + by="¶", + as=CommandHistorySplit + ) + | concatArray( + CommandHistorySplit, + separator="\n", + as=CommandHistoryClean + ); + + #event_simpleName=ProcessRollup2 + | ImageFileName=/\\(?[^\\]+)$/ + | ExecutionChain := format( + format="%s → %s (PID: %s)", + field=[ParentBaseFileName, ChildBaseFileName, RawProcessId] + ); + } +| groupBy([aid, ComputerName, TargetProcessId], function=[ + selectLast(ExecutionChain), + selectLast(CommandHistoryClean) + ], limit=max) +| CommandHistoryClean=* +``` + +## Credential Dumping Detection + +Source file: `Credential_Dumping_Detection.yml` + +```cql +#event_simpleName=ProcessRollup2 +| (CommandLine=/mimikatz|procdump|lsass|sekurlsa/i OR ImageFileName=/\\(mimikatz|procdump|pwdump)\.exe$/i) +| ParentImageFileName!=/\\(powershell|cmd)\.exe$/i +| join({#event_simpleName=UserIdentity}, field=[aid, AuthenticationId], include=[UserName], mode=left) +| join({#event_simpleName=SyntheticProcessRollup2 | ParentSHA256HashData := SHA256HashData}, + field=[aid, ParentProcessId], key=[aid, TargetProcessId], include=[ParentSHA256HashData], mode=left) +| table([aid, UserName, ImageFileName, CommandLine, ParentImageFileName, SHA256HashData, ParentSHA256HashData]) +``` + +## Detect Suspicious Windows Command-Line Activity Using System Utilities + +Source file: `Detect_Suspicious_Windows_Command-Line_Activity_Using_System_Utilities.yml` + +```cql +// Get all Windows ProcessRollup2 Events +#event_simpleName=ProcessRollup2 event_platform=Win +// Narrow to processes of interest and create FileName variable +| ImageFileName=/\\(?(whoami|net1?|systeminfo|ping|nltest|sc|hostname|ipconfig)\.exe)/i +// Get timestamp value with date and hour value +| ProcessStartTime := ProcessStartTime*1000 +| dayBucket := formatTime("%Y-%m-%d %H", field=ProcessStartTime, locale=en_US, timezone=Z) +// Force CommandLine and FileName into lower case +| CommandLine := lower(CommandLine) +| FileName := lower(FileName) +// Parse flag used in "net" command +| regex("(sc|net1?)\s+(?\S+)\s+", field=CommandLine, strict=false) +// Force netFlag to lower case +| netFlag := lower(netFlag) +// Create evaulation criteria and weighting for process usage; modified behaviorWeight integer as desired +| case { + FileName=/net1?\.exe/ AND netFlag="start" | behaviorWeight := "4" ; + FileName=/net1?\.exe/ AND netFlag="stop" | behaviorWeight := "4" ; + FileName=/net1?\.exe/ AND netFlag="stop" AND CommandLine=/falcon/i | behaviorWeight := "25" ; + FileName=/sc\.exe/ AND netFlag="start" | behaviorWeight := "4" ; + FileName=/sc\.exe/ AND netFlag="stop" | behaviorWeight := "4" ; + FileName=/sc\.exe/ AND netFlag=/(query|stop)/i AND CommandLine=/csagent/i | behaviorWeight := "25" ; + FileName=/net1?\.exe/ AND netFlag="share" | behaviorWeight := "2" ; + FileName=/net1?\.exe/ AND netFlag="user" AND CommandLine=/\/delete/i | behaviorWeight := "10" ; + FileName=/net1?\.exe/ AND netFlag="user" AND CommandLine=/\/add/i | behaviorWeight := "10" ; + FileName=/net1?\.exe/ AND netFlag="group" AND CommandLine=/\/domain\s+/i | behaviorWeight := "5" ; + FileName=/net1?\.exe/ AND netFlag="group" AND CommandLine=/admin/i | behaviorWeight := "5" ; + FileName=/net1?\.exe/ AND netFlag="localgroup" AND CommandLine=/\/add/i | behaviorWeight := "10" ; + FileName=/net1?\.exe/ AND netFlag="localgroup" AND CommandLine=/\/delete/i | behaviorWeight := "10" ; + FileName=/nltest\.exe/ | behaviorWeight := "3" ; + FileName=/systeminfo\.exe/ | behaviorWeight := "3" ; + FileName=/whoami\.exe/ | behaviorWeight := "3" ; + FileName=/ping\.exe/ | behaviorWeight := "3" ; + FileName=/hostname\.exe/ | behaviorWeight := "3" ; + FileName=/ipconfig\.exe/ | behaviorWeight := "3" ; + * } +| default(field=behaviorWeight, value=1) +// Create FileName and CommandLine one-liner +| format(format="(Score: %s) %s • %s", field=[behaviorWeight, FileName, CommandLine], as="executionDetails") +// Group and organize output +| groupby([cid,aid, dayBucket], function=[count(FileName, distinct=true, as="fileCount"), sum(behaviorWeight, as="behaviorWeight"), series(executionDetails)], limit=max) +// Set thresholds +| fileCount >= 5 OR behaviorWeight > 30 +// Add Host Search link +| format("[Host Search](https://falcon.crowdstrike.com/investigate/events/en-us/app/eam2/investigate__computer?earliest=-24h&latest=now&computer=*&aid_tok=%s&customer_tok=*)", field=["aid"], as="Host Search") +// Sort descending by behavior weighting +| sort(behaviorWeight) +| drop([@timestamp, _duration]) +``` + +## Detect and Decode Base64-Encoded PowerShell Commands - http + +Source file: `Detect_and_Decode_Base64-Encoded_PowerShell_Commands-http.yml` + +```cql +#event_simpleName=ProcessRollup2 event_platform=Win ImageFileName=/.*\\powershell\.exe/ +| CommandLine=/.*\s+\-(e|encoded|encodedcommand|enc)\s+.*/ +| length("CommandLine", as="cmdLength") +| groupby([CommandLine], function=stats([count(aid, distinct=true, as="uniqueEndpointCount"), count(aid, as="executionCount")]), limit=max) +| EncodedString := splitString(field=CommandLine, by="-e* ", index=1) +| CmdLinePrefix := splitString(field=CommandLine, by="-e* ", index=0) +| DecodedString := base64Decode(EncodedString, charset="UTF-16LE") +// Look for encoded messages in the decoded message and decode those too. +| case { + DecodedString = /encoded/i + | SubEncodedString := splitString(field=DecodedString, by="-EncodedCommand ", index=1) + | SubCmdLinePrefix := splitString(field=EncodedString, by="-EncodedCommand ", index=0) + | SubDecodedString := base64Decode(SubEncodedString, charset="UTF-16LE"); + * +} +| DecodedString=/.*https?\:\/\/.*/ +| table([executionCount, uniqueEndpoitnCount, DecodedString, CommandLine]) +| sort(executionCount, order=desc) +``` + +## Detect and Decode Base64-Encoded PowerShell Commands + +Source file: `Detect_and_Decode_Base64-Encoded_PowerShell_Commands.yml` + +```cql +#event_simpleName=ProcessRollup2 event_platform=Win ImageFileName=/.*\\powershell\.exe/ +| CommandLine=/\s+\-(e|encoded|encodedcommand|enc)\s+/i +| CommandLine=/\-(?(e|encoded|encodedcommand|enc))\s+/i +| length("CommandLine", as="cmdLength") +| groupby([psEncFlag, cmdLength, CommandLine], function=stats([count(aid, distinct=true, as="uniqueEndpointCount"), count(aid, as="executionCount")]), limit=max) +| EncodedString := splitString(field=CommandLine, by="-e* ", index=1) +| CmdLinePrefix := splitString(field=CommandLine, by="-e* ", index=0) +| DecodedString := base64Decode(EncodedString, charset="UTF-16LE") +// Look for encoded messages in the decoded message and decode those too. +| case { + DecodedString = /encoded/i + | SubEncodedString := splitString(field=DecodedString, by="-EncodedCommand ", index=1) + | SubCmdLinePrefix := splitString(field=EncodedString, by="-EncodedCommand ", index=0) + | SubDecodedString := base64Decode(SubEncodedString, charset="UTF-16LE"); + * +} +| table([executionCount, uniqueEndpoitnCount, cmdLength, DecodedString, CommandLine]) +| sort(executionCount, order=desc) +``` + +## Encoded PowerShell Command Execution + +Source file: `Encoded_Powershell_Executions.yml` + +```cql +#event_simpleName=ProcessRollup2 ImageFileName=/\\(powershell|pwsh)\.exe$/i +| replace("\\^", with="", field=CommandLine, as=cmd) +| cmd=/\s[-\/]e(c|nc?[a-z]*)?\s+(?[A-Za-z0-9+\/=]{16,})/i +| decoded := base64Decode(b64, charset="UTF-16LE") +| join({#event_simpleName=UserIdentity}, field=[aid, AuthenticationId], include=[UserName], mode=left) +| table([aid, UserName, ParentImageFileName, ImageFileName, CommandLine, decoded]) +``` + +## Powershell Command Length Anomaly Detection + +Source file: `Hunting_Powershell_Command_Length_Anomaly.yml` + +```cql +#event_simpleName=ProcessRollup2 +| ImageFileName=/\\(powershell(_ise)?|pwsh)\.exe/i +| CommandLength := length("CommandLine") | CommandLength>0 +| aid=?AID +// Classify Data into Historical and LastDay +| case { + test(@timestamp < (end() - duration(7d))) | DataSet:="Historical"; + test(@timestamp > (end() - duration(1d))) | DataSet:="LastDay"; + * +} +// Calculate Average Command Length +| groupBy([DataSet, aid], function=avg(CommandLength)) +| case { + DataSet="Historical" | rename(field="_avg", as="historicalAvg"); + DataSet="LastDay" | rename(field="_avg", as="todaysAvg"); + * +} +// Aggregate Averages +| groupBy([aid], function=[avg("historicalAvg", as=historicalAvg), avg("todaysAvg", as=todaysAvg)]) +// Calculate Percentage Increase +| PercentIncrease := (todaysAvg - historicalAvg) / historicalAvg * 100 +| format("%d", field=PercentIncrease, as=PercentIncrease) +| format(format="%.2f", field=[historicalAvg], as=historicalAvg) +// Filter and Sort Results +| PercentIncrease > 0 +| sort(PercentIncrease, limit=10000) +``` + +## LOLBin Certutil + +Source file: `LOLBin_Certutil.yml` + +```cql +in(#event_simpleName, values=["ProcessRollup2","ProcessBlocked"]) +| event_platform=Win and ImageFileName=/certutil.exe/i and CommandLine=/(https?:)/i +``` + +## LOLBin Mshta + +Source file: `LOLBin_Mshta.yml` + +```cql +in(#event_simpleName, values=["ProcessRollup2","ProcessBlocked"]) +| event_platform=Win and ImageFileName=/mshta.exe/i +| CommandLine=/mshta(?:\.exe)?\"?\s+\"?(?(?:.*?\.hta|(?=\").*?(?=\")|.*?(?=(?:\s|$))))/i +| HtaPath=/(?.*)(\\\\|\/)/i +| HtaPath=/(.*(\\\\|\/))?(?.*)$/i +``` + +## LOLBin Msiexec + +Source file: `LOLBin_Msiexec.yml` + +```cql +in(#event_simpleName, values=["ProcessRollup2","ProcessBlocked"]) +| event_platform=Win and ImageFileName=/msiexec.exe/i and CommandLine=/http/i +``` + +## LOLBin Regsvr32 + +Source file: `LOLBin_Regsvr32.yml` + +```cql +in(#event_simpleName, values=["ProcessRollup2","ProcessBlocked"]) +| event_platform=Win +| ImageFileName=/regsvr32.exe/i CommandLine=/scrobj.dll/i CommandLine=/i:/i +``` + +## LOLBin Rundll32 + +Source file: `LOLBin_Rundll32.yml` + +```cql +in(#event_simpleName, values=["ProcessRollup2","ProcessBlocked"]) +| event_platform=Win and ImageFileName=/rundll32.exe/i +| in(ParentBaseFileName, values=["cmd.exe","winword.exe","powerpnt.exe","excel.exe","outlook.exe","mshta.exe","cscript.exe","wscript.exe"]) +``` + +## LOLBin WMIC + +Source file: `LOLBin_WMIC.yml` + +```cql +in(#event_simpleName, values=["ProcessRollup2","ProcessBlocked"]) +| event_platform=Win and ImageFileName=/wmic.exe/i +``` + +## Powershell Downloads + +Source file: `Powershell_Downloads.yml` + +```cql +#event_simpleName=CommandHistory +| CommandHistory=/Invoke\-WebRequest|Net\.WebClient|Start\-BitsTransfer/i +| regex("(?https?://[^'\"]+)", field=CommandHistory) +| replace("https://", with="", field=URL, as=ShortURL) +| replace("\/.*", with="", field=ShortURL, as=otx_lookup) +| UrlBase:="https://otx.alienvault.com/indicator/domain/" +| format(format="[Alienvault](%s%s)", field=[UrlBase, otx_lookup], as=DomainLookup) +| table([DomainLookup, URL, ComputerName, UserName, CommandHistory], limit=20000) +``` + +## Rare windows shell parent process + +Source file: `Rare_Windows_Shell_Parent.yml` + +```cql +#event_simpleName=ProcessRollup2 event_platform=Win +| case { in(field=FileName, values=["powershell.exe", "cmd.exe", "pwsh.exe"]) | IsChild := "1"; * | IsChild := "0" } +| case { IsChild = "1" | ProcId := ParentProcessId | ChildProcess := FileName | ChildCommandLine := CommandLine; +IsChild = "0" | ProcId := TargetProcessId | ParentCommandLine := CommandLine | ParentFileName := FileName | ParentFilePath := FilePath | ParentSHA256HashData := SHA256HashData; } +| groupBy([ComputerName, ProcId], function=([count(ParentProcessId, distinct=true, as=EventCount), collect([ParentFileName, ParentSHA256HashData, ParentFilePath, ParentCommandLine, ChildProcess]), collect(ChildCommandLine, limit=4)]), limit=max) +| EventCount > 1 +| groupBy([ParentSHA256HashData], function=([collect([aid, ParentFileName, ParentFilePath, ParentCommandLine, ChildProcess, ChildCommandLine]), count(ComputerName, as=HostCount)])) +| HostCount < 5 +| sort([HostCount, ParentFileName], order=asc) +``` + +## Rundll32 Remote UNC DLL Ordinal Execution + +Source file: `Rundll32_Remote_UNC_DLL_Ordinal_Execution.yml` + +```cql +// OVERVIEW: Detects rundll32 loading a DLL from a remote UNC path and +// invoking an export by ordinal, a proxy-execution pattern used to run remote code. +// SOURCE HUNTPACK: EtherHiding ClickFix Hunt +// MITRE: T1218.011, T1105 +// CONF: high | FP: low | COST: low +// REQUIRES: ProcessRollup2 +// FALSE POSITIVES: Approved software deployment tooling using DLLs from network shares. +// TUNING: Exclude validated deployment shares, distribution hosts, and known command lines. +// LOOKBACK: 7d - set with the Falcon time picker. +#event_simpleName=ProcessRollup2 +| FileName=/^rundll32(\.exe)?$/i +// UNC host, optionally with WebDAV @port / @SSL suffixes (\\host\ , \\host@80\ , \\host@SSL\) +| CommandLine=/\\\\[a-z0-9._\-]+(@[a-z0-9]+)*\\/i +| CommandLine=/,#[0-9]+/i +| table([@timestamp, ComputerName, UserName, ParentBaseFileName, CommandLine]) +| sort(@timestamp, order=desc, limit=500) +``` + +## Rust Build Toolchain Spawning Interpreter or Downloader + +Source file: `Rust_Build_Toolchain_Spawning_Interpreter_or_Downloader.yml` + +```cql +// OVERVIEW: Detects Rust build tools spawning interpreters or download utilities, +// which may indicate malicious dependency or build-script execution. +// SOURCE HUNTPACK: Rust Build-Toolchain Supply-Chain Hunt +// MITRE: T1195.002, T1059 +// CONF: medium | FP: medium | COST: low +// REQUIRES: ProcessRollup2, SyntheticProcessRollup2 +// FALSE POSITIVES: Legitimate build scripts downloading dependencies or invoking shells. +// TUNING: Exclude approved CI wrappers, artifact mirrors, and known build scripts. +// LOOKBACK: 30d - set with the Falcon time picker. +#event_simpleName=/ProcessRollup2|SyntheticProcessRollup2/ +| in(ParentBaseFileName, values=["cargo.exe","cargo","rustc.exe","rustc"], ignoreCase=true) +| in(FileName, values=["powershell.exe","pwsh.exe","wscript.exe","cscript.exe","cmd.exe","curl.exe","curl","wget","bash","sh"], ignoreCase=true) +| table([@timestamp, ComputerName, UserName, ParentBaseFileName, FileName, CommandLine, ParentCommandLine]) +| sort(@timestamp, order=desc, limit=500) +``` + +## ClickFix Run Dialog Command Detection + +Source file: `clickfix_run_dialog_command_detection.yml` + +```cql +// HUNT: ClickFix Run-dialog paste recorded in RunMRU (ACR Stealer initial access) +// MITRE: T1204, T1189, T1059.003 +// CONF: high | FP: low | COST: low | REQUIRES: registry telemetry (RunMRU writes) +// FALSE POSITIVES: IT staff pasting legitimate remote-admin one-liners into Run +// TUNING: exclude your admin asset group / privileged accounts. Removing the second +// RegStringValue filter widens this to every interpreter typed into Run (noisier hunt). +#event_simpleName=/^(RegGenericValueUpdate|AsepValueUpdate|RegSystemConfigValueUpdate)$/ +| RegObjectName=/RunMRU/i +| RegStringValue=/(powershell|cmd|mshta|rundll32|conhost|curl|msiexec|certutil|bitsadmin|python)/i +| RegStringValue=/(http|\\\\|-enc|-e |hidden|iex|FromBase64|--headless)/i +| table([@timestamp, ComputerName, UserName, RegObjectName, RegValueName, RegStringValue, aid], limit=200) +``` + +## Count Windows Discovery Commands + +Source file: `count_windows_discovery_commands.yml` + +```cql +// Insert Discovery commands of interest here +event_platform=Win #event_simpleName=ProcessRollup2 FileName=/(whoami|ping|net1?|systeminfo|quser|ipconfig)/iF + +// Restrict to non-system UserSid Values +| UserSid=S-1-5-21-* + +// User case() to create discovery command counter +| case { + FileName=/whoami/iF | whoami:="1"; + FileName=/ping/iF | ping:="1"; + FileName=/net1?/iF | net:="1"; + FileName=/systeminfo/iF | systeminfo:="1"; + FileName=/quser/iF | quser:="1"; + FileName=/ipconfig/iF | ipconfig:="1"; +} + +// Aggregate results by duration used in time picker +| groupBy([UserName, UserSid], function=([sum(whoami, as=whoami), sum(ping, as=ping), sum(net, as=net), sum(systeminfo, as=systeminfo), sum(quser, as=quser), sum(ipconfig, as=ipconfig), selectLast([CommandLine])]), limit=max) + +// Rename field for clarity +| rename(field="CommandLine", as="LastCommandRun") + +// Get total number of discovery commands run per UserName/UserSid key pair +| totalDiscovery:=whoami+ping+net+systeminfo+quser+ipconfig + +// Set threshold for commands runs (optional) +| totalDiscovery>5 + +// Reorder using table for easier reading +| table([UserName, UserSid, totalDiscovery, whoami, ping, net, systeminfo, quser, ipconfig, LastCommandRun]) +``` diff --git a/.github/skills/crowdstrike-cql-scheduled-tasks/SKILL.md b/.github/skills/crowdstrike-cql-scheduled-tasks/SKILL.md new file mode 100644 index 0000000..50195fd --- /dev/null +++ b/.github/skills/crowdstrike-cql-scheduled-tasks/SKILL.md @@ -0,0 +1,185 @@ +--- +name: crowdstrike-cql-scheduled-tasks +description: "Use for CrowdStrike CQL queries on scheduled task registration, hidden tasks, task triggers, principals, run levels, startup events, and time-based persistence." +user-invocable: true +--- + +# Scheduled Tasks and Startup Persistence + +Use these complete CQL bodies as starting points. Validate event names, field availability, functions, and telemetry against the target tenant. Queries are investigative leads, not verdicts. + +### Find events triggered on an event + +Source YAML: `events_triggered_by_event.yml` + +```cql +#event_simpleName=ScheduledTaskRegistered +| parseXml(TaskXml) +| Trigger:=rename(Task.Triggers.EventTrigger.Enabled) +| Trigger=* // Remove this line if you don't care if it's empty +| table([aid, Trigger, TaskXml], limit=1000) +``` + +### Find hidden scheduled tasks + +Source YAML: `hidden_scheduled_tasks.yml` + +```cql +#event_simpleName=ScheduledTaskRegistered +| parseXml(TaskXml) +| Hidden:=rename(Task.Settings.Hidden) +| Hidden=/true/i +| table([aid,Hidden,TaskXml],limit=1000) +``` + +### Find events that are scheduled + +Source YAML: `scheduled_events.yml` + +```cql +#event_simpleName=ScheduledTaskRegistered +| parseXml(TaskXml) +| Trigger:=rename(Task.Triggers.CalendarTrigger.Enabled) +| Trigger=* // Remove this line if you don't care if it's empty +| table([aid, Trigger, TaskXml], limit=1000) +``` + +### Find events triggered at startup + +Source YAML: `startup_events.yml` + +```cql +#event_simpleName=ScheduledTaskRegistered +| parseXml(TaskXml) +| Trigger:=rename(Task.Triggers.BootTrigger.Enabled) +| Trigger=* // Remove this line if you don't care if it's empty +| table([aid, Trigger, TaskXml], limit=1000) +``` + +### Suspicious Scheduled Task Creation + +Source YAML: `suspicious_scheduled_task_creation.yml` + +```cql +#event_simpleName=ScheduledTaskRegistered event_platform=Win + +// Optional scoping for testing on a single host (leave as * for fleet-wide) +| ComputerName=?ComputerName + +// Exclude the built-in Windows task namespace (Defender scan, Update, etc.) +| TaskName!=/^\\?Microsoft\\Windows\\/i + +// To suppress recurring known-good automation after baselining, add an +// explicit author filter here, e.g.: | TaskAuthor!=/sccm-svc|rmm-deploy/i + +// Normalise the action fields into one searchable string +| TaskCmd := lower("TaskExecCommand") +| TaskArgs := lower("TaskExecArguments") +| CmdLine := format("%s %s", field=[TaskCmd, TaskArgs]) + +// --- Suspicion classification ------------------------------------------- +| case { + // Encoded PowerShell REQUIRES a second signal (download/exec intent), + // because benign monitoring/management tooling uses -encodedCommand. + CmdLine=/(powershell|pwsh)/i + AND CmdLine=/(-enc|-encodedcommand|-e\s)/i + AND CmdLine=/(downloadstring|downloadfile|iex|invoke-expression|frombase64string|net\.webclient|-w\s+hidden|-windowstyle\s+hidden)/i + | Reason := "Encoded PowerShell w/ download or exec intent" ; + + // Common LOLBins used to proxy execution + CmdLine=/\\(mshta|rundll32|regsvr32|wscript|cscript|certutil|bitsadmin|installutil)\.exe/i + | Reason := "LOLBin proxy execution" ; + + // Genuinely user-writable locations (ProgramData deliberately excluded) + CmdLine=/(\\appdata\\|\\users\\public\\|\\temp\\|\\windows\\temp\\|%temp%|%appdata%)/i + | Reason := "Payload in user-writable/temp path" ; + + // HTTP(S)/FTP URL embedded directly in the task action + CmdLine=/(http:\/\/|https:\/\/|ftp:\/\/)/i + | Reason := "Web URL in task action" ; + + // cmd one-liners chaining commands + CmdLine=/cmd(\.exe)?\s+\/c.*(&&|\|)/i + | Reason := "Chained cmd one-liner" ; + + * | Reason := "no-match" ; +} +| Reason != "no-match" + +// --- Remote creation flag (lateral movement) ---------------------------- +| case { + RemoteAddressIP4=* AND RemoteAddressIP4!="0.0.0.0" | Origin := format("REMOTE (%s)", field=[RemoteAddressIP4]) ; + RemoteAddressIP6=* | Origin := format("REMOTE (%s)", field=[RemoteAddressIP6]) ; + * | Origin := "local" ; +} + +// --- Output -------------------------------------------------------------- +| groupBy( + [ComputerName, UserName, TaskAuthor, TaskName, Reason, Origin, TaskExecCommand, TaskExecArguments], + function=[count(as=Count), min(@timestamp, as=FirstSeen), max(@timestamp, as=LastSeen)], + limit=max +) +| FirstSeen := formatTime("%F %T %Z", field=FirstSeen) +| LastSeen := formatTime("%F %T %Z", field=LastSeen) +| sort(LastSeen, order=desc) +| table([LastSeen, ComputerName, UserName, TaskAuthor, Origin, Reason, TaskName, TaskExecCommand, TaskExecArguments, Count, FirstSeen], limit=10000) +``` + +### Find tasks scheduled by logon type + +Source YAML: `task_scheduled_by_logon_type.yml` + +```cql +#event_simpleName=ScheduledTaskRegistered +| parseXml(TaskXml) +| LogonType:=rename(Task.Principals.Principal.LogonType) +| LogonType=* // Remove this line if you don't care if it's empty +| table([aid, LogonType, TaskXml], limit=1000) +``` + +### Find tasks scheduled by run level + +Source YAML: `tasks_scheduled_by_run_level.yml` + +```cql +#event_simpleName=ScheduledTaskRegistered +| parseXml(TaskXml) +| RunLevel:=rename(Task.Principals.Principal.RunLevel) +| RunLevel=* // Remove this line if you don't care if it's empty +| table([aid, RunLevel, TaskXml], limit=1000) +``` + +### Find tasks scheduled by user ID + +Source YAML: `tasks_scheduled_by_user_ID.yml` + +```cql +#event_simpleName=ScheduledTaskRegistered +| parseXml(TaskXml) +| UserId:=rename(Task.Principals.Principal.UserId) +| table([aid, UserId, TaskXml], limit=1000) +``` + +### Find tasks scheduled with ComHandler + +Source YAML: `tasks_scheduled_with_ComHandler.yml` + +```cql +#event_simpleName=ScheduledTaskRegistered +| parseXml(TaskXml) +| ComHandlerData:=rename(Task.Actions.ComHandler.Data) +| ComHandlerData=* // Remove this line if you don't care if it's empty +| table([aid, ComHandlerData, TaskXml], limit=1000) +``` + +### Find events triggered at a specific time + +Source YAML: `time_events.yml` + +```cql +#event_simpleName=ScheduledTaskRegistered +| parseXml(TaskXml) +| Trigger:=rename(Task.Triggers.TimeTrigger.Enabled) +| Trigger=* // Remove this line if you don't care if it's empty +| table([aid, Trigger, TaskXml], limit=1000) +``` diff --git a/.github/skills/crowdstrike-cql/SKILL.md b/.github/skills/crowdstrike-cql/SKILL.md new file mode 100644 index 0000000..d8ec9c5 --- /dev/null +++ b/.github/skills/crowdstrike-cql/SKILL.md @@ -0,0 +1,72 @@ +--- +name: crowdstrike-cql +description: "Write, explain, and troubleshoot CrowdStrike Falcon Next-Gen SIEM CQL. Use for CQL syntax, query structure, aggregation, query errors, or process, identity, network, endpoint-inventory, host-integrity, and data-movement hunts." +user-invocable: true +--- + +# CrowdStrike CQL + +This core skill covers CQL syntax and query-writing practices. Focused query libraries contain complete CQL bodies for process, identity, network, endpoint inventory, host integrity, and data movement investigations. + +CQL is a pipeline query language, not SQL. Event names, fields, functions, time-range handling, and request encoding depend on the Falcon query surface and tenant schema. This skill writes and explains queries; it does not execute them or make API calls. + +## Query shape + +Start with event or field filters, then add pipe-delimited stages. Do not use SQL `SELECT`, `WHERE`, or `GROUP BY` clauses. + +```cql +#event_simpleName=ProcessRollup2 +| in(field=FileName, values=["powershell.exe", "pwsh.exe"], ignoreCase=true) +| groupBy([FileName], function=count(as=executions)) +``` + +`#event_simpleName=...` is a common event selector. `Field=*` selects events where that field is present. Use field names available in the target event schema. + +## Membership filters + +Use `in` for a set of alternatives, not SQL-style `field IN (...)`: + +```cql +in(field=FileName, values=["powershell.exe", "pwsh.exe", "cmd.exe"], ignoreCase=true) +``` + +Keep `field` and `values` as named arguments when in doubt. Quote string values containing spaces or punctuation. For case-sensitive matching, omit `ignoreCase` or set it to `false` where supported. + +## Regular expressions + +Use `/pattern/` directly on a field; the trailing `i` enables case-insensitive matching: + +```cql +CommandLine=/powershell(\.exe)?/i +``` + +Do not wrap CQL regexes in SQL `REGEXP`. Escape backslashes for CQL syntax, and apply JSON escaping separately when putting a query into a JSON request body. + +## Aggregation + +Use `groupBy` with fields in a list and the aggregate under `function=`: + +```cql +| groupBy([ComputerName, UserName], function=count(as=event_count)) +| sort(event_count, order=desc) +| head(20) +``` + +Some surfaces expose an unnamed count as `_count`. Use the aggregate's actual output field when sorting or filtering. + +## Query-authoring workflow + +1. Identify the investigation question, likely event family, indicator, platform, and time range. Ask one focused question or state assumptions if a missing detail materially changes the query. +2. Start with the narrowest supported event selector and indicator filter. Do not invent event fields or assume telemetry exists in the tenant. +3. Run a filter-only query first. Add parsing, joins, aggregation, and output formatting one stage at a time. +4. Select useful output fields and cap results with `limit` or `head` where appropriate. +5. Return CQL in a fenced `cql` block and state important event/field assumptions. For a catalog entry, return YAML with `name`, `log_sources`, and `cql: |`; keep metadata outside the CQL. +6. Never execute decoded commands from a query result. Treat results as investigative leads. + +## HTTP 400 checks + +- Replace SQL clauses and `IN (...)` with CQL field filters, `in(...)`, and pipeline stages. +- Check commas, brackets, parentheses, regex delimiters, and quoting. +- Put an aggregate under `function=`, for example `groupBy([HostName], function=count(as=hits))`. +- Separate CQL escaping from JSON request escaping. +- Reduce the query to its event selector, then add one filter or pipeline stage at a time. Confirm the query surface supports each field and function.