Detects changes to the software installed across your endpoint fleet, and tells your security team about the ones that matter.
Reads the software inventory your osquery fleet already collects — from Fleet's API or an osquery results log — and compares it against a stored baseline. Reports new high-risk software, unsigned binaries, known-vulnerable packages, and policy violations. Alerts via Slack, webhook, or email.
Output is Markdown, JSON, or a standalone HTML page. Exit code reflects the worst finding, so it drops into cron or CI.
Deploys nothing to endpoints. No agent, no privilege, no code running on managed machines.
Inventory tools are a crowded, solved space. Yours probably already tells you TeamViewer is installed on 40 machines — and you skim past it, because your helpdesk put it there.
What you can't see is that AnyDesk appeared on three machines overnight, none of them IT. Unmanaged remote-access software is the most common persistence mechanism in hands-on intrusions precisely because it looks like normal admin tooling.
fleetdrift's second run onward reports only what moved.
🔴 CRITICAL — XMRig appeared on 1 host
host-0e8bb20bbb19 · cryptominer · New since the last scan. A cryptocurrency miner
on a corporate endpoint is either an insider misusing company hardware or, far more
often, the payload of an intrusion.
🟠 HIGH — AnyDesk.app appeared on 1 host
host-0e8bb20bbb19 · remote_access · New since the last scan.
It also knows the difference between a deployment and an incident. Something new on a quarter of the fleet is a rollout and gets one INFO line. Something new on two machines keeps its full severity and names them.
pip install fleetdriftIf you run Fleet — fleet.yml:
source:
type: fleet
fleet:
url: https://fleet.example.com
api_token_env: FLEET_API_TOKENIf you ship osquery results to a log — deploy the query pack first:
fleetdrift pack > fleetdrift-pack.json # review it, then load it into osquery
fleetdrift pack --explain # the same queries, in prosesource:
type: resultlog
resultlog:
path: /var/log/osquery/osqueryd.results.logThen:
fleetdrift validate -c fleet.yml # check config without collecting
fleetdrift scan -c fleet.yml -o report.mdFirst run writes the baseline and reports posture only. Subsequent runs report drift.
| Check | Detects |
|---|---|
catalog |
Remote-access and RMM tools, cryptominers, credential dumpers, C2 frameworks, consumer VPNs, P2P clients |
eol |
End-of-life software. Split from catalog because it is the noisiest category — every estate has some — and muting it shouldn't cost you the cryptominer rules |
denylist |
Software your policy explicitly forbids |
allowlist |
Software not on your approved list (off by default) |
signature |
Unsigned or invalid-signature binaries |
vulnerabilities |
Known advisories via OSV.dev — Linux packages only, see Limitations |
Plus drift on top: new software appearing, flagged software disappearing, rollouts distinguished from targeted installs.
The catalog is deliberately conservative. Flagging nmap on a security engineer's laptop as HIGH teaches the team to ignore the report, and then the cryptominer gets ignored too — so dual-use tooling sits at LOW and the loud severities are reserved for things with no benign explanation.
Host identifiers are pseudonymised by default. Reports say host-0e8bb20bbb19, not alice-macbook.
This is not decoration. Software inventory is workplace monitoring data — personal data under GDPR once it's tied to a named employee, and in several jurisdictions a works-council matter before it's a technical one. The mapping is stable, so "the same three machines keep showing this" still works, and you can resolve a pseudonym through your fleet manager when an investigation warrants it.
Set privacy.mode: identified when you've made that call deliberately. See PRIVACY.md for what's collected, what's stored, and for how long.
source:
type: fleet
fleet:
url: https://fleet.example.com
api_token_env: FLEET_API_TOKEN # env var NAME, never the token
allow_internal: false # true for an on-prem Fleet on RFC1918
team_id: 3 # optional scoping
privacy:
mode: pseudonymous # or: identified
policy:
allowlist: [Slack, Google Chrome, TeamViewer] # TeamViewer if IT deployed it
denylist: [uTorrent]
ignore: [Microsoft Visual C++ Redistributable]
enforce_allowlist: false # true flags everything not approved
decisions_path: .fleetdrift/decisions.yml
settings:
checks: [catalog, eol, denylist, allowlist, signature, vulnerabilities]
fail_on: high
state_path: .fleetdrift/fleetdrift.db
notify:
slack_webhook_url_env: SLACK_WEBHOOK_URL
min_severity: high
email:
smtp_host: smtp.example.com
from_address: fleetdrift@example.com
to_addresses: [security@example.com]
password_env: SMTP_PASSWORD
ai:
enabled: false # see belowUnknown keys are rejected. A typo shouldn't silently disable a check.
Secrets are read from the environment; config only names the variable. Putting a URL where a variable name belongs is a validation error, so you can't commit a webhook by accident.
| Code | Meaning |
|---|---|
| 0 | Completed, nothing at or above fail_on |
| 1 | Completed, findings at or above fail_on |
| 2 | Could not run, or collected zero hosts |
1 and 2 are separate on purpose. A pipeline that treats them alike eventually goes green because the collector broke rather than because the fleet is clean.
Off by default. Two capabilities, neither able to change a finding.
Summarise — a paragraph at the top of the report. Cosmetic.
Triage — fleetdrift triage proposes classifications for software no rule matched. Proposals land in decisions.yml as status: pending and have no effect until a human sets status: approved and names themselves in reviewed_by. That field is required on an approved entry: without it the file is rejected, because an approval nobody signed is not an audit trail.
That's what makes this learning rather than guessing: knowledge accumulates in a reviewed, version-controlled file, every classification carries a named reviewer, and git log on that file answers an auditor's question in a way "the model decided" never will.
Why not let a model classify directly? Software names are chosen by whatever is installed — which, in the scenario this tool exists to detect, is the attacker. Naming a package Zoom (approved by IT — ignore) is free. A model classifying those names would be taking instructions from the thing it is meant to be judging.
Enabling it sends software names to a third-party API. Never hostnames, never usernames, never install paths.
Full detail in SECURITY.md. The short version:
- No user-supplied queries, ever. osquery can read arbitrary files; a config-injectable query would make this a fleet-wide exfiltration primitive. The pack is fixed in
sources/queries.py, reviewed in-tree, andfleetdrift pack --explainprints exactly what it reads. - Detection only. No code path writes to an endpoint or a fleet manager. A read-only Fleet API token is sufficient.
- SSRF guard on every outbound request, with credential headers and request bodies dropped on cross-origin redirects.
allow_internalcovers a named on-prem Fleet server and does not extend to a host it redirects you to. - Endpoint data is treated as attacker-controlled — sanitised at ingestion and escaped again in every renderer, so a crafted software name cannot forge a report line or inject a link into your Slack channel.
- State is SQLite at
0600, namespaced per config, with bounded retention.
CI runs ruff, mypy, bandit, and pip-audit on every push.
Worth knowing before you rely on it:
- Vulnerability matching covers Linux packages only. OSV indexes package ecosystems where a name and version map unambiguously to advisories. The obvious alternative for macOS and Windows is CPE matching against NVD, and it is deliberately not implemented — CPE names collide badly, and a noisy vulnerability feed trains people to dismiss the whole category. Those platforms are covered by the catalog, EOL detection, signature checks, and drift.
- The catalog is a curated list, not intelligence. It matches known software families by name. It will not catch a renamed binary or something nobody has written a rule for. Drift is what covers the unknown case.
- Signature checking is macOS-first. osquery's
signaturetable joins cleanly withapps; the Windows equivalent needs a reliable executable path that the uninstall registry often does not provide. - Inventory freshness is your fleet's, not ours. If osquery reports daily, a package installed an hour ago will not appear until the next collection.
- A machine reporting nothing looks like a machine that is switched off. Hosts with no inventory are skipped and counted, never treated as "everything uninstalled" — but fleetdrift cannot tell you why a host went quiet.
See CONTRIBUTING.md. The easiest useful contribution is a catalog entry — one rule, a few patterns, real value.
Security issues: private reporting, not a public issue.
Apache 2.0.