Skip to content

About

Detects changes to the software installed across your endpoint fleet. Diffs against a baseline, so it reports what moved rather than what is merely present.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

fleetdrift

Detects changes to the software installed across your endpoint fleet, and tells your security team about the ones that matter.

CI Python 3.10+ License: Apache 2.0

What it does

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.

Why drift, not inventory

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.

Install

pip install fleetdrift

Usage

If you run Fleet — fleet.yml:

source:
  type: fleet
  fleet:
    url: https://fleet.example.com
    api_token_env: FLEET_API_TOKEN

If 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 prose
source:
  type: resultlog
  resultlog:
    path: /var/log/osquery/osqueryd.results.log

Then:

fleetdrift validate -c fleet.yml    # check config without collecting
fleetdrift scan -c fleet.yml -o report.md

First run writes the baseline and reports posture only. Subsequent runs report drift.

What it detects

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.

Privacy

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.

Configuration

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 below

Unknown 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.

Exit codes

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.

The AI layer

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.

Security design

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, and fleetdrift pack --explain prints 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_internal covers 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.

Limitations

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 signature table joins cleanly with apps; 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.

Contributing

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.

License

Apache 2.0.

About

Detects changes to the software installed across your endpoint fleet. Diffs against a baseline, so it reports what moved rather than what is merely present.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages