Skip to main content

Reconcile device inventory from MDM/EDR tools into Snipe-IT. Formerly GhostAssetSync.

Project description

Cairn

Every device. One source of truth.

Cairn reconciles device inventory from the MDM and EDR tools that already manage your fleet into your asset system of record (Snipe-IT), automatically. Point it at Jamf, Intune, JumpCloud, CrowdStrike, Sophos, and Microsoft Defender; it pulls each one, merges records that describe the same physical machine by serial number, and keeps your CMDB honest.

Cairn is the evolution of GhostAssetSync. The ghostsync command and the legacy settings.conf still work, so existing deployments keep running.

๐Ÿ“š Full documentation: see docs/ โ€” getting started, a complete CLI reference and config schema, every source connector and sink/CMDB reader, the drift report, scheduling, security, and an exhaustive troubleshooting + FAQ.


Why Cairn

The original tool synced one source (Jamf) to one sink (Snipe-IT) for the machine it ran on. Real fleets are messier: a laptop shows up in Jamf and CrowdStrike, a PC in Intune and Defender, and your asset database drifts out of date the moment someone forgets to update it. Cairn treats every tool as a pluggable source, every asset system as a pluggable sink, and reconciliation as a first-class step.

  • Pluggable providers โ€” add an MDM/EDR by dropping in one module. The core never changes.
  • Serial-number reconciliation โ€” one device seen by three tools becomes one asset, merged field-by-field in your trust order.
  • Two run modes โ€” agent (run on each endpoint, sync that machine) or fleet (run centrally, pull and reconcile the whole fleet).
  • Security-first โ€” HTTPS enforced, secrets via env vars, config permission checks, retry/backoff on every API call, serial masking in logs.
  • Dry-run โ€” see exactly what would change before anything is written.
  • Cross-platform single binary โ€” macOS, Windows, Linux. No Python required on the endpoint.

Supported integrations

Sources (MDM / EDR) System of record Notifications
Jamf Pro Snipe-IT Microsoft Teams
Microsoft Intune (Graph) Slack
Kandji Generic webhook
JumpCloud
Google Workspace (ChromeOS)
CrowdStrike Falcon
Sophos Central
Microsoft Defender for Endpoint
Apple Business Manager
UniFi (network gear)
CDW (procurement CSV import)
Rudder

Cairn also does writeback โ€” push your Snipe-IT asset tags back into Jamf or Intune so the MDM matches your system of record:

cairn writeback           # preview (dry-run): what would change in the MDM
cairn writeback --apply   # write the asset tags to Jamf / Intune

Writeback is dry-run by default, honors a per-target conflict policy (snipe_wins or only_if_empty), and never creates devices. See the roadmap for what's next.


Install

Download a release binary (recommended) from Releases:

  • macOS โ€” .pkg installer or cairn-macos.tar.gz
  • Windows โ€” .zip or the Inno Setup .exe installer
  • Linux โ€” .deb or cairn-linux.tar.gz

Or install from source:

git clone https://github.com/jsdosanj/cairn.git
cd cairn
python3 -m venv .venv && source .venv/bin/activate
pip install -e .
cairn --help

Easiest start (no YAML, no terminal know-how)

cairn setup     # guided wizard: pick your tools, paste credentials, test live
cairn web       # opens a local dashboard: test connections, dry-run, schedule

cairn setup walks you through Snipe-IT and each integration, tests every connection as you go, can store secrets in your OS keychain, and writes the config for you. cairn web then gives you a clickable dashboard. Prefer the terminal? The manual path is below.

Quick start

  1. Copy the example config and lock it down:

    cp config.example.yaml config.yaml
    chmod 600 config.yaml      # Cairn refuses a world-readable config
    
  2. Enable the sources you use and fill in credentials (or supply secrets via env vars โ€” see below). Minimal example:

    mode: fleet
    source_priority: [intune, jamf, crowdstrike]
    
    sources:
      jamf:
        enabled: true
        url: https://your.jamf.instance.com
        client_id: ...
        client_secret: ...
      crowdstrike:
        enabled: true
        client_id: ...
        client_secret: ...
        base_url: https://api.us-2.crowdstrike.com
    
    sinks:
      snipeit:
        enabled: true
        url: https://your-snipe-it/api/v1
        token: ...
    
  3. Validate, then dry-run, then sync:

    cairn validate              # check config + credentials wiring
    cairn sync --dry-run        # show what would change, write nothing
    cairn sync                  # do it
    

Commands

