Skip to content

Conversation

@tomsonpl
Copy link
Contributor

@tomsonpl tomsonpl commented Nov 6, 2025

Services Artifact - Suspicious Service Persistence Detection

The Services artifact provides comprehensive visibility into system service configurations across Windows, macOS, and Linux platforms. These queries identify suspicious services that may indicate persistence mechanisms, privilege escalation, or lateral movement by analyzing service executables, code signatures, file locations, and configuration anomalies.

Core Forensic Artifacts Coverage

# Artifact OS Query File Description
1 Services (Suspicious) Windows services_suspicious_windows_elastic 892ee425 Windows service persistence with signature validation, ServiceDLL hijacking, and FailureCommand detection
2 Services (Suspicious) macOS services_suspicious_darwin_elastic 5823a22e macOS launchd persistence with code signature analysis and suspicious path detection
3 Services (Suspicious) Linux services_suspicious_linux_elastic f8b0894b Linux systemd service persistence in non-standard locations

Queries by Platform


🪟 Windows - Suspicious Service Persistence Detection

Description

Identifies suspicious Windows services using multiple risk indicators: unsigned/untrusted binaries, executables in user-writable directories (Users, AppData, Temp, ProgramData), FailureCommand persistence mechanisms, and ServiceDLL hijacking with untrusted DLLs. Results prioritized by risk level. Covers MITRE ATT&CK T1543.003 (Windows Service), T1574.011 (Services Registry Permissions Weakness), T1112 (Modify Registry).

Detection Focus:

  • Services with unsigned or untrusted executables
  • Executables running from user-writable paths (Users, AppData, Temp, ProgramData)
  • FailureCommand registry entries for persistence
  • ServiceDLL hijacking with untrusted DLLs
  • Registry modification timestamps for timeline analysis

Result

Screenshot 2025-12-05 at 14 26 23

[Query results will show suspicious services with their executable paths, code signature status, ServiceDLL information, and failure command configurations prioritized by risk level]

Platform

windows

Interval

3600 seconds (1 hour)

Query ID

services_suspicious_windows_elastic

ECS Field Mappings

  • event.category["configuration"]
  • event.type["info"]
  • event.kindstate
  • event.moduleosquery
  • event.datasetosquery.windows_services
  • host.os.typewindows
  • service.namename
  • service.statestatus
  • process.command_linecmdline
  • process.executableexe_path
  • process.pidpid
  • user.nameuser_account
  • process.hash.sha256sha256
  • process.code_signature.subject_namesignature_signer
  • process.code_signature.statussignature_status
  • registry.pathregistry_path
  • file.mtimemodified_time
  • dll.pathservice_dll
  • dll.hash.sha256dll_sha256
  • dll.code_signature.statusdll_signature_status
  • tags["persistence", "threat_hunting", "services", "windows", "mitre_t1543_003", "mitre_t1574_011"]
  • threat.frameworkMITRE ATT&CK
  • threat.tactic.id["TA0003", "TA0004"]
  • threat.tactic.name["Persistence", "Privilege Escalation"]
  • threat.technique.id["T1543.003", "T1574.011"]
  • threat.technique.name["Create or Modify System Process: Windows Service", "Hijack Execution Flow: Services Registry Permissions Weakness"]

SQL Query

-- Windows Service Persistence Detection
-- Identifies suspicious services based on signature, path, and persistence indicators
-- T1543.003 (Windows Service), T1574.011 (ServiceDLL Hijacking)

