d3e0769799
- 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
158 lines
5.0 KiB
Python
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()
|