cairn setup                 interactive first-run wizard (recommended)
cairn web                   launch the local point-and-click dashboard
cairn doctor                test every configured connection
cairn drift                 reconcile sources vs Snipe-IT: missing / stale / duplicate / conflicting
cairn drift --stale-days 60 flag CMDB assets no source has seen in 60 days
cairn drift --json          emit the drift report as JSON
cairn validate              load config, initialize every provider, report readiness
cairn sync                  run a sync (agent or fleet per config)
cairn sync --dry-run        report changes, write nothing
cairn sync --full           re-sync every device (ignore incremental state)
cairn sync --mode agent     override the configured mode for one run
cairn writeback             preview pushing Snipe-IT asset tags back to your MDM
cairn writeback --apply     actually write the asset tags to Jamf/Intune
cairn schedule install      install a native scheduled auto-sync (--interval SECONDS)
cairn schedule status       show the scheduled job
cairn schedule uninstall    remove the scheduled job
cairn list-providers        list available sources / sinks / notifiers
cairn -c path/to.yaml ...   use a specific config file
cairn -v ...                debug logging

Run modes

fleet (run centrally, e.g. on a schedule): each enabled source is fully enumerated, records sharing a serial are merged using source_priority, and the reconciled devices are written to every enabled sink. This is how you keep an entire estate current.

agent (run on the endpoint, e.g. via Jamf/Intune/GPO): Cairn collects the local machine's facts, asks each source for that one serial, merges, and writes a single asset. This mirrors the original GhostAssetSync behavior.

Reconciliation

When two tools report the same serial, fields are filled in source_priority order: the highest-priority source wins each field it has a value for, and empty fields are backfilled from lower-priority sources. MAC addresses are unioned and normalized; every source's raw payload is preserved under raw[<source>]. EDR-only records that lack a serial (some Defender/Sophos endpoints) are still synced, just not merged.


Drift report (is your CMDB lying?)

cairn drift is read-only and writes nothing. It pulls every enabled source, reconciles by serial, pulls your whole Snipe-IT inventory, and diffs them โ€” so in one command you see exactly where the system of record disagrees with the tools that actually manage your fleet:

  • missing โ€” a device your MDM/EDR sees that isn't in Snipe-IT at all.
  • stale โ€” a Snipe-IT asset no source has seen in --stale-days (default 30); a retirement / lost-device candidate.
  • duplicate โ€” more than one Snipe-IT row sharing a serial.
  • conflicting โ€” present in both, but a field (hostname, model, โ€ฆ) disagrees.

Every finding carries a confidence score โ€” how sure Cairn is the finding is real, weighted by how many independent sources corroborate it. Serials are masked to the last four characters by default (--show-serials to override).

cairn drift                       # grouped, color report, worst first
cairn drift --stale-days 60       # only flag assets unseen for 60+ days
cairn drift --json -o drift.json  # machine-readable for BI / alerting

Exit code is non-zero when drift is found, so a scheduled cairn drift cleanly gates CI or fires an alert. Configured notifiers (Teams/Slack) get a digest.

Scheduling (auto-sync)

Install Cairn as a native scheduled job so it keeps Snipe-IT current on its own:

cairn schedule install --interval 3600   # sync every hour
cairn schedule status
cairn schedule uninstall

Per platform, this uses the OS-native scheduler (no daemon to babysit):

  • macOS โ€” a launchd LaunchAgent (~/Library/LaunchAgents/com.cairn.sync.plist), run at low I/O priority, logging to ~/Library/Logs/cairn.log.
  • Linux โ€” a systemd --user service + timer, falling back to cron if systemd isn't available. (Run loginctl enable-linger $USER to keep it running while logged out.)
  • Windows โ€” a Task Scheduler task (schtasks).

The interval defaults to schedule.interval in your config (or 3600s).

Efficiency

Cairn is built to run cheaply on a tight schedule:

  • Incremental sync โ€” a per-device content hash means a scheduled run only writes the devices that actually changed. Unchanged devices are skipped before any write. last_seen is excluded from the hash by default so a routine check-in doesn't cause churn. Force a full re-sync with cairn sync --full.
  • Streaming โ€” sources yield devices as they page, so memory stays flat regardless of fleet size.
  • Connection reuse โ€” pooled keep-alive sessions with retry/backoff per source.
  • Lazy loading โ€” only the providers you enable are imported.
  • Low priority โ€” scheduled jobs run niced / idle-I/O so they stay out of the way of interactive work.

State lives in ~/.cairn/state.json (override with state_path or CAIRN_STATE).


Configuration & secrets

Configuration is YAML (config.yaml / config.yml, auto-discovered in the working directory) or the legacy settings.conf. Any value can be overridden by an environment variable โ€” keep secrets out of files in production:

# Legacy vars (still honored):
export GHOST_JAMF_URL=... GHOST_SNIPE_TOKEN=... TEAMS_WEBHOOK_URL=...

# Generic nested form โ€” CAIRN_<section>__<provider>__<key>:
export CAIRN_sources__crowdstrike__client_secret=...
export CAIRN_sinks__snipeit__token=...

Precedence: environment variables > config.yaml > legacy settings.conf.