WITH services_base AS (
  SELECT
    s.name,
    s.display_name,
    s.path AS cmdline,
    CASE
      WHEN s.path LIKE '"%' THEN
        SUBSTR(s.path, 2, INSTR(SUBSTR(s.path, 2), '"') - 1)
      WHEN INSTR(LOWER(s.path), '.exe ') > 0 THEN
        SUBSTR(s.path, 1, INSTR(LOWER(s.path), '.exe') + 3)
      WHEN INSTR(LOWER(s.path), '.sys ') > 0 THEN
        SUBSTR(s.path, 1, INSTR(LOWER(s.path), '.sys') + 3)
      ELSE s.path
    END AS exe_path,
    s.status,
    s.start_type,
    s.user_account,
    s.service_type,
    s.pid,
    'HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\' || s.name AS registry_path,
    r_key.mtime AS registry_mtime,
    r_dll.data AS service_dll,
    r_failcmd.data AS failure_command
  FROM services s
  LEFT JOIN registry r_key
    ON r_key.key = 'HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\' || s.name
    AND r_key.name = 'Start'
  LEFT JOIN registry r_dll
    ON r_dll.key = 'HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\' || s.name || '\Parameters'
    AND r_dll.name = 'ServiceDll'
  LEFT JOIN registry r_failcmd
    ON r_failcmd.key = 'HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\' || s.name
    AND r_failcmd.name = 'FailureCommand'
  WHERE s.service_type NOT LIKE '%DRIVER%'
    AND s.path IS NOT NULL
    AND s.path != ''
),
services_enriched AS (
  SELECT
    sb.*,
    datetime(sb.registry_mtime, 'unixepoch') AS modified_time,
    CAST((strftime('%s', 'now') - sb.registry_mtime) / 86400 AS INTEGER) AS days_since_modified,
    (SELECT sha256 FROM hash WHERE path = sb.exe_path LIMIT 1) AS sha256,
    (SELECT subject_name FROM authenticode WHERE path = sb.exe_path LIMIT 1) AS signature_signer,
    (SELECT result FROM authenticode WHERE path = sb.exe_path LIMIT 1) AS signature_status,
    CASE WHEN sb.service_dll IS NOT NULL AND sb.service_dll != '' THEN
      (SELECT sha256 FROM hash WHERE path = sb.service_dll LIMIT 1)
    END AS dll_sha256,
    CASE WHEN sb.service_dll IS NOT NULL AND sb.service_dll != '' THEN
      (SELECT result FROM authenticode WHERE path = sb.service_dll LIMIT 1)
    END AS dll_signature_status
  FROM services_base sb
)

SELECT *
FROM services_enriched
WHERE (
  (signature_status IS NULL OR signature_status != 'trusted')
  OR exe_path LIKE 'C:\\Users\\%'
  OR exe_path LIKE '%\\AppData\\%'
  OR exe_path LIKE '%\\Temp\\%'
  OR exe_path LIKE 'C:\\Temp\\%'
  OR exe_path LIKE 'C:\\ProgramData\\%'
  OR failure_command IS NOT NULL
  OR (service_dll IS NOT NULL AND (dll_signature_status IS NULL OR dll_signature_status != 'trusted'))
)
ORDER BY
  CASE WHEN signature_status IS NULL OR signature_status != 'trusted' THEN 0 ELSE 1 END,
  CASE WHEN failure_command IS NOT NULL THEN 0 ELSE 1 END,
  CASE WHEN exe_path LIKE 'C:\\Users\\%' OR exe_path LIKE '%\\Temp\\%' THEN 0 ELSE 1 END;

🍎 macOS - LaunchD Persistence Detection

Description

Identifies third-party macOS launchd persistence mechanisms and Apple-signed services in suspicious locations. Enumerates non-Apple-signed services for threat hunting, flags Apple-signed binaries executing from user-writable paths (/Users/, /tmp/, hidden directories). Derives executable from program or program_arguments fields, enriches with code signature validation and file hashes. Excludes trusted system paths (/System/, /Library/Apple/). Covers MITRE ATT&CK T1543.001 (Launch Agent) and T1543.004 (Launch Daemon).

Detection Focus:

  • Non-Apple-signed launchd services (Launch Agents and Daemons)
  • Apple-signed binaries executing from suspicious user-writable paths
  • Services with executables in /Users/, /tmp/, or hidden directories
  • Third-party persistence mechanisms in Library directories
  • Code signature validation and hash enrichment for threat intelligence

Result

Screenshot 2025-12-05 at 14 25 54

[Query results will show suspicious launchd services with their labels, executable paths, code signatures, and file hashes sorted by signature status and launchd type]

Platform

darwin

Interval

3600 seconds (1 hour)

Query ID

services_suspicious_darwin_elastic

ECS Field Mappings

  • event.category["configuration"]
  • event.type["info"]
  • event.kindstate
  • event.moduleosquery
  • event.datasetosquery.launchd_services
  • host.os.typemacos
  • service.namelabel
  • file.pathpath
  • process.executableexecutable_path
  • process.command_lineprogram_arguments
  • user.nameusername
  • process.hash.md5md5
  • process.hash.sha256sha256
  • process.code_signature.subject_namesignature_signer
  • process.code_signature.statussignature_status
  • tags["persistence", "threat_hunting", "launchd", "macos", "mitre_t1543_001", "mitre_t1543_004"]
  • threat.frameworkMITRE ATT&CK
  • threat.tactic.id["TA0003"]
  • threat.tactic.name["Persistence"]
  • threat.technique.id["T1543.001", "T1543.004"]
  • threat.technique.name["Create or Modify System Process: Launch Agent", "Create or Modify System Process: Launch Daemon"]

SQL Query

-- macOS LaunchD Persistence Detection
-- Enumerates non-Apple and suspicious launchd services for threat hunting
-- T1543.001 (Launch Agent), T1543.004 (Launch Daemon)

