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:
@@ -32,7 +32,9 @@ param(
|
||||
|
||||
[string]$SettingsFile,
|
||||
|
||||
[switch]$WhatIf
|
||||
[switch]$WhatIf,
|
||||
|
||||
[string]$ReportPath
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
@@ -375,11 +377,13 @@ if($effectiveWhatIf) { Write-Host "*** DRY-RUN MODE ENABLED ***" -ForegroundColo
|
||||
|
||||
#region Resolve / create groups
|
||||
$groupCache = @{}
|
||||
Write-Host "`nLoading group directory..." -ForegroundColor Cyan
|
||||
$allGroupsData = (Invoke-GraphRequest "/groups?`$select=id,displayName&`$orderby=displayName" -AllPages).value
|
||||
|
||||
if($baseline.ContainsKey("groups") -and $baseline["groups"])
|
||||
{
|
||||
Write-Host "`nResolving groups..." -ForegroundColor Cyan
|
||||
$existingGroupsResp = Invoke-GraphRequest "/groups?`$select=id,displayName&`$orderby=displayName" -AllPages
|
||||
$existingGroups = $existingGroupsResp.value
|
||||
Write-Host "Resolving baseline groups..." -ForegroundColor Cyan
|
||||
$existingGroups = $allGroupsData
|
||||
|
||||
foreach($grpDef in $baseline["groups"])
|
||||
{
|
||||
@@ -412,9 +416,7 @@ if($baseline.ContainsKey("groups") -and $baseline["groups"])
|
||||
#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" -AllPages
|
||||
foreach($g in $allGroupsResp.value)
|
||||
foreach($g in $allGroupsData)
|
||||
{
|
||||
if(-not $groupCache.ContainsKey($g.displayName))
|
||||
{
|
||||
@@ -432,6 +434,7 @@ $stats = @{
|
||||
Failed = 0
|
||||
Assigned = 0
|
||||
}
|
||||
$policyResults = [System.Collections.Generic.List[PSCustomObject]]::new()
|
||||
|
||||
if($baseline.ContainsKey("policies") -and $baseline["policies"])
|
||||
{
|
||||
@@ -482,6 +485,9 @@ if($baseline.ContainsKey("policies") -and $baseline["policies"])
|
||||
$objectId = $null
|
||||
$shouldAssign = $false
|
||||
|
||||
$outcomeStatus = $null
|
||||
$outcomeObjectId = $null
|
||||
|
||||
if($existingObj)
|
||||
{
|
||||
Write-Host " Existing object found: $($existingObj.id)" -ForegroundColor Yellow
|
||||
@@ -495,22 +501,49 @@ if($baseline.ContainsKey("policies") -and $baseline["policies"])
|
||||
$objectId = $existingObj.id
|
||||
$shouldAssign = $true # still apply assignments to existing object
|
||||
$stats.Skipped++
|
||||
$outcomeStatus = "Skipped"; $outcomeObjectId = $existingObj.id
|
||||
}
|
||||
elseif($conflictResolution -eq "Update")
|
||||
{
|
||||
if($effectiveWhatIf)
|
||||
{
|
||||
Write-Host " [WHATIF] Would PATCH existing object $($existingObj.id)" -ForegroundColor Magenta
|
||||
$outcomeStatus = "WhatIf-Update"
|
||||
}
|
||||
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
|
||||
$outcomeStatus = "Updated"
|
||||
}
|
||||
$objectId = $existingObj.id
|
||||
$shouldAssign = $true
|
||||
$stats.Updated++
|
||||
$outcomeObjectId = $existingObj.id
|
||||
}
|
||||
elseif($conflictResolution -eq "Merge")
|
||||
{
|
||||
if($effectiveWhatIf)
|
||||
{
|
||||
Write-Host " [WHATIF] Would PATCH (merge) existing object $($existingObj.id)" -ForegroundColor Magenta
|
||||
$outcomeStatus = "WhatIf-Merge"
|
||||
}
|
||||
else
|
||||
{
|
||||
$mergeBody = @{}
|
||||
foreach($prop in $policyObj.PSObject.Properties)
|
||||
{
|
||||
$mergeBody[$prop.Name] = $prop.Value
|
||||
}
|
||||
$null = Invoke-GraphRequest -Url "$($typeMeta.API)/$($existingObj.id)" -HttpMethod PATCH -Content ($mergeBody | ConvertTo-Json -Depth 50)
|
||||
Write-Host " Merged into existing object." -ForegroundColor Green
|
||||
$outcomeStatus = "Merged"
|
||||
}
|
||||
$objectId = $existingObj.id
|
||||
$shouldAssign = $true
|
||||
$stats.Updated++
|
||||
$outcomeObjectId = $existingObj.id
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -521,6 +554,7 @@ if($baseline.ContainsKey("policies") -and $baseline["policies"])
|
||||
$objectId = "WHATIF-NEW"
|
||||
$shouldAssign = $true
|
||||
$stats.Created++
|
||||
$outcomeStatus = "WhatIf-Create"
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -530,6 +564,7 @@ if($baseline.ContainsKey("policies") -and $baseline["policies"])
|
||||
Write-Host " Created: $objectId" -ForegroundColor Green
|
||||
$shouldAssign = $true
|
||||
$stats.Created++
|
||||
$outcomeStatus = "Created"; $outcomeObjectId = $newObj.id
|
||||
|
||||
# Secondary settings upload (EndpointSecurity / DeviceManagementIntents)
|
||||
if($typeMeta.SettingsAPI)
|
||||
@@ -556,11 +591,28 @@ if($baseline.ContainsKey("policies") -and $baseline["policies"])
|
||||
Invoke-DeployAssignments -ObjectId $objectId -TypeMeta $typeMeta -Assignments $policyDef["assignments"] -GroupCache $groupCache -WhatIf:$effectiveWhatIf
|
||||
$stats.Assigned++
|
||||
}
|
||||
|
||||
$policyResults.Add([PSCustomObject]@{
|
||||
PolicyName = $mutatedName
|
||||
Type = $typeName
|
||||
SourcePath = $sourcePath
|
||||
ObjectId = $outcomeObjectId
|
||||
Outcome = $outcomeStatus
|
||||
Error = $null
|
||||
})
|
||||
}
|
||||
catch
|
||||
{
|
||||
Write-Warning "Failed to deploy policy '$sourcePath': $_"
|
||||
$stats.Failed++
|
||||
$policyResults.Add([PSCustomObject]@{
|
||||
PolicyName = $mutatedName
|
||||
Type = $typeName
|
||||
SourcePath = $sourcePath
|
||||
ObjectId = $null
|
||||
Outcome = "Failed"
|
||||
Error = $_.Exception.Message
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -579,4 +631,49 @@ if($effectiveWhatIf)
|
||||
{
|
||||
Write-Host "`n*** This was a dry-run (WhatIf). No changes were made. ***" -ForegroundColor Magenta
|
||||
}
|
||||
|
||||
if($policyResults.Count -gt 0)
|
||||
{
|
||||
$resolvedReportPath = if($ReportPath) { $ReportPath } else {
|
||||
$ts = Get-Date -Format 'yyyyMMdd_HHmmss'
|
||||
$baseName = [System.IO.Path]::GetFileNameWithoutExtension($baselinePathResolved)
|
||||
Join-Path (Split-Path -Parent $baselinePathResolved) "${baseName}_DeployReport_${ts}.csv"
|
||||
}
|
||||
$policyResults | Export-Csv -Path $resolvedReportPath -NoTypeInformation -Force
|
||||
Write-Host "Report : $resolvedReportPath" -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
if(-not $effectiveWhatIf -and $policyResults.Count -gt 0)
|
||||
{
|
||||
$sha256 = [System.Security.Cryptography.SHA256]::Create()
|
||||
$manifestPolicies = $policyResults | Where-Object { $_.Outcome -in @("Created","Updated","Merged","Skipped") } | ForEach-Object {
|
||||
$hash = $null
|
||||
if($_.SourcePath -and (Test-Path $_.SourcePath))
|
||||
{
|
||||
$bytes = [System.IO.File]::ReadAllBytes($_.SourcePath)
|
||||
$hash = [System.BitConverter]::ToString($sha256.ComputeHash($bytes)) -replace '-',''
|
||||
}
|
||||
[ordered]@{
|
||||
policyName = $_.PolicyName
|
||||
type = $_.Type
|
||||
objectId = $_.ObjectId
|
||||
sourcePath = $_.SourcePath
|
||||
sourceHash = $hash
|
||||
outcome = $_.Outcome
|
||||
}
|
||||
}
|
||||
$sha256.Dispose()
|
||||
|
||||
$manifest = [ordered]@{
|
||||
baselineName = $baseline["name"]
|
||||
baselinePath = $baselinePathResolved
|
||||
tenantId = $TenantId
|
||||
deployedAt = (Get-Date -Format 'o')
|
||||
policies = @($manifestPolicies)
|
||||
}
|
||||
|
||||
$manifestPath = [System.IO.Path]::ChangeExtension($baselinePathResolved, "manifest.json")
|
||||
$manifest | ConvertTo-Json -Depth 10 | Set-Content -Path $manifestPath -Encoding utf8 -Force
|
||||
Write-Host "Manifest: $manifestPath" -ForegroundColor Cyan
|
||||
}
|
||||
#endregion
|
||||
|
||||
Reference in New Issue
Block a user