// question

On a Linux system, analyze the process logs for suspicious command line activity that includes a sequence of commands indicating a pipe-to-shell operation. Identify the tool that was used to execute this piped command, paying special attention to its use in downloading and running script content.

Reference KQL answer and model-by-model comparison.

fca246a8-a585-4f28-a2df-6495973976a1
question id
3/23
models correct
1
valid answer
13%
success rate
// reference answer

Human-validated correct answer for this question.

bash
// model comparison
ModelAnswerKQL queryStatusTriesTimeCost
gemini-2.5-flash-preview-04-17
bash
DeviceProcessEvents
| where ActionType == "ProcessCreated"
| where ProcessCommandLine matches regex @".*\|\s*(bash|sh|python|pwsh)"
| project Timestamp, DeviceName, InitiatingProcessFileName, InitiatingProcessCommandLine, FileName, ProcessCommandLine, AccountName, ReportId
pass137.41s0.0233
gpt-35-turbo
no answer
no query generatedfail50.00s0.0118
gpt-4-turbo-2024-04-09
no answer
no query generatedfail50.00s0.2314
gpt-4.1
curl
DeviceProcessEvents
| where ProcessCommandLine contains '|'
| where ProcessCommandLine matches regex @"\|\s*(sh|bash|zsh|/bin/sh|/bin/bash|/bin/zsh)"
| extend download_tool = case(ProcessCommandLine has "curl", "curl", ProcessCommandLine has "wget", "wget", ProcessCommandLine has "python", "python", "other")
| extend script_url = extract("(https?://[^ |;]+)", 1, ProcessCommandLine)
| project TimeGenerated, DeviceName, AccountName, ProcessCommandLine, download_tool, script_url, InitiatingProcessCommandLine, InitiatingProcessFileName, FolderPath
fail222.32s0.0456
gpt-4.1-finetuned
python3
let whitelisted_tools = dynamic(["at", "batch", "bc", "bash", "curl", "ftp", "ksh", "mksh", "openssl", "php", "nc", "ncat", "netcat", "paste", "perl", "ps", "python2", "python3", "rpm", "sh", "sort", "ssh", "telnet", "timeout", "vi", "watch", "zsh"]);
DeviceProcessEvents
| where ProcessCommandLine contains "|" and ProcessCommandLine contains "%"
| extend pattern = extract(@"([a-zA-Z]+)\s*\|\s*\%*", 1, ProcessCommandLine)
| where isnotnull(pattern) and pattern !in (whitelisted_tools)
| summarize count() by DeviceName, SHA256, ProcessCommandLine, pattern, AccountName
| sort by count_ desc
| extend AccountName = tostring(split(AccountName, "@", 0)[0])
| extend UPNSuffix = tostring(split(AccountName, "@", 1)[1])
fail222.14s0.0266
gpt-4.1-mini
no answer
no query generatedfail50.00s0.0064
gpt-4.1-nano
unknown
DeviceProcessEvents
| where FileName in~ ('bash', 'sh', 'dash')
| where ProcessCommandLine contains '| sh' or ProcessCommandLine contains '| bash' or ProcessCommandLine contains '|python' or ProcessCommandLine contains '|curl' or ProcessCommandLine contains '|wget'
| extend toolUsed = case(
    ProcessCommandLine contains '| curl', 'curl',
    ProcessCommandLine contains '| wget', 'wget',
    ProcessCommandLine contains '| python', 'python',
    'unknown')
| project ProcessId, ProcessCommandLine, toolUsed, InitiatingProcessFileName, InitiatingProcessCreationTime, DeviceName
fail13.76s0.0010
gpt-4o
no answer
no query generatedfail50.00s0.0653
gpt-5-high
sh
let LinuxDevices = DeviceInfo
| where OSPlatform =~ "Linux"
| distinct DeviceId;
let SuspiciousPipeToShell =
DeviceProcessEvents
| where ActionType == "ProcessCreated"
| where DeviceId in (LinuxDevices)
| where isnotempty(ProcessCommandLine)
| extend cmd_l = tolower(ProcessCommandLine)
| where cmd_l contains "|"
| extend parts = split(cmd_l, "|")
| extend rhs = trim(" ", tostring(parts[array_length(parts)-1])),
         lhs = trim(" ", tostring(parts[0]))
