Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 122aa2d4e3 |
+1
-3
@@ -21,10 +21,8 @@ Exporting */
|
|||||||
*.backup/
|
*.backup/
|
||||||
*.backup
|
*.backup
|
||||||
|
|
||||||
# Graph metadata cache
|
# Graph metadata cache (now stored in the platform-specific data folder)
|
||||||
GraphMetaData.xml
|
GraphMetaData.xml
|
||||||
%LOCALAPPDATA%/
|
|
||||||
*%LOCALAPPDATA%*
|
|
||||||
CloudAPIPowerShellManagement/
|
CloudAPIPowerShellManagement/
|
||||||
|
|
||||||
# Local application settings (contains secrets on non-macOS platforms)
|
# Local application settings (contains secrets on non-macOS platforms)
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ The launcher caches tenant display names in `Settings.json` so the TUI can show
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `Start-IntuneToolkit.ps1` | Unified reverse-numbered `fzf`/numbered menu; remembers tenants; launches all other tools. |
|
| `Start-IntuneToolkit.ps1` | Unified reverse-numbered `fzf`/numbered menu; remembers tenants; launches all other tools. |
|
||||||
| `Scripts/Start-HeadlessIntune.ps1` | Single-action wrapper (`Export`, `Import`, `DeployCISBaseline`, `GenerateReports`) with optional interactive TUI. |
|
| `Scripts/Start-HeadlessIntune.ps1` | Single-action wrapper (`Export`, `Import`, `DeployCISBaseline`, `GenerateReports`) with optional interactive TUI. |
|
||||||
| `Scripts/Export-SettingsReport.py` | Generate a flat CSV of policy settings/values. Settings Catalog names are resolved from `configurationSettings.json` (auto-exported with Settings Catalog). |
|
| `Scripts/Export-SettingsReport.py` | Generate a flat CSV of policy settings/values. Includes a `Platform` column and resolves Settings Catalog names from `configurationSettings.json` (auto-exported with Settings Catalog). |
|
||||||
| `Scripts/Export-Policies.ps1` | Export policies to JSON. |
|
| `Scripts/Export-Policies.ps1` | Export policies to JSON. |
|
||||||
| `Scripts/Import-Policies.ps1` | Import policies from JSON. |
|
| `Scripts/Import-Policies.ps1` | Import policies from JSON. |
|
||||||
| `Scripts/Initialize-IntuneAuth.ps1` | One-time Entra app setup; also supports `-RotateSecret`, `-Delete`, `-DeleteApp`. |
|
| `Scripts/Initialize-IntuneAuth.ps1` | One-time Entra app setup; also supports `-RotateSecret`, `-Delete`, `-DeleteApp`. |
|
||||||
|
|||||||
@@ -9,6 +9,23 @@
|
|||||||
- This lets `Scripts/Export-SettingsReport.py` resolve `settingDefinitionId` values to the human-readable names shown in the Intune portal without any manual steps.
|
- This lets `Scripts/Export-SettingsReport.py` resolve `settingDefinitionId` values to the human-readable names shown in the Intune portal without any manual steps.
|
||||||
- Errors during definition export are logged but do not fail the policy export.
|
- Errors during definition export are logged but do not fail the policy export.
|
||||||
|
|
||||||
|
- **`Scripts/Export-SettingsReport.py`**
|
||||||
|
- New `Platform` column between `Policy` and `Setting`.
|
||||||
|
- For Settings Catalog, platform is read from the `platforms` field (e.g. `macOS`, `windows10`).
|
||||||
|
- For legacy policies, platform is inferred from `platform`/`platformType` or from `@odata.type` (e.g. `#microsoft.graph.iosCompliancePolicy` → `iOS`).
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **`Extensions/MSGraph.psm1`**
|
||||||
|
- `Get-GraphMetaData` now stores `GraphMetaData.xml` in the cross-platform data folder (`Get-CloudApiDataFolder`) instead of the literal Windows path `%LOCALAPPDATA%\CloudAPIPowerShellManagement\GraphMetaData.xml`.
|
||||||
|
- Removed the stray `%LOCALAPPDATA%\CloudAPIPowerShellManagement` folder from the repository and moved the existing `GraphMetaData.xml` to the correct macOS app-data location.
|
||||||
|
|
||||||
|
- **`Extensions/MSALAuthentication.psm1`**
|
||||||
|
- On non-Windows platforms the toolkit now skips `TokenCacheHelperEx` compilation with an informational log instead of throwing a `System.Security.Cryptography.ProtectedData.dll` error.
|
||||||
|
- Applied the same skip to the legacy `Add-MSALPrereq_old` function for consistency.
|
||||||
|
|
||||||
|
- **`.gitignore`**
|
||||||
|
- Removed the literal `%LOCALAPPDATA%` ignore patterns; kept `GraphMetaData.xml` and `CloudAPIPowerShellManagement/` ignores as safeguards.
|
||||||
|
|
||||||
### Modified
|
### Modified
|
||||||
- **`AGENTS.md`**
|
- **`AGENTS.md`**
|
||||||
- Added `Scripts/Export-SettingsReport.py` to the main entry points table and noted the automatic Settings Catalog name resolution.
|
- Added `Scripts/Export-SettingsReport.py` to the main entry points table and noted the automatic Settings Catalog name resolution.
|
||||||
|
|||||||
@@ -554,7 +554,12 @@ function Add-MSALPrereq
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (-not ("TokenCacheHelperEx" -as [type]))
|
if (-not (Test-IsWindowsPlatform))
|
||||||
|
{
|
||||||
|
$global:SkipTokenCacheHelperEx = $true
|
||||||
|
Write-Log "Token cache serialization is only supported on Windows. Skipping TokenCacheHelperEx."
|
||||||
|
}
|
||||||
|
elseif (-not ("TokenCacheHelperEx" -as [type]))
|
||||||
{
|
{
|
||||||
[System.Collections.Generic.List[string]] $RequiredAssemblies = New-Object System.Collections.Generic.List[string]
|
[System.Collections.Generic.List[string]] $RequiredAssemblies = New-Object System.Collections.Generic.List[string]
|
||||||
|
|
||||||
@@ -723,23 +728,31 @@ function Add-MSALPrereq_old
|
|||||||
$script:msalFile = $msalPath
|
$script:msalFile = $msalPath
|
||||||
}
|
}
|
||||||
|
|
||||||
$RequiredAssemblies.Add('System.Security.dll')
|
if (-not (Test-IsWindowsPlatform))
|
||||||
$RequiredAssemblies.Add('mscorlib.dll')
|
|
||||||
if($PSVersionTable.PSVersion.Major -ge 7)
|
|
||||||
{
|
|
||||||
$RequiredAssemblies.Add('System.Security.Cryptography.ProtectedData.dll')
|
|
||||||
}
|
|
||||||
|
|
||||||
$RequiredAssemblies.Add('System.Threading.dll')
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Add-Type -Path ($global:AppRootFolder + "\CS\TokenCacheHelperEx.cs") -ReferencedAssemblies $RequiredAssemblies
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
{
|
||||||
$global:SkipTokenCacheHelperEx = $true
|
$global:SkipTokenCacheHelperEx = $true
|
||||||
Write-LogError "Failed to compile TokenCacheHelperEx. The access token will not be cached. Check write access to the CS folder and ASR policies" $_.Exception
|
Write-Log "Token cache serialization is only supported on Windows. Skipping TokenCacheHelperEx."
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$RequiredAssemblies.Add('System.Security.dll')
|
||||||
|
$RequiredAssemblies.Add('mscorlib.dll')
|
||||||
|
if($PSVersionTable.PSVersion.Major -ge 7)
|
||||||
|
{
|
||||||
|
$RequiredAssemblies.Add('System.Security.Cryptography.ProtectedData.dll')
|
||||||
|
}
|
||||||
|
|
||||||
|
$RequiredAssemblies.Add('System.Threading.dll')
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Add-Type -Path ($global:AppRootFolder + "\CS\TokenCacheHelperEx.cs") -ReferencedAssemblies $RequiredAssemblies
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
$global:SkipTokenCacheHelperEx = $true
|
||||||
|
Write-LogError "Failed to compile TokenCacheHelperEx. The access token will not be cached. Check write access to the CS folder and ASR policies" $_.Exception
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if(Test-IsWindowsPlatform)
|
if(Test-IsWindowsPlatform)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1186,7 +1186,9 @@ function Get-GraphMetaData
|
|||||||
# There also no other version information in response headers. Use file date to update every week
|
# There also no other version information in response headers. Use file date to update every week
|
||||||
Write-Log "Load Graph MetaData file"
|
Write-Log "Load Graph MetaData file"
|
||||||
$url = "https://graph.microsoft.com/beta/`$metadata"
|
$url = "https://graph.microsoft.com/beta/`$metadata"
|
||||||
$fileFullPath = [Environment]::ExpandEnvironmentVariables("%LOCALAPPDATA%\CloudAPIPowerShellManagement\GraphMetaData.xml")
|
$dataFolder = if(Get-Command Get-CloudApiDataFolder -ErrorAction SilentlyContinue) { Get-CloudApiDataFolder } else { [Environment]::ExpandEnvironmentVariables("%LOCALAPPDATA%\macOS_IntuneManagement") }
|
||||||
|
[void][IO.Directory]::CreateDirectory($dataFolder)
|
||||||
|
$fileFullPath = Join-Path $dataFolder "GraphMetaData.xml"
|
||||||
$fi = [IO.FileInfo]$fileFullPath
|
$fi = [IO.FileInfo]$fileFullPath
|
||||||
$maxAge = (Get-Date).AddDays(-14)
|
$maxAge = (Get-Date).AddDays(-14)
|
||||||
if($fi.Exists -and ($fi.LastWriteTime -gt $maxAge -or $fi.CreationTime -gt $maxAge))
|
if($fi.Exists -and ($fi.LastWriteTime -gt $maxAge -or $fi.CreationTime -gt $maxAge))
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Covers Settings Catalog policies (human-readable names resolved from
|
|||||||
configurationSettings.json when present) and flat Device Configuration /
|
configurationSettings.json when present) and flat Device Configuration /
|
||||||
Compliance Policy objects.
|
Compliance Policy objects.
|
||||||
|
|
||||||
Output columns: Policy, Setting, Value
|
Output columns: Policy, Platform, Setting, Value
|
||||||
With --include-assignments: adds AssignmentState, IncludeTargets, ExcludeTargets
|
With --include-assignments: adds AssignmentState, IncludeTargets, ExcludeTargets
|
||||||
Group names resolved from MigrationTable.json (created by IntuneManagement export).
|
Group names resolved from MigrationTable.json (created by IntuneManagement export).
|
||||||
"""
|
"""
|
||||||
@@ -21,7 +21,19 @@ from typing import Any, Optional
|
|||||||
|
|
||||||
OUTPUT_FILE = "settings-report.csv"
|
OUTPUT_FILE = "settings-report.csv"
|
||||||
|
|
||||||
BASE_FIELDNAMES = ["Policy", "Setting", "Value"]
|
BASE_FIELDNAMES = ["Policy", "Platform", "Setting", "Value"]
|
||||||
|
|
||||||
|
_PLATFORM_LABELS = {
|
||||||
|
"windows10": "Windows 10/11",
|
||||||
|
"windows10X": "Windows 10X",
|
||||||
|
"windows": "Windows",
|
||||||
|
"macOS": "macOS",
|
||||||
|
"iOS": "iOS",
|
||||||
|
"android": "Android",
|
||||||
|
"androidEnterprise": "Android Enterprise",
|
||||||
|
"linux": "Linux",
|
||||||
|
"chromeOS": "Chrome OS",
|
||||||
|
}
|
||||||
ASSIGNMENT_FIELDNAMES = ["AssignmentState", "IncludeTargets", "ExcludeTargets"]
|
ASSIGNMENT_FIELDNAMES = ["AssignmentState", "IncludeTargets", "ExcludeTargets"]
|
||||||
|
|
||||||
_SKIP_KEYS = {
|
_SKIP_KEYS = {
|
||||||
@@ -83,6 +95,51 @@ def _choice_label(catalog: dict[str, Any], setting_id: str, value_id: str) -> st
|
|||||||
return suffix.title() if suffix.islower() else suffix or value_id
|
return suffix.title() if suffix.islower() else suffix or value_id
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_platforms(value: Any) -> str:
|
||||||
|
if value is None or value == "":
|
||||||
|
return ""
|
||||||
|
if isinstance(value, str):
|
||||||
|
items = [v.strip() for v in value.split(",")]
|
||||||
|
elif isinstance(value, list):
|
||||||
|
items = [str(v).strip() for v in value]
|
||||||
|
else:
|
||||||
|
items = [str(value).strip()]
|
||||||
|
labels = [_PLATFORM_LABELS.get(item, item) for item in items if item]
|
||||||
|
return "; ".join(labels)
|
||||||
|
|
||||||
|
|
||||||
|
def _platform_from_odata(odata_type: str) -> str:
|
||||||
|
lower = odata_type.lower()
|
||||||
|
if "macos" in lower:
|
||||||
|
return "macOS"
|
||||||
|
if "ios" in lower:
|
||||||
|
return "iOS"
|
||||||
|
if "android" in lower:
|
||||||
|
return "Android"
|
||||||
|
if "windows" in lower:
|
||||||
|
return "Windows"
|
||||||
|
if "linux" in lower:
|
||||||
|
return "Linux"
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_platform(policy: dict, category: str = "") -> str:
|
||||||
|
"""Best-effort platform/OS extraction for Settings Catalog and legacy policies."""
|
||||||
|
# Settings Catalog direct fields
|
||||||
|
platforms = policy.get("platforms")
|
||||||
|
if platforms:
|
||||||
|
return _normalize_platforms(platforms)
|
||||||
|
|
||||||
|
# Legacy policies sometimes expose platform/platformType directly
|
||||||
|
for key in ("platform", "platformType"):
|
||||||
|
val = policy.get(key)
|
||||||
|
if val:
|
||||||
|
return _normalize_platforms(val)
|
||||||
|
|
||||||
|
# Infer from @odata.type (e.g. #microsoft.graph.iosCompliancePolicy)
|
||||||
|
return _platform_from_odata(policy.get("@odata.type", ""))
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Assignment resolution
|
# Assignment resolution
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -151,7 +208,7 @@ def _summarize_assignments(policy: dict, groups: dict[str, str]) -> dict[str, st
|
|||||||
# Settings Catalog recursive walker
|
# Settings Catalog recursive walker
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def _walk(si: dict, catalog: dict[str, Any], policy: str,
|
def _walk(si: dict, catalog: dict[str, Any], policy: str, platform: str,
|
||||||
parent: str = "") -> list[dict]:
|
parent: str = "") -> list[dict]:
|
||||||
rows: list[dict] = []
|
rows: list[dict] = []
|
||||||
otype = si.get("@odata.type", "")
|
otype = si.get("@odata.type", "")
|
||||||
@@ -162,17 +219,19 @@ def _walk(si: dict, catalog: dict[str, Any], policy: str,
|
|||||||
|
|
||||||
children: list[dict] = []
|
children: list[dict] = []
|
||||||
|
|
||||||
|
base_row = {"Policy": policy, "Platform": platform}
|
||||||
|
|
||||||
if "ChoiceSettingInstance" in otype and "Collection" not in otype:
|
if "ChoiceSettingInstance" in otype and "Collection" not in otype:
|
||||||
csv_val = si.get("choiceSettingValue", {})
|
csv_val = si.get("choiceSettingValue", {})
|
||||||
value = _choice_label(catalog, sid, csv_val.get("value", ""))
|
value = _choice_label(catalog, sid, csv_val.get("value", ""))
|
||||||
rows.append({"Policy": policy, "Setting": name, "Value": value})
|
rows.append({**base_row, "Setting": name, "Value": value})
|
||||||
children = csv_val.get("children", [])
|
children = csv_val.get("children", [])
|
||||||
|
|
||||||
elif "SimpleSettingInstance" in otype and "Collection" not in otype:
|
elif "SimpleSettingInstance" in otype and "Collection" not in otype:
|
||||||
raw = si.get("simpleSettingValue", {})
|
raw = si.get("simpleSettingValue", {})
|
||||||
value = str(raw.get("value", "")) if isinstance(raw, dict) else str(raw)
|
value = str(raw.get("value", "")) if isinstance(raw, dict) else str(raw)
|
||||||
if value:
|
if value:
|
||||||
rows.append({"Policy": policy, "Setting": name, "Value": value})
|
rows.append({**base_row, "Setting": name, "Value": value})
|
||||||
|
|
||||||
elif "SimpleSettingCollectionInstance" in otype:
|
elif "SimpleSettingCollectionInstance" in otype:
|
||||||
vals = [
|
vals = [
|
||||||
@@ -180,14 +239,14 @@ def _walk(si: dict, catalog: dict[str, Any], policy: str,
|
|||||||
for v in si.get("simpleSettingCollectionValue", [])
|
for v in si.get("simpleSettingCollectionValue", [])
|
||||||
]
|
]
|
||||||
if vals:
|
if vals:
|
||||||
rows.append({"Policy": policy, "Setting": name, "Value": "; ".join(vals)})
|
rows.append({**base_row, "Setting": name, "Value": "; ".join(vals)})
|
||||||
|
|
||||||
elif "ChoiceSettingCollectionInstance" in otype:
|
elif "ChoiceSettingCollectionInstance" in otype:
|
||||||
items = si.get("choiceSettingCollectionValue", [])
|
items = si.get("choiceSettingCollectionValue", [])
|
||||||
vals = [_choice_label(catalog, sid, item.get("value", ""))
|
vals = [_choice_label(catalog, sid, item.get("value", ""))
|
||||||
for item in items if isinstance(item, dict)]
|
for item in items if isinstance(item, dict)]
|
||||||
if vals:
|
if vals:
|
||||||
rows.append({"Policy": policy, "Setting": name, "Value": "; ".join(vals)})
|
rows.append({**base_row, "Setting": name, "Value": "; ".join(vals)})
|
||||||
|
|
||||||
elif "GroupSettingCollectionInstance" in otype:
|
elif "GroupSettingCollectionInstance" in otype:
|
||||||
for group in si.get("groupSettingCollectionValue", []):
|
for group in si.get("groupSettingCollectionValue", []):
|
||||||
@@ -195,7 +254,7 @@ def _walk(si: dict, catalog: dict[str, Any], policy: str,
|
|||||||
|
|
||||||
for child in children:
|
for child in children:
|
||||||
if isinstance(child, dict):
|
if isinstance(child, dict):
|
||||||
rows.extend(_walk(child, catalog, policy, parent=name))
|
rows.extend(_walk(child, catalog, policy, platform, parent=name))
|
||||||
|
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
@@ -223,10 +282,11 @@ def process_settings_catalog(root: Path, catalog: dict[str, Any],
|
|||||||
with path.open(encoding="utf-8") as f:
|
with path.open(encoding="utf-8") as f:
|
||||||
policy = json.load(f)
|
policy = json.load(f)
|
||||||
policy_name = policy.get("name") or path.stem
|
policy_name = policy.get("name") or path.stem
|
||||||
|
platform = _extract_platform(policy, "SettingsCatalog")
|
||||||
assignment_cols = _summarize_assignments(policy, groups) if include_assignments else {}
|
assignment_cols = _summarize_assignments(policy, groups) if include_assignments else {}
|
||||||
for setting in policy.get("settings", []):
|
for setting in policy.get("settings", []):
|
||||||
si = setting.get("settingInstance", {})
|
si = setting.get("settingInstance", {})
|
||||||
for row in _walk(si, catalog, policy_name):
|
for row in _walk(si, catalog, policy_name, platform):
|
||||||
row.update(assignment_cols)
|
row.update(assignment_cols)
|
||||||
rows.append(row)
|
rows.append(row)
|
||||||
return rows
|
return rows
|
||||||
@@ -248,6 +308,7 @@ def process_flat_category(root: Path, category: str,
|
|||||||
if not isinstance(policy, dict):
|
if not isinstance(policy, dict):
|
||||||
continue
|
continue
|
||||||
policy_name = policy.get("displayName") or policy.get("name") or path.stem
|
policy_name = policy.get("displayName") or policy.get("name") or path.stem
|
||||||
|
platform = _extract_platform(policy, category)
|
||||||
assignment_cols = _summarize_assignments(policy, groups) if include_assignments else {}
|
assignment_cols = _summarize_assignments(policy, groups) if include_assignments else {}
|
||||||
for key, value in policy.items():
|
for key, value in policy.items():
|
||||||
if key in _SKIP_KEYS or value is None:
|
if key in _SKIP_KEYS or value is None:
|
||||||
@@ -258,7 +319,7 @@ def process_flat_category(root: Path, category: str,
|
|||||||
value_str = value_str[:497] + "..."
|
value_str = value_str[:497] + "..."
|
||||||
else:
|
else:
|
||||||
value_str = str(value)
|
value_str = str(value)
|
||||||
row = {"Policy": policy_name, "Setting": key, "Value": value_str}
|
row = {"Policy": policy_name, "Platform": platform, "Setting": key, "Value": value_str}
|
||||||
row.update(assignment_cols)
|
row.update(assignment_cols)
|
||||||
rows.append(row)
|
rows.append(row)
|
||||||
return rows
|
return rows
|
||||||
|
|||||||
Reference in New Issue
Block a user