public/Connect-MsecAdmin.ps1
|
function Connect-MsecAdmin { <# .SYNOPSIS Signs in AS YOU, with delegated write scopes, for the commands that change something. Read commands keep using the app; this session is only for writes. .DESCRIPTION THE APP CANNOT DO THIS, ON PURPOSE. Every Graph permission New-MsecApp consents is *.Read.All, so the certificate in Key Vault cannot change anything - that is the module's central promise and it is enforced by the token, not by naming. Writes therefore run as a person instead: attributable to a named account, subject to your Conditional Access and MFA, bounded by your own Defender RBAC rather than tenant-wide application permissions, and impossible from an unattended pipeline unless somebody deliberately sets one up. NOT THE -AsCurrentUser PATTERN, AND HERE IS WHY. Connect-MsecTeams borrows the Az context's token, which works because Azure PowerShell's first-party app holds the scopes those commands need. Measured on a live tenant, its Graph token carries Application.ReadWrite.All, Group.ReadWrite.All, Directory.AccessAsUser.All and User.Read.All - and nothing for security. There is no SecurityAlert.ReadWrite.All in it, so borrowing cannot resolve an alert. This asks for consent properly instead. CONSENT REQUESTED IS NOT CONSENT GRANTED. Connect-MgGraph succeeds when a tenant declines a scope; the context simply comes back without it, and the first write then fails with a 403 that names nothing. Every requested scope is checked against what was actually granted, and a missing one is reported by name here rather than discovered later. IT WILL REFUSE A DIFFERENT TENANT FROM THE ONE YOU ARE READING. If Connect-Msec holds a session, this must sign in to the same tenant. Reading one tenant and writing to another is the kind of mistake that is obvious afterwards and invisible at the time. .PARAMETER Scope Delegated scopes to request. Defaults to resolving alerts and incidents. Ask for the narrowest set that covers what you intend to do in this session. .PARAMETER TenantId Tenant to sign in to. Defaults to the tenant Connect-Msec is using, which is almost always what you want. .PARAMETER PassThru Emit the Graph context as well as the summary. .EXAMPLE Connect-Msec -KeyVaultName kv-msec # reads, as the app Connect-MsecAdmin # writes, as you .EXAMPLE # Only what this session needs. Connect-MsecAdmin -Scope SecurityAlert.ReadWrite.All .OUTPUTS One PSCustomObject describing the session: Account, TenantId, GrantedScope. .NOTES Needs the Microsoft.Graph.Authentication module, which is NOT a dependency of msec - it is only required by the write commands. Disconnect with Disconnect-MgGraph. Connect-Msec and this session are independent; disconnecting one leaves the other alone. #> [CmdletBinding()] [OutputType([PSCustomObject])] param( [string[]] $Scope = @('SecurityIncident.ReadWrite.All', 'SecurityAlert.ReadWrite.All'), [string] $TenantId, [switch] $PassThru ) if (-not (Get-Module -ListAvailable -Name Microsoft.Graph.Authentication)) { throw 'Microsoft.Graph.Authentication is required for Connect-MsecAdmin. Install with: Install-Module Microsoft.Graph.Authentication -Scope CurrentUser' } Import-Module Microsoft.Graph.Authentication -ErrorAction Stop # Default to the tenant being READ, so a write cannot land somewhere else by accident. if (-not $TenantId -and $script:MsecSession -and $script:MsecSession.TenantId) { $TenantId = $script:MsecSession.TenantId } $connectArgs = @{ Scopes = $Scope; NoWelcome = $true; ErrorAction = 'Stop' } if ($TenantId) { $connectArgs['TenantId'] = $TenantId } Connect-MgGraph @connectArgs $context = Get-MgContext if (-not $context) { throw 'Connect-MgGraph returned no context. The sign-in did not complete.' } # See the help: a declined scope is not an error, it is an absence. $granted = @($context.Scopes) $missing = @($Scope | Where-Object { $_ -notin $granted }) if ($missing.Count) { throw ("Signed in as $($context.Account), but the tenant did not grant: $($missing -join ', '). " + "Connect-MgGraph does not fail on a declined scope - it returns a context without it, and the " + "first write would 403 naming nothing. An administrator must consent these for the Microsoft " + "Graph PowerShell application. Granted: $($granted -join ', ')") } # Reading one tenant and writing to another is silently wrong rather than loudly broken. if ($script:MsecSession -and $script:MsecSession.TenantId -and $context.TenantId -and $context.TenantId -ne $script:MsecSession.TenantId) { Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null throw ("Connect-Msec is reading tenant $($script:MsecSession.TenantId) but this sign-in landed on " + "$($context.TenantId). Refusing rather than writing to a different tenant than you are reading. " + 'Pass -TenantId explicitly if that is genuinely what you intend.') } $script:MsecAdminSession = [PSCustomObject]@{ PSTypeName = 'MsecAdminSession' Account = [string] $context.Account TenantId = [string] $context.TenantId GrantedScope = $granted ConnectedUtc = [datetime]::UtcNow } Write-Verbose "Write session established as $($context.Account) in tenant $($context.TenantId)." $script:MsecAdminSession if ($PassThru) { $context } } |