Apologies it took so long to get back to this question. Thank you both for your enlightening responses. Fortunately, after reading through them, I managed to come across a working solution. | eval Parent_User=coalesce(ParentUser,null())
| eval user=coalesce(User,Account_Name)
| eval parent_image=coalesce(ParentImage, Creator_Process_Name)
| eval ParentCMD=coalesce(ParentCommandLine, null())
| eval parent_pid=coalesce(ParentProcessId, tonumber(Creator_Process_ID, 16))
| eval process_pid=coalesce(ProcessId, tonumber(New_Process_ID, 16))
| eval process_image=coalesce(Image,New_Process_Name)
| eval command=coalesce(CommandLine, Process_Command_Line, script_content)
| eval processInfo = '_time' + "|,|" + 'parent_image' + "|,|" + 'parent_pid' + "|,|" + 'command'
| convert timeformat="%F %T" ctime(_time) AS time
| table *
| outputlookup tempEvents.csv create_empty=true allow_updates=false output_format=splunk_mv_csv I already posted this above, but ultimately, all I'm doing is sending all my events to a lookup table before filtering it down. The important part is the processInfo field which a basic concatination of the above fields and split by a unique delimitor. The lookup overwrites itself each run. | lookup tempEvents.csv ComputerName AS ComputerName process_pid AS parent_pid process_image AS parent_image OUTPUT processInfo AS grand_processInfo _time AS grand_time
| eval min_grand_time = mvindex(mvsort(mvmap(grand_time, _time - grand_time)), 0)
| eval grand_processInfo = mvdedup(grand_processInfo)
| eval grand_processInfo = mvappend(grand_processInfo, "")
| eval true_grandparent=null()
| foreach mode=multivalue grand_processInfo [ eval split_mv = split(<<ITEM>>, "|,|"), true_grandparent = if((_time - tonumber(mvindex(split_mv, 0))) = min_grand_time, mvappend(true_grandparent, tostring(<<ITEM>>)), true_grandparent)]
| rex field=true_grandparent "^\d+\|,\|(?<grandparent_image>.+?)\|,\|(?<grandparent_pid>.+?)\|,\|(?<parent_commandline>.+$)" Here, the lookup command re-adds events from the lookup table which match the ComputerName to the lookup's ComputerName fields, the process_id as the lookup's parent_id, and the process_image as the lookup's parent_image. The original issue was caused due to the fact that the lookup command would add multiple events where there should idealy only be a single event which matches for all three of those fields(ComputerName, process_id, and process_image). This is just a consequences of how Windows uses process_ids. They're only unique for as a long as a process is open. As soon as a process is ended, its process id can be recycled. As such, my solution hinged on comparing the _time field for the search events and the lookup events. In the first line after the lookup command, I declare a new field called min_grand_time and use mvmap to itterate over the grand_time field from the lookup table. I subtract each value in grand_time from the current search event's _time field to get a positive integer(time doesn't move backwards, after all). The resulting mv field is then sorted using mvsort(this isn't actually a sort based on number values, but it works out regardless). After the sort, I can use mvindex to return the value at the first index to get the value closest to 0. The next line is a dedup which I noticed was needed in grand_processInfo. I only realized after I added the dedup that the extra events are caused by me querying both sysmon and windows_event logs(a log from each exists for each process created on a system). I'll adjust the seach later by changing out the first table command(just prior to the initial outputlookup) and using stats to filter it down before sending it to the lookup table. After the dedup, grand_processInfo is single value field......however, I need it register as a mv field for the purpose of the following foreach command. To do so without actually adding anything, I use mvappend to add an empty string. I then create new field called true_grandparent for use in the foreach command. I may not need to declare a field prior to using it in a foreach, but I did so anyway. The foreach is where the magic happens which I realized I could use from ITWhisperer(thank you for that). Using it, I itterate over the grand_processInfo field(which is why I needed to use mvappend earlier in the case that grand_processInfo was a single value field). In the loop, I declare a new field named split_mv where I use the split command to split the current <<ITEM>> along its delimiter which I created at the start of the search. After it's been split into a new mv field with the _time, parent_image, parent_id, and command fields, I declare a new field called true_grandparent where if the _time for a search event minus the _time for a lookup event equals the value found in the field min_grand_time which I declared earlier, then I use mvappend to add the current <<ITEM>> to true_grandparent. Otherwise, it simply stays the same. By running this foreach over each value of grand_processInfo, I'm guarnteed to only get only one value appended to true_grandparent. Finally, I simply use a rex command on true_grandparent to split out the fields again. It isn't shown in the above code snippets, but I also add a fillnull command to 'fill in' the blank spots that could occur if a process's grandparent process isn't found in the lookup table. | fillnull value="N/A" great_grandparent_image, grandparent_image, great_grandparent_pid, grandparent_pid, grandparent_commandline, parent_commandline Hopefully the above explanation makes sense. I've been testing over the past few days, and fortunately, it does seem to work exactly as intended. With any luck, others attempting to do the same thing will be able to follow some variation of the above steps to achieve the same result. Thank you both, ITWhisperer and PickleRick for your expertise.
... View more