// question

Investigate recent file modification events on Linux that could reveal an adversary appending commands to a user’s ~/.profile for persistence. Determine the exact command that was added.

Reference KQL answer and model-by-model comparison.

bbdb06bc-bab6-4f5b-8232-ba3fbed51d77
question id
4/23
models correct
1
valid answer
17%
success rate
// reference answer

Human-validated correct answer for this question.

echo '# Atomic Red Team was here... T1546.004' >> ~/.profile
// model comparison
ModelAnswerKQL queryStatusTriesTimeCost
gemini-2.5-flash-preview-04-17
# Atomic Red Team was here... T1546.004
DeviceProcessEvents
| where ActionType == "ProcessCreated"
| where ProcessCommandLine contains ">>"
| where ProcessCommandLine contains ".profile" and (ProcessCommandLine contains "/home/" or ProcessCommandLine contains "~/")
| project Timestamp, DeviceName, AccountName, InitiatingProcessFileName, ProcessCommandLine, InitiatingProcessCommandLine
| order by Timestamp desc
pass580.22s0.0417
gpt-35-turbo
no answer
no query generatedfail50.00s0.0110
gpt-4-turbo-2024-04-09
no answer
no query generatedfail50.00s0.2325
gpt-4.1
no answer
no query generatedfail50.00s0.0388
gpt-4.1-finetuned
no answer
no query generatedfail50.00s0.0646
gpt-4.1-mini
no answer
no query generatedfail50.00s0.0061
gpt-4.1-nano
no answer
no query generatedfail50.00s0.0014
gpt-4o
no answer
no query generatedfail50.00s0.0668
gpt-5-high
# Atomic Red Team was here... T1546.004
DeviceFileEvents
| where ActionType in ("FileCreated", "FileModified")
| where FolderPath matches regex "^(/home/[^/]+|/root)/\\.profile$"
| join kind=inner (
    DeviceInfo
    | where OSPlatform =~ "Linux"
    | project DeviceId, DeviceName, OSPlatform, MachineGroup
) on DeviceId
| extend TargetUser = iif(FolderPath matches regex "^/root/\\.profile$", "root", extract("/home/([^/]+)/", 1, FolderPath))
// Attempt to extract the exact content appended using common techniques
| extend AddedEchoS   = extract("echo\\s+(?:-e\\s+)?'([^']+)'[^\\n]*>>\\s*(?:/home|~|/root).*/\\.profile", 1, InitiatingProcessCommandLine)
| extend AddedEchoD   = extract("echo\\s+(?:-e\\s+)?\\\"([^\\\"]+)\\\"[^\\n]*>>\\s*(?:/home|~|/root).*/\\.profile", 1, InitiatingProcessCommandLine)
| extend AddedPrintf  = extract("printf\\s+(?:-e\\s+)?['\\\"]([^'\\\"]+)['\\\"]\\s*>>\\s*(?:/home|~|/root).*/\\.profile", 1, InitiatingProcessCommandLine)
| extend AddedSedA    = extract("sed[^\\n]*\\$\\s*a\\\\([^\\n]+)['\\\"]?\\s+(?:/home|~|/root).*/\\.profile", 1, InitiatingProcessCommandLine)
| extend AddedTeeHere = extract("<<<\\s*'([^']+)'[^\\n]*tee\\s+-a\\s+(?:/home|~|/root).*/\\.profile", 1, InitiatingProcessCommandLine)
// Handle base64: echo <b64> | base64 -d >> ~/.profile
| extend AddedB64Raw1 = extract("echo\\s+'([A-Za-z0-9+/=]{16,})'\\s*\\|\\s*base64\\s+-d[^\\n]*>>\\s*(?:/home|~|/root).*/\\.profile", 1, InitiatingProcessCommandLine)
| extend AddedB64Raw2 = extract("echo\\s+\\\"([A-Za-z0-9+/=]{16,})\\\"\\s*\\|\\s*base64\\s+-d[^\\n]*>>\\s*(?:/home|~|/root).*/\\.profile", 1, InitiatingProcessCommandLine)
| extend AddedB64      = coalesce(AddedB64Raw1, AddedB64Raw2)
| extend AddedB64Decoded = iif(isnotempty(AddedB64), base64_decode_tostring(AddedB64), "")
| extend AppendedCommand = coalesce(AddedEchoS, AddedEchoD, AddedPrintf, AddedSedA, AddedTeeHere, AddedB64Decoded)
| project TimeGenerated, DeviceName, DeviceId, MachineGroup, FolderPath, TargetUser, ActionType,
          InitiatingProcessAccountName, InitiatingProcessFileName, InitiatingProcessCommandLine,
          AppendedCommand