WITH services_base AS (
  SELECT
    l.label,
    l.name,
    l.path,
    l.program,
    l.program_arguments,
    l.run_at_load,
    l.keep_alive,
    COALESCE(NULLIF(l.username, ''), 'root') AS username,
    CASE
      WHEN l.program IS NOT NULL AND l.program != '' THEN l.program
      WHEN l.program_arguments IS NOT NULL AND l.program_arguments != '' THEN
        CASE
          WHEN INSTR(l.program_arguments, ' ') > 0
            THEN SUBSTR(l.program_arguments, 1, INSTR(l.program_arguments, ' ') - 1)
          ELSE l.program_arguments
        END
      ELSE NULL
    END AS executable_path,
    CASE
      WHEN l.path LIKE '/Library/LaunchDaemons/%' THEN 'system_daemon'
      WHEN l.path LIKE '/Library/LaunchAgents/%' THEN 'system_agent'
      WHEN l.path LIKE '%/Library/LaunchAgents/%' THEN 'user_agent'
      ELSE 'other'
    END AS launchd_type
  FROM launchd l
  WHERE l.path IS NOT NULL
    AND l.path != ''
),
services_enriched AS (
  SELECT
    sb.*,
    (SELECT authority FROM signature WHERE path = sb.executable_path LIMIT 1) AS signature_signer,
    CASE
      WHEN (SELECT authority FROM signature WHERE path = sb.executable_path LIMIT 1) IS NOT NULL
        AND (SELECT authority FROM signature WHERE path = sb.executable_path LIMIT 1) != ''
      THEN 1 ELSE 0
    END AS is_signed,
    CASE
      WHEN (SELECT authority FROM signature WHERE path = sb.executable_path LIMIT 1) IS NOT NULL
        AND (SELECT authority FROM signature WHERE path = sb.executable_path LIMIT 1) != ''
      THEN 'signed' ELSE 'unsigned'
    END AS signature_status,
    (SELECT md5 FROM hash WHERE path = sb.executable_path LIMIT 1) AS md5,
    (SELECT sha256 FROM hash WHERE path = sb.executable_path LIMIT 1) AS sha256
  FROM services_base sb
)

SELECT *
FROM services_enriched
WHERE
  regex_match(path, '^(/System/|/Library/Apple/|/usr/libexec/)', 0) IS NULL
  AND (
    regex_match(COALESCE(signature_signer, ''), '(Apple|Software Signing)', 0) IS NULL
    OR regex_match(COALESCE(executable_path, ''), '(^/Users/|^/tmp/|^/var/tmp/|^/private/tmp/|/[.])', 0) IS NOT NULL
  )
ORDER BY
  is_signed ASC,
  launchd_type,
  label;

🐧 Linux - Systemd Service Persistence Detection

Description

Identifies suspicious Linux systemd service units in non-standard locations. Targets user-writable paths (/home, /tmp, /var/tmp), user systemd directories (~/.config/systemd, ~/.local/share/systemd), unusual unit states (linked, transient), recently modified units in /etc/systemd/system, and runtime-generated units in /run. Prioritizes results by risk level. Covers MITRE ATT&CK T1543.002 (Systemd Service).

Detection Focus:

  • Services in user-writable paths (/home, /tmp, /var/tmp)
  • User-level systemd units in ~/.config/systemd or ~/.local/share/systemd
  • Linked or transient unit states indicating dynamic service creation
  • Recently modified service units in /etc/systemd/system (last 30 days)
  • Runtime-generated units in /run/systemd directory

Result

Screenshot 2025-12-05 at 14 26 10

[Query results will show suspicious systemd service units with their paths, states, modification times, and file hashes prioritized by risk level (user-writable paths first)]

Platform

linux

Interval

3600 seconds (1 hour)

Query ID

services_suspicious_linux_elastic

ECS Field Mappings

  • event.category["configuration"]
  • event.type["info"]
  • event.kindstate
  • event.moduleosquery
  • event.datasetosquery.systemd_services
  • host.os.typelinux
  • service.nameunit_id
  • service.stateactive_state
  • file.pathfragment_path
  • file.mtimemodified_time
  • file.hash.sha256sha256
  • tags["persistence", "threat_hunting", "systemd", "linux", "mitre_t1543_002"]
  • threat.frameworkMITRE ATT&CK
  • threat.tactic.id["TA0003"]
  • threat.tactic.name["Persistence"]
  • threat.technique.id["T1543.002"]
  • threat.technique.name["Create or Modify System Process: Systemd Service"]

SQL Query

-- Linux Systemd Service Persistence Detection
-- Identifies suspicious service units in non-standard locations
-- T1543.002 (Systemd Service)

