feat(baseline): declarative Intune baseline deployer

- 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
This commit is contained in:
2026-04-14 14:59:29 +02:00
parent 87b7af25a7
commit c4b8f4aaf6
5 changed files with 881 additions and 0 deletions

View File

@@ -0,0 +1,582 @@
#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