Citadel's Query Packs: Twelve Named Collections, and What Each One Watches
Citadel's query pack library reached twelve this week. Six of those packs arrived in one change in August, which took collection past macOS and onto Linux and cross-platform posture. Two more landed today, both macOS, both about what is allowed to run on a machine and what the operating system's own defences have already noticed.
A fleet's collection schedule is the least glamorous and most consequential thing about it. Every question anyone asks under a deadline โ which hosts run the vulnerable build, when that daemon first appeared โ is answered from what was already being collected, or it is not answered at all. So the library is worth setting out in full.
What a pack is here
A pack in Citadel is a named set of osquery queries with a schedule attached to each one. An entry carries an interval, a platform scope, and a collection mode: differential, where a host reports only what changed since its last run, or snapshot, where the full result is recorded every time. The pack itself carries a target โ a label that resolves to a set of hosts. macos-hardening targets the macOS label and reaches the Macs. admin-config targets All Hosts and reaches everything.
Those four properties per query are the whole model, and they are where the cost lives. A gauge that moves on every read (load average, free disk) is cheaper as a snapshot, because a differential of a moving number emits an added row and a removed row on every single run. Configuration is the opposite: after the first collection, a host that changes nothing ships nothing, and the removals matter as much as the additions โ an authorised SSH key disappearing is as interesting as one appearing.
The pack list is where that shape is visible at a glance:

The pack names, their query counts and their modification dates are the real library. The fleet the host counts resolve to is an example estate, not a customer's.
The twelve
| Pack | Target | Queries | What it collects |
|---|---|---|---|
admin-config |
All Hosts | 6 | Local accounts, privileged group membership, sudoers rules and SSH trust. |
citadel-monitoring |
All Hosts | 3 | The agent watching itself: heartbeat, per-query cost accounting, event publisher health. |
network-posture |
All Hosts | 5 | Interfaces, addresses, DNS resolvers, default routes and established outbound connections. |
system-health |
All Hosts | 9 | CPU load, memory and swap pressure, disk space, uptime, thermal state and the largest processes. |
linux-inventory |
Linux | 4 | Linux OS, kernel and dpkg/APT software inventory. |
linux-system-state |
Linux | 8 | Systemd units, cron, kernel modules, network state and local password state. |
macos-admin-state |
macOS | 5 | Sharing services, system extensions, password policy and launchd overrides. |
macos-code-integrity-and-identity |
macOS | 5 | Signing state of running code, secure boot policy, authorization-chain plugins, MDM-imposed configuration and backup state. |
macos-endpoint-protection |
macOS | 6 | XProtect state and detections, endpoint-protection agent presence, security-relevant unified-log activity, quarantined downloads. |
macos-hardening |
macOS | 8 | Application firewall, System Integrity Protection, Gatekeeper, FileVault and removable media. |
macos-incident-response |
macOS | 17 | Persistence, logon and network state โ the pack that answers "what changed on this box" after the fact. |
macos-vuln-management |
macOS | 10 | Daily software inventory, so a CVE can be matched to hosts without touching an endpoint. |
Behind those twelve sit 81 query definitions in a single file, scheduled 86 times. The gap in either direction is deliberate. Six query names โ crontab, etc_hosts, kernel_info, listening_ports, mounts, os_version โ appear in two packs each, but the packs target disjoint labels, so no host ever collects one of them twice; keeping the intervals identical across both is what makes the same query mean the same thing fleet-wide. And one definition, es_process_events, is defined and not scheduled anywhere, because the EndpointSecurity publisher reports itself inactive on every enrolled host and scheduling it would buy a permanently empty query. It stays in the file, runnable as a live query, until the agent configuration that would populate it lands.
The Linux packs are the part that took the library off macOS. They exist as counterparts rather than as guards bolted into the macOS packs: linux-inventory is what macos-vuln-management is, and linux-system-state is what macos-incident-response is. They also needed a label to reach anything at all. Citadel's built-in labels match on the platform string exactly, so Ubuntu Linux and CentOS Linux between them miss Debian, Rocky, Alma, Amazon Linux and a Synology box โ all of which land in All Hosts and nothing else. So the library ships its own Linux label alongside the packs, matching on platform_like first and falling back to an enumeration, because a live Debian 12 host reports an empty platform_like and only the enumeration catches it.
What the two new macOS packs see
The existing macOS packs answer whether a control is switched on (macos-hardening), what is installed (macos-vuln-management), and what changed (macos-incident-response). Neither of the two that landed today overlaps with those.
macos-endpoint-protection is about what the Mac's own defences have already noticed and what a user has already brought in. XProtect's detections are collected hourly rather than daily, because a detection is the one row in the library anyone would want paged on, and it costs nothing on a clean host. The signature set and the plugin blocklist are collected daily, which is already faster than Apple ships them. Alongside those: whether a third-party protection agent is installed and actually running, files under ~/Downloads still carrying a quarantine attribute, and a ten-minute window of security-subsystem log activity โ Gatekeeper refusals, TCC denials, XProtect and MRT activity.
macos-code-integrity-and-identity is about what is permitted to execute and who is permitted to act. Its hourly query is running processes with no trusted signature, which is the only query in the whole library that answers "what is executing right now that should not be" โ a process that starts and exits inside a day is exactly the case a daily interval would miss. Underneath it, daily: the secure boot policy, non-builtin authorization plugins that sit in the login chain, MDM-imposed settings, and whether the machine has a backup destination configured at all.
Both packs are deliberately narrow about their own boundaries. macos-endpoint-protection does not schedule system_extensions, because macos-admin-state already collects it hourly and both packs reach the same Macs. It does not collect the XProtect version either, because the package receipts carry it and macos-vuln-management already collects the whole receipts table daily. That discipline is the reason the fleet's result volume stays legible.
Reading the results
Collection is half of it. The other half is per-pack reporting, which is where a fleet owner finds out whether the schedule is doing what it was written to do.