WITH services_base AS (
  SELECT
    su.id AS unit_id,
    su.load_state,
    su.active_state,
    su.sub_state,
    su.description,
    su.unit_file_state,
    su.fragment_path,
    f.mtime,
    datetime(f.mtime, 'unixepoch') AS modified_time,
    CAST((strftime('%s', 'now') - f.mtime) / 86400 AS INTEGER) AS days_since_modified,
    (SELECT sha256 FROM hash WHERE path = su.fragment_path LIMIT 1) AS sha256
  FROM systemd_units su
  LEFT JOIN file f ON f.path = su.fragment_path
  WHERE su.id LIKE '%.service'
    AND su.fragment_path IS NOT NULL
    AND su.fragment_path != ''
)

SELECT *
FROM services_base
WHERE (
  fragment_path LIKE '/home/%'
  OR fragment_path LIKE '/root/%'
  OR fragment_path LIKE '/tmp/%'
  OR fragment_path LIKE '/var/tmp/%'
  OR fragment_path LIKE '%/.config/systemd/%'
  OR fragment_path LIKE '%/.local/share/systemd/%'
  OR unit_file_state IN ('linked', 'linked-runtime', 'transient')
  OR (fragment_path LIKE '/etc/systemd/system/%' AND days_since_modified < 30)
  OR fragment_path LIKE '/run/systemd/%'
)
ORDER BY
  CASE WHEN fragment_path LIKE '/home/%' OR fragment_path LIKE '/tmp/%' THEN 0 ELSE 1 END,
  CASE WHEN fragment_path LIKE '%/.config/systemd/%' THEN 0 ELSE 1 END,
  days_since_modified ASC;

MITRE ATT&CK Coverage Summary

Technique ID Technique Name Platform
T1543.003 Create or Modify System Process: Windows Service Windows
T1574.011 Hijack Execution Flow: Services Registry Permissions Weakness Windows
T1543.001 Create or Modify System Process: Launch Agent macOS
T1543.004 Create or Modify System Process: Launch Daemon macOS
T1543.002 Create or Modify System Process: Systemd Service Linux

This PR was AI assisted with Claude Code

@tomsonpl tomsonpl self-assigned this Nov 6, 2025
@tomsonpl tomsonpl added documentation Improvements or additions to documentation. Applied to PRs that modify *.md files. Integration:osquery_manager Osquery Manager labels Nov 6, 2025
@tomsonpl tomsonpl marked this pull request as ready for review November 6, 2025 21:18
@tomsonpl tomsonpl requested a review from a team as a code owner November 6, 2025 21:18
@tomsonpl tomsonpl requested review from paul-tavares and szwarckonrad and removed request for a team November 6, 2025 21:18
…es-artifact

# Conflicts:
#	packages/osquery_manager/artifacts_matrix.md
…es-artifact

# Conflicts:
#	packages/osquery_manager/artifacts_matrix.md
Copy link
Contributor

@ferullo ferullo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you see results for all the Windows columns? I ran the Windows query and the failure_*, service_dll, servicedll_* fields were always empty for me.

Comment on lines 26 to 30
"key": "process.executable",
"value": {
"field": "path"
}
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not the path, it is the command line to run. I think it should go under process.command_line and the table column should be something like cmd. Check out the Elastic Endpoint service result for an example of what I mean.

image

Copy link
Contributor

@ferullo ferullo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry to keep leaving disjoint comments...

Windows registry and file paths are case insensitive. Should that be accounted for in the query?

@tomsonpl
Copy link
Contributor Author

Thanks @ferullo - I totally forgot about your comments, I'll review them on Monday :)

@raqueltabuyo
Copy link

QQ - In terms of the query itself, why the failure commands are considered for persistence?

@andrewkroh andrewkroh added the Team:Defend Workflows Security team for Endpoint and OSQuery workflows [elastic/security-defend-workflows] label Nov 25, 2025
tomsonpl and others added 9 commits November 27, 2025 14:43
…r PR feedback

- Renamed 'path' to 'cmd' and mapped to process.command_line (per @ferullo review)
- Added executable_path extraction (handles quoted paths and paths with args)
- Updated hash/authenticode JOINs to use extracted executable path
- Removed failure_actions_raw (binary blob not useful in ECS)
- Updated ECS mappings accordingly
…ry-builder guidelines

- Added LEFT JOIN to hash table for MD5/SHA256 of program executable
- Added LEFT JOIN to signature table for code signing validation
- Added ECS mappings for file.hash.* and file.code_signature.*
- Added MITRE ATT&CK references in comments
- Added 'code_signing' tag
…ry-builder guidelines

