yuzu

procfetch

What it doesEnumerates running processes with SHA-1 hashes of executables
Version1.0.0
KindCollector · read-only · gathered (crossplatform.process.fetch)
PlatformsWindows ✅ · macOS ✅ · Linux ✅
Actionsprocfetch_fetch (definition crossplatform.process.fetch)
Securitysecurable Inventory · operation Read · risk Low · dispatch ReadOnly · approval gate None
Rolesexecute: endpoint-admin, endpoint-operator · author: content-author

How it works

procfetch_fetch is the plugin’s only action. Each leg enumerates every process the OS exposes, resolves the executable image path, and SHA-1-hashes the file at that path, streaming one pipe-delimited row per process via write_output() as it goes rather than collecting the whole list first. Hashes are cached by path within a single run so a binary loaded by many processes (svchost.exe, cfprefsd, MTLCompilerService) is only read and hashed once. The three legs are native, in-process enumeration — /proc on Linux, libproc on macOS, CreateToolhelp32Snapshot on Windows — with OpenSSL EVP (POSIX) or BCrypt (Windows) doing the hashing; no subprocess is spawned anywhere in the plugin. It is deliberately not a general process inventory: processes (crossplatform.process.list/.query) already lists PID and name only, cheaply and without touching disk; procfetch exists specifically to add the executable path and a file hash, which costs one file read and one hash per unique binary.

flowchart LR
  OP[Operator / workflow] --> SRV[Server<br/>authz: Inventory.Read]
  SRV -- gRPC mTLS --> HOST[Agent plugin host] --> EX[procfetch.execute]
  EX --> WIN[Windows leg<br/>CreateToolhelp32Snapshot + BCrypt SHA-1]
  EX --> MAC[macOS leg<br/>libproc proc_listpids/proc_pidpath + OpenSSL EVP SHA-1]
  EX --> LIN[Linux leg<br/>/proc enumeration + OpenSSL EVP SHA-1]
  WIN & MAC & LIN --> ROWS[rows, no typed status] --> RS[(ResponseStore)] --> API[REST /api/responses]

OS capability

ActionWindowsmacOSLinux
procfetch_fetch✅ supported · rung 1 · CreateToolhelp32Snapshot + BCrypt SHA-1✅ supported · rung 1 · libproc (proc_listpids/proc_pidpath) + OpenSSL EVP SHA-1✅ supported · rung 1 · /proc enumeration + OpenSSL EVP SHA-1

Privileges and prerequisites

OSRuns asExtra grant neededMeasuredIf the read is refused
WindowsSYSTEM (measured; no row in docs/agent-privilege-model.md for this plugin)None observed at SYSTEM. Even as SYSTEM, OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, ...) is refused for a handful of protected/system processes — the sample’s [System Process], System, Secure System, Registry rows carry no path or hash.2026-09-07, bare-metal, SYSTEMthat row’s path and sha1 fields are empty; no error is surfaced and enumeration continues (procfetch_plugin.cpp:251-264)
macOSunprivileged (agent daemon)None observed unprivileged — proc_listpids/proc_pidpath resolved paths and hashes for processes owned by root and other users in the sample (e.g. postgres, XprotectService) without elevation.2026-09-07, macOS 26.6.2, euid 501 (jsmith)proc_pidpath failing falls back to proc_name() for a short name only, leaving path and sha1 empty (procfetch_plugin.cpp:216-223) — not observed in this capture
Linuxroot (captured in a container; not measured unprivileged)resolve_exe reads the /proc/<pid>/exe symlink, which the kernel restricts to the same UID or a reader holding CAP_SYS_PTRACE; an unprivileged, non-matching-UID read is expected to fail2026-09-06, Debian 13 (trixie) aarch64 container, euid 0std::filesystem::read_symlink throws filesystem_error, caught and turned into an empty path, so path and sha1 are empty and the row still emits (procfetch_plugin.cpp:134-140)

No external binaries, no subprocesses, no network access — every leg is a native OS call plus an in-process hash (agents/plugins/procfetch/src/procfetch_plugin.cpp, no popen/CreateProcess/socket use anywhere in the file).

Data contract

Inputs

The action takes no parameters.

Outputs

Pipe-delimited rows, one per process, in the fixed field order pid|name|path|sha1. name and path are pipe-escaped (a literal | becomes \|) before being written, so the delimiter itself is never ambiguous. path and sha1 are empty strings, not -, when the OS would not hand back an image path or the file could not be opened/hashed — there is no placeholder row for “no processes”: the row is only ever missing fields, never absent.

crossplatform.process.fetch — pid|name|path|sha1

FieldTypeValuesAvailableExampleDescription
pidint64-Windows, Linux, macOS1052The process ID assigned by the OS. Values: integer.
namestring-Windows, Linux, macOSsvchost.exeThe process’s short or executable name, as reported by the OS. Values: free text.
pathstring-Windows, Linux, macOS/usr/bin/dashAbsolute path to the process’s executable image on disk; empty when the OS refuses or fails to resolve it (a protected process, a permission-denied read, or a pseudo-process with no backing binary). Values: absolute path or empty.
sha1string-Windows, Linux, macOSe5355187cbd952a5bc9dfca430d10383f0f8750cLowercase hex SHA-1 of the executable file’s contents, cached per path so a binary shared by several processes is hashed once; empty when the file could not be opened or its path could not be resolved. Values: 40-char lowercase hex or empty.

Result status

This plugin does not set a typed result status; the agent records UNDECLARED and the sample shows UNDECLARED / UNKNOWN /.

Where the data goes

Sample output

Windows — captured: windows Microsoft Windows NT 10.0.26200.0 x64 · bare-metal · 2026-09-07 · SYSTEM · leg-hash 514b81c2df7e

