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
166 lines
5.9 KiB
PowerShell
166 lines
5.9 KiB
PowerShell
#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
|