- Added LEFT JOIN to hash table for MD5/SHA256 of unit file (fragment_path)
- Enables integrity monitoring to detect tampered service definitions
- Added ECS mappings for file.hash.md5 and file.hash.sha256
- Added MITRE ATT&CK reference in comments (T1543.002)
- Added 'integrity_monitoring' tag
- Added LOWER() to hash/authenticode JOINs for case-insensitive matching
- Windows paths are case-insensitive but SQLite comparisons are case-sensitive
- Added inline comment explaining FailureCommand as persistence vector
- Added note about LOWER() usage in query header
…noise

- Use regex_match() to exclude Apple paths (/System/, /Library/Apple/)
- Exclude Apple-signed services regardless of path
- Add service_type classification column
- Add ECS mapping for service.type
- Reduce results from ~600 to ~10-20
…focus

- Update description to reflect threat hunting use case
- Add process.command_line ECS mapping
- Rename path field to exe_path for clarity
- Add process.pid mapping
- Focus on suspicious indicators (unsigned, unusual paths, persistence)
…tection

- Rename from services_systemd_linux_elastic to services_suspicious_linux
- Focus on suspicious locations (user dirs, /tmp, ~/.config/systemd)
- Add file modification time and hash enrichment
- Add threat_hunting and suspicious tags
- Add 120s timeout for complex query
- Rename query IDs to services_suspicious_* pattern
- Update descriptions to reflect threat hunting focus
- Add MITRE ATT&CK references
- Update last modified date
…osquery-services-artifact

# Conflicts:
#	packages/osquery_manager/kibana/osquery_saved_query/osquery_manager-5823a22e-5add-416d-a142-de323400edb0.json
#	packages/osquery_manager/kibana/osquery_saved_query/osquery_manager-892ee425-60e7-4eb6-ba25-6e97dc3e2ea0.json
#	packages/osquery_manager/kibana/osquery_saved_query/osquery_manager-f8b0894b-772d-4242-8e19-dbc5d7ae2e06.json
@tomsonpl
Copy link
Contributor Author

Did you see results for all the Windows columns? I ran the Windows query and the failure_*, service_dll, servicedll_* fields were always empty for me.

I adjusted the query a bit, and also reproduced failure attack and some dlls on my machine. The data is there - now available on the screenshots.

"interval": "3600",
"timeout": 120,
"platform": "darwin",
"query": "-- macOS Suspicious LaunchD Services Detection (Optimized)\n-- Source: Native osquery tables (launchd, signature, hash)\n-- Focus: Third-party persistence only - excludes all Apple services\n-- MITRE ATT&CK: T1543.001 (Launch Agent), T1543.004 (Launch Daemon)\n\nWITH services_enriched AS (\n SELECT\n l.label,\n l.name,\n l.path,\n l.program,\n l.program_arguments,\n l.run_at_load,\n l.keep_alive,\n COALESCE(l.username, 'root') AS username,\n COALESCE(s.signed, 0) AS is_signed,\n s.authority AS signer,\n CASE WHEN s.signed = 1 THEN 'signed' ELSE 'unsigned' END AS signature_status,\n (SELECT sha256 FROM hash WHERE path = l.program LIMIT 1) AS sha256,\n -- Classify service type for analysis\n CASE\n WHEN l.path LIKE '/Library/LaunchDaemons/%' THEN 'System-Daemon'\n WHEN l.path LIKE '/Library/LaunchAgents/%' THEN 'System-Agent'\n WHEN l.path LIKE '%/Library/LaunchAgents/%' THEN 'User-Agent'\n ELSE 'Other'\n END AS service_type\n FROM launchd l\n LEFT JOIN signature s ON s.path = l.program\n WHERE l.path IS NOT NULL AND l.path != ''\n)\n\nSELECT *\nFROM services_enriched\n-- EXCLUDE all Apple/system services using consolidated regex\nWHERE regex_match(\n path,\n '^(/System/|/Library/Apple/|/usr/libexec/)',\n 0\n) IS NULL\n AND regex_match(\n COALESCE(signer, ''),\n '(Apple|Software Signing)',\n 0\n) IS NULL\n-- Focus on suspicious indicators\nAND (\n -- Unsigned is always suspicious\n is_signed = 0\n -- Or in suspicious locations\n OR program LIKE '/Users/%'\n OR program LIKE '/tmp/%'\n OR program LIKE '/var/tmp/%'\n OR program LIKE '/private/tmp/%'\n OR program LIKE '%/.%'\n -- Or third-party with persistence enabled\n OR (is_signed = 1 AND run_at_load = '1')\n)\nORDER BY\n is_signed ASC,\n service_type,\n label;",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to offer an SQL update diff but I kept hitting a wall putting my thoughts into code so I'll just leave this comment for now.

  1. Can we modify this to return all (1) non-Apple services and (2) all services, even if signed by Apple, run from suspicious locations (for both, regardless of whether they run at load)? That seems a small superset of what this query currently returns.
  2. Lots of services seem to have a NULL program because the program is actually found in program_arguments. Those services appear unsigned because signatures are only checked against program in the WITH clause.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @ferullo, thanks - these are great tips 👍

