// question

An attacker on a Linux host may try to enumerate installed shells by reading the system file that lists valid shells. Using process or syslog data, determine which command was executed to perform this enumeration.

Reference KQL answer and model-by-model comparison.

bf23c7dc-1004-4949-8262-4c1d1ef87702
question id
16/23
models correct
1
valid answer
70%
success rate
// reference answer

Human-validated correct answer for this question.

cat /etc/shells
// model comparison
ModelAnswerKQL queryStatusTriesTimeCost
gemini-2.5-flash-preview-04-17
no answer
no query generatedpass50.00s0.0363
gpt-35-turbo
cat
DeviceProcessEvents
| where ProcessCommandLine contains 'cat /etc/shells' or ProcessCommandLine contains 'less /etc/shells' or ProcessCommandLine contains 'more /etc/shells'
fail13.47s0.0042
gpt-4-turbo-2024-04-09
no answer
no query generatedfail50.00s0.2278
gpt-4.1
cat /etc/shells
DeviceProcessEvents
| where ProcessCommandLine has "/etc/shells"
| project TimeGenerated, DeviceName, AccountName, FileName, ProcessCommandLine, InitiatingProcessAccountName, InitiatingProcessCommandLine
pass17.90s0.0115
gpt-4.1-finetuned
cat /etc/shells
DeviceProcessEvents
| where AccountDomain != "" 
| where AccountName != ""
//Detect direct `/bin/sh /etc/shells` and similar, or indirect `/etc/shells |awk -F: '/bin/{print $1}'` and similar
| where (ProcessCommandLine in~ ("cat /etc/shells", "more /etc/shells", "less /etc/shells", "tail /etc/shells", "head /etc/shells", "awk -F: '/bin/{print $1}' /etc/shells", "/etc/shells |awk -F: '/bin/{print $1}'", "/etc/shells |awk -F:' '{print $1}'", "/bin/{print ", "|awk -F: '/bin/'") and ProcessCommandLine contains "/etc/shells") 
or (ProcessCommandLine has "/etc/shells" and InitiatingProcessFileName == "/bin/sh" )
//Raise if OS identified as Linux.
| summarize count() by bin(TimeGenerated, 1h), AccountName, InitiatingProcessAccountSid, InitiatingProcessCommandLine, InitiatingProcessFileName, DeviceName, InitiatingProcessId, ProcessCommandLine,ProcessId, Type, InitiatingProcessParentFileName
| extend timestamp = bin(TimeGenerated, 1h)
| extend Account_0_Name = AccountName
| extend Account_0_Sid = InitiatingProcessAccountSid
| extend Host_0_HostName = DeviceName
| extend Process_0_ProcessId = ProcessId
| extend Process_0_CommandLine = InitiatingProcessCommandLine
| extend Process_1_ProcessId = InitiatingProcessId
| extend Process_1_ProcessId = ProcessId
| extend Process_1_CommandLine = ProcessCommandLine
| extend Process_0_FileName = InitiatingProcessParentFileName
| extend Type = strcat(Type, "_" , InitiatingProcessParentFileName)
pass225.41s0.0258
gpt-4.1-mini
cat /etc/shells
DeviceProcessEvents
| where ProcessCommandLine contains "/etc/shells" or FileName contains "etc/shells"
| project DeviceName, AccountName, ProcessCommandLine, TimeGenerated
| sort by TimeGenerated desc
pass128.28s0.0022
gpt-4.1-nano
cat /etc/shells
DeviceProcessEvents
| where ProcessCommandLine has_any ('cat', 'less', 'more', 'head')
| where ProcessCommandLine contains '/etc/shells'
| project TimeGenerated, DeviceName, ProcessCommandLine
pass25.52s0.0010
gpt-4o
cat /etc/shells
let suspicious_file="/etc/shells";
DeviceProcessEvents
| where ProcessCommandLine has suspicious_file or InitiatingProcessCommandLine has suspicious_file
| project TimeGenerated, DeviceId, DeviceName, AccountName, InitiatingProcessCommandLine, InitiatingProcessCreationTime
pass228.09s0.0289
gpt-5-high
cat
let di = DeviceInfo
| summarize arg_max(TimeGenerated, OSPlatform) by DeviceId
| project DeviceId, OSPlatform;
union isfuzzy=true
(
  DeviceProcessEvents
  | where ActionType == "ProcessCreated"
  | where ProcessCommandLine contains "/etc/shells" or ProcessCommandLine contains "getent shells" 
      or InitiatingProcessCommandLine contains "/etc/shells" or InitiatingProcessCommandLine contains "getent shells"
  | extend SourceTable = "DeviceProcessEvents"
),
(
  DeviceEvents
  | where ProcessCommandLine contains "/etc/shells" or ProcessCommandLine contains "getent shells" 
      or InitiatingProcessCommandLine contains "/etc/shells" or InitiatingProcessCommandLine contains "getent shells"
  | extend SourceTable = "DeviceEvents"
)
| join kind=inner di on DeviceId
| where OSPlatform =~ "Linux"
| extend EnumerationMethod = case(
    ProcessCommandLine contains "/etc/shells" or InitiatingProcessCommandLine contains "/etc/shells", "Direct read of /etc/shells",
    ProcessCommandLine contains "getent shells" or InitiatingProcessCommandLine contains "getent shells", "getent shells via NSS",
    "Other"
  )
