// question

During a Linux investigation, you notice processes spawning curl and wget commands that pull a script from a remote GitHub raw URL and pipe it into bash. Identify the name of the script that was retrieved and executed.

Reference KQL answer and model-by-model comparison.

d0c88567-803d-4dca-99b4-7ce65e7b257c
question id
6/23
models correct
1
valid answer
26%
success rate
// reference answer

Human-validated correct answer for this question.

echo-art-fish.sh
// model comparison
ModelAnswerKQL queryStatusTriesTimeCost
gemini-2.5-flash-preview-04-17
no answer
no query generatedfail50.00s0.0330
gpt-35-turbo
no answer
no query generatedfail50.00s0.0114
gpt-4-turbo-2024-04-09
LinEnum.sh
DeviceProcessEvents
| where ProcessCommandLine has_any('curl', 'wget') and ProcessCommandLine contains 'bash'
| extend ScriptUrl = tostring(split(ProcessCommandLine, '|')[0])
| extend ScriptName = tostring(split(ScriptUrl, '/')[array_length(split(ScriptUrl, '/')) - 1])
| project DeviceName, FileName, ProcessCommandLine, ScriptUrl, ScriptName
fail114.74s0.0732
gpt-4.1
no answer
no query generatedfail50.00s0.0434
gpt-4.1-finetuned
no answer
no query generatedfail50.00s0.0426
gpt-4.1-mini
pipe-to-shell.sh
DeviceProcessEvents
| where ProcessCommandLine contains @"curl" or ProcessCommandLine contains @"wget"
| where ProcessCommandLine contains @"raw.githubusercontent.com"
| where ProcessCommandLine contains @"bash" or ProcessCommandLine contains @"| bash"
| extend ScriptUrl = extract(@"https?://raw\.githubusercontent\.com[^\s|]+", 0, ProcessCommandLine)
| extend ScriptName = extract(@"[^/]+$", 0, ScriptUrl)
| project TimeGenerated, DeviceName, ProcessCommandLine, ScriptUrl, ScriptName
| distinct ScriptName, DeviceName, TimeGenerated, ProcessCommandLine, ScriptUrl
fail120.65s0.0029
gpt-4.1-nano
no answer
no query generatedfail50.00s0.0017
gpt-4o
AutoSUID.sh
DeviceProcessEvents
| where InitiatingProcessCommandLine contains "curl" or InitiatingProcessCommandLine contains "wget"
| where InitiatingProcessCommandLine matches regex @"https:\/\/raw.githubusercontent.com\/.*\.sh" and InitiatingProcessCommandLine contains "bash"
| extend ScriptName = extract(@"[\w-]+\.sh", 0, InitiatingProcessCommandLine)
| project Timestamp, DeviceId, DeviceName, InitiatingProcessCommandLine, ScriptName
fail124.29s0.0201
gpt-5-high
pipe-to-shell.sh
let linux_devices = DeviceInfo
| where OSPlatform == "Linux"
| project DeviceId;
let gh_hosts = dynamic(["raw.githubusercontent.com","gist.githubusercontent.com","github.com"]);
let pipe_indicators = dynamic(["| bash","|bash","| sh","|sh","bash -c","sh -c","bash <(","sh <("]);
// Path A: Process events where the full pipeline is visible in the command line
let process_hits = DeviceProcessEvents
| where DeviceId in (linux_devices)
| where ProcessCommandLine has_any ("curl","wget")
| where ProcessCommandLine has_any (gh_hosts)
| where ProcessCommandLine has_any (pipe_indicators)
| extend Url = extract(@"https?://[^\s'""|)><]+", 0, ProcessCommandLine)
| project TimeGenerated, DeviceId, DeviceName, AccountName, InitiatingProcessAccountName, FileName, ProcessCommandLine, Url;
// Path B: Network events from curl/wget to GitHub raw hosts (captures URL even if pipeline not in process cmdline)
let network_hits = DeviceNetworkEvents
| where DeviceId in (linux_devices)
| where InitiatingProcessFileName in ("curl","wget")
| where RemoteUrl has_any (gh_hosts)
| project TimeGenerated, DeviceId, DeviceName, AccountName=InitiatingProcessAccountName, InitiatingProcessAccountName, FileName=InitiatingProcessFileName, ProcessCommandLine=InitiatingProcessCommandLine, Url=RemoteUrl;
union process_hits, network_hits
| where isnotempty(Url)
| extend ScriptName = tostring(extract(@"([^/?#]+)(?:\?[^#]*)?(?:#.*)?$", 1, Url))
| where isnotempty(ScriptName)
| summarize FirstSeen=min(TimeGenerated), LastSeen=max(TimeGenerated), DeviceCount=dcount(DeviceId), Devices=make_set(DeviceName), Examples=make_set(Url, 5) by ScriptName
| order by LastSeen desc
fail1181.17s0.0818
gpt-5-mini-high
echo-art-fish.sh
// Identify scripts downloaded from GitHub raw URLs via curl/wget piped to bash
let github_raw_patterns = dynamic(["raw.githubusercontent.com", "raw.githubusercontent.com", "raw.github.com","githubusercontent.com"]);
let curl_wget_filter =  (s:string) {
    s has "curl" and s has "|" and s has "bash" or
    s has "wget" and (s has "-O-" or s has "-qO-" or s has "-O -") and s has "|" and s has "bash"
};
// Search DeviceProcessEvents (preferred) and DeviceEvents
let procEvents = DeviceProcessEvents
| where isnotempty(ProcessCommandLine)
| where curl_wget_filter(ProcessCommandLine) or curl_wget_filter(InitiatingProcessCommandLine)
| extend CmdLine = coalesce(ProcessCommandLine, InitiatingProcessCommandLine)
| extend RemoteUrl = extract(@"((https?://)[^\s'\|]+raw[^\s'\|]+|https?://[^\s'\|]+raw\.githubusercontent\.com[^\s'\|]+|https?://[^\s'\|]*raw\.githubusercontent\.com[^\s'\|]+|https?://raw\.githubusercontent\.com[^\s'\|]+|https?://raw\.githubusercontent\.com[^\s'\|]+)", 1, CmdLine)
| extend RemoteUrl = iff(isempty(RemoteUrl), extract(@"https?://[^\s'\|]+githubusercontent\.com[^\s'\|]+", 0, CmdLine), RemoteUrl)
| extend ScriptName = trim_end(@"/", tostring(parse_url(RemoteUrl).Path))
| extend ScriptName = replace_regex(ScriptName, @"^.*/", "")
| project TimeGenerated, DeviceName, ProcessId, ProcessCreationTime, FileName, CmdLine, RemoteUrl, ScriptName
;
let devEvents = DeviceEvents
| where isnotempty(ProcessCommandLine) or isnotempty(InitiatingProcessCommandLine)
| where curl_wget_filter(ProcessCommandLine) or curl_wget_filter(InitiatingProcessCommandLine)
| extend CmdLine = coalesce(ProcessCommandLine, InitiatingProcessCommandLine)
| extend RemoteUrl = extract(@"https?://[^\s'\|]+(raw\.githubusercontent\.com|raw\.github\.com|rawgithubusercontent)\/[^\s'\|]+", 0, CmdLine)
| extend RemoteUrl = iff(isempty(RemoteUrl), extract(@"https?://[^\s'\|]+githubusercontent\.com[^\s'\|]+", 0, CmdLine), RemoteUrl)
| extend ScriptName = trim_end(@"/", tostring(parse_url(RemoteUrl).Path))
| extend ScriptName = replace_regex(ScriptName, @"^.*/", "")
| project TimeGenerated, DeviceName, ProcessId=InitiatingProcessId, ProcessCreationTime=InitiatingProcessCreationTime, FileName=InitiatingProcessFileName, CmdLine, RemoteUrl, ScriptName
;
// Combine and return distinct script names with sample evidence
procEvents
| union devEvents
| where isnotempty(RemoteUrl)
| summarize count(), Samples = make_list(pack_all(), 10) by ScriptName
| order by count_ desc
| project ScriptName, Count = count_, Samples
pass125.69s0.0049
gpt-5-mini-low
no answer
no query generatedfail50.00s0.0149
gpt-5-mini-medium
no answer
no query generatedfail50.00s0.0155
gpt-5-nano-high
echo-art-fish.sh
DeviceProcessEvents
| where InitiatingProcessCommandLine contains "curl" or InitiatingProcessCommandLine contains "wget"
| where InitiatingProcessCommandLine contains "raw.githubusercontent.com" or InitiatingProcessCommandLine contains "githubusercontent.com"
| where InitiatingProcessCommandLine contains "| bash" or InitiatingProcessCommandLine contains "| /bin/bash" or InitiatingProcessCommandLine contains "| /bin/sh"
| extend ScriptUrl = extract("https?://[^ ]+", 0, InitiatingProcessCommandLine)
| extend ScriptName = extract("https?://[^/]+/.*/([^/?#]+)(?:[?#].*)?$", 1, ScriptUrl)
| where isnotempty(ScriptName)
| summarize FirstSeen = min(TimeGenerated), DeviceCount = dcount(DeviceId), Hits = count() by ScriptName, ScriptUrl
| order by Hits desc
pass262.30s0.0042
gpt-5-nano-medium
no answer
no query generatedfail50.00s0.0093
grok-3-beta
echo-art-fish.sh
DeviceProcessEvents
| where ProcessCommandLine contains 'curl' or ProcessCommandLine contains 'wget'
| where ProcessCommandLine contains 'github'
| where ProcessCommandLine contains '| bash'
| extend ScriptName = extract('.*\\/([^\\/\\?\\s]+)', 1, ProcessCommandLine)
| project Timestamp, DeviceName, ProcessCommandLine, ScriptName
| order by Timestamp desc
pass539.67s0.0947
grok-3-mini-beta
pipe-to-shell.sh
DeviceProcessEvents
| where ProcessCommandLine matches regex @'\bcurl\b.*\|\b.*\bbash\b' or ProcessCommandLine matches regex @'\bwget\b.*\|\b.*\bbash\b'
| extend FullURL = extract(@'(https?://[^\s|]+)', 1, ProcessCommandLine)
| extend arr = split(FullURL, '/')
| extend ScriptName = arr[array_length(arr) - 1]
| project TimeGenerated, DeviceName, ProcessCommandLine, ScriptName
| summarize ScriptNames = make_set(ScriptName) by DeviceName
fail336.57s0.0045
o1-high
LinEnum.sh
DeviceProcessEvents
| where ActionType == "ProcessCreated"
| where ProcessCommandLine has_any ("curl", "wget")
| where ProcessCommandLine has "raw.githubusercontent.com" and ProcessCommandLine has "bash"
| extend DownloadedUrl = extract(@"(https?:\/\/[^\s|]+)", 0, ProcessCommandLine)
| extend ScriptName = extract(@"\/([^\/\?]+)(\?.*)?$", 1, DownloadedUrl)
| project TimeGenerated, DeviceId, DeviceName, AccountName, ProcessCommandLine, DownloadedUrl, ScriptName
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by DeviceId, DeviceName, AccountName, ScriptName
| order by EndTime desc
fail2115.36s0.6308
o1-low
echo-art-fish.sh
DeviceProcessEvents
| where ActionType == "ProcessCreated"
| where tolower(InitiatingProcessCommandLine) has_any ("curl", "wget")
  and tolower(InitiatingProcessCommandLine) has "raw.githubusercontent.com"
  and tolower(InitiatingProcessCommandLine) has "| bash"
