#requires -Version 7.0 <# .SYNOPSIS Deploy an Intune or CIS M365 baseline to multiple tenants from a CSV manifest. .DESCRIPTION Reads a CSV file with one row per tenant, invokes Deploy-IntuneBaseline.ps1 or Deploy-CISM365Baseline.ps1 for each row, and aggregates all per-tenant reports into a single combined CSV summary. CSV columns (Deploy-IntuneBaseline mode): TenantId, BaselinePath, AppId, Secret, Certificate, AuthMode, ConflictResolution, WhatIf CSV columns (Deploy-CISM365Baseline mode): TenantId, BaselinePath, AppId, Secret, Certificate, AuthMode, Mode, Workloads, WhatIf All columns except TenantId and BaselinePath are optional. .PARAMETER CsvPath Path to the CSV manifest file. .PARAMETER ScriptMode Which deployment script to invoke per tenant: 'Intune' or 'CIS'. Default: Intune. .PARAMETER OutputDir Directory for per-tenant reports and the combined summary. Default: same directory as CsvPath. .PARAMETER WhatIf Propagates WhatIf to every tenant run, overriding the CSV column. .EXAMPLE ./Scripts/Invoke-BaselineBatch.ps1 -CsvPath ./tenants.csv -ScriptMode Intune .EXAMPLE ./Scripts/Invoke-BaselineBatch.ps1 -CsvPath ./tenants.csv -ScriptMode CIS -WhatIf #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$CsvPath, [ValidateSet("Intune","CIS")] [string]$ScriptMode = "Intune", [string]$OutputDir, [switch]$WhatIf ) $ErrorActionPreference = "Stop" $csvResolved = Resolve-Path $CsvPath | Select-Object -ExpandProperty Path if (-not (Test-Path $csvResolved)) { throw "CSV not found: $CsvPath" } $rows = Import-Csv -Path $csvResolved if (-not $rows -or $rows.Count -eq 0) { throw "CSV is empty: $CsvPath" } $scriptDir = Split-Path -Parent $PSScriptRoot $intuneScript = Join-Path $scriptDir "Scripts/Deploy-IntuneBaseline.ps1" $cisScript = Join-Path $scriptDir "Scripts/Deploy-CISM365Baseline.ps1" $targetScript = if ($ScriptMode -eq "CIS") { $cisScript } else { $intuneScript } if (-not (Test-Path $targetScript)) { throw "Deployment script not found: $targetScript" } $resolvedOutputDir = if ($OutputDir) { $OutputDir } else { Split-Path -Parent $csvResolved } if (-not (Test-Path $resolvedOutputDir)) { New-Item -ItemType Directory -Path $resolvedOutputDir | Out-Null } $ts = Get-Date -Format 'yyyyMMdd_HHmmss' $batchSummary = [System.Collections.Generic.List[PSCustomObject]]::new() $rowIndex = 0 foreach ($row in $rows) { $rowIndex++ $tenantId = $row.TenantId?.Trim() $baselinePath = $row.BaselinePath?.Trim() if ([string]::IsNullOrWhiteSpace($tenantId) -or [string]::IsNullOrWhiteSpace($baselinePath)) { Write-Warning "Row $rowIndex skipped: TenantId or BaselinePath is empty." $batchSummary.Add([PSCustomObject]@{ Row = $rowIndex TenantId = $tenantId Baseline = $baselinePath Outcome = 'Skipped-InvalidRow' ReportPath = $null Error = 'TenantId or BaselinePath empty' }) continue } $tenantReportPath = Join-Path $resolvedOutputDir "${tenantId}_${ts}.csv" Write-Host "" Write-Host "======================================================" -ForegroundColor Cyan Write-Host "Tenant $rowIndex/$($rows.Count): $tenantId" -ForegroundColor Cyan Write-Host "Baseline : $baselinePath" -ForegroundColor Cyan Write-Host "======================================================" -ForegroundColor Cyan $params = @{ TenantId = $tenantId BaselinePath = $baselinePath } if ($row.PSObject.Properties['AppId'] -and $row.AppId) { $params.AppId = $row.AppId } if ($row.PSObject.Properties['Secret'] -and $row.Secret) { $params.Secret = $row.Secret } if ($row.PSObject.Properties['Certificate'] -and $row.Certificate) { $params.Certificate = $row.Certificate } if ($row.PSObject.Properties['AuthMode'] -and $row.AuthMode) { $params.AuthMode = $row.AuthMode } if ($WhatIf -or ($row.PSObject.Properties['WhatIf'] -and $row.WhatIf -match '(?i)^true|yes|1$')) { $params.WhatIf = $true } if ($ScriptMode -eq "Intune") { if ($row.PSObject.Properties['ConflictResolution'] -and $row.ConflictResolution) { $params.ConflictResolution = $row.ConflictResolution } $params.ReportPath = $tenantReportPath } else { if ($row.PSObject.Properties['Mode'] -and $row.Mode) { $params.Mode = $row.Mode } if ($row.PSObject.Properties['Workloads'] -and $row.Workloads) { $params.Workloads = $row.Workloads -split '\s*[,;]\s*' } } $outcome = 'Success' $errorMsg = $null try { & $targetScript @params } catch { $outcome = 'Failed' $errorMsg = $_.Exception.Message Write-Warning "Tenant $tenantId failed: $errorMsg" } $batchSummary.Add([PSCustomObject]@{ Row = $rowIndex TenantId = $tenantId Baseline = $baselinePath Outcome = $outcome ReportPath = if ($ScriptMode -eq "Intune" -and (Test-Path $tenantReportPath)) { $tenantReportPath } else { $null } Error = $errorMsg }) } $summaryPath = Join-Path $resolvedOutputDir "BatchSummary_${ts}.csv" $batchSummary | Export-Csv -Path $summaryPath -NoTypeInformation -Force Write-Host "" Write-Host "======================================================" -ForegroundColor Green Write-Host "Batch complete. $($rows.Count) tenant(s) processed." -ForegroundColor Green Write-Host "Summary: $summaryPath" -ForegroundColor Green $failed = $batchSummary | Where-Object { $_.Outcome -ne 'Success' } if ($failed) { Write-Host "Failed tenants:" -ForegroundColor Red $failed | ForEach-Object { Write-Host " $($_.TenantId): $($_.Error)" -ForegroundColor Red } } Write-Host "======================================================" -ForegroundColor Green