feat(intune): Microsoft Intune/Graph inventory source — Phase 1 (devices + OS-EOL)
Third inventory source next to Wazuh/Nessus: pulls Intune managed devices
via Microsoft Graph (app-only client-credentials) → assets + OS-level EOL.
- Migration 033: INTUNE assetsource label + assets.intune_device_id /
defender_machine_id (+ indexes). Model updated.
- app/integrations/graph_client.py: client-credentials token cache (mirrors
wazuh_client), paginated managedDevices, get_detected_apps (phase 2),
test_connection. Plain httpx, no msal dependency.
- app/services/intune_service.py: run_intune_sync — find-or-create asset
(intune_device_id → hostname → auto-create) mirroring nessus_sync,
source=INTUNE, refresh OS/version, OS-EOL via eol_service.check_os_eol,
id-keyed lifecycle reconcile.
- asset_lifecycle.reconcile_intune_by_seen_ids (mirrors the Nessus one).
- app/routers/intune.py: POST /api/v1/integrations/intune/test (admin),
/sync (editor, fire-and-forget 202). Registered in main.py.
- scheduler: intune_sync_nightly (sync def) at 02:10.
- Settings: encrypted intune_config (PROTECTED_SETTING_KEYS); settings PUT
now MERGES secret subfields (blank/"***set***" keeps the stored secret)
so JSON configs can be edited without re-typing secrets; client_secret
added to redaction subfields.
- Frontend: "Microsoft Intune (Graph API)" settings card (tenant/client/
secret + Test/Sync), intune/defender source badges, Asset type field.
- .env.example documents the Entra app registration + permissions.
Migration 033 required: alembic upgrade head. detectedApps→EOL/M365 is
phase 2 (toggle present, off by default until the per-package helpers land).