| extend rhs_first = tostring(split(rhs, " ")[0]),
         rhs_second = tostring(split(rhs, " ")[1])
| extend rhs_first_base = tostring(split(rhs_first, "/")[array_length(split(rhs_first, "/"))-1])
// Identify the shell used to execute the piped content
| extend ToolUsedToExecute = case(
    rhs_first_base == "busybox" and rhs_second in ("sh","ash"), strcat("busybox ", rhs_second),
    rhs_first_base in ("sh","bash","zsh","ksh","dash","ash","/bin/sh","/bin/bash","/usr/bin/sh","/usr/bin/bash"), rhs_first_base,
    ""
  )
| where ToolUsedToExecute != ""
// Identify the downloader on the left side of the pipe
| extend DownloaderTool = case(
    lhs contains "curl", "curl",
    lhs contains "wget", "wget",
    lhs contains "aria2c", "aria2c",
    lhs contains "lwp-download", "lwp-download",
    lhs contains "fetch", "fetch",
    lhs contains "tftp", "tftp",
    lhs contains " ncat", "ncat",
    lhs contains " nc ", "nc",
    lhs contains "openssl", "openssl",
    lhs contains "python", "python",
    lhs contains "perl", "perl",
    lhs contains "php", "php",
    lhs contains " ftp ", "ftp",
    ""
  )
| where DownloaderTool != ""
// Extract a URL token if present (no regex needed)
| extend tokens = split(cmd_l, " ")
| mv-apply token = tokens on (
    where token startswith "http://" or token startswith "https://" or token startswith "ftp://"
    | summarize ScriptUrl = any(token)
  )
// Supportive indicators
| extend HasBase64Decode = iif(cmd_l contains "base64 -d" or cmd_l contains "base64 --decode" or cmd_l contains "openssl base64 -d", true, false)
| extend WgetStdout = iif(lhs contains "wget" and (lhs contains "-qO-" or lhs contains "-O-" or lhs contains "-O -" or lhs contains "-q -O -"), true, false)
| extend FromStdOut = case(
    DownloaderTool == "wget", WgetStdout,
    DownloaderTool == "curl", true,
    DownloaderTool in ("nc","ncat","openssl","python","perl","php","aria2c","fetch","lwp-download","tftp","ftp"), true,
    false
  )
| extend PipeSnippet = strcat(lhs, " | ", rhs_first, iif(isnotempty(rhs_second), strcat(" ", rhs_second), ""))
| project TimeGenerated, DeviceId, DeviceName, AccountName, FileName, FolderPath, ProcessId, ProcessCommandLine,
          PipeSnippet, ToolUsedToExecute, DownloaderTool, ScriptUrl, FromStdOut, HasBase64Decode,
          InitiatingProcessFileName, InitiatingProcessCommandLine, ReportId
| order by TimeGenerated desc;
// Detailed results
SuspiciousPipeToShell
pass3333.78s0.2063
gpt-5-mini-high
no answer
no query generatedfail50.00s0.0198
gpt-5-mini-low
no answer
no query generatedfail50.00s0.0206
gpt-5-mini-medium
curl
let tools = "curl|wget|fetch|aria2c|ftp|tftp|busybox|python|perl|nc|socat|bash";
let shells = "sh|bash|ash|zsh|ksh|/bin/sh|/bin/bash";
let pipePattern = strcat("(?i)\\b(", tools, ")\\b[\\s\\S]{0,500}?\\|\\s*(", shells, ")\\b");