| extend ScriptName = extract(@"raw\.githubusercontent\.com[^ ]+/(?<script>[^ ]+)", 1, tolower(InitiatingProcessCommandLine))
| project Timestamp, DeviceName, InitiatingProcessCommandLine, ScriptName
| order by Timestamp desc
pass361.01s0.5518
o3-high
LinEnum.sh
// ------------------------------------------------------------------------------------------------
// Detect scripts fetched from GitHub raw via curl / wget and immediately executed through a pipe
// (e.g.  curl https://raw.githubusercontent.com/user/repo/script.sh | bash )
// ------------------------------------------------------------------------------------------------
// Time range will be added automatically by the hunting portal, so no explicit where TimeGenerated
// clause is required here.
let pipeIndicators = dynamic(["|bash","| bash","|sh","| sh","bash -","sh -"]);
DeviceProcessEvents
// 1.  Focus on the typical downloader utilities and the shell that is used to execute the script
| where FileName  in~ ("bash","sh","curl","wget")
// 2.  Command-line must reference GitHub raw content and contain a pipe that forwards the content to a shell
| where ProcessCommandLine has "raw.githubusercontent.com"
      and ProcessCommandLine has_any(pipeIndicators)
// 3.  Pull the full raw URL out of the command line (greedy until we hit whitespace or the pipe symbol)
| extend RawUrl = extract(@"((?:http|https)://[^\s'""|<>]+)", 1, ProcessCommandLine)
| where isnotempty(RawUrl)
// 4.  Derive just the script name (the last element after the final '/')
| extend ScriptName = extract(@"([^/]+)$", 1, RawUrl)
// 5.  Return the interesting columns; hunting portal will add the time filter
| project TimeGenerated,
          DeviceName,
          AccountName,
          Executable       = FileName,
          ProcessCommandLine,
          RawUrl,
          ScriptName