| project TimeGenerated, DeviceName, AccountName, SourceTable, ActionType, FileName,
          ProcessCommandLine, InitiatingProcessFileName, InitiatingProcessCommandLine,
          InitiatingProcessParentFileName, InitiatingProcessParentId, ProcessId,
          DeviceId, MachineGroup, EnumerationMethod
| order by TimeGenerated desc
fail195.13s0.0539
gpt-5-mini-high
cat /etc/shells
let toolsPattern = "(?i)\b(cat|less|more|tail|head|awk|sed|grep|strings|cut|xargs|stat|ls|getent|python|perl|ruby|sh|bash)\b";

// normalize candidate command field from both tables
let proc = DeviceProcessEvents
| extend Cmd = coalesce(ProcessCommandLine, InitiatingProcessCommandLine, tostring(AdditionalFields.CommandLine), tostring(AdditionalFields.command), tostring(AdditionalFields))
| where isnotempty(Cmd)
| where tolower(Cmd) contains "/etc/shells" or tolower(Cmd) contains "etc/shells" or (Cmd matches regex toolsPattern and tolower(Cmd) contains "/etc/")
| extend Tool = tolower(extract(toolsPattern, 1, Cmd))
| project TimeGenerated, DeviceName, DeviceId, AccountName, InitiatingProcessFileName, FileName, ProcessId, Tool, Cmd;

let dev = DeviceEvents
| extend Cmd = coalesce(ProcessCommandLine, InitiatingProcessCommandLine, tostring(AdditionalFields.CommandLine), tostring(AdditionalFields.command), tostring(AdditionalFields))
| where isnotempty(Cmd)
| where tolower(Cmd) contains "/etc/shells" or tolower(Cmd) contains "etc/shells" or (Cmd matches regex toolsPattern and tolower(Cmd) contains "/etc/")
| extend Tool = tolower(extract(toolsPattern, 1, Cmd))
| project TimeGenerated, DeviceName, DeviceId, AccountName, InitiatingProcessFileName, FileName, ProcessId, Tool, Cmd;

(proc
| union dev)
// restrict to Linux machines to reduce noise and volume (adjust/remove as needed)
| join kind=inner (
    DeviceInfo
    | where isnotempty(OSPlatform) and tolower(OSPlatform) contains "linux"
    | project DeviceId
) on DeviceId
| summarize Count = count(), FirstSeen = min(TimeGenerated), LastSeen = max(TimeGenerated) by DeviceName, DeviceId, AccountName, Tool, Cmd
| order by LastSeen desc
| top 200 by LastSeen
| project DeviceName, DeviceId, AccountName, Tool=coalesce(Tool, "(not-extracted)"), CommandLine=Cmd, Count, FirstSeen, LastSeen
pass593.99s0.0250
gpt-5-mini-low
cat /etc/shells
// Search for enumeration of the shells file (/etc/shells) across process and file/device events