// Combine process and device events where a command line is available
(DeviceProcessEvents
| where isnotempty(ProcessCommandLine)
| extend CommandLine = tostring(ProcessCommandLine)
| project TimeGenerated, SourceTable = "DeviceProcessEvents", DeviceName, DeviceId, AccountName, InitiatingProcessAccountName, ProcessId, ProcessCreationTime, CommandLine, InitiatingProcessFileName, InitiatingProcessCommandLine
)
| union (
DeviceEvents
| where isnotempty(ProcessCommandLine)
| extend CommandLine = tostring(ProcessCommandLine)
| project TimeGenerated, SourceTable = "DeviceEvents", DeviceName, DeviceId, AccountName = coalesce(AccountName, InitiatingProcessAccountName), InitiatingProcessAccountName, ProcessId = InitiatingProcessId, ProcessCreationTime = InitiatingProcessCreationTime, CommandLine, InitiatingProcessFileName, InitiatingProcessCommandLine
)
| extend cmd_lc = tolower(CommandLine)
// Pre-filter to reduce noise: look for download/tool keywords or obvious pipe-to-shell hints
| where cmd_lc matches regex strcat("(?i).*(", tools, ").*") or cmd_lc contains "|sh" or cmd_lc contains "| bash" or cmd_lc contains "|/bin/sh" or cmd_lc contains "|/bin/bash"

// Detect explicit pipe-to-shell patterns and capture downloader and target shell
| extend pipe_match = extract(pipePattern, 0, cmd_lc)
| extend detected_tool_pipe = extract(pipePattern, 1, cmd_lc)
| extend pipe_shell = extract(pipePattern, 2, cmd_lc)
| extend is_pipe = strlen(pipe_match) > 0

// Detect subshell or backtick usage that contains a download tool (e.g. sh -c "$(curl ...)")
| extend contains_dollar_paren = cmd_lc contains "$("
| extend contains_backtick = cmd_lc contains "`"
| extend detected_tool_subshell = extract(strcat("(?i)\\b(", tools, ")\\b"), 1, cmd_lc)
| extend is_subshell = (contains_dollar_paren or contains_backtick) and strlen(detected_tool_subshell) > 0

// Consolidate detected tool
| extend detected_tool = iff(is_pipe, detected_tool_pipe, iff(is_subshell, detected_tool_subshell, ""))

// Keep only rows with evidence
| where is_pipe or is_subshell or detected_tool != ""

| project TimeGenerated, SourceTable, DeviceName, DeviceId, AccountName, InitiatingProcessAccountName, ProcessId, ProcessCreationTime, CommandLine, detected_tool, pipe_shell, is_pipe, is_subshell, InitiatingProcessFileName, InitiatingProcessCommandLine

