Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
{
"version": "2.0.0",
"osx": {
"options": {
"env": {
"PATH": "/opt/homebrew/opt/powershell/bin:${env:PATH}"
}
}
},
"tasks": [
{
"label": "Install Dependencies",
Expand Down
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.0.0] - 2026-05-30

### Added

- **Interactive TUI**: Added a zero-argument `Invoke-IntuneHydration` console wizard for Azure cloud, operation mode, workload targets, platform filtering, optional Graph consent prompting, verbose logging, and final confirmation. The tenant ID is discovered after browser sign-in.
- **Dry-run first workflow**: Added a TUI dry-run create mode and review screen before Graph write calls.
- **TUI documentation and media**: Updated README/help docs, screenshot, demo capture, and VHS source files for the guided console experience.

### Changed

- **Default invocation**: Bare module and repository-wrapper invocation now launch the TUI; settings-file and parameter-based automation modes remain available.
- **Execution settings**: TUI selections now resolve through the same execution settings shape as parameter and settings-file runs.
- **Common parameter handling**: `-WhatIf` and `-Verbose` now flow into TUI review and execution settings.
- **Wrapper parity**: The repository wrapper now matches the module command parameter surface, including `-StaticGroups`, `-MobileApps`, `-CISBaselines`, and `-Platform`.

## [0.8.1] - 2026-05-25

### Added
Expand Down
17 changes: 8 additions & 9 deletions IntuneHydrationKit.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# Module manifest for IntuneHydrationKit

# Version number of this module
ModuleVersion = '0.8.1'
ModuleVersion = '1.0.0'

# ID used to uniquely identify this module
GUID = 'f755f41b-d5fc-48db-8b11-62b7ed71b1cd'
Expand Down Expand Up @@ -104,15 +104,14 @@ To update to the latest version:
Update-Module -Name IntuneHydrationKit
```

## v0.8.1
## v1.0.0

- **Authentication:** Interactive sign-in now uses a themed browser PKCE flow with fresh-token retry and no persistent refresh-token cache.
- **Mobile Apps:** Added bundled WinGet app templates and WinGet-backed Win32 app import support under the Mobile Apps workflow.
- **Pre-flight checks:** Device Filter runs now validate selected Intune workload access before imports and report concise authorization guidance.
- **Runtime permission checks:** Added selected-import access checks and clearer Global Administrator guidance for tenants where PIM-elevated roles may not be accepted by downstream Intune authorization.
- **Mobile Apps:** Fixed new mobile app names to append ` - [IHD]` after the app name instead of prefixing `[IHD]`.
- **Mobile Apps:** Fixed legacy Windows mobile app TemplateId matching for nested Store and M365 templates.
- **Sovereign clouds:** Centralized Graph environment metadata for consistent GCC High and DoD endpoint handling.
- **Interactive TUI:** Calling `Invoke-IntuneHydration` with no arguments now launches a guided console flow for Azure cloud, operation mode, workload targets, platform filtering, optional Graph consent prompting, verbose logging, and final confirmation. The tenant ID is discovered after browser sign-in.
- **Dry-run first workflow:** The TUI includes a dry-run create mode and review screen before Graph write calls, making manual previews the default path.
- **Unified execution path:** TUI selections resolve into the same settings shape as parameter and settings-file runs, preserving logging, reports, pre-flight checks, platform filtering, and deletion protections.
- **Common parameter handling:** `-WhatIf` and `-Verbose` are reflected in TUI execution settings and review output.
- **Wrapper parity:** The repository wrapper now matches the module command surface, including `-StaticGroups`, `-MobileApps`, `-CISBaselines`, `-Platform`, and zero-argument TUI invocation.
- **Documentation:** Updated README, help content, TUI screenshot, demo capture, and VHS source files for the new guided experience.

'@
}
Expand Down
34 changes: 32 additions & 2 deletions Invoke-IntuneHydration.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

For new users, consider installing from PSGallery:
Install-Module IntuneHydrationKit
Invoke-IntuneHydration -SettingsPath ./settings.json
Invoke-IntuneHydration
.PARAMETER SettingsPath
Path to the settings JSON file. Use this for settings file-based invocation.
.PARAMETER TenantId
Expand Down Expand Up @@ -46,18 +46,30 @@
Process enrollment profiles (Autopilot, ESP)
.PARAMETER DynamicGroups
Process dynamic groups
.PARAMETER StaticGroups
Process static groups
.PARAMETER DeviceFilters
Process device filters
.PARAMETER ConditionalAccess
Process Conditional Access starter pack policies
.PARAMETER MobileApps
Process mobile app templates
.PARAMETER CISBaselines
Process bundled CIS baseline policies
.PARAMETER All
Enable all targets
.PARAMETER Platform
Filter imports by platform. Valid values: Windows, macOS, iOS, Android, Linux, All.
.PARAMETER ReportOutputPath
Output directory for reports
.PARAMETER ReportFormats
Report formats to generate (markdown, json)
.PARAMETER WhatIf
Run in dry-run mode without making changes to Intune
.EXAMPLE
./Invoke-IntuneHydration.ps1

Launch the interactive console wizard.
.EXAMPLE
./Invoke-IntuneHydration.ps1 -SettingsPath ./settings.json

Expand All @@ -67,7 +79,7 @@

Run with all imports enabled using interactive authentication.
#>
[CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'SettingsFile')]
[CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'InteractiveTui')]
param(
[Parameter(ParameterSetName = 'SettingsFile', Mandatory = $true, Position = 0)]
[ValidateScript({ Test-Path $_ })]
Expand Down Expand Up @@ -138,6 +150,10 @@ param(
[Parameter(ParameterSetName = 'ServicePrincipal')]
[switch]$DynamicGroups,

[Parameter(ParameterSetName = 'Interactive')]
[Parameter(ParameterSetName = 'ServicePrincipal')]
[switch]$StaticGroups,

[Parameter(ParameterSetName = 'Interactive')]
[Parameter(ParameterSetName = 'ServicePrincipal')]
[switch]$DeviceFilters,
Expand All @@ -146,10 +162,24 @@ param(
[Parameter(ParameterSetName = 'ServicePrincipal')]
[switch]$ConditionalAccess,

[Parameter(ParameterSetName = 'Interactive')]
[Parameter(ParameterSetName = 'ServicePrincipal')]
[switch]$MobileApps,

[Parameter(ParameterSetName = 'Interactive')]
[Parameter(ParameterSetName = 'ServicePrincipal')]
[switch]$CISBaselines,

[Parameter(ParameterSetName = 'Interactive')]
[Parameter(ParameterSetName = 'ServicePrincipal')]
[switch]$All,

[Parameter(ParameterSetName = 'SettingsFile')]
[Parameter(ParameterSetName = 'Interactive')]
[Parameter(ParameterSetName = 'ServicePrincipal')]
[ValidateSet('Windows', 'macOS', 'iOS', 'Android', 'Linux', 'All')]
[string[]]$Platform = @('All'),

[Parameter(ParameterSetName = 'Interactive')]
[Parameter(ParameterSetName = 'ServicePrincipal')]
[string]$ReportOutputPath,
Expand Down
22 changes: 20 additions & 2 deletions Private/Auth/Connect-HydrationGraphViaBrowser.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ function Connect-HydrationGraphViaBrowser {
[ValidateSet('Global', 'USGov', 'USGovDoD', 'Germany', 'China')]
[string]$Environment,

[Parameter()]
[switch]$ForceConsent,

[string]$ClientId = '14d82eec-204b-4c2f-b7e8-296a70dab67e'
)

Expand All @@ -24,19 +27,34 @@ function Connect-HydrationGraphViaBrowser {
-ClientId $ClientId `
-TenantId $TenantId `
-AuthorityHost $environmentInfo.AuthorityHost `
-Scopes $resolvedScopes
-Scopes $resolvedScopes `
-ForceConsent:$ForceConsent

$secureToken = ConvertTo-SecureString -String $tokens.access_token -AsPlainText -Force
try {
Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
Connect-MgGraph -AccessToken $secureToken -Environment $Environment -NoWelcome -ErrorAction Stop
return
} catch {
if ($attempt -eq 2) {
throw
}

Write-Verbose "Browser Graph token could not connect; retrying with a fresh browser sign-in: $_"
continue
}

$tenantIdFromToken = Get-HydrationTenantIdFromAccessToken -AccessToken $tokens.access_token
$connectedTenantId = if ($tenantIdFromToken) {
$tenantIdFromToken
} elseif ($TenantId -ne 'organizations') {
$TenantId
} else {
throw 'Unable to determine the signed-in tenant ID after interactive authentication.'
}

return [pscustomobject]@{
TenantId = $connectedTenantId
Environment = $Environment
}
}
}
7 changes: 4 additions & 3 deletions Private/Auth/Get-HydrationAuthParameters.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ function Get-HydrationAuthParameters {
[Parameter(Mandatory)]
[hashtable]$AuthenticationSettings,

[Parameter(Mandatory)]
[Parameter()]
[string]$TenantId
)

$authParams = @{
TenantId = $TenantId
$authParams = @{}
if ($TenantId) {
$authParams['TenantId'] = $TenantId
}

if ($AuthenticationSettings.environment) {
Expand Down
70 changes: 68 additions & 2 deletions Private/Auth/Get-HydrationGraphScopes.ps1
Original file line number Diff line number Diff line change
@@ -1,8 +1,23 @@
function Get-HydrationGraphScopes {
[CmdletBinding()]
param()
param(
[Parameter()]
[hashtable]$Imports,

return @(
[Parameter()]
[switch]$Create,

[Parameter()]
[switch]$Delete,

[Parameter()]
[hashtable]$MobileAppConfiguration = @{},

[Parameter()]
[string[]]$MobileAppPlatforms = @('All')
)

$allScopes = @(
'DeviceManagementConfiguration.ReadWrite.All',
'DeviceManagementServiceConfig.ReadWrite.All',
'DeviceManagementManagedDevices.ReadWrite.All',
Expand All @@ -16,4 +31,55 @@ function Get-HydrationGraphScopes {
'LicenseAssignment.Read.All',
'Organization.Read.All'
)

if (-not $Imports -or $Imports.Count -eq 0) {
return $allScopes
}

$scopes = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
foreach ($scope in @('Organization.Read.All', 'LicenseAssignment.Read.All')) {
[void]$scopes.Add($scope)
}

$scopeMap = @{
dynamicGroups = @('Group.ReadWrite.All')
staticGroups = @('Group.ReadWrite.All')
deviceFilters = @('DeviceManagementConfiguration.ReadWrite.All')
conditionalAccess = @('Policy.Read.All', 'Policy.ReadWrite.ConditionalAccess', 'Application.Read.All', 'Directory.ReadWrite.All')
complianceTemplates = @('DeviceManagementConfiguration.ReadWrite.All', 'DeviceManagementScripts.ReadWrite.All')
openIntuneBaseline = @('DeviceManagementConfiguration.ReadWrite.All', 'DeviceManagementServiceConfig.ReadWrite.All', 'DeviceManagementApps.ReadWrite.All', 'DeviceManagementScripts.ReadWrite.All')
enrollmentProfiles = @('DeviceManagementServiceConfig.ReadWrite.All', 'DeviceManagementConfiguration.ReadWrite.All', 'Group.ReadWrite.All')
appProtection = @('DeviceManagementApps.ReadWrite.All')
notificationTemplates = @('DeviceManagementServiceConfig.ReadWrite.All')
mobileApps = @('DeviceManagementApps.ReadWrite.All')
cisBaselines = @('DeviceManagementConfiguration.ReadWrite.All')
}

foreach ($importKey in $Imports.Keys) {
if (-not $Imports[$importKey] -or -not $scopeMap.ContainsKey($importKey)) {
continue
}

foreach ($scope in $scopeMap[$importKey]) {
[void]$scopes.Add($scope)
}
}

$includeCreateOnlyScopes = $Create.IsPresent -or -not $Delete.IsPresent
if ($includeCreateOnlyScopes -and $Imports.ContainsKey('staticGroups') -and $Imports.staticGroups) {
foreach ($scope in @('Application.Read.All', 'Directory.ReadWrite.All')) {
[void]$scopes.Add($scope)
}
}

$remediationEnabled = $true
if ($MobileAppConfiguration -and $MobileAppConfiguration.ContainsKey('remediationEnabled') -and $null -ne $MobileAppConfiguration.remediationEnabled) {
$remediationEnabled = [bool]$MobileAppConfiguration.remediationEnabled
}
if ($Imports.ContainsKey('mobileApps') -and $Imports.mobileApps -and $remediationEnabled -and
(Test-HydrationMobileAppsIncludeWinGet -Configuration $MobileAppConfiguration -Platforms $MobileAppPlatforms)) {
[void]$scopes.Add('DeviceManagementScripts.ReadWrite.All')
}

return @($allScopes | Where-Object { $scopes.Contains($_) })
}
44 changes: 43 additions & 1 deletion Private/Auth/Get-HydrationGraphWorkloadAccessProbe.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@ function Get-HydrationGraphWorkloadAccessProbe {
[hashtable]$MobileAppConfiguration = @{},

[Parameter()]
[string[]]$MobileAppPlatforms = @('All')
[string[]]$MobileAppPlatforms = @('All'),

[Parameter()]
[string[]]$AppProtectionPlatforms = @('All'),

[Parameter()]
[string[]]$BaselinePlatforms = @('All')
)

$probes = [System.Collections.Generic.List[hashtable]]::new()
Expand Down Expand Up @@ -53,5 +59,41 @@ function Get-HydrationGraphWorkloadAccessProbe {
}
}

$appProtectionProbePlatforms = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)

if ($Imports.ContainsKey('appProtection') -and $Imports.appProtection) {
foreach ($endpointInfo in (Get-AppProtectionEndpointInfo -Platform $AppProtectionPlatforms)) {
[void]$appProtectionProbePlatforms.Add($endpointInfo.Platform)
}
}

if ($Imports.ContainsKey('openIntuneBaseline') -and $Imports.openIntuneBaseline) {
foreach ($endpointInfo in (Get-AppProtectionEndpointInfo -Platform $BaselinePlatforms)) {
[void]$appProtectionProbePlatforms.Add($endpointInfo.Platform)
}
}

foreach ($endpointInfo in (Get-AppProtectionEndpointInfo)) {
if ($appProtectionProbePlatforms.Contains($endpointInfo.Platform)) {
$probes.Add(@{
Workload = 'App Protection'
Endpoint = $endpointInfo.Endpoint
Uri = "$($endpointInfo.Endpoint)`?`$top=1"
RequiredScope = 'DeviceManagementApps.ReadWrite.All'
RoleHint = 'Use a Global Administrator account with active Intune app protection access; PIM-elevated roles can still be rejected by the downstream Intune service.'
})
}
}

if ($Imports.ContainsKey('conditionalAccess') -and $Imports.conditionalAccess) {
$probes.Add(@{
Workload = 'Conditional Access'
Endpoint = 'beta/identity/conditionalAccess/policies'
Uri = 'beta/identity/conditionalAccess/policies?$top=1&$select=id,displayName,state'
RequiredScope = 'Policy.ReadWrite.ConditionalAccess'
RoleHint = 'Use a Global Administrator, Security Administrator, or Conditional Access Administrator account with active Conditional Access access.'
})
}

return $probes.ToArray()
}
38 changes: 38 additions & 0 deletions Private/Auth/Get-HydrationTenantIdFromAccessToken.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
function Get-HydrationTenantIdFromAccessToken {
[CmdletBinding()]
[OutputType([string])]
param(
[Parameter()]
[AllowNull()]
[string]$AccessToken
)

if ([string]::IsNullOrWhiteSpace($AccessToken)) {
return $null
}

$tokenParts = $AccessToken -split '\.'
if ($tokenParts.Count -lt 2) {
return $null
}

$payload = $tokenParts[1].Replace('-', '+').Replace('_', '/')
switch ($payload.Length % 4) {
2 { $payload += '==' }
3 { $payload += '=' }
0 { }
default { return $null }
}

try {
$payloadJson = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($payload))
$claims = $payloadJson | ConvertFrom-Json -ErrorAction Stop
if ($claims.tid -match '^[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}$') {
return $claims.tid
}
} catch {
return $null
}

return $null
}
Loading
Loading