I think I understood your suggestions, but please tell me if you think there's still something that needs adjustments.

Changes:

  1. Broader scope: Query now returns ALL non-Apple-signed services regardless of run_at_load, plus Apple-signed services (Software Signing) running from suspicious locations (/tmp/, /Users/, /var/tmp/, hidden /. directories)
  2. Fixed NULL program: Added executable_path derivation that extracts from program first, falls back to first element of program_arguments when program is NULL. Signature and hash lookups now use this derived path.

Changes:
- Broaden scope to return ALL non-Apple-signed services (not just those
  with run_at_load enabled) for comprehensive threat hunting
- Add detection of Apple-signed services in suspicious locations
  (/tmp, /Users, /var/tmp, hidden directories)
- Fix NULL program issue by deriving executable_path from program field
  or first element of program_arguments
- Use subqueries for signature lookups to avoid duplicate rows
- Fix is_signed logic to check authority presence instead of signed
  boolean (which indicates notarization, not signing)
- Fix username handling for empty strings with NULLIF
- Add process.args ECS mapping for program_arguments
- Update artifacts_matrix.md description
Copy link
Contributor

@ferullo ferullo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice. This is obviously a lot of work.

}
},
{
"key": "process.args",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be process.command_line

process.args is an array, process.command_line is the joined string.

Comment on lines 44 to 60
"key": "file.hash.sha256",
"value": {
"field": "sha256"
}
},
{
"key": "file.code_signature.subject_name",
"value": {
"field": "signer"
}
},
{
"key": "file.code_signature.status",
"value": {
"field": "signature_status"
}
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should be under process.* not file.*. file.path is the path to the service configuration file but these hashes are for the process executable.

"interval": "3600",
"timeout": 120,
"platform": "linux",
"query": "-- Linux Suspicious Systemd Services Detection\n-- Source: Native osquery tables (systemd_units, file, hash)\n-- Focus: Detect suspicious/malicious systemd services - NOT a full inventory\n-- MITRE ATT&CK: T1543.002 (Systemd Service)\n\nWITH services_enriched AS (\n SELECT\n su.id,\n su.load_state,\n su.active_state,\n su.sub_state,\n su.description,\n su.unit_file_state,\n su.fragment_path,\n f.mtime AS file_mtime,\n CAST((strftime('%s', 'now') - f.mtime) / 86400 AS INTEGER) AS days_since_modified,\n (SELECT sha256 FROM hash WHERE path = su.fragment_path LIMIT 1) AS sha256\n FROM systemd_units su\n LEFT JOIN file f ON f.path = su.fragment_path\n WHERE su.id LIKE '%.service'\n AND su.fragment_path IS NOT NULL\n AND su.fragment_path != ''\n)\n\nSELECT *\nFROM services_enriched\nWHERE (\n -- HIGH RISK: Service unit in user home directories\n fragment_path LIKE '/home/%'\n OR fragment_path LIKE '/root/%'\n -- HIGH RISK: Service unit in temp directories\n OR fragment_path LIKE '/tmp/%'\n OR fragment_path LIKE '/var/tmp/%'\n -- HIGH RISK: User systemd units (persistence location)\n OR fragment_path LIKE '%/.config/systemd/%'\n OR fragment_path LIKE '%/.local/share/systemd/%'\n -- MEDIUM RISK: Unusual unit_file_state (not standard enabled/disabled/static)\n OR unit_file_state IN ('linked', 'linked-runtime', 'transient')\n -- MEDIUM RISK: Service in /etc/systemd but not standard package install\n OR (fragment_path LIKE '/etc/systemd/system/%' AND days_since_modified < 30)\n -- MEDIUM RISK: Service in /run (runtime generated)\n OR fragment_path LIKE '/run/systemd/%'\n)\nORDER BY\n CASE WHEN fragment_path LIKE '/home/%' OR fragment_path LIKE '/tmp/%' THEN 0 ELSE 1 END,\n CASE WHEN fragment_path LIKE '%/.config/systemd/%' THEN 0 ELSE 1 END,\n days_since_modified ASC;",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the below snippet the actual criteria doesn't match the comment. If we just want to return new services I guess update the comment and we can just remove the fragment path check?

 -- MEDIUM RISK: Service in /etc/systemd but not standard package install
 OR (fragment_path LIKE '/etc/systemd/system/%' AND days_since_modified < 30)

Also, the hashes are for the service file itself not the executable that's run unlike on macOS (I haven't checked which Windows does). Since the executable path isn't included I assume you just can't retrieve it to join against? In that case can we just drop the hash in this table?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point, removed the comment, and the hash 👍

