Files
vulncheck/app/routers/intune.py
T
vulncheck 79a2c002a0 feat(intune): a setting keeps Intune/Defender inventory out of the app-CVE scan
An operator who wants only Defender TVM CVEs on Intune devices had no way to
get that. Intune devices reach the app-CVE scan three ways: the Intune sync's
pass over detectedApps, the Defender sync feeding its software inventory into
the same pipeline, and the App-Scan job (nightly, data job, asset button)
fetching detectedApps for every Intune-inventory asset. Only the first read a
setting (`detected_apps`); the other two had none, so unticking it still left
app-scan findings on those devices.

New Intune setting `app_scan_intune` (default on, behaviour unchanged) gates
all three. Off: no detectedApps fetch in the App-Scan job, no app-CVE pass in
either sync. EOL, M365 and the iOS OS passes keep running, and a host with a
live Wazuh agent is still scanned from syscollector. Each run retracts the
app-scan source from package findings on ACTIVE Intune-inventory assets, so
the findings it left behind can close; a finding with another source (e.g.
defender) stays open, and the audit reason names the setting.
2026-10-29 11:28:36 +01:00

103 lines
3.9 KiB
Python

"""
Microsoft Intune / Graph integration HTTP endpoints.
- POST /api/v1/integrations/intune/test auth/connectivity probe
- POST /api/v1/integrations/intune/sync trigger device+inventory sync
Config lives in settings table key `intune_config` (encrypted JSON:
tenant_id, client_id, client_secret, verify_ssl, auto_create_assets,
detected_apps, defender_tvm, app_scan_intune).
"""
from __future__ import annotations
import logging
import threading
import time
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from app.auth.dependencies import RequireAdmin, RequireEditor, audited
from app.services.sync_run_service import record_sync_run
from app.database import get_db, SessionLocal
from app.models.user import User
from app.services.intune_service import load_intune_config, run_intune_sync, _build_client
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/v1/integrations/intune", tags=["Intune"])
# Last/current manual-sync state so the GUI can poll for completion (the sync
# itself is fire-and-forget 202). ponytail: module-level state assumes the
# single uvicorn worker we ship with; if you add --workers, move this to the DB.
_INTUNE_SYNC: dict = {"running": False, "result": None, "error": None,
"started_at": None, "finished_at": None}
@router.post("/test", dependencies=[audited("Intune connection test")])
def test_intune(
db: Session = Depends(get_db),
current_user: User = Depends(RequireAdmin),
):
"""Verify the Intune/Graph credentials: acquire a token + read 1 device."""
cfg = load_intune_config(db)
if not cfg:
raise HTTPException(400, "Intune is not configured (tenant_id/client_id/client_secret missing).")
client = _build_client(cfg)
try:
return client.test_connection()
finally:
client.close()
def _run_intune_sync_threaded() -> None:
db = SessionLocal()
try:
with record_sync_run("intune", "manual") as run:
stats = run_intune_sync(db)
run.stats.update(stats)
result = {k: v for k, v in stats.items() if k != "errors"}
_INTUNE_SYNC["result"] = result
_INTUNE_SYNC["error"] = None
logger.info("Intune sync (manual) done: %s", result)
except Exception as e:
_INTUNE_SYNC["result"] = None
_INTUNE_SYNC["error"] = str(e)
logger.error("Intune sync (manual) failed: %s", e)
finally:
db.close()
_INTUNE_SYNC["running"] = False
_INTUNE_SYNC["finished_at"] = time.time()
@router.post("/sync", status_code=202, dependencies=[audited("Intune sync")])
def sync_intune(
db: Session = Depends(get_db),
current_user: User = Depends(RequireEditor),
):
"""Kick off an Intune device/inventory sync in the background (202).
Fire-and-forget so a large tenant can't trip the reverse-proxy request
timeout. Poll GET /sync/status for completion."""
if not load_intune_config(db):
raise HTTPException(400, "Intune is not configured.")
if _INTUNE_SYNC["running"]:
return {"status": "already_running", "detail": "An Intune sync is already in progress."}
_INTUNE_SYNC.update({"running": True, "result": None, "error": None,
"started_at": time.time(), "finished_at": None})
threading.Thread(target=_run_intune_sync_threaded, daemon=True).start()
return {"status": "started", "detail": "Intune sync started in the background."}
@router.get("/sync/status")
def sync_intune_status(current_user: User = Depends(RequireEditor)):
"""Poll target for the GUI: current/last manual-sync state + result stats."""
s = _INTUNE_SYNC
state = ("running" if s["running"]
else "error" if s["error"]
else "done" if s["result"]
else "idle")
return {"state": state, "running": s["running"], "result": s["result"],
"error": s["error"], "finished_at": s["finished_at"]}