# Intune Manager - Web Edition # Startet einen lokalen HTTP-Server und oeffnet das moderne Web-Frontend. # # Aufruf: # .\Start.ps1 # .\Start.ps1 -Port 8088 -NoBrowser # .\Start.ps1 -TenantId "..." -ClientId "..." [CmdletBinding()] param( [int]$Port = 8077, # Optional: ueberschreibt die persistierten Settings nur fuer diesen Lauf. # Wenn nicht gesetzt -> Werte aus %APPDATA%\IntuneAppManager-Web\settings.json # (bzw. den Defaults beim Erststart) werden verwendet. [string]$TenantId = "", [string]$ClientId = "", [switch]$NoBrowser, [switch]$AutoInstall, # ohne Rueckfrage installieren [switch]$SkipModuleCheck # Pre-Flight ueberspringen (z.B. CI) ) $ErrorActionPreference = "Stop" $root = Split-Path -Parent $MyInvocation.MyCommand.Path # ============================================================ # Pre-Flight: PowerShell-Module pruefen / installieren # (nur User-Scope, nichts systemweites) # ============================================================ function Test-RequiredModules { [CmdletBinding()] param( [hashtable[]]$Required, [switch]$AutoInstall ) Write-Host "Pruefe PowerShell-Module..." -ForegroundColor Cyan $missing = @() $outdated = @() foreach ($req in $Required) { $name = $req.Name $minV = [version]$req.MinVersion $available = @(Get-Module -Name $name -ListAvailable -ErrorAction SilentlyContinue) if ($available.Count -eq 0) { Write-Host " [FEHLT] $name (>= $minV)" -ForegroundColor Yellow $missing += $req } else { $maxV = ($available | Sort-Object Version -Descending | Select-Object -First 1).Version if ($maxV -lt $minV) { Write-Host " [ALT] $name v$maxV (<$minV)" -ForegroundColor Yellow $outdated += @{ Name = $name; Have = $maxV; Need = $minV } } else { Write-Host " [OK] $name v$maxV" -ForegroundColor Green } } } if ($missing.Count -eq 0 -and $outdated.Count -eq 0) { return $true } # NuGet-Provider sicherstellen, sonst schlaegt Install-Module schweigend fehl $nuget = Get-PackageProvider -Name NuGet -ErrorAction SilentlyContinue if (-not $nuget -or $nuget.Version -lt [version]"2.8.5.201") { Write-Host "" Write-Host "NuGet-PackageProvider fehlt � wird im User-Scope nachgezogen..." -ForegroundColor Yellow try { Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Scope CurrentUser -Force -ErrorAction Stop | Out-Null } catch { throw "NuGet-Provider konnte nicht installiert werden: $($_.Exception.Message)" } } # PSGallery als trusted markieren, sonst kommt fuer jede Install-Aktion ein Prompt $gallery = Get-PSRepository -Name PSGallery -ErrorAction SilentlyContinue if ($gallery -and $gallery.InstallationPolicy -ne 'Trusted') { try { Set-PSRepository -Name PSGallery -InstallationPolicy Trusted -ErrorAction Stop } catch {} } if (-not $AutoInstall) { Write-Host "" Write-Host "Folgendes wird im User-Scope (CurrentUser) installiert/aktualisiert:" -ForegroundColor Cyan foreach ($m in $missing) { Write-Host " + $($m.Name) min. $($m.MinVersion)" -ForegroundColor White } foreach ($o in $outdated) { Write-Host " ~ $($o.Name) v$($o.Have) -> min. $($o.Need)" -ForegroundColor White } Write-Host "" $answer = Read-Host "Jetzt automatisch installieren? [J]a / [N]ein" if ($answer -notmatch '^(j|J|y|Y)') { Write-Host "" Write-Host "Abbruch. Du kannst die Module manuell installieren:" -ForegroundColor Yellow foreach ($m in $missing) { Write-Host " Install-Module $($m.Name) -Scope CurrentUser -Force" -ForegroundColor Gray } foreach ($o in $outdated) { Write-Host " Update-Module $($o.Name) -Scope CurrentUser -Force" -ForegroundColor Gray } return $false } } foreach ($m in $missing) { Write-Host "" Write-Host "Installiere $($m.Name) (CurrentUser)..." -ForegroundColor Cyan try { Install-Module -Name $m.Name -MinimumVersion $m.MinVersion -Scope CurrentUser -Force -AllowClobber -ErrorAction Stop Write-Host " -> OK" -ForegroundColor Green } catch { Write-Host " -> Fehler: $($_.Exception.Message)" -ForegroundColor Red return $false } } foreach ($o in $outdated) { Write-Host "" Write-Host "Aktualisiere $($o.Name) auf min. $($o.Need)..." -ForegroundColor Cyan try { Install-Module -Name $o.Name -MinimumVersion $o.Need -Scope CurrentUser -Force -AllowClobber -ErrorAction Stop Write-Host " -> OK" -ForegroundColor Green } catch { Write-Host " -> Fehler: $($_.Exception.Message)" -ForegroundColor Red return $false } } return $true } if (-not $SkipModuleCheck) { $required = @( @{ Name = "Microsoft.Graph.Authentication"; MinVersion = "2.0.0" } ) if (-not (Test-RequiredModules -Required $required -AutoInstall:$AutoInstall)) { Write-Host "" Write-Host "Server-Start abgebrochen � benoetigte Module fehlen." -ForegroundColor Red exit 1 } Write-Host "" } # Quellmodule laden (Reihenfolge wichtig) . (Join-Path $root "src/Models.ps1") . (Join-Path $root "src/Graph.ps1") . (Join-Path $root "src/Api.ps1") . (Join-Path $root "src/PolicyIO.ps1") . (Join-Path $root "src/Router.ps1") . (Join-Path $root "src/Server.ps1") # Settings aus Persistenz (Datei) laden, optional per Parameter ueberschreiben $script:Settings = Read-Settings if ($TenantId) { $script:Settings.connection.tenantId = $TenantId } if ($ClientId) { $script:Settings.connection.clientId = $ClientId } # Globale Konfiguration. TenantId/ClientId/Scopes spiegeln die Settings � # Connect-Endpoint und Co. lesen weiterhin $script:Config, das nach jedem # Settings-Save aktualisiert wird. $script:Config = @{ TenantId = $script:Settings.connection.tenantId ClientId = $script:Settings.connection.clientId ClientIdRo = $script:Settings.connection.clientIdRo Scopes = @($script:Settings.connection.scopes) ScopesRo = @($script:Settings.connection.scopesRo) WebRoot = (Join-Path $root "www") ReportDir = (Join-Path $env:TEMP "IntuneAppManager-Reports") } if (-not (Test-Path $script:Config.ReportDir)) { New-Item -ItemType Directory -Path $script:Config.ReportDir -Force | Out-Null } # Import/Export von Compliance Policies & Configuration Profiles benoetigt den # Scope DeviceManagementConfiguration.* — bei Bedarf ergaenzen, damit das # Verbindungs-Token die Policies lesen/schreiben darf. if ($script:Config.Scopes -notcontains 'DeviceManagementConfiguration.ReadWrite.All') { $script:Config.Scopes = @($script:Config.Scopes) + 'DeviceManagementConfiguration.ReadWrite.All' } if ($script:Config.ScopesRo -notcontains 'DeviceManagementConfiguration.Read.All') { $script:Config.ScopesRo = @($script:Config.ScopesRo) + 'DeviceManagementConfiguration.Read.All' } # Session-State (im PowerShell-Prozess gehalten) $script:State = [pscustomobject]@{ Connected = $false ReadOnly = $false Account = $null TenantId = $null Groups = @() # abt-hm Gruppen RpaGroups = @() Apps = @() Users = @{} # cache by id GroupMembers = @{} # cache groupId -> member ids Session = @() # geplante Zuweisungen } $script:ToolVersion = "0.1.25" $script:BuildStamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" Write-Host "" Write-Host "==============================================" -ForegroundColor Cyan Write-Host " Intune Manager - Web Edition" -ForegroundColor Cyan Write-Host " Version: $script:ToolVersion" -ForegroundColor Green Write-Host " BUILD: $script:BuildStamp" -ForegroundColor DarkGray Write-Host "==============================================" -ForegroundColor Cyan Write-Host "" # ============================================================ # Alte Instanzen beenden: laeuft das Tool bereits, haelt der alte Prozess den # Port (HttpListener via http.sys) � ein zweiter Start scheitert dann still und # das alte Fenster bedient weiter mit altem Code. Darum alle anderen Start.ps1- # Prozesse DIESES Pfads vorher beenden, damit ein Neustart wirklich neu laedt. # ============================================================ try { $thisScript = $PSCommandPath if ([string]::IsNullOrWhiteSpace($thisScript)) { $thisScript = Join-Path $PSScriptRoot 'Start.ps1' } # Nur echte Server-Instanzen treffen: per -File gestartet UND auf diesen # Start.ps1-Pfad zeigend. So werden -Command-Prozesse (Diagnose etc.), die # den Pfad nur als String enthalten, NICHT versehentlich beendet. $stale = @(Get-CimInstance Win32_Process -Filter "Name='powershell.exe' OR Name='pwsh.exe'" -ErrorAction SilentlyContinue | Where-Object { $_.ProcessId -ne $PID -and $_.CommandLine -and ($_.CommandLine -like "*$thisScript*") -and ($_.CommandLine -like "*-File*") }) foreach ($p in $stale) { Write-Host " Beende alte Instanz (PID $($p.ProcessId))..." -ForegroundColor Yellow try { Stop-Process -Id $p.ProcessId -Force -ErrorAction Stop } catch { Write-Host " konnte PID $($p.ProcessId) nicht beenden: $($_.Exception.Message)" -ForegroundColor DarkYellow } } if ($stale.Count -gt 0) { Start-Sleep -Milliseconds 800 } # kurz warten bis der Port frei ist } catch { Write-Host " (Konnte alte Instanzen nicht pruefen: $($_.Exception.Message))" -ForegroundColor DarkGray } $url = "http://localhost:$Port/" Write-Host " URL: $url" -ForegroundColor Green Write-Host " WebRoot: $($script:Config.WebRoot)" -ForegroundColor DarkGray Write-Host " Reports: $($script:Config.ReportDir)" -ForegroundColor DarkGray Write-Host " Beenden: Ctrl+C" -ForegroundColor DarkGray Write-Host "" if (-not $NoBrowser) { Start-Process $url } Start-WebServer -Port $Port