| order by TimeGenerated desc
fail273.57s0.0418
o3-mini-high
echo-art-fish.sh
DeviceProcessEvents
| where ProcessCommandLine has_any ("curl", "wget")
| where ProcessCommandLine contains "raw.githubusercontent.com"
| where ProcessCommandLine contains "| bash"
| extend ScriptName = extract(@"https?://raw\.githubusercontent\.com/[^/]+/[^/]+/[^/]+/([^ \|]+)", 1, ProcessCommandLine)
| where isnotempty(ScriptName)
| summarize Occurrences = count() by ScriptName, ProcessCommandLine
| order by Occurrences desc
pass113.12s0.0088
o3-mini-low
LinEnum.sh
DeviceProcessEvents
| where ProcessCommandLine has_any ("curl", "wget")
| where ProcessCommandLine contains "raw.githubusercontent.com"
| where ProcessCommandLine contains "bash"
| extend ScriptName = extract(@"raw\.githubusercontent\.com/\S+/([^/]+\.sh)", 1, ProcessCommandLine)
| where isnotempty(ScriptName)
| distinct ScriptName
fail161.96s0.0116
o4-mini-high
echo-art-fish.sh
DeviceProcessEvents
| where TimeGenerated between (startofday(ago(7d)) .. now())  // timerange will be set by the external control
| where ProcessCommandLine has_any ("curl", "wget")
| where ProcessCommandLine contains "raw.githubusercontent.com"
| where ProcessCommandLine contains "bash" or ProcessCommandLine contains "|"
// Extract the raw.githubusercontent.com URL
| extend DownloadUrl = extract(@"(https?://raw\.githubusercontent\.com/[^\s|]+)", 1, ProcessCommandLine)
| where isnotempty(DownloadUrl)
// Parse out the script name (last segment of the URL)
| extend ScriptName = tostring(split(DownloadUrl, "/")[array_length(split(DownloadUrl, "/")) - 1])
| project TimeGenerated, DeviceName, InitiatingProcessFileName, ProcessCommandLine, DownloadUrl, ScriptName
| sort by TimeGenerated desc
pass130.30s0.0134
o4-mini-low
pipe-to-shell.sh
DeviceProcessEvents
| where ActionType == "ProcessCreated"
// Only look for curl or wget invocations
| where ProcessCommandLine contains "curl" or ProcessCommandLine contains "wget"
// Focus on raw GitHub URLs
| where ProcessCommandLine contains "raw.githubusercontent.com"
// Extract the full URL being fetched
| extend DownloadUrl = extract(@"(https?://raw\.githubusercontent\.com/[^\s'""\|]+)", 1, ProcessCommandLine)
| where isnotempty(DownloadUrl)
// Derive the script name from the URL by taking the last path segment
| extend ScriptName = tostring(split(DownloadUrl, "/")[-1])
// Filter for typical shell script extensions
| where ScriptName endswith ".sh" or ScriptName endswith ".bash"
// Summarize by script name to see which scripts were pulled and executed
| summarize Count = count() by ScriptName
| sort by Count desc
fail4108.17s0.0301