diff --git a/ExecMaintenanceScripts/Scripts/Add-CippUser.ps1 b/ExecMaintenanceScripts/Scripts/Add-CippUser.ps1 new file mode 100644 index 000000000000..d04ad2358c9d --- /dev/null +++ b/ExecMaintenanceScripts/Scripts/Add-CippUser.ps1 @@ -0,0 +1,67 @@ +#requires -Version 7.2 + +[CmdletBinding(DefaultParameterSetName = 'interactive')] +Param( + [Parameter(Mandatory = $true, ParameterSetName = 'noninteractive')] + [ValidateSet('readonly', 'editor', 'admin')] + $Role, + [Parameter(Mandatory = $true, ParameterSetName = 'noninteractive')] + $SelectedUsers, + [Parameter(ParameterSetName = 'noninteractive')] + [Parameter(ParameterSetName = 'interactive')] + $ExpirationHours = 1 +) + +$ResourceGroup = '##RESOURCEGROUP##' +$Subscription = '##SUBSCRIPTION##' + +if (!(Get-Module -ListAvailable Microsoft.PowerShell.ConsoleGuiTools)) { + Install-Module Microsoft.PowerShell.ConsoleGuiTools -Force +} + +$Context = Get-AzContext +if (!$Context) { + Write-Host "`n- Connecting to Azure" + $Context = Connect-AzAccount -Subscription $Subscription +} +Write-Host "Connected to $($Context.Account)" + +$swa = Get-AzStaticWebApp -ResourceGroupName $ResourceGroup +$Domain = $swa.CustomDomain | Select-Object -First 1 +if ($Domain -eq $null) { $Domain = $swa.DefaultHostname } +Write-Host "CIPP SWA - $($swa.name)" + +if (!$Role) { + $Role = @('readonly', 'editor', 'admin') | Out-ConsoleGridView -OutputMode Single -Title 'Select CIPP Role' +} + +$CurrentUsers = Get-AzStaticWebAppUser -Name $swa.name -ResourceGroupName $ResourceGroup -AuthProvider all | Select-Object DisplayName, Role + +$AllUsers = Get-AzADUser -Filter "userType eq 'Member' and accountEnabled eq true" | Select-Object DisplayName, UserPrincipalName + + +$SelectedUsers = $AllUsers | Where-Object { $CurrentUsers.DisplayName -notcontains $_.UserPrincipalName } | Sort-Object -Property DisplayName | Out-ConsoleGridView -Title "Select users for role '$Role'" +Write-Host "Selected users: $($SelectedUsers.UserPrincipalName -join ', ')" + +Write-Host 'Generating invite links...' +$InviteList = foreach ($User in $SelectedUsers) { + $UserInvite = @{ + InputObject = $swa + Domain = $Domain + Provider = 'aad' + UserDetail = $User.UserPrincipalName + Role = $Role + NumHoursToExpiration = $ExpirationHours + } + $Invite = New-AzStaticWebAppUserRoleInvitationLink @UserInvite + + [PSCustomObject]@{ + User = $User.UserPrincipalName + Role = $Role + Link = $Invite.InvitationUrl + Expires = $Invite.ExpiresOn + } +} +$InviteList +$InviteList | Export-Csv -Path '.\cipp-invites.csv' -Append +Write-Host 'Invitations exported to .\cipp-invites.csv' diff --git a/ExecMaintenanceScripts/Scripts/Enable-FunctionAppGitHubActions.ps1 b/ExecMaintenanceScripts/Scripts/Enable-FunctionAppGitHubActions.ps1 new file mode 100644 index 000000000000..a47273afad9e --- /dev/null +++ b/ExecMaintenanceScripts/Scripts/Enable-FunctionAppGitHubActions.ps1 @@ -0,0 +1,38 @@ +$ResourceGroup = '##RESOURCEGROUP##' +$Subscription = '##SUBSCRIPTION##' +$FunctionName = '##FUNCTIONAPP##' + +$Logo = @' + _____ _____ _____ _____ + / ____|_ _| __ \| __ \ + | | | | | |__) | |__) | + | | | | | ___/| ___/ + | |____ _| |_| | | | + \_____|_____|_| |_| + +'@ +Write-Host $Logo + +Write-Host '- Connecting to Azure' +Connect-AzAccount -Identity -Subscription $Subscription | Out-Null + +Write-Host 'Checking deployment settings' +$DeploymentSettings = & az functionapp deployment source show --resource-group $ResourceGroup --name $FunctionName | ConvertFrom-Json + +if (!($DeploymentSettings.isGitHubAction)) { + Write-Host 'Creating GitHub action, follow the prompts to log into GitHub' + $GitHubRepo = ([uri]$DeploymentSettings.repoUrl).LocalPath.TrimStart('/') + az functionapp deployment github-actions add --repo $GitHubRepo --branch $DeploymentSettings.branch --resource-group $ResourceGroup --name $FunctionName --login-with-github +} + +$DeploymentSettings = & az functionapp deployment source show --resource-group $ResourceGroup --name $FunctionName | ConvertFrom-Json +if ($DeploymentSettings.isGitHubAction) { + $cipp = Get-AzFunctionApp -ResourceGroupName $ResourceGroup + $cipp.ApplicationSettings['WEBSITE_RUN_FROM_PACKAGE'] = 1 + $cipp | Update-AzFunctionAppSetting -AppSetting $cipp.ApplicationSettings + + Write-Host "GitHub action created and project set to run from package, navigate to $($DeploymentSettings.repoUrl)/actions and run the 'Build and deploy Powershell project to Azure Function App'" +} +else { + Write-Host 'GitHub action not set up for deployment, try running the script again.' +} diff --git a/ExecMaintenanceScripts/Scripts/Grant-CippConditionalAccess.ps1 b/ExecMaintenanceScripts/Scripts/Grant-CippConditionalAccess.ps1 new file mode 100644 index 000000000000..b4460c2994d4 --- /dev/null +++ b/ExecMaintenanceScripts/Scripts/Grant-CippConditionalAccess.ps1 @@ -0,0 +1,122 @@ +if (!(Get-Module -ListAvailable Microsoft.Graph)) { + Install-Module Microsoft.Graph -Confirm:$false -Force -AllowPrerelease +} + +$ResourceGroup = '##RESOURCEGROUP##' +$Subscription = '##SUBSCRIPTION##' +$FunctionName = '##FUNCTIONAPP##' +$TokenIP = '##TOKENIP##' + +$Logo = @' + _____ _____ _____ _____ + / ____|_ _| __ \| __ \ + | | | | | |__) | |__) | + | | | | | ___/| ___/ + | |____ _| |_| | | | + \_____|_____|_| |_| + +'@ +Write-Host $Logo + +Write-Host '=== Conditional Access Management ===' +if (Test-Path -Path '.\cipp-function-namedLocation.json') { + $UseCache = Read-Host -Prompt 'Used cached Named Location for CIPP? (Y/n)' + if ($UseCache -ne 'n') { + $ipNamedLocation = Get-Content -Path '.\cipp-function-namedLocation.json' | ConvertFrom-Json -AsHashtable + } +} + +if (!($ipNamedLocation)) { + Write-Host "`n- Connecting to Azure" + Connect-AzAccount -Identity -Subscription $Subscription | Out-Null + $Function = Get-AzFunctionApp -ResourceGroupName $ResourceGroup -Name $FunctionName + + Write-Host 'Getting Function App IP addresses' + # Get possible IPs from function app + $PossibleIpAddresses = (($Function | Select-Object -ExpandProperty PossibleOutboundIpAddress) + ',' + $TokenIP) -split ',' + + # Convert possible IP addresses to ipv4CidrRange list + $ipRanges = foreach ($Ip in $PossibleIpAddresses) { + $Cidr = '{0}/32' -f $Ip + @{ + '@odata.type' = '#microsoft.graph.iPv4CidrRange' + 'cidrAddress' = $Cidr + } + } + + # Return ipNamedLocation object + $ipNamedLocation = @{ + '@odata.type' = '#microsoft.graph.ipNamedLocation' + displayName = ('CyberDrain Improved Partner Portal - {0}' -f $FunctionName) + isTrusted = $true + ipRanges = $ipRanges + } + + $ipNamedLocation | ConvertTo-Json -Depth 10 | Out-File -Path '.\cipp-function-namedLocation.json' + Write-Host 'Named location policy created and saved to .\cipp-function-namedLocation.json' +} + +Write-Host "`n- Connecting to Customer Graph API, ensure you log in from a system that is allowed through the Conditional Access policy" +Select-MgProfile -Name 'beta' +$GraphOptions = @{ + Scopes = @('Policy.Read.All', 'Policy.ReadWrite.ConditionalAccess', 'Application.Read.All') + UseDeviceAuthentication = $true +} + +do { + Connect-MgGraph @GraphOptions + $Context = Get-MgContext + if ($Context) { + Write-Host "Connected as $($Context.Account) ($($Context.TenantId))" + $Switch = Read-Host -Prompt 'Switch Accounts? (y/N)' + if ($Switch -eq 'y') { + Disconnect-MgGraph | Out-Null + } + } +} +while (!(Get-MgContext)) + +Write-Host "`n- Getting existing policies" +$Policies = Get-MgIdentityConditionalAccessPolicy +Write-Host($Policies.displayName -join "`n") + +Write-Host "`n- Named Location Check" +$NamedLocations = Get-MgIdentityConditionalAccessNamedLocation +if ($NamedLocations.displayName -notcontains $ipNamedLocation.displayName) { + Write-Host "Creating Named Location: '$($ipNamedLocation.displayName)'" + $NamedLocation = New-MgIdentityConditionalAccessNamedLocation -BodyParameter $ipNamedLocation +} +else { + $NamedLocation = $NamedLocations | Where-Object { $_.displayName -eq $ipNamedLocation.displayName } + Write-Host "Named Location exists: '$($NamedLocation.displayName)'" + Update-MgIdentityConditionalAccessNamedLocation -NamedLocationId $NamedLocation.Id -BodyParameter $ipNamedLocation +} + +Write-Host "`n- Conditional access policy check" +$ConfigPolicy = Read-Host -Prompt 'Exclude CIPP from existing CA policies? (Y/n)' +if ($ConfigPolicy -ne 'n') { + foreach ($Policy in $Policies) { + Write-Host "- Policy: $($Policy.displayName)" + $Conditions = $Policy.Conditions + $ExcludeLocations = $Conditions.Locations.ExcludeLocations + $IncludeLocations = $Conditions.Locations.IncludeLocations + if ($ExcludeLocations -eq 'AllTrusted' -or $ExcludeLocations -contains $NamedLocation.Id) { + Write-Host 'Named location already excluded' + } + elseif ($IncludeLocations -eq 'AllTrusted' -or $IncludeLocations -contains $NamedLocation.Id) { + Write-Host 'Named location is already included' + } + else { + Write-Host 'Adding exclusion for named location' + $Locations = [system.collections.generic.list[string]]::new() + foreach ($Location in $ExcludeLocations) { + $Locations.Add($Location) | Out-Null + } + $Locations.Add($NamedLocation.Id) | Out-Null + $Conditions.Locations.ExcludeLocations = [string[]]$Locations + if (!($Conditions.Locations.IncludeLocations)) { $Conditions.Locations.IncludeLocations = 'All' } + Update-MgIdentityConditionalAccessPolicy -ConditionalAccessPolicyId $Policy.Id -Conditions $Conditions + } + } + Write-Host "`nDone." +} diff --git a/ExecScheduledCommand/function.json b/ExecScheduledCommand/function.json new file mode 100644 index 000000000000..e4c27b23b985 --- /dev/null +++ b/ExecScheduledCommand/function.json @@ -0,0 +1,10 @@ +{ + "bindings": [ + { + "name": "QueueItem", + "type": "queueTrigger", + "direction": "in", + "queueName": "scheduledcommandprocessor" + } + ] +} diff --git a/ExecScheduledCommand/run.ps1 b/ExecScheduledCommand/run.ps1 new file mode 100644 index 000000000000..615b6b8471cf --- /dev/null +++ b/ExecScheduledCommand/run.ps1 @@ -0,0 +1,86 @@ +# Input bindings are passed in via param block. +param($QueueItem, $TriggerMetadata) + +$Table = Get-CippTable -tablename 'ScheduledTasks' +$task = $QueueItem.TaskInfo +$commandParameters = $QueueItem.Parameters + +$tenant = $QueueItem.Parameters['TenantFilter'] +Write-Host 'started task' +try { + try { + $results = & $QueueItem.command @commandParameters + } + catch { + $results = "Task Failed: $($_.Exception.Message)" + + } + + Write-Host 'ran the command' + if ($results -is [String]) { + $results = @{ Results = $results } + } + if ($results -is [array]) { + $results = $results | Where-Object { $_ -is [string] } + $results = $results | ForEach-Object { @{ Results = $_ } } + } + + $results = $results | Select-Object * -ExcludeProperty RowKey, PartitionKey + + $StoredResults = $results | ConvertTo-Json -Compress -Depth 20 | Out-String + if ($StoredResults.Length -gt 64000 -or $task.Tenant -eq 'AllTenants') { + $StoredResults = @{ Results = 'The results for this query are too long to store in this table, or the query was meant for All Tenants. Please use the options to send the results to another target to be able to view the results. ' } | ConvertTo-Json -Compress + } +} +catch { + $errorMessage = $_.Exception.Message + if ($task.Recurrence -gt 0) { $State = 'Failed - Planned' } else { $State = 'Failed' } + Update-AzDataTableEntity @Table -Entity @{ + PartitionKey = $task.PartitionKey + RowKey = $task.RowKey + Results = "$errorMessage" + TaskState = $State + } + Write-LogMessage -API 'Scheduler_UserTasks' -tenant $tenant -message "Failed to execute task $($task.Name): $errorMessage" -sev Error +} + + +$TableDesign = '' +$HTML = ($results | Select-Object * -ExcludeProperty RowKey, PartitionKey | ConvertTo-Html -Fragment) -replace '