- Add Deploy-IntuneBaseline.ps1 for YAML-driven policy + assignment deployment - Add ConvertTo-IntuneBaseline.ps1 to convert export folders to baseline manifests - Add example OpenIntuneBaseline YAML in Baselines/ - Supports mutations, group auto-creation, idempotency, and WhatIf mode
583 lines
26 KiB
PowerShell
583 lines
26 KiB
PowerShell
#requires -Version 7.0
|
|
<#
|
|
.SYNOPSIS
|
|
Deploys a declarative Intune baseline from a YAML manifest.
|
|
.DESCRIPTION
|
|
Reads a baseline YAML file, creates missing groups, imports policies,
|
|
applies name mutations, and assigns objects — all in a single command.
|
|
Ideal for seeding new tenants with OpenIntuneBaseline-style configurations.
|
|
.EXAMPLE
|
|
./Scripts/Deploy-IntuneBaseline.ps1 -BaselinePath ./Baselines/mybaseline.yaml -TenantId "contoso.onmicrosoft.com"
|
|
.EXAMPLE
|
|
./Scripts/Deploy-IntuneBaseline.ps1 -BaselinePath ./Baselines/mybaseline.yaml -WhatIf
|
|
#>
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$BaselinePath,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$TenantId,
|
|
|
|
[string]$AppId,
|
|
|
|
[string]$Secret,
|
|
|
|
[string]$Certificate,
|
|
|
|
[ValidateSet("AppOnly","Browser","DeviceCode")]
|
|
[string]$AuthMode = "AppOnly",
|
|
|
|
[string]$RedirectUri,
|
|
|
|
[string]$SettingsFile,
|
|
|
|
[switch]$WhatIf
|
|
)
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
|
|
#region Helper functions
|
|
function Get-DefaultSettingsPath
|
|
{
|
|
if($IsWindows -or $env:OS -eq "Windows_NT")
|
|
{
|
|
if($env:LOCALAPPDATA) { return (Join-Path $env:LOCALAPPDATA "macOS_IntuneManagement\Settings.json") }
|
|
return (Join-Path $env:USERPROFILE "AppData\Local\macOS_IntuneManagement\Settings.json")
|
|
}
|
|
if($IsMacOS) { return (Join-Path $HOME "Library/Application Support/macOS_IntuneManagement/Settings.json") }
|
|
return (Join-Path $HOME ".local/share/macOS_IntuneManagement/Settings.json")
|
|
}
|
|
|
|
function Resolve-RelativePath
|
|
{
|
|
param([string]$Path, [string]$BasePath)
|
|
if([System.IO.Path]::IsPathRooted($Path)) { return $Path }
|
|
$baseDir = Split-Path -Parent $BasePath
|
|
return Join-Path $baseDir $Path
|
|
}
|
|
|
|
function Test-YamlModule
|
|
{
|
|
return [bool](Get-Module -ListAvailable -Name powershell-yaml)
|
|
}
|
|
|
|
function Install-YamlModule
|
|
{
|
|
Write-Host "powershell-yaml module is required but not installed." -ForegroundColor Yellow
|
|
if(-not $WhatIf)
|
|
{
|
|
$confirm = Read-Host "Install powershell-yaml from PSGallery now? [Y/n]"
|
|
if($confirm -match "^\s*n")
|
|
{
|
|
throw "powershell-yaml is required. Install it with: Install-Module powershell-yaml -Scope CurrentUser -Force"
|
|
}
|
|
Install-Module powershell-yaml -Scope CurrentUser -Force
|
|
Import-Module powershell-yaml -Force
|
|
}
|
|
}
|
|
|
|
function Get-BaselineTypeMap
|
|
{
|
|
return @{
|
|
"Applications" = @{ API = "/deviceAppManagement/mobileApps"; AssignmentsType = "mobileAppAssignments"; AssignmentODataType = "#microsoft.graph.mobileAppAssignment"; HasIntent = $true; NameProp = "displayName"; SettingsAPI = $null }
|
|
"DeviceConfiguration" = @{ API = "/deviceManagement/deviceConfigurations"; AssignmentsType = "assignments"; AssignmentODataType = "#microsoft.graph.deviceConfigurationAssignment"; HasIntent = $false; NameProp = "displayName"; SettingsAPI = $null }
|
|
"SettingsCatalog" = @{ API = "/deviceManagement/configurationPolicies"; AssignmentsType = "assignments"; AssignmentODataType = "#microsoft.graph.deviceManagementConfigurationPolicyAssignment"; HasIntent = $false; NameProp = "name"; SettingsAPI = $null }
|
|
"CompliancePolicies" = @{ API = "/deviceManagement/deviceCompliancePolicies"; AssignmentsType = "assignments"; AssignmentODataType = "#microsoft.graph.deviceCompliancePolicyAssignment"; HasIntent = $false; NameProp = "displayName"; SettingsAPI = $null }
|
|
"CompliancePoliciesV2" = @{ API = "/deviceManagement/compliancePolicies"; AssignmentsType = "assignments"; AssignmentODataType = "#microsoft.graph.deviceCompliancePolicyAssignment"; HasIntent = $false; NameProp = "name"; SettingsAPI = $null }
|
|
"AdministrativeTemplates" = @{ API = "/deviceManagement/groupPolicyConfigurations"; AssignmentsType = "assignments"; AssignmentODataType = "#microsoft.graph.groupPolicyConfigurationAssignment"; HasIntent = $false; NameProp = "displayName"; SettingsAPI = $null }
|
|
"EndpointSecurity" = @{ API = "/deviceManagement/intents"; AssignmentsType = "assignments"; AssignmentODataType = "#microsoft.graph.deviceManagementIntentAssignment"; HasIntent = $false; NameProp = "displayName"; SettingsAPI = "updateSettings" }
|
|
"DeviceManagementIntents" = @{ API = "/deviceManagement/intents"; AssignmentsType = "assignments"; AssignmentODataType = "#microsoft.graph.deviceManagementIntentAssignment"; HasIntent = $false; NameProp = "displayName"; SettingsAPI = "updateSettings" }
|
|
"AppProtection" = @{ API = "/deviceAppManagement/managedAppPolicies"; AssignmentsType = "assignments"; AssignmentODataType = "#microsoft.graph.targetedManagedAppPolicyAssignment"; HasIntent = $false; NameProp = "displayName"; SettingsAPI = $null }
|
|
"AppConfigurationManagedDevice" = @{ API = "/deviceAppManagement/mobileAppConfigurations"; AssignmentsType = "assignments"; AssignmentODataType = "#microsoft.graph.managedDeviceMobileAppConfigurationAssignment"; HasIntent = $false; NameProp = "displayName"; SettingsAPI = $null }
|
|
"PlatformScripts" = @{ API = "/deviceManagement/deviceManagementScripts"; AssignmentsType = "deviceManagementScriptAssignments"; AssignmentODataType = "#microsoft.graph.deviceManagementScriptAssignment"; HasIntent = $false; NameProp = "displayName"; SettingsAPI = $null }
|
|
"MacScripts" = @{ API = "/deviceManagement/deviceShellScripts"; AssignmentsType = "deviceManagementScriptAssignments"; AssignmentODataType = "#microsoft.graph.deviceManagementScriptAssignment"; HasIntent = $false; NameProp = "displayName"; SettingsAPI = $null }
|
|
"DeviceHealthScripts" = @{ API = "/deviceManagement/deviceHealthScripts"; AssignmentsType = "deviceHealthScriptAssignments"; AssignmentODataType = "#microsoft.graph.deviceHealthScriptAssignment"; HasIntent = $false; NameProp = "displayName"; SettingsAPI = $null }
|
|
"MacCustomAttributes" = @{ API = "/deviceManagement/deviceCustomAttributeShellScripts"; AssignmentsType = "deviceManagementScriptAssignments"; AssignmentODataType = "#microsoft.graph.deviceManagementScriptAssignment"; HasIntent = $false; NameProp = "displayName"; SettingsAPI = $null }
|
|
"EnrollmentRestrictions" = @{ API = "/deviceManagement/deviceEnrollmentConfigurations"; AssignmentsType = "enrollmentConfigurationAssignments"; AssignmentODataType = "#microsoft.graph.enrollmentConfigurationAssignment"; HasIntent = $false; NameProp = "displayName"; SettingsAPI = $null }
|
|
"EnrollmentStatusPage" = @{ API = "/deviceManagement/deviceEnrollmentConfigurations"; AssignmentsType = "enrollmentConfigurationAssignments"; AssignmentODataType = "#microsoft.graph.enrollmentConfigurationAssignment"; HasIntent = $false; NameProp = "displayName"; SettingsAPI = $null }
|
|
"Autopilot" = @{ API = "/deviceManagement/windowsAutopilotDeploymentProfiles"; AssignmentsType = "assignments"; AssignmentODataType = "#microsoft.graph.windowsAutopilotDeploymentProfileAssignment"; HasIntent = $false; NameProp = "displayName"; SettingsAPI = $null }
|
|
"TermsAndConditions" = @{ API = "/deviceManagement/termsAndConditions"; AssignmentsType = "assignments"; AssignmentODataType = "#microsoft.graph.termsAndConditionsAssignment"; HasIntent = $false; NameProp = "displayName"; SettingsAPI = $null }
|
|
"PolicySets" = @{ API = "/deviceAppManagement/policySets"; AssignmentsType = "assignments"; AssignmentODataType = "#microsoft.graph.policySetAssignment"; HasIntent = $false; NameProp = "displayName"; SettingsAPI = $null }
|
|
"UpdatePolicies" = @{ API = "/deviceManagement/windowsUpdateForBusinessConfigurations"; AssignmentsType = "assignments"; AssignmentODataType = "#microsoft.graph.windowsUpdateForBusinessConfigurationAssignment"; HasIntent = $false; NameProp = "displayName"; SettingsAPI = $null }
|
|
"FeatureUpdates" = @{ API = "/deviceManagement/windowsFeatureUpdateProfiles"; AssignmentsType = "assignments"; AssignmentODataType = "#microsoft.graph.windowsFeatureUpdateProfileAssignment"; HasIntent = $false; NameProp = "displayName"; SettingsAPI = $null }
|
|
"QualityUpdates" = @{ API = "/deviceManagement/windowsQualityUpdateProfiles"; AssignmentsType = "assignments"; AssignmentODataType = "#microsoft.graph.windowsQualityUpdateProfileAssignment"; HasIntent = $false; NameProp = "displayName"; SettingsAPI = $null }
|
|
}
|
|
}
|
|
|
|
function Invoke-SanitizeObject
|
|
{
|
|
param($Obj)
|
|
$propsToRemove = @("id","createdDateTime","lastModifiedDateTime","source","status","version","isAssigned","publishingState")
|
|
foreach($prop in $propsToRemove)
|
|
{
|
|
if($Obj.PSObject.Properties[$prop])
|
|
{
|
|
$Obj.PSObject.Properties.Remove($prop)
|
|
}
|
|
}
|
|
return $Obj
|
|
}
|
|
|
|
function Invoke-ApplyMutation
|
|
{
|
|
param($Obj, $NameProp, [hashtable]$Mutation)
|
|
if(-not $Mutation) { return $Obj }
|
|
|
|
$search = $Mutation["search"]
|
|
$replace = $Mutation["replace"]
|
|
$prefix = $Mutation["prefix"]
|
|
|
|
foreach($prop in @($NameProp, "description"))
|
|
{
|
|
if($Obj.PSObject.Properties[$prop] -and $Obj.$prop)
|
|
{
|
|
$val = $Obj.$prop
|
|
if($search -and $replace)
|
|
{
|
|
$val = $val -replace $search, $replace
|
|
}
|
|
elseif($prefix)
|
|
{
|
|
if(-not $val.StartsWith($prefix))
|
|
{
|
|
$val = "$prefix$val"
|
|
}
|
|
}
|
|
$Obj.$prop = $val
|
|
}
|
|
}
|
|
return $Obj
|
|
}
|
|
|
|
function Get-ExistingObject
|
|
{
|
|
param($Api, $NameProp, $NameValue)
|
|
$escaped = $NameValue -replace "'","''"
|
|
$filter = "$NameProp eq '$escaped'"
|
|
$url = "$Api`?`$filter=$filter"
|
|
try
|
|
{
|
|
$resp = Invoke-GraphRequest -Url $url
|
|
if($resp.value -and $resp.value.Count -gt 0)
|
|
{
|
|
return $resp.value[0]
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
# Some APIs don't support $filter; swallow and return null
|
|
}
|
|
return $null
|
|
}
|
|
|
|
function New-CloudOnlyGroup
|
|
{
|
|
param([string]$DisplayName, [string]$MailNickname, [bool]$SecurityEnabled = $true)
|
|
$body = @{
|
|
displayName = $DisplayName
|
|
mailEnabled = $false
|
|
mailNickname = $MailNickname
|
|
securityEnabled = $SecurityEnabled
|
|
} | ConvertTo-Json -Depth 5
|
|
return Invoke-GraphRequest -Url "/groups" -HttpMethod POST -Content $body
|
|
}
|
|
|
|
function Invoke-DeployAssignments
|
|
{
|
|
param(
|
|
[string]$ObjectId,
|
|
[hashtable]$TypeMeta,
|
|
[array]$Assignments,
|
|
[hashtable]$GroupCache,
|
|
[switch]$WhatIf
|
|
)
|
|
if(-not $Assignments -or $Assignments.Count -eq 0) { return }
|
|
|
|
$assignmentList = @()
|
|
foreach($ass in $Assignments)
|
|
{
|
|
$targetType = $ass["targetType"]
|
|
$groupName = $ass["groupName"]
|
|
$intent = $ass["intent"]
|
|
|
|
$odataType = switch($targetType)
|
|
{
|
|
"Group" { "#microsoft.graph.groupAssignmentTarget" }
|
|
"AllUsers" { "#microsoft.graph.allLicensedUsersAssignmentTarget" }
|
|
"AllDevices" { "#microsoft.graph.allDevicesAssignmentTarget" }
|
|
"ExcludeGroup" { "#microsoft.graph.exclusionGroupAssignmentTarget" }
|
|
default { throw "Unknown targetType: $targetType" }
|
|
}
|
|
|
|
$targetPayload = @{
|
|
"@odata.type" = $odataType
|
|
}
|
|
if($targetType -in @("Group","ExcludeGroup"))
|
|
{
|
|
if(-not $groupName) { throw "groupName is required for targetType $targetType" }
|
|
$gid = $GroupCache[$groupName]
|
|
if(-not $gid) { throw "Group '$groupName' not found in cache" }
|
|
$targetPayload["groupId"] = $gid
|
|
}
|
|
|
|
$payload = @{
|
|
"@odata.type" = $TypeMeta.AssignmentODataType
|
|
target = $targetPayload
|
|
}
|
|
if($TypeMeta.HasIntent -and $intent)
|
|
{
|
|
$payload["intent"] = $intent.ToString().ToLower()
|
|
}
|
|
$assignmentList += $payload
|
|
}
|
|
|
|
if($assignmentList.Count -eq 0) { return }
|
|
|
|
$assignBody = @{
|
|
$TypeMeta.AssignmentsType = $assignmentList
|
|
} | ConvertTo-Json -Depth 50 -Compress
|
|
|
|
$assignUrl = "$($TypeMeta.API)/$ObjectId/assign"
|
|
if($WhatIf)
|
|
{
|
|
Write-Host " [WHATIF] Would assign $($assignmentList.Count) target(s) to $assignUrl" -ForegroundColor Magenta
|
|
return
|
|
}
|
|
$null = Invoke-GraphRequest -Url $assignUrl -HttpMethod POST -Content $assignBody
|
|
}
|
|
#endregion
|
|
|
|
#region Initialize Runtime
|
|
$projectRoot = Split-Path -Parent $PSScriptRoot
|
|
$runtimeModule = Join-Path $projectRoot "Runtime/IntuneManagement.Runtime.psd1"
|
|
if(-not (Test-Path $runtimeModule))
|
|
{
|
|
throw "Could not find IntuneManagement.Runtime.psd1 in $projectRoot"
|
|
}
|
|
|
|
$settingsPath = $SettingsFile
|
|
if(-not $settingsPath)
|
|
{
|
|
$settingsPath = Get-DefaultSettingsPath
|
|
}
|
|
|
|
# Pre-load auth from settings
|
|
if($AuthMode -eq "AppOnly" -and (Test-Path $settingsPath) -and (-not $AppId -or (-not $Secret -and -not $Certificate)))
|
|
{
|
|
try
|
|
{
|
|
$raw = Get-Content -Path $settingsPath -Raw -ErrorAction Stop
|
|
$settingsObj = ConvertFrom-Json $raw -AsHashtable -ErrorAction Stop
|
|
if($settingsObj -and $settingsObj.ContainsKey($TenantId))
|
|
{
|
|
$tenantNode = $settingsObj[$TenantId]
|
|
if(-not $AppId -and $tenantNode.ContainsKey("GraphAzureAppId"))
|
|
{
|
|
$AppId = $tenantNode["GraphAzureAppId"]
|
|
}
|
|
if(-not $Secret -and $tenantNode.ContainsKey("GraphAzureAppSecret"))
|
|
{
|
|
$Secret = $tenantNode["GraphAzureAppSecret"]
|
|
}
|
|
if(-not $Certificate -and $tenantNode.ContainsKey("GraphAzureAppCert"))
|
|
{
|
|
$Certificate = $tenantNode["GraphAzureAppCert"]
|
|
}
|
|
}
|
|
|
|
if(-not $Secret -and $IsMacOS -and $AppId)
|
|
{
|
|
try
|
|
{
|
|
$keychainSecret = security find-generic-password -a "IntuneManagement" -s "IntuneMgmt-$AppId" -w 2>$null
|
|
if($keychainSecret) { $Secret = $keychainSecret }
|
|
}
|
|
catch { }
|
|
}
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
$invokeParams = @{
|
|
Silent = $true
|
|
JSonSettings = $true
|
|
JSonFile = $settingsPath
|
|
TenantId = $TenantId
|
|
AppId = $AppId
|
|
AuthMode = $AuthMode
|
|
}
|
|
if($RedirectUri) { $invokeParams.RedirectUri = $RedirectUri }
|
|
if($AuthMode -eq "AppOnly" -and $Secret) { $invokeParams.Secret = $Secret }
|
|
elseif($AuthMode -eq "AppOnly") { $invokeParams.Certificate = $Certificate }
|
|
|
|
Import-Module $runtimeModule -Force
|
|
Initialize-IntuneManagementRuntime -View "IntuneGraphAPI" @invokeParams
|
|
#endregion
|
|
|
|
#region Ensure Graph connectivity
|
|
if(-not (Get-Command Invoke-GraphRequest -ErrorAction SilentlyContinue))
|
|
{
|
|
throw "Graph runtime did not load Invoke-GraphRequest. Aborting."
|
|
}
|
|
|
|
Write-Host "`nConnecting to Microsoft Graph..." -ForegroundColor Cyan
|
|
try
|
|
{
|
|
$org = Invoke-GraphRequest "/organization"
|
|
Write-Host "Connected to tenant: $($org.value[0].displayName) ($($org.value[0].id))" -ForegroundColor Green
|
|
}
|
|
catch
|
|
{
|
|
throw "Failed to connect to Graph. Ensure auth parameters are correct. Error: $_"
|
|
}
|
|
#endregion
|
|
|
|
#region Dependency check
|
|
if(-not (Test-YamlModule))
|
|
{
|
|
Install-YamlModule
|
|
}
|
|
Import-Module powershell-yaml -Force
|
|
#endregion
|
|
|
|
#region Load and validate baseline
|
|
$baselinePathResolved = Resolve-Path $BaselinePath | Select-Object -ExpandProperty Path
|
|
if(-not (Test-Path $baselinePathResolved))
|
|
{
|
|
throw "Baseline file not found: $BaselinePath"
|
|
}
|
|
|
|
Write-Host "`nLoading baseline: $baselinePathResolved" -ForegroundColor Cyan
|
|
$yamlText = Get-Content $baselinePathResolved -Raw
|
|
$yamlRoot = ConvertFrom-Yaml -Yaml $yamlText
|
|
|
|
if(-not $yamlRoot -or -not $yamlRoot.ContainsKey("baseline"))
|
|
{
|
|
throw "Invalid baseline YAML: missing 'baseline' root node."
|
|
}
|
|
$baseline = $yamlRoot["baseline"]
|
|
|
|
$conflictResolution = if($baseline.ContainsKey("conflictResolution")) { $baseline["conflictResolution"] } else { "Skip" }
|
|
$baselineWhatIf = if($baseline.ContainsKey("whatIf")) { [bool]$baseline["whatIf"] } else { $false }
|
|
$effectiveWhatIf = $WhatIf.IsPresent -or $baselineWhatIf
|
|
|
|
$globalMutation = $null
|
|
if($baseline.ContainsKey("tenantMutation"))
|
|
{
|
|
$globalMutation = $baseline["tenantMutation"]
|
|
}
|
|
|
|
Write-Host "Baseline name : $($baseline["name"])" -ForegroundColor Cyan
|
|
Write-Host "Conflict mode : $conflictResolution" -ForegroundColor Cyan
|
|
if($effectiveWhatIf) { Write-Host "*** DRY-RUN MODE ENABLED ***" -ForegroundColor Magenta }
|
|
#endregion
|
|
|
|
#region Resolve / create groups
|
|
$groupCache = @{}
|
|
if($baseline.ContainsKey("groups") -and $baseline["groups"])
|
|
{
|
|
Write-Host "`nResolving groups..." -ForegroundColor Cyan
|
|
$existingGroupsResp = Invoke-GraphRequest "/groups?`$select=id,displayName&`$orderby=displayName&`$top=999"
|
|
$existingGroups = $existingGroupsResp.value
|
|
|
|
foreach($grpDef in $baseline["groups"])
|
|
{
|
|
$displayName = $grpDef["displayName"]
|
|
$existing = $existingGroups | Where-Object { $_.displayName -eq $displayName } | Select-Object -First 1
|
|
if($existing)
|
|
{
|
|
Write-Host " Group exists: $displayName ($($existing.id))" -ForegroundColor Green
|
|
$groupCache[$displayName] = $existing.id
|
|
}
|
|
else
|
|
{
|
|
$mailNick = $grpDef["mailNickname"]
|
|
$secEnabled = if($grpDef.ContainsKey("securityEnabled")) { [bool]$grpDef["securityEnabled"] } else { $true }
|
|
if($effectiveWhatIf)
|
|
{
|
|
Write-Host " [WHATIF] Would create group: $displayName" -ForegroundColor Magenta
|
|
$groupCache[$displayName] = "WHATIF-$displayName"
|
|
}
|
|
else
|
|
{
|
|
Write-Host " Creating group: $displayName" -ForegroundColor Yellow
|
|
$newGrp = New-CloudOnlyGroup -DisplayName $displayName -MailNickname $mailNick -SecurityEnabled $secEnabled
|
|
$groupCache[$displayName] = $newGrp.id
|
|
Write-Host " Created: $($newGrp.id)" -ForegroundColor Green
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region Pre-load all existing groups for assignment resolution
|
|
Write-Host "`nPre-loading group directory..." -ForegroundColor Cyan
|
|
$allGroupsResp = Invoke-GraphRequest "/groups?`$select=id,displayName&`$orderby=displayName&`$top=999"
|
|
foreach($g in $allGroupsResp.value)
|
|
{
|
|
if(-not $groupCache.ContainsKey($g.displayName))
|
|
{
|
|
$groupCache[$g.displayName] = $g.id
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region Process policies
|
|
$typeMap = Get-BaselineTypeMap
|
|
$stats = @{
|
|
Created = 0
|
|
Updated = 0
|
|
Skipped = 0
|
|
Failed = 0
|
|
Assigned = 0
|
|
}
|
|
|
|
if($baseline.ContainsKey("policies") -and $baseline["policies"])
|
|
{
|
|
$policies = $baseline["policies"]
|
|
Write-Host "`nDeploying $($policies.Count) policy(ies)..." -ForegroundColor Cyan
|
|
|
|
foreach($policyDef in $policies)
|
|
{
|
|
$sourcePath = Resolve-RelativePath -Path $policyDef["sourcePath"] -BasePath $baselinePathResolved
|
|
$typeName = $policyDef["type"]
|
|
|
|
if(-not $typeMap.ContainsKey($typeName))
|
|
{
|
|
Write-Warning "Unknown policy type '$typeName'. Skipping."
|
|
$stats.Failed++
|
|
continue
|
|
}
|
|
$typeMeta = $typeMap[$typeName]
|
|
|
|
if(-not (Test-Path $sourcePath))
|
|
{
|
|
Write-Warning "Policy file not found: $sourcePath"
|
|
$stats.Failed++
|
|
continue
|
|
}
|
|
|
|
try
|
|
{
|
|
$jsonRaw = Get-Content $sourcePath -Raw
|
|
$policyObj = $jsonRaw | ConvertFrom-Json -Depth 100
|
|
|
|
# Sanitize
|
|
$policyObj = Invoke-SanitizeObject -Obj $policyObj
|
|
|
|
# Mutate
|
|
$mutation = $globalMutation
|
|
if($policyDef.ContainsKey("mutation"))
|
|
{
|
|
$mutation = $policyDef["mutation"]
|
|
}
|
|
$policyObj = Invoke-ApplyMutation -Obj $policyObj -NameProp $typeMeta.NameProp -Mutation $mutation
|
|
$mutatedName = $policyObj.($typeMeta.NameProp)
|
|
|
|
Write-Host "`nPolicy: $mutatedName [$typeName]" -ForegroundColor Cyan
|
|
|
|
# Idempotency check
|
|
$existingObj = Get-ExistingObject -Api $typeMeta.API -NameProp $typeMeta.NameProp -NameValue $mutatedName
|
|
$objectId = $null
|
|
$shouldAssign = $false
|
|
|
|
if($existingObj)
|
|
{
|
|
Write-Host " Existing object found: $($existingObj.id)" -ForegroundColor Yellow
|
|
if($conflictResolution -eq "Error")
|
|
{
|
|
throw "Conflict: object '$mutatedName' already exists and conflictResolution is Error."
|
|
}
|
|
elseif($conflictResolution -eq "Skip")
|
|
{
|
|
Write-Host " Skipping import (Skip mode)." -ForegroundColor Yellow
|
|
$objectId = $existingObj.id
|
|
$shouldAssign = $true # still apply assignments to existing object
|
|
$stats.Skipped++
|
|
}
|
|
elseif($conflictResolution -eq "Update")
|
|
{
|
|
if($effectiveWhatIf)
|
|
{
|
|
Write-Host " [WHATIF] Would PATCH existing object $($existingObj.id)" -ForegroundColor Magenta
|
|
}
|
|
else
|
|
{
|
|
$patchBody = $policyObj | Select-Object * | ConvertTo-Json -Depth 50
|
|
$null = Invoke-GraphRequest -Url "$($typeMeta.API)/$($existingObj.id)" -HttpMethod PATCH -Content $patchBody
|
|
Write-Host " Updated existing object." -ForegroundColor Green
|
|
}
|
|
$objectId = $existingObj.id
|
|
$shouldAssign = $true
|
|
$stats.Updated++
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if($effectiveWhatIf)
|
|
{
|
|
Write-Host " [WHATIF] Would POST new object to $($typeMeta.API)" -ForegroundColor Magenta
|
|
$objectId = "WHATIF-NEW"
|
|
$shouldAssign = $true
|
|
$stats.Created++
|
|
}
|
|
else
|
|
{
|
|
$postBody = $policyObj | ConvertTo-Json -Depth 50
|
|
$newObj = Invoke-GraphRequest -Url $typeMeta.API -HttpMethod POST -Content $postBody
|
|
$objectId = $newObj.id
|
|
Write-Host " Created: $objectId" -ForegroundColor Green
|
|
$shouldAssign = $true
|
|
$stats.Created++
|
|
|
|
# Secondary settings upload (EndpointSecurity / DeviceManagementIntents)
|
|
if($typeMeta.SettingsAPI)
|
|
{
|
|
$baseName = [System.IO.Path]::GetFileNameWithoutExtension($sourcePath)
|
|
$settingsPathCandidate = Join-Path (Split-Path -Parent $sourcePath) "$baseName`_Settings.json"
|
|
if(Test-Path $settingsPathCandidate)
|
|
{
|
|
$settingsRaw = Get-Content $settingsPathCandidate -Raw
|
|
$settingsJson = $settingsRaw | ConvertFrom-Json
|
|
# The toolkit exports settings as { "settings": [...] }
|
|
$settingsBody = $settingsJson | ConvertTo-Json -Depth 50
|
|
$settingsUrl = "$($typeMeta.API)/$objectId/$($typeMeta.SettingsAPI)"
|
|
Write-Host " Uploading settings from $settingsPathCandidate" -ForegroundColor Cyan
|
|
$null = Invoke-GraphRequest -Url $settingsUrl -HttpMethod POST -Content $settingsBody
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# Assignments
|
|
if($shouldAssign -and $policyDef.ContainsKey("assignments"))
|
|
{
|
|
Invoke-DeployAssignments -ObjectId $objectId -TypeMeta $typeMeta -Assignments $policyDef["assignments"] -GroupCache $groupCache -WhatIf:$effectiveWhatIf
|
|
$stats.Assigned++
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
Write-Warning "Failed to deploy policy '$sourcePath': $_"
|
|
$stats.Failed++
|
|
}
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region Summary
|
|
Write-Host "`n========================================" -ForegroundColor Cyan
|
|
Write-Host "Baseline deployment summary" -ForegroundColor Cyan
|
|
Write-Host "========================================" -ForegroundColor Cyan
|
|
Write-Host "Created : $($stats.Created)"
|
|
Write-Host "Updated : $($stats.Updated)"
|
|
Write-Host "Skipped : $($stats.Skipped)"
|
|
Write-Host "Assigned: $($stats.Assigned)"
|
|
Write-Host "Failed : $($stats.Failed)"
|
|
if($effectiveWhatIf)
|
|
{
|
|
Write-Host "`n*** This was a dry-run (WhatIf). No changes were made. ***" -ForegroundColor Magenta
|
|
}
|
|
#endregion
|