// Process events: commands that reference /etc/shells or use common read utilities
let proc_hits = DeviceProcessEvents
| where isnotempty(ProcessCommandLine)
| where ProcessCommandLine has "/etc/shells"
    or (ProcessCommandLine has "cat" and ProcessCommandLine has "shells")
    or (ProcessCommandLine has "grep" and ProcessCommandLine has "shells")
    or (ProcessCommandLine has "less" and ProcessCommandLine has "shells")
    or (ProcessCommandLine has "more" and ProcessCommandLine has "shells")
    or (ProcessCommandLine has "awk" and ProcessCommandLine has "shells")
    or (ProcessCommandLine has "sed" and ProcessCommandLine has "shells")
    or (ProcessCommandLine has "head" and ProcessCommandLine has "shells")
    or (ProcessCommandLine has "tail" and ProcessCommandLine has "shells")
    or (ProcessCommandLine has "strings" and ProcessCommandLine has "shells")
    or (ProcessCommandLine has "python" and ProcessCommandLine has "shells")
    or (ProcessCommandLine has "perl" and ProcessCommandLine has "shells")
| project TimeGenerated, DeviceName, Account = InitiatingProcessAccountName, ProcessId, ParentProcessId = InitiatingProcessParentId, ProcessCommandLine, ProcessFileName = InitiatingProcessFileName, SourceTable = "DeviceProcessEvents";

// File events: file reads or references to /etc/shells
let file_hits = DeviceFileEvents
| where (isnotempty(FileName) and FileName has "shells")
    or (isnotempty(FolderPath) and FolderPath has "/etc/shells")
    or (isnotempty(PreviousFileName) and PreviousFileName has "shells")
    or (isnotempty(AdditionalFields) and tostring(AdditionalFields) has "/etc/shells")
| project TimeGenerated, DeviceName, Account = InitiatingProcessAccountName, ProcessId = InitiatingProcessId, ParentProcessId = InitiatingProcessParentId, ProcessCommandLine = InitiatingProcessCommandLine, FileName, FolderPath, ActionType, SourceTable = "DeviceFileEvents";

// Generic device events (syslog-like) that may reference /etc/shells in AdditionalFields or file fields
let evt_hits = DeviceEvents
| where (isnotempty(FileName) and FileName has "shells")
    or (isnotempty(FolderPath) and FolderPath has "/etc/shells")
    or (isnotempty(AdditionalFields) and tostring(AdditionalFields) has "/etc/shells")
| project TimeGenerated, DeviceName, Account = AccountName, ProcessId = InitiatingProcessId, ParentProcessId = InitiatingProcessParentId, ProcessCommandLine = InitiatingProcessCommandLine, FileName, FolderPath, ActionType, SourceTable = "DeviceEvents";

// Combine, normalize and summarize for investigation
proc_hits
| union file_hits
| union evt_hits
| extend ProcessCommandLine = coalesce(ProcessCommandLine, strcat(FileName, " ", FolderPath, " ", tostring(ActionType)))
| summarize FirstSeen = min(TimeGenerated), LastSeen = max(TimeGenerated), Count = count(), ExampleDevices = make_set(DeviceName, 5), ExampleCommands = make_set(ProcessCommandLine, 10), ExampleFiles = make_set(FileName, 5) by Account = coalesce(Account, "<unknown>"), SourceTable, ProcessId, ParentProcessId
| order by LastSeen desc
| limit 500
pass360.37s0.0143
gpt-5-mini-medium
no answer
no query generatedfail50.00s0.0160
gpt-5-nano-high
no answer
no query generatedfail50.00s0.0073
gpt-5-nano-medium
no answer
no query generatedfail50.00s0.0080
grok-3-beta
cat /etc/shells
DeviceProcessEvents
| where ProcessCommandLine has '/etc/shells' or ProcessCommandLine has 'cat /etc/shells' or ProcessCommandLine has 'more /etc/shells' or ProcessCommandLine has 'less /etc/shells' or ProcessCommandLine has 'nano /etc/shells' or ProcessCommandLine has 'vim /etc/shells' or ProcessCommandLine has 'vi /etc/shells'
| join kind=inner (
    DeviceInfo
    | where OSPlatform has 'Linux' or OSDistribution has 'Linux'
) on DeviceId
| project TimeGenerated, DeviceName, ProcessCommandLine, AccountName, InitiatingProcessAccountName, InitiatingProcessCommandLine
| order by TimeGenerated desc
pass340.73s0.0617
grok-3-mini-beta
cat /etc/shells
DeviceProcessEvents
| where ProcessCommandLine contains '/etc/shells'
| project TimeGenerated, DeviceName, AccountName, ProcessCommandLine
| order by TimeGenerated desc
pass115.66s0.0016
o1-high
cat
DeviceProcessEvents
| join kind=inner (
    DeviceInfo
    | where OSPlatform == "Linux"
) on DeviceId
| where ProcessCommandLine has "/etc/shells"
| project TimeGenerated, DeviceName, AccountName, ProcessCommandLine
pass273.09s0.3824
o1-low
cat
DeviceInfo
| where OSPlatform == "Linux"
| join (
    DeviceProcessEvents
    | where ProcessCommandLine has "/etc/shells"
) on DeviceId
| project TimeGenerated, DeviceName, AccountName, ProcessCommandLine, InitiatingProcessCommandLine
| order by TimeGenerated desc
pass130.13s0.2281
o3-high
cat
// Detect enumeration of interactive shells on Linux hosts
let lookback       = 7d;                       // will be overwritten by portal / function-caller
let shell_viewers  = dynamic(["cat","less","more","head","tail","awk","sed","grep","cut","strings"]);
DeviceProcessEvents
| where TimeGenerated >= ago(lookback)
// ---- Step 1 : narrow down to commands that reference the shells file ----
| where ProcessCommandLine has "/etc/shells" 
      or (FileName =~ "getent" and ProcessCommandLine has "shells")
      or (FileName in~ (shell_viewers) and ProcessCommandLine has "shells")