| order by TimeGenerated desc
fail1155.22s0.0932
gpt-5-mini-high
no answer
no query generatedfail50.00s0.0245
gpt-5-mini-low
echo '# Atomic Red Team was here... T1546.004' >> ~/.profile
let profileFileEvents = DeviceFileEvents
| where FileName == ".profile" or FileName endswith ".profile" or FolderPath has '/home/'
| where tolower(ActionType) contains "modify" or tolower(ActionType) contains "write" or tolower(ActionType) contains "change" or tolower(ActionType) contains "created"
| project FileEventTime=Timestamp, DeviceId, DeviceName, FileName, FolderPath, InitiatingProcessId, InitiatingProcessAccountName, InitiatingProcessCommandLine, ActionType, AdditionalFields;

let procCandidates = DeviceProcessEvents
| where ProcessCommandLine has ".profile" or ProcessCommandLine matches regex @"(?i)(>>\s*~?\/|tee\s+-a\s+.*\.profile|printf\s+.*>>.*\.profile|echo\s+.*>>.*\.profile|cat\s+<<.*>>.*\.profile)"
| project ProcessTime=Timestamp, DeviceId, ProcessId, ProcAccountName=InitiatingProcessAccountName, ProcessCommandLine, ProcessCreationTime;

profileFileEvents
| join kind=leftouter procCandidates on DeviceId, $left.InitiatingProcessId == $right.ProcessId
| extend AppendedText_Echo = extract('echo\\s+[\'\"]?(.*?)[\'\"]?\\s*>>\\s*(?:~|/home/[^/]+)?/\\.profile', 1, ProcessCommandLine)
| extend AppendedText_Printf = extract('printf\\s+[\'\"]?(.*?)[\'\"]?\\s*>>\\s*(?:~|/home/[^/]+)?/\\.profile', 1, ProcessCommandLine)
| extend AppendedText_Tee = extract('echo\\s+[\'\"]?(.*?)[\'\"]?\\s*\\|\\s*tee\\s+-a\\s+(?:~|/home/[^/]+)?/\\.profile', 1, ProcessCommandLine)
| extend Appended = coalesce(AppendedText_Echo, AppendedText_Printf, AppendedText_Tee, tostring(AdditionalFields['content']))
| project FileEventTime, ProcessTime, DeviceName, DeviceId, Account=coalesce(InitiatingProcessAccountName, ProcAccountName), FilePath= strcat(FolderPath, "/", FileName), ActionType, ProcessCommandLine, Appended
| order by FileEventTime desc
| take 200
pass161.53s0.0119
gpt-5-mini-medium
no answer
no query generatedfail50.00s0.0184
gpt-5-nano-high
no answer
no query generatedfail50.00s0.0093
gpt-5-nano-medium
no answer
no query generatedfail50.00s0.0091
grok-3-beta
no answer
no query generatedfail50.00s0.0896
grok-3-mini-beta
# Atomic Red Team was here... T1546.004
DeviceInfo
| where OSPlatform == "Linux"
| project DeviceId
| join kind=inner (
    DeviceProcessEvents
    | where ProcessCommandLine matches regex @'.*\.profile.*'
    | where ProcessCommandLine matches regex @'.*(echo|cat|tee).*>>.*\.profile.*'  // This targets commands likely appending to .profile
    | project DeviceId, TimeGenerated, DeviceName, ProcessCommandLine, FolderPath, FileName
  ) on DeviceId
