Writing Queries That Actually Return Something Useful
Crowdstrike's query language is based on FalconQL, and it's more restrictive than most people realize when they first start writing detections. The syntax looks simple, but the platform enforces strict validation before any query touches your endpoints. I learned this the hard way during an investigation when my query ran for 47 minutes and returned nothing because I'd used a case-sensitive string match on a field that gets normalized at collection time. You need to understand the core operators first. The comparison operators are straightforward:
=for exact matches!=for exclusions<and>for range queries=~for case-insensitive regex matching
The boolean operators follow standard precedence. AND binds tighter than OR, which means process_name = "cmd.exe" AND device_id = 12345 OR rule_name = "test" is evaluated as (process_name = "cmd.exe" AND device_id = 12345) OR rule_name = "test". Almost everyone gets tripped up by this on their first detection rule. Put parentheses around the OR conditions explicitly. Most people try to write everything in one monolithic query. That's inefficient and hard to maintain. Break your conditions into reusable fragments and combine them. Basic process activity query:
metadata_update_timestamp > "2024-01-15T00:00:00Z" AND process_name = "powershell.exe" AND parent_process_name != "explorer.exe" This gives you PowerShell runs that didn't originate from the desktop shell. It catches scripted execution without the noise from GUI-triggered sessions. You'll get false positives from installers and maintenance tasks, but the volume drops dramatically compared to just querying for powershell.exe alone. Network connection queries need different handling:
Get the Full Details
event_simpleName = "NetworkConnect" AND dest_port = 443 AND NOT ip_is_private(dest_ip) The ip_is_private() function filters internal addresses. Without it, you're sifting through hundreds of routine API calls to internal services. With it, you isolate actual external traffic. This alone makes the difference between a query that runs in 30 seconds and one that times out. File creation and modification tracking:
event_simpleName IN ("FileCreate", "FileModify") AND file_extension = "ps1" AND dir > "\\Windows\\System32\\" The IN operator lets you batch multiple event types. I use this pattern constantly for tracking script activity outside protected directories. The backslash escaping matters — FalconQL treats single backslashes as escape characters, so paths need double escaping or you should use the raw string literal format if your version supports it.
Advanced Patterns and Where They Break
Wildcard matching works with * and ?, but only at the beginning or end of a token in most contexts. process_name = "*mimikatz*" returns results. process_name = "*mimi*katz*" does not. Don't waste time debugging why a mid-token wildcard isn't working. Regex support via =~ is powerful but slow. Every regex match scans the full index, which means queries with complex patterns can take 10x longer than equivalent string comparisons. Keep your regex simple. process_command_line =~ "/[a-zA-Z0-9]{8,}\\.exe/" is acceptable. process_command_line =~ "^(?!.*(?i)(kaspersky|defender|symantec)).*(psexec|wmic|certutil)" will make your detection engineers swear at their monitors. Here's a pattern most people don't know about: the time_bucket function. It groups events into time windows for aggregation within queries.
count() OVER (time_bucket("5m")) WHERE process_name = "certutil.exe" AND flags CONTAINS "decode" This finds certutil decode operations grouped into five-minute buckets. Useful for spotting rapid sequential executions that indicate automated tooling. The syntax changed in one of the 2023 updates and broke several of my existing detections. Check your version documentation if a query that worked two weeks ago suddenly throws a parse error. I had a specific problem last year where a query using device_id returned inconsistent results across different time ranges. The issue was that some devices had been renamed or migrated between groups, and device_id doesn't always resolve cleanly when you're crossing group boundaries. The workaround was switching to host_group_names CONTAINS "Production" combined with the sensor ID field instead, which stays stable across reorganizations. It took me three days to figure out because the behavior isn't documented anywhere obvious.
Known Limitations You Need to Accept
FalconQL has hard limits. A single query can scan roughly 50 million events before it gets throttled. If your environment generates more than that in your target time window, the query either times out or returns partial results depending on your tier. There's no warning before this happens. You just get an empty result set and assume nothing matched when actually the backend gave up. Boolean logic depth is capped. More than about eight nested conditions and the parser starts dropping branches silently. I've seen detections miss indicators because a deeply nested NOT (A AND B AND C OR D) expression got simplified incorrectly by the query engine. Always test your full complexity against a known-good dataset before deploying to production. Another issue: contains checks on array fields like flags or hashes don't support partial matching. flags CONTAINS "debug" only matches when the exact flag value is "debug". It won't match "debug+verbose" or "DEBUG". Use regex if you need substring matching on these fields.
If you need to correlate across multiple data types — process events with network events for the same PID — FalconQL doesn't support joins. You have to export the results and cross-reference externally, or use Falcon's built-in investigation graphs which are slower but handle the correlation for you. There's no query-only solution that's faster than both approaches combined.
Query Templates for Common Scenarios
Suspicious parent-child process relationships: parent_process_name = "winword.exe" AND child_process_name IN ("powershell.exe", "cmd.exe", "mshta.exe", "rundll32.exe") DLL side-loading indicators:
dll_loaded_by_process = "svchost.exe" AND dll_original_filename != "svchost.exe" AND NOT path_contains("Windows\\System32") Living-off-the-land binary abuse: process_name IN ("certutil", "msiexec", "schtasks", "regsvr32", "installutil") AND process_command_line CONTAINS "--help" = FALSE
The last one excludes normal help output by filtering out invocations that contain --help. You could refine this further by adding exclusions for known legitimate use cases like scheduled task maintenance, but the baseline catches most malicious usage patterns. Registry persistence detection: registry_key_path CONTAINS "Run" AND registry_value_data CONTAINS "http" AND timestamp > "2024-06-01T00:00:00Z"
Registry Run keys with URLs as values are almost never legitimate. I've seen this catch both RedLine stealers and Cobalt Strike config delivery in under a minute of scanning. Bookmark your successful queries. Falcon saves your recent queries in the sidebar, but they disappear after about two weeks. I keep a text file with every query that proved useful, formatted with the intent comment on the line above. When I need to run something similar months later, I copy-paste and adjust instead of rebuilding from memory.