== action=procfetch_fetch
0|[System Process]||
4|System||
332|Secure System||
376|Registry||
1052|smss.exe|C:\Windows\System32\smss.exe|2c84c834b6326b8fc02cc3262d9035e16fc27847
1504|csrss.exe|C:\Windows\System32\csrss.exe|17397f105f7d51112261ecc501fe99b8fcf4de67
1596|wininit.exe|C:\Windows\System32\wininit.exe|54fea18cc8aaefa2759701df35a4d4487a594593
1604|csrss.exe|C:\Windows\System32\csrss.exe|17397f105f7d51112261ecc501fe99b8fcf4de67
1676|services.exe|C:\Windows\System32\services.exe|56aea1cb2ceada37df439990113529c2c67a61c8
1716|winlogon.exe|C:\Windows\System32\winlogon.exe|c2c0e4e27796545d94c5ef879ee429c608a42dbf
1724|LsaIso.exe|C:\Windows\System32\LsaIso.exe|839d5e8e57687cbb0d21420e386f73c6742e8588
1740|lsass.exe|C:\Windows\System32\lsass.exe|258a9984be57a5000517fd76abff440f5562a315
… 12 of 184 rows shown
[result_status] UNDECLARED / UNKNOWN

macOS — captured: macos macOS 26.6.2 arm64 · bare-metal · 2026-09-07 · euid 501 (jsmith) · leg-hash 514b81c2df7e

== action=procfetch_fetch
10151|plugin_capture|/private/tmp/claude-501/-Users-jsmith-yuzu-dev/00000000-0000-0000-0000-000000000000/scratchpad/d2/plugin_capture|87ea43e2b7d3dd2eb6e7db6a20041b1f77d20272
10143|mdworker_shared|/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/Metadata.framework/Versions/A/Support/mdworker_shared|19ca6c856d80cfc6b83800c821b3129f40b135c7
10125|Code Helper (Plugin)|/Applications/Visual Studio Code.app/Contents/Frameworks/Code Helper (Plugin).app/Contents/MacOS/Code Helper (Plugin)|0478d377b6ffa6659d823d3c72d25dc9b8816bd6
10121|mdworker_shared|/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/Metadata.framework/Versions/A/Support/mdworker_shared|19ca6c856d80cfc6b83800c821b3129f40b135c7
10120|mdworker_shared|/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/Metadata.framework/Versions/A/Support/mdworker_shared|19ca6c856d80cfc6b83800c821b3129f40b135c7
10119|mdworker_shared|/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/Metadata.framework/Versions/A/Support/mdworker_shared|19ca6c856d80cfc6b83800c821b3129f40b135c7
10118|Code Helper (Plugin)|/Applications/Visual Studio Code.app/Contents/Frameworks/Code Helper (Plugin).app/Contents/MacOS/Code Helper (Plugin)|0478d377b6ffa6659d823d3c72d25dc9b8816bd6
9945|biomesyncd|/usr/libexec/biomesyncd|231b0daf0f1dd48345aade47d3adfbbb5a75844f
9654|head|/usr/bin/head|a78fc55bfe98bda12ebd3c8190d12b3f09159407
9653|ssh|/usr/bin/ssh|8ccefa7581a171018a6cc737649ef7854e75d4c0
9647|zsh|/bin/zsh|95f7d2b76c248d1dc8c84c37e3e5cddc93246b6c
9621|spotlightknowledged|/System/Library/Frameworks/CoreSpotlight.framework/spotlightknowledged|8d9cd67b92a13d9f23b7c546737edce879c47436
… 12 of 818 rows shown
[result_status] UNDECLARED / UNKNOWN

Linux — captured: linux Debian GNU/Linux 13 (trixie) aarch64 · container · 2026-09-06 · euid 0 · leg-hash 514b81c2df7e

== action=procfetch_fetch
1|sh|/usr/bin/dash|e5355187cbd952a5bc9dfca430d10383f0f8750c
9|plugin-capture|/src/builddir/tools/plugin-capture/plugin-capture|c93a85ef1e07f11e88bcea0d85f828adc1069521
[result_status] UNDECLARED / UNKNOWN

Caveats and known gaps

  1. Windows hashing opens a UTF-8 path via the ANSI CreateFileA. get_process_image_path converts the wide image path to UTF-8 via from_wide (procfetch_plugin.cpp:263), then sha1_of_file passes that UTF-8 string straight to CreateFileA (procfetch_plugin.cpp:267), which interprets it as the ANSI code page, not UTF-8. A process whose executable path contains non-ASCII characters will fail to open, silently leaving sha1 empty with no error surfaced.
  2. This plugin never sets a typed result status. Both captured samples read UNDECLARED / UNKNOWN / regardless of whether any row’s path/hash resolution failed — a caller cannot distinguish “clean run” from “every row’s hash silently failed” without inspecting the rows themselves.
  3. Protected Windows processes report name only. OpenProcess is refused even running as SYSTEM for a handful of processes ([System Process], System, Secure System, Registry in the sample); those rows carry empty path and sha1 with no row-level error indicator.
  4. SHA-1, not SHA-256. The metadata description positions this action for “security auditing” and “IOC matching” (content/definitions/procfetch.yaml:16-17), but the hash is SHA-1 on every leg (EVP_sha1() / BCRYPT_SHA1_ALGORITHM), which is broken for collision resistance; it remains adequate for matching against a known-hash IOC list (a preimage property SHA-1 still holds) but not for detecting a deliberately-collided binary.
  5. Linux exe resolution needs same-UID or CAP_SYS_PTRACE. resolve_exe reads /proc/<pid>/exe (procfetch_plugin.cpp:134-140); the capture was taken as root in a two-process container, so an unprivileged multi-user host was not exercised — expect path/sha1 to go empty for other users’ processes there.

Source and tests