Snipe-IT field mapping

Snipe-IT custom fields are mapped by label to any device attribute (or a nested extra.<source>.<key> path) in config, so you decide what lands where without touching code:

sinks:
  snipeit:
    field_map:
      "Operating System": os_name
      "OS Version": os_version
      "MAC Address": mac_addresses
      "Falcon Risk": extra.crowdstrike.reduced_functionality_mode

See config.example.yaml for every option.


Security

  • HTTPS enforced on every API and webhook URL (localhost exempt for dev).
  • Config permissions โ€” Cairn refuses to start on a world-readable config and warns on group-readable; chmod 600 it.
  • Secrets via env โ€” every credential can be supplied by environment variable.
  • TLS verification on, retry/backoff with Retry-After respected.
  • Serial masking โ€” only the last 4 chars of a serial appear in logs and notifications.
  • Report vulnerabilities via a GitHub issue tagged [SECURITY].

Architecture

src/cairn/
โ”œโ”€โ”€ cli.py            argparse entrypoint (sync / validate / list-providers)
โ”œโ”€โ”€ orchestrator.py   pull -> reconcile-by-serial -> upsert -> notify
โ”œโ”€โ”€ config.py         YAML + env + legacy .ini loader
โ”œโ”€โ”€ models.py         NormalizedDevice + merge_devices (the lingua franca)
โ”œโ”€โ”€ http.py           retrying session, HTTPS enforcement, OAuth2 client creds
โ”œโ”€โ”€ registry.py       maps config keys -> provider classes (lazy import)
โ”œโ”€โ”€ state.py          incremental-sync hash store (skip unchanged devices)
โ”œโ”€โ”€ scheduler.py      install native scheduled jobs (launchd / systemd / schtasks)
โ”œโ”€โ”€ system_info.py    local machine facts (macOS / Windows / Linux) for agent mode
โ”œโ”€โ”€ sources/          DeviceSource plugins: jamf, intune, jumpcloud,
โ”‚                     crowdstrike, sophos, defender
โ”œโ”€โ”€ sinks/            AssetSink plugins: snipeit
โ””โ”€โ”€ notifiers/        teams, slack, webhook

Adding a source is one file implementing DeviceSource.fetch_all() (plus an optional server-side find_by_serial) and one line in registry.py.


Development

pip install -e ".[dev]"
pytest                      # 26 tests: models, config, sink, orchestrator, providers

Provider tests mock the upstream APIs with responses, so the suite is fast and offline.


Packaging & releases

Tag a release and CI builds all three platforms and attaches them to a GitHub Release:

git tag v1.0.0 && git push origin v1.0.0

Build locally with the scripts in installers/ (PyInstaller one-file binary, then .pkg / .deb / .zip). See installers/README.md.


License

GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later) โ€” the same license as Snipe-IT, which Cairn integrates with. See LICENSE.

AGPL is a strong copyleft: if you modify Cairn and run it as a network service, you must make your modified source available to its users. For most teams running it internally to sync their own fleet, that changes nothing.

Formerly GhostAssetSync.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

cairn_sync-1.3.1.tar.gz (114.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

cairn_sync-1.3.1-py3-none-any.whl (118.8 kB view details)

Uploaded Python 3

File details

Details for the file cairn_sync-1.3.1.tar.gz.

File metadata

  • Download URL: cairn_sync-1.3.1.tar.gz
  • Upload date:
  • Size: 114.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for cairn_sync-1.3.1.tar.gz
Algorithm Hash digest
SHA256 0fa526d5f31500ff998cf2558c0f89f5ef2dc2c5c148e5295b27028c00875178
MD5 a34aecb96e6c762a3da1e605733f7147
BLAKE2b-256 0e974a40ccd23ecdbffbfb1bddaedea4f3c07fd2624d0532043fb0435660cbb7

See more details on using hashes here.

Provenance

The following attestation bundles were made for cairn_sync-1.3.1.tar.gz:

Publisher: publish.yml on jsdosanj/cairn

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file cairn_sync-1.3.1-py3-none-any.whl.

File metadata

  • Download URL: cairn_sync-1.3.1-py3-none-any.whl
  • Upload date:
  • Size: 118.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for cairn_sync-1.3.1-py3-none-any.whl
Algorithm Hash digest
SHA256 884afb6850e41583ef9f30b63a1d09fe8d2b9285fcf96d35834caf852fa5a0e3
MD5 c94bcfd617538e11a9bcdc59823d697f
BLAKE2b-256 c94d64196fe0f946ba282425efbe1ba75fdef0af770cda730b317b3ee07d7ff3

See more details on using hashes here.

Provenance

The following attestation bundles were made for cairn_sync-1.3.1-py3-none-any.whl:

Publisher: publish.yml on jsdosanj/cairn

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page