// Summarize
| summarize FirstSeen = min(TimeGenerated), LastSeen = max(TimeGenerated), Count = count(), Examples = make_list(CommandLine, 5) by DeviceName, DeviceId, AccountName, detected_tool, pipe_shell, InitiatingProcessFileName, InitiatingProcessCommandLine
| order by LastSeen desc
| take 200
fail493.14s0.0341
gpt-5-nano-high
no answer
no query generatedfail50.00s0.0106
gpt-5-nano-medium
no answer
no query generatedfail50.00s0.0089
grok-3-beta
curl
DeviceProcessEvents
| where ProcessCommandLine contains '|' and ProcessCommandLine matches regex @'(sh|bash|zsh|ksh|tcsh|dash)\s'
| where ProcessCommandLine contains 'curl' or ProcessCommandLine contains 'wget' or ProcessCommandLine contains 'http'
| join kind=inner (
    DeviceInfo
    | where OSPlatform == 'Linux'
) on DeviceId
| project DeviceName, FileName, ProcessCommandLine, ProcessCreationTime, InitiatingProcessFileName, InitiatingProcessCommandLine
| order by ProcessCreationTime desc
fail224.25s0.0759
grok-3-mini-beta
bash
DeviceInfo
| where OSPlatform == 'Linux'
| join kind=inner (DeviceProcessEvents) on DeviceId
| where ProcessCommandLine contains '|' and (ProcessCommandLine contains 'curl' or ProcessCommandLine contains 'wget') and (ProcessCommandLine contains 'bash' or ProcessCommandLine contains 'sh')
| project TimeGenerated, DeviceId, DeviceName, ProcessCommandLine, InitiatingProcessFileName
| summarize arg_max(TimeGenerated, *) by DeviceId, ProcessCommandLine
| order by TimeGenerated desc
pass227.78s0.0034
o1-high
curl
DeviceInfo
| where OSPlatform == "Linux"
| join kind=inner (
    DeviceProcessEvents
    | where ActionType == "ProcessCreated"
    | where ProcessCommandLine has "|" // check for pipe in command line
    | where ProcessCommandLine has_any ("curl", "wget") // check for download tools
    | where ProcessCommandLine has_any ("bash", "sh", "zsh", "python", "perl") // check for shell execution
) on DeviceId
| project TimeGenerated, DeviceName, AccountName, ProcessCommandLine, InitiatingProcessCommandLine, FolderPath, FileName, SHA256, OSPlatform
| order by TimeGenerated desc
fail289.71s0.7373
o1-low
curl
let LinuxDevices = DeviceInfo
| where OSPlatform == "Linux"
| distinct DeviceId;
DeviceProcessEvents
| where DeviceId in (LinuxDevices)
| where tolower(ProcessCommandLine) has "|"       // searching for pipe
| where tolower(ProcessCommandLine) has_any ("bash", "sh")  // searching for piping to a shell
| where tolower(ProcessCommandLine) has_any ("curl", "wget", "python", "nc")
| extend ToolUsed = extract(@"(curl|wget|python|nc)", 1, tolower(ProcessCommandLine))
| project TimeGenerated, DeviceName, AccountName, ProcessCommandLine, ToolUsed, InitiatingProcessCommandLine
| order by TimeGenerated desc
fail129.60s0.2759
o3-high
curl
//---------------------------------------------------------------------
//  Linux – downloader  |  shell   pipe-execution hunting query
//---------------------------------------------------------------------
// Time range is controlled by the portal; keep a safety filter as well
let lookBack = 14d;                // change if you run the query standalone

// 1. Build the regular expressions (case-insensitive)
let downloader_regex = @"(curl|wget|python[0-9]*|perl|php|nc|ncat|openssl|busybox|ftp)";
let shell_regex      = @"(bash|sh|zsh|ksh|dash)";
let pipe_to_shell_re = strcat(@"(?i)\b", downloader_regex, @"\b.*\|.*\b", shell_regex, @"\b");

// 2. Identify Linux devices
let LinuxDevices = DeviceInfo
    | where OSPlatform =~ "Linux"
    | project DeviceId;

// 3. Find suspicious processes that match the pipe-to-shell pattern
let SuspiciousProc = DeviceProcessEvents
    | where TimeGenerated >= ago(lookBack)
    | where DeviceId in (LinuxDevices)
    | where ProcessCommandLine has "|"                           // quick pre-filter
    | where ProcessCommandLine matches regex pipe_to_shell_re     // precise pattern
    | extend DownloaderTool = tolower(extract(downloader_regex, 1, ProcessCommandLine)),
             TargetShell    = tolower(extract(shell_regex,      1, ProcessCommandLine))
    | project TimeGenerated, DeviceName, AccountName, FileName, ProcessId, ProcessCommandLine,
              DownloaderTool, TargetShell,
              InitiatingProcessParentFileName, InitiatingProcessParentId, DeviceId;

