Threat Hunting with Osquery: Queries That Find Real Attackers
A hunt is a comparison, not a query. Which osquery tables map to which attacker tactics, how to write filters that return a short list, and how baselining turns a one time look into real detection.


Osquery threat hunting does not fail because the queries are wrong. It fails because teams run a clever query once, read the rows, and call that a hunt.
A hunt is a comparison. You form a hypothesis about how an attacker would behave on your hosts, you pull the state that would prove or disprove it, and you measure that state against what normal looks like on your own fleet. Osquery is very good at the middle step. It reads live system state as SQL, so the question "which processes are running from a path nobody should be executing from" becomes a query instead of a ticket. But a query with no baseline is just a report, and a report with forty thousand rows is worse than no report at all.
So the useful version of this topic is not a list of magic queries. It is a method: which tables map to which attacker behavior, how to write the query so the result is small enough to act on, and how to turn a one time look into a repeatable hunt. That is what this piece covers.
Attacker tactics mapped to osquery tables
A hunt starts with a tactic, not a table. Pick the behavior, then read the table that would expose it.
What makes osquery good for threat hunting?
Three things. It reads ground truth from the host instead of inferring it from the network. It speaks SQL, so a hunter can join process data against network data against file data in one statement. And it runs the same query across every endpoint at once, which means a hypothesis can be tested against the whole fleet in the time it takes to type it.
That last point is where the value sits. Most hunts are not about finding one clever indicator. They are about answering "does this behavior appear anywhere I did not expect it." Osquery answers fleet wide questions directly. If you are new to the query mechanics, our guide to writing osquery queries covers the SQL surface, and the most useful osquery tables piece is the reference you will keep open while hunting.
Which osquery queries find persistence?
Persistence is where hunting pays off, because attackers have to leave something behind to survive a reboot, and the places they can hide are finite. The MITRE ATT&CK framework catalogs them: scheduled tasks (T1053), service creation (T1543), and startup entries (T1547) cover most of what you will see. Osquery has a table for each.
On Windows, scheduled tasks and services are the two most abused footholds. Read them directly:
SELECT name, action, path, enabled, next_run_time FROM scheduled_tasks WHERE enabled = 1 AND action NOT LIKE 'C:\\Windows\\%' AND action NOT LIKE 'C:\\Program Files%';
The filter matters more than the table. Every host has dozens of legitimate scheduled tasks. What you care about is the task whose action runs from a temp directory, a user profile, or a scripting host. The WHERE clause is the hypothesis: attackers rarely install persistence under the Windows or Program Files trees, so anything outside them is worth a look.
On Linux and macOS the same idea moves to crontab, startup_items, and the launch agent paths. A cron entry that pipes a remote script into a shell is a classic:
SELECT command, path, hour, minute FROM crontab WHERE command LIKE '%curl%' OR command LIKE '%wget%' OR command LIKE '%/tmp/%' OR command LIKE '%base64%';
There is a table that saves you a lot of work here. The startup_items table aggregates autostart locations across platforms into one place, so you do not have to remember every registry run key and launch agent path by heart. Query it, then narrow by source and status. The pattern is always the same: pull the universe of persistence, then subtract the known good.
How do you hunt for suspicious processes and network activity?
Execution and command and control are the loudest phases of an intrusion, and they leave state in tables you can join. The single most productive process hunt is for binaries that are running but no longer exist on disk, which is what a fileless payload or a deleted dropper looks like:
SELECT pid, name, path, cmdline FROM processes WHERE on_disk = 0 AND path != '';
A process with on_disk = 0 is executing from a binary that has been removed or replaced since it launched. On a normal host that list is short and boring. When it is not boring, you have found something. From there you pivot to the network side and ask which of those processes are talking to the outside world:
SELECT p.name, p.path, s.remote_address, s.remote_port
FROM process_open_sockets s
JOIN processes p ON p.pid = s.pid
WHERE s.remote_address NOT IN ('127.0.0.1', '::1')
AND s.remote_port != 0;Join that against your own list of expected outbound destinations and the noise drops fast. A beaconing implant shows up as a process you do not recognize holding a connection to an address you have never seen. For the credential and lateral movement phase, shell_history, logged_in_users, and authorized_keys tell you who ran what and which keys grant access. An SSH key added to a service account that no human should log into is a strong signal, and it is one query away.
Why a query alone is not a hunt
Here is the trap. You write a good query, run it across the fleet, and get back a wall of rows. Now what. Without a sense of what is normal, every result looks equally suspicious, and an analyst ends up guessing.
Run the numbers. A fleet of 5,000 hosts running a single persistence query can return 40,000 rows of scheduled tasks and cron jobs, because most of them are legitimate. If an analyst reviews even a tenth of that by hand at two minutes a row, that is roughly 133 hours of work to clear one hunt. Nobody has 133 hours. So the hunt gets abandoned, or worse, skimmed, which produces false confidence.
The fix is baselining. A hunt is only useful when the query result is measured against what your fleet looked like last week. The scheduled task that appeared on nine hosts overnight matters. The one that has been on all 5,000 hosts for a year does not. Osquery supports this directly through scheduled queries and differential logging: instead of returning the full result every run, the daemon can emit only what changed. The difference between osqueryi and osqueryd is exactly this, the interactive shell for exploring a hypothesis and the daemon for watching for change over time. Package your best hunts as scheduled queries and you convert a manual review into an alert on deviation. Our guide to osquery query packs shows how to version those queries like code.
Why this matters more than it used to
The window to catch an intruder is shrinking, then widening in the wrong way. Mandiant's M-Trends 2026 report, built on more than 500,000 hours of incident response during 2025, found global median dwell time rose to 14 days, up from 11 the year before. Exploitation of vulnerabilities remained the top initial access vector for the sixth year running, at 32 percent of intrusions. Attackers are getting in through known weaknesses and staying for two weeks before anyone notices.
Fourteen days is a long time to hunt in and a long time to be blind. If your endpoint state is queryable and baselined, a persistence artifact planted on day one is a deviation you can catch on day two. If your only visibility is a scan cycle and a SIEM full of alerts, you are relying on the attacker to trip a signature. The hunt exists precisely for the intrusions that do not.
A practical hunting loop
Turn all of this into something repeatable:
- Start with a tactic, not a table. Pick one attacker behavior, such as persistence through scheduled tasks, and write the hypothesis in a sentence.
- Write the query to be small. Subtract known good in the
WHEREclause so the result is a short list, not a data dump. - Run it interactively across the fleet with osqueryi to see the shape of the result and refine the filter.
- Promote the good ones to scheduled queries with differential logging, so future changes surface as alerts instead of hunts.
- Baseline. Compare each result against last week. Investigate the deltas, document the false positives, and fold that knowledge back into the filter.
The first four steps are mechanical and osquery does them well. The fifth is where hunting programs stall, because separating a real deviation from benign fleet churn at scale is judgment work, not a query. That is the problem Artemes AI is built around: pairing deep endpoint context with AI driven analysis that reads the actual system state to decide which deviations are worth a human's time, and what the response should be, instead of handing an analyst 40,000 rows and wishing them luck. Osquery supplies the ground truth. Something still has to reason over it.
Frequently asked questions
Can osquery replace an EDR for threat hunting?
Not entirely. Osquery gives you queryable host state and scheduled monitoring, which covers a large part of proactive hunting. It does not block, quarantine, or provide the kernel level event stream a full EDR captures. Many teams run osquery alongside an EDR to add flexible, ad hoc query capability the EDR does not offer.
Which osquery tables are most useful for threat hunting?
For persistence: scheduled_tasks, services, startup_items, and crontab. For execution and command and control: processes, process_open_sockets, and listening_ports. For credentials and lateral movement: shell_history, logged_in_users, and authorized_keys.
How do I map osquery queries to MITRE ATT&CK?
Tag each query with the technique it tests, such as T1053 for scheduled task persistence, and group them into packs by tactic. Several open projects publish osquery configuration files already mapped to the ATT&CK matrix, which are a good starting point before you tune them to your environment.
How often should hunting queries run?
Interactive hunts run on demand. Promote the reliable ones to scheduled queries with differential logging so the daemon reports only what changed. Persistence and network hunts on a short interval, heavier inventory hunts less often, tuned so the agent stays within its performance budget.
The takeaway
Stop thinking of osquery threat hunting as a collection of clever queries and start thinking of it as a loop: hypothesis, small query, fleet wide run, scheduled monitoring, and a baseline to measure against. The tables map cleanly to attacker tactics, so the raw material is never the hard part. The hard part is deciding which of the results is a real deviation and which is your own fleet being itself. Get the baseline right and osquery turns a two week dwell time into a two day one. Skip it and you have a very fast way to generate rows nobody reads.
See which of your findings actually matter
Artemes AI combines deep endpoint context with AI-driven analysis to show which findings are actually exploitable on your hosts—not just everything a scanner can list. We are onboarding early access teams now.

Alex Gibson
Alex writes about configuration drift, operational security evidence, endpoint telemetry, AI-assisted triage, and the practical work of turning signals into better remediation decisions.