The pack name, its query and row totals, and every metric description in frame are the real report. The hosts those rows are attributed to are the example estate's.
The four tiles at the top are the pack's last 24 hours: rows collected, hosts that reported, distinct queries seen, and the added-versus-removed split that tells you whether the volume is churn or accumulation. Below them, each metric is charted per hour, and each carries the sentence explaining what a movement in it means. A cliff in result rows means collection stopped, not that the fleet went quiet. Hosts dropping off the reporting line are hosts that stopped running the pack.
One detail in that frame is worth pointing at: Agent check-ins is flat at zero, and it should be. That series counts osquery_info reports, which citadel-monitoring schedules and macos-incident-response does not. A pack-scoped metric is empty when the pack does not schedule the query behind it, and the panel shows the empty line rather than hiding the metric โ which is the behaviour you want the first time you go looking for a number that is not there.
Further down the same page, the report breaks the window out by query and by host, so "this pack produced 2,581 rows" resolves into which query produced them and which machine they came from. That is usually the first question after the number itself.
Alongside
Two other changes landed in the same window and both are about where the server can run rather than what it collects. The Citadel server now builds as a Synology DSM .spk package, so a NAS can host it from DSM's own package manager. And a Helm chart under deploy/helm/citadel covers the server with MySQL and Redis for a Kubernetes deployment.
Where this goes
The library is written against a fleet that is small and real, and every query in it was run against live enrolled hosts before it shipped โ which is how ip_forwarding turned out to return nothing on Linux and needed a separate query, and how the Linux label turned out to need its enumeration. That is slower than importing an upstream pack wholesale, and it is the difference between a schedule that produces evidence and a schedule that produces volume.
The next questions are the ones the library cannot answer yet: Linux firewall state, which was left out rather than shipped untested, and RPM-based inventory, which waits for the first RPM host to validate against. Both will land in the change that can prove them.
Reference
- osquery schema โ osquery
- Query packs โ osquery documentation
- XProtect and macOS malware protection โ Apple
- Endpoint Security framework โ Apple
- Helm chart best practices โ Helm