// 4. Optional: bring in network traffic from the very same process
SuspiciousProc
| join kind=leftouter (
    DeviceNetworkEvents
    | where TimeGenerated >= ago(lookBack)
    | project DeviceId, InitiatingProcessId, RemoteUrl, RemoteIP, RemotePort, Protocol, NetEventTime = TimeGenerated
) on $left.DeviceId == $right.DeviceId and $left.ProcessId == $right.InitiatingProcessId
| project TimeGenerated, NetEventTime,
          DeviceName, AccountName, DownloaderTool, TargetShell,
          ProcessCommandLine,
          RemoteUrl, RemoteIP, RemotePort, Protocol,
          FileName, ProcessId, InitiatingProcessParentFileName, InitiatingProcessParentId
| order by TimeGenerated desc
fail281.31s0.0553
o3-mini-high
curl
DeviceProcessEvents
| extend cmd = iif(isnotempty(ProcessCommandLine), ProcessCommandLine, InitiatingProcessCommandLine)
| where isnotempty(cmd)
| where tolower(cmd) matches regex ".*(curl|wget).*\\|\\s*(sh|bash).*"
| extend downloadTool = tolower(extract("(?i)(curl|wget)", 1, cmd))
| project TimeGenerated, DeviceName, cmd, downloadTool
| sort by TimeGenerated desc
fail347.94s0.0297
o3-mini-low
curl
DeviceProcessEvents
| where isnotempty(InitiatingProcessCommandLine)
| where InitiatingProcessCommandLine has "|" and (InitiatingProcessCommandLine has "curl" or InitiatingProcessCommandLine has "wget")
| extend DownloadTool = case(InitiatingProcessCommandLine contains "curl", "curl",
                              InitiatingProcessCommandLine contains "wget", "wget",
                              "unknown")
| project TimeGenerated, DeviceName, InitiatingProcessCommandLine, DownloadTool
| sort by TimeGenerated desc
fail123.41s0.0106
o4-mini-high
curl
// Timerange is applied externally by the UI
DeviceInfo
| where OSPlatform has "Linux"
| project DeviceId, OSPlatform
| join kind=inner (
    DeviceProcessEvents
    // look for a pipeline to a shell with a download tool
    | where ProcessCommandLine has "|"
    | where ProcessCommandLine matches regex @"(?i)\b(?:curl|wget|fetch|perl|python)\b.*\|\s*(?:bash|sh)\b"
) on DeviceId
| extend
    Downloader = extract(@"(?i)\b(curl|wget|fetch|perl|python)\b", 1, ProcessCommandLine),
    Shell      = extract(@"(?i)\|\s*(bash|sh)\b", 1, ProcessCommandLine),
    URL        = extract(@"(?i)(?:curl|wget|fetch)\s+.*?(https?://\S+)", 1, ProcessCommandLine)
| project
    TimeGenerated,
    DeviceName,
    OSPlatform,
    AccountName = InitiatingProcessAccountName,
    ProcessFileName = FileName,
    ProcessCommandLine,
    Downloader,
    Shell,
    URL
| sort by TimeGenerated desc
fail4133.12s0.0525
o4-mini-low
curl
let LinuxDevices = DeviceInfo
  | where OSPlatform == "Linux"
  | distinct DeviceId;
DeviceProcessEvents
| where DeviceId in (LinuxDevices)
// Note: the TimeGenerated filter is set via the external time range picker
| where ProcessCommandLine matches regex "(?i)\\b(?:curl|wget|fetch)\\b.*\\|\\s*(?:bash|sh|zsh)\\b"
| extend 
    DownloaderTool = tostring(extract("(?i)\\b(curl|wget|fetch)\\b", 1, ProcessCommandLine)),
    ShellUsed = tostring(extract("\\|\\s*(bash|sh|zsh)\\b", 1, ProcessCommandLine))
| project 
    TimeGenerated,
    DeviceName,
    AccountName = InitiatingProcessAccountName,
    DownloaderTool,
    ShellUsed,
    ProcessCommandLine,
    InitiatingProcessFileName,
    ProcessId
| order by TimeGenerated desc
fail266.20s0.0235