// ---- Step 2 : Pick the interesting fields for triage ----
| project TimeGenerated,
          DeviceName,
          DeviceId,
          AccountName,
          AccountDomain,
          FileName,
          ProcessCommandLine,
          ParentProcessName          = InitiatingProcessFileName,
          ParentProcessCommandLine   = InitiatingProcessCommandLine,
          ParentAccount              = strcat(InitiatingProcessAccountDomain, "\\", InitiatingProcessAccountName),
          RemoteSession              = iff(IsInitiatingProcessRemoteSession, "Yes", "No"),
          ReportId
// ---- Step 3 : Optional grouping to reduce noise ----
| summarize FirstSeen=min(TimeGenerated), LastSeen=max(TimeGenerated), EventCount=count()
            by DeviceId, DeviceName, AccountName, FileName, ProcessCommandLine,
               ParentProcessName, ParentProcessCommandLine, ParentAccount, RemoteSession, ReportId
| order by LastSeen desc
fail147.06s0.0275
o3-mini-high
cat
union DeviceProcessEvents, DeviceEvents
| where tolower(InitiatingProcessCommandLine) has "/etc/shells"
| project TimeGenerated, DeviceName, InitiatingProcessFileName, InitiatingProcessCommandLine, ActionType, DeviceId
| sort by TimeGenerated desc
pass111.48s0.0113
o3-mini-low
cat
DeviceProcessEvents
| where InitiatingProcessCommandLine contains "/etc/shells"
| project TimeGenerated, DeviceName, InitiatingProcessCommandLine, FileName, FolderPath
| sort by TimeGenerated desc
pass131.35s0.0113
o4-mini-high
cat /etc/shells
let ShellProcessEvents = DeviceProcessEvents
| where ProcessCommandLine matches regex @"\b(cat|grep|awk|sed|less|more|tail|head)\b.*\/etc\/shells"
| project TimeGenerated, DeviceName, AccountName=InitiatingProcessAccountName, CommandLine=ProcessCommandLine;

// If syslog data is available, look for raw slash etc shells reads in logs
let ShellSyslogEvents = Syslog
| where SyslogMessage has "/etc/shells"
| project TimeGenerated, DeviceName=HostName, AccountName="(unknown)", CommandLine=SyslogMessage;

// Combine both sources for a full view
ShellProcessEvents
| union ShellSyslogEvents
| sort by TimeGenerated desc
pass144.35s0.0155
o4-mini-low
cat /etc/shells
let ShellFilePath = "/etc/shells";
// 1) Find any processes that reference /etc/shells in their command line
let ProcHits = DeviceProcessEvents
  | where ProcessCommandLine contains ShellFilePath
  | project TimeGenerated, DeviceName, AccountName, CommandLine = ProcessCommandLine;
// 2) Find any file‐access events reading the /etc/shells file
let FileHits = DeviceFileEvents
  | where FileName == "shells" and FolderPath endswith "/etc"
  | where ActionType has "Read"
  | project TimeGenerated, DeviceName, AccountName = InitiatingProcessAccountName, CommandLine = InitiatingProcessCommandLine;
// Combine and sort
union ProcHits, FileHits
| sort by TimeGenerated desc
pass131.60s0.0097