Public/Connect-Argus.ps1
|
function Connect-Argus { <# .SYNOPSIS Connects ARGUS to Microsoft Graph and, optionally, Exchange Online and SharePoint Online. .DESCRIPTION Opens the sessions ARGUS needs to run its read-only governance checks. Microsoft Graph is always connected through Microsoft.Graph.Authentication (Connect-MgGraph). Exchange Online (ExchangeOnlineManagement) and SharePoint Online (PnP.PowerShell) are optional: when a module is missing or the connection fails, a warning is shown and the checks that depend on that service are reported as Skipped. Only read scopes are requested: the list comes from Get-ArgusRequiredPermission -Aggregate. Interactive (default) is delegated sign-in in the browser. The first time ARGUS is used in a tenant, sign in with an account that can grant tenant-wide admin consent for delegated Microsoft Graph permissions (Application Administrator, Cloud Application Administrator or Global Administrator) and accept "Consent on behalf of your organization". With delegated access the data ARGUS can read is limited by the signed-in user's own roles: Global Reader plus Reports Reader is recommended. Certificate and ManagedIdentity are app-only (unattended). Their Graph application permissions must be granted by a Privileged Role Administrator or Global Administrator. After sign-in ARGUS reads the tenant name and domains, detects Entra ID P1/P2 and checks whether usage reports conceal names. The Graph token is kept for this PowerShell process only. .PARAMETER Service Services to connect. Graph is always connected, even if not listed. Valid values: Graph, Exchange, SharePoint, Commerce, All. All connects every service whose module and prerequisites are available; the others are skipped with a warning (for example SharePoint without -ClientId in Interactive mode, or Commerce outside Windows or app-only). Commerce (MSCommerce module) reads the self-service purchase and trial policies; it signs in with its own Microsoft app through the Windows broker, so it is available only for Interactive sign-in on Windows. .PARAMETER TenantId Tenant ID (GUID) or primary domain. Optional for Interactive, mandatory for Certificate. .PARAMETER ClientId Application (client) ID of the app registration. Mandatory for Certificate. For Interactive it is used only for SharePoint through PnP.PowerShell (required there); Graph and Exchange Online always sign in with their own Microsoft first-party apps. For ManagedIdentity it selects a user-assigned identity. .PARAMETER CertificateThumbprint Thumbprint of the certificate (CurrentUser or LocalMachine store) used for app-only authentication. .PARAMETER ManagedIdentity Use the managed identity of the current host. .PARAMETER Organization Initial .onmicrosoft.com domain of the tenant (for example contoso.onmicrosoft.com) used for app-only Exchange Online connections. Detected from the tenant when omitted. .PARAMETER SharePointAdminUrl SharePoint admin center URL (for example https://contoso-admin.sharepoint.com). Derived from the tenant's initial domain when omitted. .EXAMPLE Connect-Argus Interactive delegated sign-in to Microsoft Graph only. .EXAMPLE Connect-Argus -Service Graph, Exchange, SharePoint -ClientId 00000000-0000-0000-0000-000000000000 Interactive sign-in to Graph, Exchange Online and SharePoint Online (PnP needs its own app registration). .EXAMPLE Connect-Argus -Service All -ClientId 00000000-0000-0000-0000-000000000000 Interactive sign-in to every service: Graph, Exchange Online, SharePoint (PnP, with the given app registration) and MSCommerce (Windows only). Without -ClientId, SharePoint is skipped with a warning. .EXAMPLE Connect-Argus -Service Graph, Commerce Interactive sign-in to Graph and to the MSCommerce policies (Windows only), for the Copilot self-service purchase check. .EXAMPLE Connect-Argus -TenantId contoso.onmicrosoft.com -ClientId 00000000-0000-0000-0000-000000000000 -CertificateThumbprint 0123456789ABCDEF0123456789ABCDEF01234567 -Service Graph, Exchange Unattended app-only connection with a certificate. .EXAMPLE Connect-Argus -ManagedIdentity App-only connection using the system-assigned managed identity of the host. .OUTPUTS None. Connection details are kept in the module session; see Test-ArgusPrerequisite. .NOTES ARGUS is strictly read-only: it never requests write scopes and never calls write endpoints. .LINK Disconnect-Argus .LINK Get-ArgusRequiredPermission .LINK Test-ArgusPrerequisite #> [CmdletBinding(DefaultParameterSetName = 'Interactive')] [OutputType([void])] param( [Parameter()] [ValidateSet('Graph', 'Exchange', 'SharePoint', 'Commerce', 'All')] [string[]] $Service = @('Graph'), [Parameter(ParameterSetName = 'Interactive')] [Parameter(ParameterSetName = 'Certificate', Mandatory)] [Parameter(ParameterSetName = 'ManagedIdentity')] [ValidateNotNullOrEmpty()] [string] $TenantId, [Parameter(ParameterSetName = 'Interactive')] [Parameter(ParameterSetName = 'Certificate', Mandatory)] [Parameter(ParameterSetName = 'ManagedIdentity')] [ValidateNotNullOrEmpty()] [string] $ClientId, [Parameter(ParameterSetName = 'Certificate', Mandatory)] [ValidateNotNullOrEmpty()] [string] $CertificateThumbprint, [Parameter(ParameterSetName = 'ManagedIdentity', Mandatory)] [switch] $ManagedIdentity, [Parameter()] [ValidateNotNullOrEmpty()] [string] $Organization, [Parameter()] [ValidatePattern('^https://[a-zA-Z0-9-]+-admin\.sharepoint\.[a-z.]+/?$')] [string] $SharePointAdminUrl ) if ('All' -in $Service) { $Service = @($script:ArgusServices.Keys) } $mode = if ($ManagedIdentity) { 'ManagedIdentity' } else { $PSCmdlet.ParameterSetName } $authMode = @{ Interactive = 'Interactive (delegated)'; Certificate = 'Certificate (app-only)'; ManagedIdentity = 'Managed identity (app-only)' }[$mode] $requested = @((Get-ArgusRequiredPermission -Aggregate).GraphPermissions) if ($null -ne $script:ArgusConnection) { Write-Verbose 'Closing the previous ARGUS session.' Disconnect-Argus } # ---------------------------------------------------------------- Microsoft Graph $graph = @{ NoWelcome = $true; ContextScope = 'Process'; ErrorAction = 'Stop' } switch ($mode) { 'Interactive' { # Graph always uses the Microsoft Graph Command Line Tools app here; -ClientId is the PnP # app registration, which is usually not configured for Graph sign-in. $graph.Scopes = $requested if ($TenantId) { $graph.TenantId = $TenantId } } 'Certificate' { $graph.ClientId = $ClientId $graph.TenantId = $TenantId $graph.CertificateThumbprint = $CertificateThumbprint } 'ManagedIdentity' { $graph.Identity = $true if ($ClientId) { $graph.ClientId = $ClientId } } } Write-Verbose "Connecting to Microsoft Graph ($authMode)." Connect-MgGraph @graph | Out-Null $context = Get-MgContext if ($null -eq $context) { throw 'Microsoft Graph sign-in did not complete.' } $granted = @($context.Scopes | Where-Object { $_ }) $state = [ordered]@{ ConnectedServices = [string[]]@('Graph') OpenedServices = [string[]]@('Graph') GrantedScopes = if ($granted.Count -gt 0 -or $mode -eq 'Interactive') { [string[]]$granted } else { $null } Licenses = $null ConcealedReportNames = $null TenantId = [string]$context.TenantId TenantName = $null TenantDefaultDomain = $null TenantInitialDomain = $null Account = if ($context.Account) { [string]$context.Account } else { '{0} ({1})' -f $context.AppName, $context.ClientId } AuthMode = $authMode ConnectedAt = [datetimeoffset]::Now } $script:ArgusConnection = [pscustomobject]$state if ($mode -eq 'Interactive') { $missing = @($requested | Where-Object { $_ -notin $granted -and ($_ -replace '\.Read\.', '.ReadWrite.') -notin $granted }) if ($missing.Count -gt 0) { Write-Warning ("Consent was not granted for: {0}. Checks that need these permissions will be Skipped. " -f ($missing -join ', ') + 'Sign in again with an Application Administrator or Global Administrator and accept "Consent on behalf of your organization".') } } try { $org = @(Invoke-ArgusGraphRequest -Uri '/organization?$select=id,displayName,verifiedDomains' -All) | Select-Object -First 1 if ($org) { $state.TenantName = [string]$org.displayName $domains = @($org.verifiedDomains) $state.TenantDefaultDomain = ($domains | Where-Object { $_.isDefault } | Select-Object -First 1).name $state.TenantInitialDomain = ($domains | Where-Object { $_.isInitial } | Select-Object -First 1).name } } catch { Write-Warning "Tenant details could not be read: $($_.Exception.Message)" } try { $skus = @(Invoke-ArgusGraphRequest -Uri '/subscribedSkus?$select=skuId,skuPartNumber,capabilityStatus,servicePlans' -All) $state.Licenses = [string[]]@(Get-ArgusLicenseInfo -Sku $skus) } catch { Write-Warning "Licences could not be read, so Entra ID P1/P2 is treated as unknown: $($_.Exception.Message)" } try { $settings = Invoke-ArgusGraphRequest -Uri '/admin/reportSettings' if ($settings -and $settings.PSObject.Properties['displayConcealedNames']) { $state.ConcealedReportNames = [bool]$settings.displayConcealedNames } } catch { Write-Verbose "Report settings could not be read: $($_.Exception.Message)" } $script:ArgusConnection = [pscustomobject]$state # ---------------------------------------------------------------- Exchange Online if ('Exchange' -in $Service) { $exchangeOrganization = if ($Organization) { $Organization } else { $state.TenantInitialDomain } if (-not (Get-ArgusModuleInfo -Name 'ExchangeOnlineManagement').Installed) { Write-Warning 'ExchangeOnlineManagement is not installed (Install-Module ExchangeOnlineManagement). Exchange checks will be Skipped.' } elseif ($mode -ne 'Interactive' -and -not $exchangeOrganization) { Write-Warning 'App-only Exchange Online connections need -Organization (initial .onmicrosoft.com domain). Exchange checks will be Skipped.' } else { $exchange = @{ ShowBanner = $false; ErrorAction = 'Stop' } switch ($mode) { 'Interactive' { if ($context.Account) { $exchange.UserPrincipalName = [string]$context.Account } } 'Certificate' { $exchange.AppId = $ClientId $exchange.CertificateThumbprint = $CertificateThumbprint $exchange.Organization = $exchangeOrganization } 'ManagedIdentity' { $exchange.ManagedIdentity = $true $exchange.Organization = $exchangeOrganization if ($ClientId) { $exchange.ManagedIdentityAccountId = $ClientId } } } try { Import-Module ExchangeOnlineManagement -ErrorAction Stop # WAM (default since ExchangeOnlineManagement 3.7.0) fails in many PowerShell 7 hosts with a # NullReferenceException; -DisableWAM (3.7.2+) uses the browser sign-in instead. if ($mode -eq 'Interactive' -and (Get-Command Connect-ExchangeOnline).Parameters.ContainsKey('DisableWAM')) { $exchange.DisableWAM = $true } Connect-ExchangeOnline @exchange | Out-Null $state.ConnectedServices += 'Exchange' $state.OpenedServices += 'Exchange' } catch { Write-Warning "Exchange Online connection failed: $($_.Exception.Message). Exchange checks will be Skipped." } } } # ---------------------------------------------------------------- SharePoint Online (PnP) if ('SharePoint' -in $Service) { $adminUrl = if ($SharePointAdminUrl) { $SharePointAdminUrl.TrimEnd('/') } elseif ($state.TenantInitialDomain) { 'https://{0}-admin.sharepoint.com' -f ($state.TenantInitialDomain -split '\.')[0] } if (-not (Get-ArgusModuleInfo -Name 'PnP.PowerShell').Installed) { Write-Warning 'PnP.PowerShell is not installed (Install-Module PnP.PowerShell). Classic site checks will report the sites as not evaluated.' } elseif ($mode -eq 'Interactive' -and -not $ClientId) { Write-Warning 'Interactive PnP.PowerShell sign-in needs -ClientId (your own Entra app registration). SharePoint (PnP) was not connected.' } elseif (-not $adminUrl) { Write-Warning 'The SharePoint admin URL could not be determined; pass -SharePointAdminUrl. SharePoint (PnP) was not connected.' } else { $pnp = @{ Url = $adminUrl; ErrorAction = 'Stop' } switch ($mode) { 'Interactive' { $pnp.Interactive = $true; $pnp.ClientId = $ClientId; if ($TenantId) { $pnp.Tenant = $TenantId } } 'Certificate' { $pnp.ClientId = $ClientId; $pnp.Thumbprint = $CertificateThumbprint; $pnp.Tenant = $TenantId } 'ManagedIdentity' { $pnp.ManagedIdentity = $true; if ($ClientId) { $pnp.UserAssignedManagedIdentityClientId = $ClientId } } } try { Import-Module PnP.PowerShell -ErrorAction Stop Connect-PnPOnline @pnp | Out-Null $state.ConnectedServices += 'SharePoint' $state.OpenedServices += 'SharePoint' } catch { Write-Warning "SharePoint (PnP) connection failed: $($_.Exception.Message). Classic site checks will report the sites as not evaluated." } } } # ---------------------------------------------------------------- Microsoft 365 commerce (MSCommerce) if ('Commerce' -in $Service) { if (-not (Get-ArgusModuleInfo -Name 'MSCommerce').Installed) { Write-Warning 'MSCommerce is not installed (Install-Module MSCommerce). Self-service purchase checks will be Skipped.' } elseif ($mode -ne 'Interactive' -or -not $IsWindows) { Write-Warning 'MSCommerce supports only interactive sign-in on Windows. Self-service purchase checks will be Skipped.' } else { try { # MSCommerce runs in its own pwsh process (its MSAL version conflicts with Graph/EXO/PnP); # the policies are read now and kept in memory for the runs of this session. $script:ArgusCommercePolicies = @(Get-ArgusCommercePolicy -ErrorAction Stop) $state.ConnectedServices += 'Commerce' $state.OpenedServices += 'Commerce' } catch { Write-Warning "MSCommerce connection failed: $($_.Exception.Message). Self-service purchase checks will be Skipped." } } } $script:ArgusConnection = [pscustomobject]$state $script:ArgusDataCache = @{} $tenantLabel = if ($state.TenantName) { $state.TenantName } else { $state.TenantId } $licenseLabel = if ($null -eq $state.Licenses) { 'unknown' } elseif (@($state.Licenses).Count) { ($state.Licenses | ForEach-Object { $script:ArgusLicenses[$_].DisplayName }) -join ', ' } else { 'no Entra ID P1/P2' } # Shown by default; -InformationAction SilentlyContinue hides it. $informationAction = if ($PSBoundParameters.ContainsKey('InformationAction')) { $PSBoundParameters['InformationAction'] } else { 'Continue' } Write-Information -MessageData ("ARGUS connected to {0} as {1} ({2}). Services: {3}. Licences: {4}." -f $tenantLabel, $state.Account, $authMode, ($state.ConnectedServices -join ', '), $licenseLabel) -InformationAction $informationAction } |