Another example of using match instead of join where I know I may get too many results. I have quite a few parameters set on this one, so its unlikely to run into limitations within CS, but another thing I layered on was setting time buckets to match between two event types. This will allow you to round to the nearest 30 minutes (expand that time as needed to address potential gaps) and checks that the LNK write occurred before the process execution for powershell or cmd. Commented as much as I could to help. But hopefully its useful.

Hypothesis: Look for a LNK file write (does not capture alternative extensions in this iteration, but you can use LnkFileWritten by itself if you want), followed by cmd or powershell execution from explorer.exe. This indicates a LNK file was written, and then within the same time window powershell or cmd was executed. This is likely a LNK file executing this activity, but keep in mind that parent process of explorer.exe is also in line with interactive shells, ps1 execution among many other things, so its not a for sure marker.

// Set the LNK File Write query as a table to match up with PowerShell or CMD ProcessRollup events defineTable(query={ in(field=#event_simpleName, values=[PeFileWritten,FileCreateInfo,NewExecutableWritten,LnkFileWritten]) | event_platform=Win | FileName=/.lnk$/i | FilePath = /\\Users\\/i | !FilePath=/Windows\\Recent|\\AppData\\Roaming\\Microsoft\\/i | !FilePath=/\\Users\\[^\\]+\\OneDrive?[^\\]+\\/i // Set unique fields for LNK events to prevent any issues with process event matching | lnkFileName := FileName | lnkFilePath := FilePath | lnkEventTime := @timestamp // Set a 30 minute time span in milliseconds -- first integer is the time in minutes you want your span to be -- MUST BE THE SAME AS THE PROCESS TIMESPAN | timeInterval := 30 * (60 * 1000) // Perform initial math for the floor function because you cannot perform math within the floor function | timeBucketTemp := @timestamp / timeInterval | lnkTimeBucket := (math:floor(timeBucketTemp) * timeInterval) }, include=[ComputerName, lnkFileName, lnkFilePath, ContextProcessId, ContextBaseFileName, ContextImageFileName, lnkEventTime, lnkTimeBucket], name="lnkFileWrite") // Perform ProcessRollup search to find full process path associated with the LNK Query | #event_simpleName="ProcessRollup2" | event_platform=Win | FileName=/(powershell|cmd)\.exe/i | ParentBaseFileName = /explorer.exe/i // Check the length of the command line to ensure it is not an interactive shell being launched by the user -- cmd.exe should be ~30 characters and powershell.exe should be ~60 without arguments | commandLength:=length(CommandLine) | test(commandLength > 65) // Set a 30 minute time span in milliseconds -- first integer is the time in minutes you want your span to be -- MUST BE THE SAME AS THE LNK TIMESPAN | timeInterval := 30 * (60 * 1000) // Perform initial math for the floor function because you cannot perform math within the floor function | timeBucketTemp := @timestamp / timeInterval | processTimeBucket := (math:floor(timeBucketTemp) * timeInterval) // Match Process Events with LNK Events | match(table="lnkFileWrite", field=[ComputerName,processTimeBucket], column=[ComputerName,lnkTimeBucket]) // Ensure any matching events have the LNK write before the process execution | test(lnkEventTime < @timestamp) // CHANGE ME to your base URL for Process and Graph Explorer Links | rootURL := "https://falcon.us-2.crowdstrike.com/" | ProcessStartTime := round(ProcessStartTime) | processStart:=formattime(field=ProcessStartTime, format="%m/%d %H:%M:%S") // Create URLs for Process and Graph Explorers | format("%sinvestigate/process-explorer/%s/%s?_cid=%s", field=["rootURL", "aid", "TargetProcessId", "cid"], as="ProcessExplorer") | format("%sgraphs/process-explorer/graph?id=pid:%s:%s", field=["rootURL", "aid", "TargetProcessId"], as="GraphExplorer") // Aggregate results by the offending full process path to determine its uniqueness across the environment | groupBy([ComputerName],function=([count(as=eventCount), min(@timestamp, as=processFirstSeen), collect([#event_simpleName,ImageFileName,CommandLine,lnkFilePath,lnkFileName,ContextBaseFileName,ContextImageFileName,lnkEventTime,UserName,ProcessExplorer,GraphExplorer,ContextProcessId,TargetProcessId])]), limit=1000) // Format Process First and Last Seen | formattime(field=processFirstSeen, format="%Y/%m/%d %H:%M:%S", as=processFirstSeen) | formattime(field=lnkEventTime, format="%Y/%m/%d %H:%M:%S", as=lnkEventTime)