Unified Asset Management & Synchronisation Platform with AI-powered matching
Automated daily synchronisation of devices, users, and accessories between Jamf Pro, Snipe-IT, Azure AD / Microsoft Entra ID, and HiBob. Runs as an ECS Fargate scheduled task on AWS with zero-touch user provisioning.
- What it does
- Data flow & sources of truth
- Modules
- Matching engine
- AI features
- Deployment
- Configuration
- Operations
- Documentation
Every morning at 6am UTC, the suite:
- Provisions new starters from Azure AD into Snipe-IT
- Enriches existing Snipe-IT profiles with job titles, departments, phone numbers from Azure
- Matches every Jamf computer to its Snipe-IT user based on the actual local account on the machine
- Auto-creates Snipe-IT users for anyone who has a Mac but isn't in Snipe-IT yet (pulled from Azure AD)
- Self-heals wrong assignments by cross-referencing all 3 platforms
- Marks pending assets for leavers (Azure AD) while keeping the assignment for tracking
- Syncs HiBob equipment into Snipe-IT accessories with name normalisation
- Posts to Slack any case a human needs to review
No manual intervention needed for the 95% happy path. Ambiguous cases go to a Slack channel for review.
Azure AD HiBob
(leavers, starters, (equipment,
job titles, depts) accessories)
| |
v v
Jamf Pro ---------> Snipe-IT <---- HiBob Sync
(local user (asset inventory,
accounts) user records,
accessories)
| |
| confirmed |
+--- match ----------->+ (checkout asset to user)
| |
+<-- EA only ----------+ (Snipe-IT asset ID written to Jamf)
| |
+<-- verified ---------+ (confirmed name/email written back)
data only
| System | Source of truth for |
|---|---|
| Jamf Local Accounts | Who actually uses each machine (username + full name from the Mac) |
| Azure AD | Employee lifecycle β disabled/leaver status, job titles, departments |
| HiBob | Equipment/accessories assigned to employees |
| Snipe-IT | Asset inventory (consumer β receives data, never feeds back to identity fields) |
The golden rule: user identity information flows into Snipe-IT, never out of Snipe-IT into Jamf or Azure unless verified by a confirmed cross-platform match.
| Module | Time | Description |
|---|---|---|
| Correction | 06:15 | Validates existing Snipe-IT assignments against Jamf local accounts. Auto-corrects mismatches on exact matches; flags fuzzy/AI mismatches for Slack review. |
| User Match | 06:30 | Main provisioning. Matches Jamf computers to Snipe-IT users, creates assets, checks out. Auto-creates Snipe-IT users from Azure AD if needed. |
| Snipe-to-Jamf | 07:00 | Writes Snipe-IT asset ID EA back to Jamf (identity fields are never touched). |
| Leavers | 07:30 | Sets Pending status on assets for disabled/leaver Azure AD users. Keeps the assignment for tracking. |
| Module | Time | Description |
|---|---|---|
| Model Sync | Sun 01:00 | Ensures hardware models exist in Snipe-IT, creates missing ones. |
| Username Standardize | Sun 02:30 | Strips @domain from Snipe-IT usernames for consistency. |
| Cleanup | Sun 03:00 | Merges duplicate users, removes junk accounts. |
| AI Audit | Sun 04:00 | AI-powered cross-platform audit β finds security risks, data inconsistencies, anomalies. Posts structured Slack report. |
| Reconciliation | Sun 05:00 | Inventory diff Jamf β Snipe-IT. Identifies devices in one system but not the other. |
| Azure Starters | Mon 06:00 | Creates Snipe-IT users for new hires from Azure AD starters group. |
| User Enrichment | Mon 06:30 | Pushes Azure AD job titles, departments, phone numbers to Snipe-IT. |
| Peripherals Sync | Mon 08:00 | Syncs HiBob equipment to Snipe-IT accessories with name mapping. |
| Module | Description |
|---|---|
| WakeUp | Sends MDM redeploy commands to unresponsive Jamf devices. |
The matching engine identifies which Snipe-IT user owns each Jamf computer. Priority order:
config/user_overrides.json holds permanent mappings for edge cases (surname changes, custom local account names, test accounts). Fastest path, zero API calls.
Exact match against Snipe-IT user names. Disambiguates same-name users via email.
Uses the original Jamf location email directly (jane.doe@company.com) before falling back to reconstructed email.
Normalised prefix match (dots/dashes/underscores ignored).
Exact username match, case-insensitive.
Catches surname changes: Jamf local account meganwilkie still matches Snipe-IT user with username megan.wilkie@company.com even after the Snipe-IT name changed to "Megan Macdougall".
LCS + bigram Dice coefficient with surname bonus. Minimum score 14, required margin 20% between top two candidates.
When fuzzy rejects, the LLM reasons about the local account + all candidates + Azure AD data. Resolves:
- Nicknames: Tom β Thomas, Jonny β Jonathan, Rich β Richard
- Typos: "James Fird" β James Ford
- Surname changes via Azure aliases:
meganwilkieβ Megan Macdougall - Can decide to keep current assignment if AI concludes the Jamf local account is stale (old surname)
If nothing matches in Snipe-IT but the Jamf local user exists in Azure AD as active, the system creates the Snipe-IT user automatically with their Azure data and assigns the machine.
- Jamf local account is source of truth β if a local account matches a Snipe-IT user (disabled or not), the machine gets reassigned to that user. A machine whose local account belongs to
[Disabled] Xis still X's machine (notice period, returning, etc.) - Only auto-correct on exact matches β fuzzy/AI mismatches go to Slack for human review instead
- Pending assets untouched by User Match or Correction
- Checkout failure rollback β reverts to original user if new checkout fails
- Sandbox / test / demo accounts deprioritised β local accounts whose username or realname contains
sandbox,demo,service,test,temp, ortemporaryget -20 in the primary-picker score, so real users always win when both are present - Common non-person accounts skipped β
admin,shared,guest,createfuture,xdesign,payables,iossandboxaccount, etc. viaMATCHING_SKIP_USERNAMESenv var /matching.skip_usernamesyaml key
- Asset name = serial number (e.g.
WWLWWKCWGC). Auto-renamed on every full-run via Correction / User Match flow. - Asset tag =
CF-####progressive number (e.g.CF-0813). On asset creation,SnipeITClient.next_cf_tag()scans the max existingCF-<n>and returnsCF-<n+1>. The mutex inrun_all_modules_startupprevents two processes racing on the next number.
- Model: Claude Haiku (fast, cheap)
- Called only when fuzzy matching is ambiguous
- Cross-references Snipe-IT + Azure AD data
- Persistent cache in S3 (30-day TTL) β same query = 0 API calls
- Rate-limit circuit breaker β if the API returns usage-limit errors, AI is disabled for the rest of the run
- Model: Claude Sonnet (deep reasoning)
- Runs every Sunday at 04:00 UTC
- Collects data from Jamf + Snipe-IT + Azure AD
- Identifies: security risks, compliance gaps, users with excessive devices, untracked assets, offboarding process gaps
- Posts structured Slack report with severity ratings and specific recommendations
~$0.10β$0.50/month for typical operation (most calls hit the cache).
terraform/
modules/jamf-snipeit-suite/ # Reusable module
ecr.tf ECR repository + lifecycle policy
ecs.tf Cluster, task definition, security group
iam.tf Execution role, task role, EventBridge role
secrets.tf 12 SSM SecureString parameters
eventbridge.tf Daily 6am UTC scheduled trigger
cloudwatch.tf Log group (90-day retention)
s3_cache.tf AI resolver cache bucket
environments/prod/
main.tf Provider (account-locked), module call
Security hardening:
- Account lock:
allowed_account_ids = ["054904986477"] - Region lock:
eu-west-1only - All 12 credentials in SSM SecureString (encrypted at rest, not visible in task definition)
- Egress-only security group (no inbound)
- IAM least privilege (SSM parameters scoped to this project only)
- Sanitised logging (no credentials in CloudWatch)
- Random 24-char passwords for auto-created users
Cost: ~$2β5/month (Fargate pay-per-use, ~30min/day + ECR storage + CloudWatch + AI API)
cd terraform/environments/prod
cp terraform.tfvars.example terraform.tfvars # fill in secrets
terraform init
terraform plan
terraform apply
# Push Docker image
aws ecr get-login-password --region eu-west-1 | \
docker login --username AWS --password-stdin <ECR_URL>
docker build --platform linux/amd64 -t <ECR_URL>:latest .
docker push <ECR_URL>:latestEventBridge triggers the task automatically at 06:00 UTC daily.
# Dry run all modules
docker compose --profile run-once run --rm -e DRY_RUN=true run-once
# Run scheduler mode (daily cron)
docker compose up -d
# Single module
docker compose run --rm cli user-match --dry-run1. YAML file (config/config.yaml) β used for local development
jamf:
base_url: "https://your-instance.jamfcloud.com"
username: "api-user"
password: "api-password"
snipeit:
base_url: "https://your-snipeit.example.com"
api_token: "your-token"
azure:
tenant_id: "..."
client_id: "..."
client_secret: "..."
leavers_group_id: "..."
disabled_group_id: "..."
starters_group_id: "..."
matching:
email_domain: "company.com"
skip_usernames:
- "admin"
- "shared"2. Environment variables β used for Fargate (no config file mounted)
Every YAML key has an env var equivalent: JAMF_BASE_URL, SNIPEIT_API_TOKEN, AZURE_CLIENT_SECRET, etc. The app automatically uses env vars when config.yaml is missing.
For edge cases that matching can't solve automatically:
{
"overrides": {
"meganwilkie": {
"snipe_user_id": 687,
"snipe_user_name": "Megan Macdougall",
"reason": "Surname change after marriage"
}
}
}Baked into the Docker image. Takes precedence over all matching logic.
- OPERATIONS.md β day-to-day tasks (adding overrides, checking logs, investigating alerts)
- SECURITY.md β security policy, credential rotation, reporting vulnerabilities
- CONTRIBUTING.md β contribution guidelines
src/
clients/ API client wrappers (Jamf, Snipe-IT, Azure, HiBob, Slack)
core/ Config loader, client factory, state management
infra/ Audit CSV, health server, progress tracker
matching/ UserMatcher + AI resolver + user overrides
modules/
lifecycle/ Azure Starters, User Enrichment, Leavers
sync/ User Match, Correction, Snipe-to-Jamf, Model Sync, Peripherals
maintenance/ Cleanup, Reconciliation, Username Standardize, WakeUp, AI Audit
main.py CLI entry point
docker_scheduler.py Docker mode with scheduler + on-demand menu
terraform/
modules/jamf-snipeit-suite/ Reusable ECS Fargate module
environments/prod/ Production deployment
config/
config.yaml.example Template (safe to commit)
equipment_mapping.json HiBob name β Snipe-IT accessory mapping
user_overrides.json Manual matching overrides
MIT β see LICENSE
Davide Caputo β TechOps