Files
macOS_IntuneManagement/Scripts/Export-ObjectInventoryReport.py
tomas.kracmar d3e0769799 release: v4.1.0 — restructure entry points, add CIS baselines, reporting tools and fzf hints
- Restructure launchers: Start-IntuneToolkit.ps1 moves to repo root;
  Start-HeadlessIntune.ps1 moves to Scripts/; TUI helper moves to Scripts/Private/
- Add AGENTS.md with project architecture, entry points, and security notes
- Add CIS M365 baseline assets (CISM365-v7, M365-CIS-Rapid) and reporting scripts
- Add Python reporting utilities (Export-SettingsReport, Export-AssignmentReport,
  Export-ObjectInventoryReport) and CA wizard helpers
- Update Deploy-IntuneBaseline.ps1 with Merge conflict resolution, ReportPath,
  and optimized group loading
- Update Initialize-IntuneAuth.ps1 with -RotateSecret and configurable secret expiry
- Update Extensions for Settings Catalog definition auto-export
- Update README with v4.1.0, new entry points and script catalog
- Bump VERSION to 4.1.0
- Harden .gitignore against .DS_Store, __pycache__, .venv-pdf/, local exports,
  Settings.json and IntuneManagement.log
2026-06-14 15:24:42 +02:00

158 lines
5.0 KiB
Python

#!/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()