release: v4.1.0 — restructure entry points, add CIS baselines, reporting tools and fzf hints
- 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
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
#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
|
||||
Reference in New Issue
Block a user