| project TimeGenerated, DeviceName, ProcessCommandLine, FolderPath, FileName
| order by TimeGenerated desc
| limit 100
pass449.42s0.0060
o1-high
no answer
no query generatedfail50.00s0.9462
o1-low
echo '# Atomic Red Team was here... T1546.004' >> ~/.profile
union DeviceFileEvents, DeviceProcessEvents
| where DeviceId in (
    DeviceInfo
    | where OSPlatform =~ "Linux"
    | distinct DeviceId
)
| where (
    Type == "DeviceFileEvents" and ActionType == "FileModified" and (FileName == ".profile" or FileName endswith ".profile")
) or (
    Type == "DeviceProcessEvents" and ActionType == "ProcessCreated" and ProcessCommandLine has " >> " and ProcessCommandLine has ".profile"
)
| project TimeGenerated, DeviceName, ActionType, FolderPath, FileName, ProcessCommandLine, InitiatingProcessCommandLine, AdditionalFields
| order by TimeGenerated desc
pass4108.22s0.8149
o3-high
# Atomic Red Team was here... T1546.004
// ---------------------------------------------------------------------
// Possible persistence via commands appended to ~/.profile (Linux)
// ---------------------------------------------------------------------
// 1) Identify Linux devices that have been onboarded
let LinuxDevices = DeviceInfo
    | where OSPlatform startswith "Linux"
    | summarize by DeviceId;

// 2) File-modification events that touch a user profile file (~/.profile)
let ProfileFileEvents = DeviceFileEvents
    | where DeviceId in (LinuxDevices)
    | where ActionType in~ ("FileCreated","FileWritten","FileModified","FileUpdated")
    // FolderPath normally contains the directory, FileName the file.  Cover edge cases where only FolderPath is populated.
    | where FileName == ".profile"
        or FolderPath endswith "/.profile"
        or FolderPath has "/.profile"
    | project FileEventTime = TimeGenerated,
              DeviceId,
              DeviceName,
              TargetPath = iff(isempty(FileName), FolderPath, strcat(FolderPath,"/",FileName)),
              InitiatingUser = InitiatingProcessAccountName,
              FileEvent_CommandLine = InitiatingProcessCommandLine;

// 3) Process events that appear to write into .profile ( echo/printf/tee … >> ~/.profile )
let WriterProcessEvents = DeviceProcessEvents
    | where DeviceId in (LinuxDevices)
    | where ProcessCommandLine has ".profile" and ProcessCommandLine matches regex @"(>>|>|tee\s+-a)"
    | project ProcTime = TimeGenerated,
              DeviceId,
              DeviceName,
              WriterUser = AccountName,
              WriterProc_CommandLine = ProcessCommandLine;

// 4) Correlate file event and writer process within ± 2 minutes
ProfileFileEvents
| join kind=leftouter (
        WriterProcessEvents
) on DeviceId
| where isnull(ProcTime) or (ProcTime between (FileEventTime - 2m .. FileEventTime + 2m))

// 5) Pull out the exact text that was appended (best-effort)
| extend AddedCommand = extract(@"(?:echo|printf)\s+(.+?)\s*(?:>>|>|tee|1>>|2>>)", 1, coalesce(FileEvent_CommandLine, WriterProc_CommandLine))
| project FileEventTime,
          DeviceName,
          InitiatingUser,
          WriterUser,
          FileEvent_CommandLine,
          WriterProc_CommandLine,
          AddedCommand,
          TargetPath
| order by FileEventTime desc
fail4198.45s0.1048
o3-mini-high
no answer
no query generatedfail50.00s0.0347
o3-mini-low
no answer
no query generatedfail50.00s0.0409
o4-mini-high
no answer
no query generatedfail50.00s0.0612
o4-mini-low
no answer
no query generatedfail50.00s0.0479