#!/usr/bin/env python3 """Generate an object inventory CSV from Intune backup JSON files. One row per JSON object. Includes assignment summary columns. Output columns: PolicyType, ObjectName, ObjectType, ObjectId, Description, AssignmentState, AssignmentCount, IncludeTargets, ExcludeTargets, SourceFile """ from __future__ import annotations import argparse import csv import json from pathlib import Path from typing import Iterator _EXCLUDED_DIRS = {"reports", "__archive__"} _GROUP_TARGET_TYPES = { "#microsoft.graph.groupAssignmentTarget", "#microsoft.graph.exclusionGroupAssignmentTarget", } FIELDNAMES = [ "PolicyType", "ObjectName", "ObjectType", "ObjectId", "Description", "AssignmentState", "AssignmentCount", "IncludeTargets", "ExcludeTargets", "SourceFile", ] def parse_args() -> argparse.Namespace: p = argparse.ArgumentParser(description=__doc__) p.add_argument("--root", required=True, help="Path to backup root (e.g. tenant-state/intune).") p.add_argument("--output", default="object-inventory.csv", help="Output CSV path (default: object-inventory.csv).") return p.parse_args() def _safe(value: object) -> str: return "" if value is None else str(value).strip() def _resolve_target_name(target: dict) -> tuple[str, str]: """Returns (intent, display_name).""" ttype = _safe(target.get("@odata.type")) if ttype == "#microsoft.graph.allDevicesAssignmentTarget": return "include", "All devices" if ttype == "#microsoft.graph.allLicensedUsersAssignmentTarget": return "include", "All users" if ttype == "#microsoft.graph.exclusionGroupAssignmentTarget": name = (_safe(target.get("groupDisplayName") or target.get("groupName") or target.get("groupId")) or "Unresolved group") return "exclude", name if ttype in _GROUP_TARGET_TYPES: name = (_safe(target.get("groupDisplayName") or target.get("groupName") or target.get("groupId")) or "Unresolved group") return "include", name return "include", (_safe(target.get("groupDisplayName") or target.get("id")) or "Unknown target") def _summarize_assignments(payload: dict) -> dict[str, str]: assignments = payload.get("assignments") if not isinstance(assignments, list): return {"AssignmentState": "NotExported", "AssignmentCount": "0", "IncludeTargets": "", "ExcludeTargets": ""} valid = [a for a in assignments if isinstance(a, dict)] if not valid: return {"AssignmentState": "Unassigned", "AssignmentCount": "0", "IncludeTargets": "", "ExcludeTargets": ""} include: list[str] = [] exclude: list[str] = [] for assignment in valid: target = assignment.get("target") or {} intent, name = _resolve_target_name(target) explicit = _safe(assignment.get("intent")).lower() if explicit == "exclude" or intent == "exclude": exclude.append(name) else: include.append(name) return { "AssignmentState": "Assigned", "AssignmentCount": str(len(valid)), "IncludeTargets": "; ".join(sorted(set(include))), "ExcludeTargets": "; ".join(sorted(set(exclude))), } def _iter_rows(root: Path) -> Iterator[dict]: for path in sorted(root.rglob("*.json")): try: rel = path.relative_to(root) except ValueError: continue if any(part in _EXCLUDED_DIRS for part in rel.parts): continue try: payload = json.loads(path.read_text(encoding="utf-8")) except Exception: continue if not isinstance(payload, dict): continue policy_type = rel.parts[0] if rel.parts else "" object_name = (_safe(payload.get("displayName")) or _safe(payload.get("name")) or path.stem.split("__")[0]) assignment_summary = _summarize_assignments(payload) yield { "PolicyType": policy_type, "ObjectName": object_name, "ObjectType": _safe(payload.get("@odata.type")), "ObjectId": _safe(payload.get("id")), "Description": _safe(payload.get("description")), "SourceFile": rel.as_posix(), **assignment_summary, } def main() -> None: args = parse_args() root = Path(args.root).resolve() out_path = Path(args.output) if not root.exists(): raise SystemExit(f"Backup root not found: {root}") rows = sorted( _iter_rows(root), key=lambda r: (r["PolicyType"].lower(), r["ObjectName"].lower()), ) out_path.parent.mkdir(parents=True, exist_ok=True) with out_path.open("w", newline="", encoding="utf-8") as f: writer = csv.DictWriter(f, fieldnames=FIELDNAMES, extrasaction="ignore") writer.writeheader() writer.writerows(rows) print(f"Written {len(rows)} rows → {out_path}") if __name__ == "__main__": main()