{
"key": "file.code_signature.subject_name",
"value": {
"field": "cert_subject"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider renaming cert_subject to signature_subject for consistency within this query or signer for consistency with macOS.

{
"key": "service.type",
"value": {
"field": "start_type"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This column doesn't seem to be what ECS says service.type should be and it feels different from the type of data being put in this column on macOS. Maybe just drop mapping this column?

Comment on lines 50 to 66
"key": "file.hash.sha256",
"value": {
"field": "sha256"
}
},
{
"key": "file.code_signature.subject_name",
"value": {
"field": "cert_subject"
}
},
{
"key": "file.code_signature.status",
"value": {
"field": "signature_status"
}
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these should move from file.* to process.* since they correspond to the process.executable binary and on macOS file.path is being used for the service configuration file.

"interval": "3600",
"timeout": 180,
"platform": "windows",
"query": "-- Windows Suspicious Services Detection\n-- Source: Native osquery tables (services, registry, hash, authenticode)\n-- Focus: Detect suspicious/malicious services - NOT a full inventory\n-- Filters: Unsigned binaries, unusual paths, persistence mechanisms, recent changes\n-- MITRE ATT&CK: T1543.003 (Windows Service), T1574.011 (ServiceDLL Hijacking), T1112 (Modify Registry)\n\nWITH services_registry AS (\n SELECT\n s.name,\n s.display_name,\n s.path AS cmd,\n CASE\n WHEN s.path LIKE '\"%' THEN\n SUBSTR(s.path, 2, INSTR(SUBSTR(s.path, 2), '\"') - 1)\n WHEN INSTR(LOWER(s.path), '.exe ') > 0 THEN\n SUBSTR(s.path, 1, INSTR(LOWER(s.path), '.exe') + 3)\n WHEN INSTR(LOWER(s.path), '.sys ') > 0 THEN\n SUBSTR(s.path, 1, INSTR(LOWER(s.path), '.sys') + 3)\n ELSE s.path\n END AS exe_path,\n s.status,\n s.start_type,\n s.user_account,\n s.service_type,\n s.pid,\n 'HKEY_LOCAL_MACHINE\\System\\CurrentControlSet\\Services\\' || s.name AS registry_path,\n r_key.mtime AS service_registry_mtime,\n r_dll.data AS service_dll,\n r_failcmd.data AS failure_command\n FROM services s\n LEFT JOIN registry r_key ON r_key.key = 'HKEY_LOCAL_MACHINE\\System\\CurrentControlSet\\Services\\' || s.name AND r_key.name = 'Start'\n LEFT JOIN registry r_dll ON r_dll.key = 'HKEY_LOCAL_MACHINE\\System\\CurrentControlSet\\Services\\' || s.name || '\\Parameters' AND r_dll.name = 'ServiceDll'\n LEFT JOIN registry r_failcmd ON r_failcmd.key = 'HKEY_LOCAL_MACHINE\\System\\CurrentControlSet\\Services\\' || s.name AND r_failcmd.name = 'FailureCommand'\n WHERE s.service_type NOT LIKE '%DRIVER%'\n AND s.path IS NOT NULL\n AND s.path != ''\n),\nservices_enriched AS (\n SELECT\n sr.*,\n CAST((strftime('%s', 'now') - sr.service_registry_mtime) / 86400 AS INTEGER) AS days_since_modified,\n (SELECT sha256 FROM hash WHERE path = sr.exe_path LIMIT 1) AS sha256,\n (SELECT subject_name FROM authenticode WHERE path = sr.exe_path LIMIT 1) AS cert_subject,\n (SELECT result FROM authenticode WHERE path = sr.exe_path LIMIT 1) AS signature_status,\n CASE WHEN sr.service_dll IS NOT NULL AND sr.service_dll != '' THEN\n (SELECT sha256 FROM hash WHERE path = sr.service_dll LIMIT 1)\n END AS servicedll_sha256,\n CASE WHEN sr.service_dll IS NOT NULL AND sr.service_dll != '' THEN\n (SELECT result FROM authenticode WHERE path = sr.service_dll LIMIT 1)\n END AS servicedll_signature_status\n FROM services_registry sr\n)\n\nSELECT *\nFROM services_enriched\nWHERE (\n -- HIGH RISK: Unsigned or untrusted binary (not in trusted paths)\n ((signature_status IS NULL OR signature_status != 'trusted')\n AND exe_path NOT LIKE 'C:\\\\Windows\\\\System32\\\\%'\n AND exe_path NOT LIKE 'C:\\\\Windows\\\\SysWOW64\\\\%')\n -- HIGH RISK: Running from user-writable directories\n OR exe_path LIKE 'C:\\\\Users\\\\%'\n OR exe_path LIKE '%\\\\AppData\\\\%'\n OR exe_path LIKE '%\\\\Temp\\\\%'\n OR exe_path LIKE 'C:\\\\Temp\\\\%'\n OR exe_path LIKE 'C:\\\\ProgramData\\\\%'\n -- HIGH RISK: Has persistence via FailureCommand\n OR failure_command IS NOT NULL\n -- HIGH RISK: ServiceDLL with untrusted signature (DLL hijacking)\n OR (service_dll IS NOT NULL AND (servicedll_signature_status IS NULL OR servicedll_signature_status != 'trusted'))\n)\nORDER BY\n CASE WHEN signature_status IS NULL OR signature_status != 'trusted' THEN 0 ELSE 1 END,\n CASE WHEN failure_command IS NOT NULL THEN 0 ELSE 1 END,\n CASE WHEN exe_path LIKE 'C:\\\\Users\\\\%' OR exe_path LIKE '%\\\\Temp\\\\%' THEN 0 ELSE 1 END;",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about changing this snippet to drop the exe_path checks. There's nothing special about c:\windows, malware could place itself in that directory.

 -- HIGH RISK: Unsigned or untrusted binary (not in trusted paths)
 ((signature_status IS NULL OR signature_status != 'trusted')
 AND exe_path NOT LIKE 'C:\\Windows\\System32\\%'
 AND exe_path NOT LIKE 'C:\\Windows\\SysWOW64\\%')

- Add _elastic suffix to query ID for consistency
- Add structured ECS fields: event.category, event.type, event.kind,
  event.module, event.dataset, host.os.type
- Add MITRE ATT&CK threat framework mappings
- Change hash/signature fields from file.* to process.* namespace
- Rename column aliases for clarity (cert_subject → signature_signer)
- Add human-readable mtime_utc timestamp
- Simplify query comments and description
- Add _elastic suffix to query ID for consistency
- Add structured ECS fields: event.category, event.type, event.kind,
  event.module, event.dataset, host.os.type
- Add MITRE ATT&CK threat framework mappings
- Change hash/signature fields from file.* to process.* namespace
- Rename column aliases (signer → signature_signer, service_type → launchd_type)
- Simplify query structure with renamed CTEs
- Add _elastic suffix to query ID for consistency
- Add structured ECS fields: event.category, event.type, event.kind,
  event.module, event.dataset, host.os.type
- Add MITRE ATT&CK threat framework mappings
- Add file.mtime ECS mapping for modification time
- Rename column (id → unit_id) for clarity
- Add human-readable mtime_utc timestamp
Align query ID references with renamed queries across all platforms.
- Rename Windows registry_mtime_utc to modified_time for ECS consistency
- Rename Linux mtime_utc to modified_time for ECS consistency
- Add file.mtime ECS mapping to Windows services query
- Align both queries with file.mtime ECS field standard
{
"key": "file.mtime",
"value": {
"field": "modified_time"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comes back as seconds since epoch. IMO it would be cool if in the query results the type was a date string. Especially since for Windows it seems the time is a data string and it seems friendlier for the times to always be the same format across OSes and queries.

…ents

- Address review feedback: Change process.* to file.* for hash/signature fields
  (hash and signature apply to the executable file, not the running process)
- Windows: Fix ECS mappings for sha256, code_signature.subject_name, code_signature.status
- macOS: Fix ECS mappings for md5, sha256, code_signature.subject_name, code_signature.status
- Linux: Remove raw mtime column, only expose datetime-formatted modified_time
- Add COALESCE null safety for days_since_modified calculations
- Clean up column selection in Windows query CTE
- Replace event.kind/module/dataset/host.os.type with event.action
- Add "osquery" tag, remove verbose MITRE ATT&CK tags
- Remove threat.framework and threat.tactic/technique context fields
- Clean up MITRE references from SQL query comments
- Fix ECS mappings: file.hash.* → process.hash.* for service executable enrichment
- Fix ECS mappings: file.code_signature.* → process.code_signature.*
- Add VirusTotal link (vt_link) for quick hash lookup
- Clean up inline comment formatting
- Fix ECS mappings: file.hash.sha256 → process.hash.sha256
- Fix ECS mappings: file.code_signature.* → process.code_signature.*
- Add VirusTotal link (vt_link) for quick hash lookup
- Remove sha256 hash computation (hash table unreliable for unit files)
- Remove file.hash.sha256 ECS mapping (column no longer exists)
@elasticmachine
Copy link

💚 Build Succeeded

History

cc @tomsonpl

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation. Applied to PRs that modify *.md files. Integration:osquery_manager Osquery Manager Team:Defend Workflows Security team for Endpoint and OSQuery workflows [elastic/security-defend-workflows]

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants