diff --git a/AddAlert/function.json b/AddAlert/function.json index 3bd167116eae..1de186148741 100644 --- a/AddAlert/function.json +++ b/AddAlert/function.json @@ -5,10 +5,7 @@ "type": "httpTrigger", "direction": "in", "name": "Request", - "methods": [ - "get", - "post" - ] + "methods": ["get", "post"] }, { "type": "http", @@ -16,9 +13,10 @@ "name": "Response" }, { - "name": "starter", - "direction": "in", - "type": "durableClient" + "type": "queue", + "direction": "out", + "name": "Subscription", + "queueName": "AlertSubscriptions" } ] -} \ No newline at end of file +} diff --git a/AddAlert/run.ps1 b/AddAlert/run.ps1 index 7499c4ec7a51..58091f3d0721 100644 --- a/AddAlert/run.ps1 +++ b/AddAlert/run.ps1 @@ -11,8 +11,7 @@ $Results = foreach ($Tenant in $tenants) { try { $TenantID = if ($tenant -ne 'AllTenants') { (get-tenants | Where-Object -Property defaultDomainName -EQ $Tenant).customerId - } - else { + } else { 'AllTenants' } if ($Request.body.SetAlerts) { @@ -58,11 +57,10 @@ $Results = foreach ($Tenant in $tenants) { EventType = $eventType ExecutingUser = $Request.headers.'x-ms-client-principal' } - New-CIPPGraphSubscription @params + Push-OutputBinding -Name Subscription -Value $Params } } - } - else { + } else { foreach ($eventType in $Request.body.EventTypes.value) { $params = @{ TenantFilter = $tenant @@ -78,8 +76,7 @@ $Results = foreach ($Tenant in $tenants) { } "Successfully added Alert for $($Tenant) to queue." Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -tenant $tenant -message "Successfully added Alert for $($Tenant) to queue." -Sev 'Info' - } - catch { + } catch { Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -tenant $tenant -message "Failed to add Alert for for $($Tenant) to queue" -Sev 'Error' "Failed to add Alert for for $($Tenant) to queue $($_.Exception.message)" } diff --git a/AddAlertSubscription_Queue/function.json b/AddAlertSubscription_Queue/function.json new file mode 100644 index 000000000000..12e3dfc77c46 --- /dev/null +++ b/AddAlertSubscription_Queue/function.json @@ -0,0 +1,12 @@ +{ + "scriptFile": "../Modules/CippEntryPoints/CippEntryPoints.psm1", + "entryPoint": "Receive-CippQueueTrigger", + "bindings": [ + { + "name": "QueueItem", + "type": "queueTrigger", + "direction": "in", + "queueName": "AlertSubscriptions" + } + ] +} diff --git a/AddChocoApp/Choco.app.json b/AddChocoApp/Choco.app.json index b7229ef3ccf8..7ef3f82d640f 100644 --- a/AddChocoApp/Choco.app.json +++ b/AddChocoApp/Choco.app.json @@ -1,5 +1,4 @@ { - "displayName": "", "installCommandLine": "", "uninstallCommandLine": "", @@ -11,7 +10,7 @@ "fileName": "IntunePackage.intunewin", "@odata.type": "#microsoft.graph.win32LobApp", "applicableArchitectures": "x86, x64", - + "installExperience": { "runAsAccount": "user", "deviceRestartBehavior": "allow", @@ -19,39 +18,40 @@ }, "detectionRules": [ { - "@odata.type": "#microsoft.graph.win32LobAppFileSystemDetection", - "path": "%programfiles%\\7-zip", - "fileOrFolderName": "7z.exe", - "check32BitOn64System": false, - "detectionType": "exists" } + "@odata.type": "#microsoft.graph.win32LobAppFileSystemDetection", + "path": "%programfiles%\\7-zip", + "fileOrFolderName": "7z.exe", + "check32BitOn64System": false, + "detectionType": "exists" + } + ], + "returncode": [ + { + "returnCode": 0, + "type": "success", + "@odata.type": "#microsoft.graph.win32LobAppReturnCode" + }, + { + "returnCode": 1707, + "type": "Success", + "@odata.type": "#microsoft.graph.win32LobAppReturnCode" + }, + { + "returnCode": 1641, + "type": "hardReboot", + "@odata.type": "#microsoft.graph.win32LobAppReturnCode" + }, + { + "returnCode": 1618, + "type": "retry", + "@odata.type": "#microsoft.graph.win32LobAppReturnCode" + }, + { + "returnCode": 3010, + "type": "softReboot", + "@odata.type": "#microsoft.graph.win32LobAppReturnCode" + } ], - "returncode": [ - { - "returnCode": 0, - "type": "success", - "@odata.type": "#microsoft.graph.win32LobAppReturnCode" - }, - { - "returnCode": 1707, - "type": "Success", - "@odata.type": "#microsoft.graph.win32LobAppReturnCode" - }, - { - "returnCode": 1641, - "type": "hardReboot", - "@odata.type": "#microsoft.graph.win32LobAppReturnCode" - }, - { - "returnCode": 1618, - "type": "retry", - "@odata.type": "#microsoft.graph.win32LobAppReturnCode" - }, - { - "returnCode": 3010, - "type": "softReboot", - "@odata.type": "#microsoft.graph.win32LobAppReturnCode" - } - ], "minimumNumberOfProcessors": "1", "minimumFreeDiskSpaceInMB": "8", "minimumCpuSpeedInMHz": "4", @@ -60,7 +60,6 @@ "v10_1607": true }, "notes": "CIPP Uploaded application", - "minimumMemoryInMB": "1" - - -} \ No newline at end of file + "minimumMemoryInMB": "1", + "setupFilePath": "install.ps1" +} diff --git a/AddMSPApp/Immybot.app.json b/AddMSPApp/Immybot.app.json index f41df4601b6a..8be12d944925 100644 --- a/AddMSPApp/Immybot.app.json +++ b/AddMSPApp/Immybot.app.json @@ -60,5 +60,6 @@ "v10_1607": true }, "notes": "CIPP Uploaded application", - "minimumMemoryInMB": "1" + "minimumMemoryInMB": "1", + "setupFilePath": "install.ps1" } diff --git a/AddMSPApp/automate.app.json b/AddMSPApp/automate.app.json index 145947b741b0..0c3fa83967b8 100644 --- a/AddMSPApp/automate.app.json +++ b/AddMSPApp/automate.app.json @@ -59,5 +59,6 @@ "v10_1607": true }, "notes": "CIPP Uploaded application", - "minimumMemoryInMB": "1" + "minimumMemoryInMB": "1", + "setupFilePath": "install.ps1" } diff --git a/AddMSPApp/cwcommand.app.json b/AddMSPApp/cwcommand.app.json index 876235d1e840..2d032177b625 100644 --- a/AddMSPApp/cwcommand.app.json +++ b/AddMSPApp/cwcommand.app.json @@ -60,5 +60,6 @@ "v10_1607": true }, "notes": "CIPP Uploaded application", - "minimumMemoryInMB": "1" + "minimumMemoryInMB": "1", + "setupFilePath": "install.ps1" } diff --git a/AddMSPApp/datto.app.json b/AddMSPApp/datto.app.json index 25c470f0aa44..cdd10dfd8aeb 100644 --- a/AddMSPApp/datto.app.json +++ b/AddMSPApp/datto.app.json @@ -60,5 +60,6 @@ "v10_1607": true }, "notes": "CIPP Uploaded application", - "minimumMemoryInMB": "1" + "minimumMemoryInMB": "1", + "setupFilePath": "install.ps1" } diff --git a/AddMSPApp/huntress.app.json b/AddMSPApp/huntress.app.json index 0c8081d644c8..46c8a22057ae 100644 --- a/AddMSPApp/huntress.app.json +++ b/AddMSPApp/huntress.app.json @@ -60,5 +60,6 @@ "v10_1607": true }, "notes": "CIPP Uploaded application", - "minimumMemoryInMB": "1" + "minimumMemoryInMB": "1", + "setupFilePath": "install.ps1" } diff --git a/AddMSPApp/syncro.app.json b/AddMSPApp/syncro.app.json index 6eb781fda8bd..1d5f0e01fb4b 100644 --- a/AddMSPApp/syncro.app.json +++ b/AddMSPApp/syncro.app.json @@ -60,5 +60,6 @@ "v10_1607": true }, "notes": "CIPP Uploaded application", - "minimumMemoryInMB": "1" + "minimumMemoryInMB": "1", + "setupFilePath": "install.ps1" } diff --git a/AddScheduledItems/function.json b/AddScheduledItem/function.json similarity index 94% rename from AddScheduledItems/function.json rename to AddScheduledItem/function.json index 925eab5aeae1..b0ca1676cc0b 100644 --- a/AddScheduledItems/function.json +++ b/AddScheduledItem/function.json @@ -1,19 +1,19 @@ -{ - "bindings": [ - { - "authLevel": "anonymous", - "type": "httpTrigger", - "direction": "in", - "name": "Request", - "methods": [ - "get", - "post" - ] - }, - { - "type": "http", - "direction": "out", - "name": "Response" - } - ] +{ + "bindings": [ + { + "authLevel": "anonymous", + "type": "httpTrigger", + "direction": "in", + "name": "Request", + "methods": [ + "get", + "post" + ] + }, + { + "type": "http", + "direction": "out", + "name": "Response" + } + ] } \ No newline at end of file diff --git a/AddScheduledItem/run.ps1 b/AddScheduledItem/run.ps1 new file mode 100644 index 000000000000..8568323c6059 --- /dev/null +++ b/AddScheduledItem/run.ps1 @@ -0,0 +1,30 @@ +using namespace System.Net +param($Request, $TriggerMetadata) +$APIName = $TriggerMetadata.FunctionName +Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message 'Accessed this API' -Sev 'Debug' +$task = $Request.Body +$Table = Get-CIPPTable -TableName 'ScheduledTasks' + +$propertiesToCheck = @('Webhook', 'Email', 'PSA') +$PostExecution = ($propertiesToCheck | Where-Object { $task.PostExecution.$_ -eq $true }) -join ',' +$Parameters = ($task.Parameters | ConvertTo-Json -Compress) +if ($Parameters -eq 'null') { $Parameters = '' } +$entity = @{ + PartitionKey = [string]'ScheduledTask' + TaskState = [string]'Planned' + RowKey = [string]"$(New-Guid)" + Tenant = [string]$task.TenantFilter + Name = [string]$task.Name + Command = [string]$task.Command.value + Parameters = [string]$Parameters + ScheduledTime = [string]$task.ScheduledTime + Recurrence = [string]$task.Recurrence.value + PostExecution = [string]$PostExecution + Results = 'Planned' +} +Write-Host "entity: $($entity | ConvertTo-Json)" +Add-AzDataTableEntity @Table -Entity $entity +Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = @{ Results = 'Task added successfully.' } + }) \ No newline at end of file diff --git a/AddScheduledItems/run.ps1 b/AddScheduledItems/run.ps1 deleted file mode 100644 index b8dbc7843324..000000000000 --- a/AddScheduledItems/run.ps1 +++ /dev/null @@ -1,20 +0,0 @@ -using namespace System.Net -param($Request, $TriggerMetadata) -$APIName = $TriggerMetadata.FunctionName -Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message 'Accessed this API' -Sev 'Debug' -$task = $Request.Body | ConvertFrom-Json -$Table = Get-CIPPTable -TableName 'ScheduledTasks' -Add-AzDataTableEntity @Table -Entity @{ - PartitionKey = 'ScheduledTask' - TaskState = 'Scheduled' - RowKey = $task.TaskID - Command = $task.Command - Parameters = $task.Parameters - ScheduledTime = $task.ScheduledTime - Results = 'Not Executed' - # add more properties here based on what properties your tasks have -} -Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ - StatusCode = [HttpStatusCode]::OK - Body = 'Task added successfully.' -}) \ No newline at end of file diff --git a/ExecBECRemediate/run.ps1 b/ExecBECRemediate/run.ps1 index 10e0e33de695..c47647d1c5ab 100644 --- a/ExecBECRemediate/run.ps1 +++ b/ExecBECRemediate/run.ps1 @@ -20,8 +20,8 @@ try { $GraphRequest = New-GraphPostRequest -uri "https://graph.microsoft.com/v1.0/users/$SuspectUser" -tenantid $TenantFilter -type PATCH -body $passwordProfile -verbose $GraphRequest = New-GraphPostRequest -uri "https://graph.microsoft.com/v1.0/users/$SuspectUser" -tenantid $TenantFilter -type PATCH -body '{"accountEnabled":"false"}' -verbose $GraphRequest = New-GraphPostRequest -uri "https://graph.microsoft.com/v1.0/users/$SuspectUser/revokeSignInSessions" -tenantid $TenantFilter -type POST -body '{}' -verbose - $Mailboxes = New-ExoRequest -tenantid $TenantFilter -cmdlet "get-inboxrule" -cmdParams @{Mailbox = $SuspectUser } -anchor $SuspectUser | ForEach-Object { - New-ExoRequest -tenantid $TenantFilter -cmdlet "Disable-InboxRule" -cmdParams @{Confirm = $false; Identity = $_.Identity } -anchor $SuspectUser + $Mailboxes = New-ExoRequest -anchor $SuspectUser -tenantid $TenantFilter -cmdlet "get-inboxrule" -cmdParams @{Mailbox = $SuspectUser } | ForEach-Object { + New-ExoRequest -anchor $SuspectUser -tenantid $TenantFilter -cmdlet "Disable-InboxRule" -cmdParams @{Confirm = $false; Identity = $_.Identity } } $results = [pscustomobject]@{"Results" = "Executed Remediation for $SuspectUser and tenant $($TenantFilter). The temporary password is $password and must be changed at next logon." } Write-LogMessage -API "BECRemediate" -tenant $tenantfilter -message "Executed Remediation for $SuspectUser" -sev "Info" diff --git a/ExecExtensionMapping/run.ps1 b/ExecExtensionMapping/run.ps1 index b7d24f1a1b32..b120dd08a090 100644 --- a/ExecExtensionMapping/run.ps1 +++ b/ExecExtensionMapping/run.ps1 @@ -12,58 +12,19 @@ Write-Host 'PowerShell HTTP trigger function processed a request.' $Table = Get-CIPPTable -TableName CippMapping if ($Request.Query.List) { - #Get available mappings - $Mappings = [pscustomobject]@{} - Get-AzDataTableEntity @Table | ForEach-Object { - $Mappings | Add-Member -NotePropertyName $_.RowKey -NotePropertyValue @{ label = "$($_.HaloPSAName)"; value = "$($_.HaloPSA)" } - } - #Get Available TEnants - $Tenants = Get-Tenants - #Get available halo clients - $Table = Get-CIPPTable -TableName Extensionsconfig - try { - $Configuration = ((Get-AzDataTableEntity @Table).config | ConvertFrom-Json -ErrorAction Stop).HaloPSA - $Token = Get-HaloToken -configuration $Configuration - $i = 1 - $RawHaloClients = do { - $Result = Invoke-RestMethod -Uri "$($Configuration.ResourceURL)/Client?page_no=$i&page_size=999&pageinate=true" -ContentType 'application/json' -Method GET -Headers @{Authorization = "Bearer $($token.access_token)" } - $Result.clients | Select-Object * -ExcludeProperty logo - $i++ - $pagecount = [Math]::Ceiling($Result.record_count / 999) - } while ($i -le $pagecount) - } catch { $RawHaloClients = @() } - $HaloClients = $RawHaloClients | ForEach-Object { - [PSCustomObject]@{ - label = $_.name - value = $_.id - } - } - $HaloClients = $RawHaloClients | ForEach-Object { - [PSCustomObject]@{ - name = $_.name - value = "$($_.id)" + switch ($Request.Query.List) { + 'Halo' { + $body = Get-HaloMapping -CIPPMapping $Table } } - $MappingObj = [PSCustomObject]@{ - Tenants = @($Tenants) - HaloClients = @($HaloClients) - Mappings = $Mappings - } - $body = $MappingObj } try { if ($Request.Query.AddMapping) { - foreach ($Mapping in ([pscustomobject]$Request.body.mappings).psobject.properties) { - $AddObject = @{ - PartitionKey = 'Mapping' - RowKey = "$($mapping.name)" - 'HaloPSA' = "$($mapping.value.value)" - 'HaloPSAName' = "$($mapping.value.label)" + switch ($Request.Query.AddMapping) { + 'Halo' { + $body = Set-HaloMapping -CIPPMapping $Table -APIName $APIName -Request $Request } - Add-AzDataTableEntity @Table -Entity $AddObject -Force - Write-LogMessage -API $APINAME -user $request.headers.'x-ms-client-principal' -message "Added mapping for $($mapping.name)." -Sev 'Info' } - $body = [pscustomobject]@{'Results' = 'Successfully edited mapping table.' } } } catch { Write-LogMessage -API $APINAME -user $request.headers.'x-ms-client-principal' -message "mapping API failed. $($_.Exception.Message)" -Sev 'Error' 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..d08f4fe6ccaa --- /dev/null +++ b/ExecScheduledCommand/run.ps1 @@ -0,0 +1,74 @@ +# 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.GetType() -eq [String]) { + $results = @{ Results = $results } + } + $results = $results | Select-Object *, @{l = 'TaskInfo'; e = { $QueueItem.TaskInfo } } -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 '', "$TableDesign
" | Out-String +$title = "Scheduled Task $($task.Name) - $($task.ExpectedRunTime)" +Write-Host $title +switch -wildcard ($task.PostExecution) { + "*psa*" { Send-CIPPAlert -Type 'psa' -Title $title -HTMLContent $HTML } + "*email*" { Send-CIPPAlert -Type 'email' -Title $title -HTMLContent $HTML } + "*webhook*" { Send-CIPPAlert -Type 'webhook' -Title $title -JSONContent $($Results | ConvertTo-Json) } +} + +Write-Host "ran the command" + +if ($task.Recurrence -le '0' -or $task.Recurrence -eq $null) { + Update-AzDataTableEntity @Table -Entity @{ + PartitionKey = $task.PartitionKey + RowKey = $task.RowKey + Results = "$StoredResults" + TaskState = 'Completed' + } +} +else { + $nextRun = (Get-Date).AddDays($task.Recurrence) + $nextRunUnixTime = [int64]($nextRun - (Get-Date "1/1/1970")).TotalSeconds + Update-AzDataTableEntity @Table -Entity @{ + PartitionKey = $task.PartitionKey + RowKey = $task.RowKey + Results = "$StoredResults" + TaskState = 'Planned' + ScheduledTime = "$nextRunUnixTime" + } +} +Write-LogMessage -API "Scheduler_UserTasks" -tenant $tenant -message "Successfully executed task: $($task.name)" -sev Info \ No newline at end of file diff --git a/GraphHelper.psm1 b/GraphHelper.psm1 index f1fb90791a84..0be92f2e26d4 100644 --- a/GraphHelper.psm1 +++ b/GraphHelper.psm1 @@ -73,7 +73,8 @@ function Get-GraphToken($tenantid, $scope, $AsApp, $AppID, $refreshToken, $Retur if ($script:AccessTokens.$TokenKey -and [int](Get-Date -UFormat %s -Millisecond 0) -lt $script:AccessTokens.$TokenKey.expires_on) { Write-Host 'Graph: cached token' $AccessToken = $script:AccessTokens.$TokenKey - } else { + } + else { Write-Host 'Graph: new token' $AccessToken = (Invoke-RestMethod -Method post -Uri "https://login.microsoftonline.com/$($tenantid)/oauth2/v2.0/token" -Body $Authbody -ErrorAction Stop) $ExpiresOn = [int](Get-Date -UFormat %s -Millisecond 0) + $AccessToken.expires_in @@ -85,7 +86,8 @@ function Get-GraphToken($tenantid, $scope, $AsApp, $AppID, $refreshToken, $Retur if ($ReturnRefresh) { $header = $AccessToken } else { $header = @{ Authorization = "Bearer $($AccessToken.access_token)" } } return $header #Write-Host $header['Authorization'] - } catch { + } + catch { # Track consecutive Graph API failures $TenantsTable = Get-CippTable -tablename Tenants $Filter = "PartitionKey eq 'Tenants' and (defaultDomainName eq '{0}' or customerId eq '{0}')" -f $tenantid @@ -103,7 +105,8 @@ function Get-GraphToken($tenantid, $scope, $AsApp, $AppID, $refreshToken, $Retur $Tenant.LastGraphError = if ( $_.ErrorDetails.Message) { $msg = $_.ErrorDetails.Message | ConvertFrom-Json "$($msg.error):$($msg.error_description)" - } else { + } + else { $_.Exception.message } $Tenant.GraphErrorCount++ @@ -116,7 +119,8 @@ function Get-GraphToken($tenantid, $scope, $AsApp, $AppID, $refreshToken, $Retur function Write-LogMessage ($message, $tenant = 'None', $API = 'None', $user, $sev) { try { $username = ([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($user)) | ConvertFrom-Json).userDetails - } catch { + } + catch { $username = $user } @@ -159,7 +163,8 @@ function New-GraphGetRequest { if ($scope -eq 'ExchangeOnline') { $AccessToken = Get-ClassicAPIToken -resource 'https://outlook.office365.com' -Tenantid $tenantid $headers = @{ Authorization = "Bearer $($AccessToken.access_token)" } - } else { + } + else { $headers = Get-GraphToken -tenantid $tenantid -scope $scope -AsApp $asapp } @@ -187,14 +192,16 @@ function New-GraphGetRequest { if ($CountOnly) { $Data.'@odata.count' $nextURL = $null - } else { + } + else { if ($data.value) { $data.value } else { ($Data) } if ($noPagination) { $nextURL = $null } else { $nextURL = $data.'@odata.nextLink' } } - } catch { + } + catch { $Message = ($_.ErrorDetails.Message | ConvertFrom-Json -ErrorAction SilentlyContinue).error.message if ($Message -eq $null) { $Message = $($_.Exception.Message) } - if ($Message -ne 'Request not applicable to target tenant.') { + if ($Message -ne 'Request not applicable to target tenant.' -and $Tenant) { $Tenant.LastGraphError = $Message $Tenant.GraphErrorCount++ Update-AzDataTableEntity @TenantsTable -Entity $Tenant @@ -205,7 +212,8 @@ function New-GraphGetRequest { $Tenant.LastGraphError = '' Update-AzDataTableEntity @TenantsTable -Entity $Tenant return $ReturnedData - } else { + } + else { Write-Error 'Not allowed. You cannot manage your own tenant or tenants not under your scope' } } @@ -220,19 +228,16 @@ function New-GraphPOSTRequest ($uri, $tenantid, $body, $type, $scope, $AsApp, $N try { $ReturnedData = (Invoke-RestMethod -Uri $($uri) -Method $TYPE -Body $body -Headers $headers -ContentType 'application/json; charset=utf-8') - } catch { - #setting ErrorMess because the error from a failed json conversion overwrites the exception. + } + catch { $ErrorMess = $($_.Exception.Message) - try { - $Message = ($_.ErrorDetails.Message | ConvertFrom-Json -ErrorAction Stop).error.message - } catch { - $Message = $ErrorMess - } - + $Message = ($_.ErrorDetails.Message | ConvertFrom-Json -ErrorAction SilentlyContinue).error.message + if (!$Message) { $Message = $ErrorMess } throw $Message } return $ReturnedData - } else { + } + else { Write-Error 'Not allowed. You cannot manage your own tenant or tenants not under your scope' } } @@ -250,7 +255,8 @@ function Get-ClassicAPIToken($tenantID, $Resource) { if ($script:classictoken.$TokenKey -and [int](Get-Date -UFormat %s -Millisecond 0) -lt $script:classictoken.$TokenKey.expires_on) { Write-Host 'Classic: cached token' return $script:classictoken.$TokenKey - } else { + } + else { Write-Host 'Using classic' $uri = "https://login.microsoftonline.com/$($TenantID)/oauth2/token" $Body = @{ @@ -264,7 +270,8 @@ function Get-ClassicAPIToken($tenantID, $Resource) { if (!$script:classictoken) { $script:classictoken = [HashTable]::Synchronized(@{}) } $script:classictoken.$TokenKey = Invoke-RestMethod $uri -Body $body -ContentType 'application/x-www-form-urlencoded' -ErrorAction SilentlyContinue -Method post return $script:classictoken.$TokenKey - } catch { + } + catch { # Track consecutive Graph API failures $TenantsTable = Get-CippTable -tablename Tenants $Filter = "PartitionKey eq 'Tenants' and (defaultDomainName eq '{0}' or customerId eq '{0}')" -f $tenantid @@ -273,6 +280,7 @@ function Get-ClassicAPIToken($tenantID, $Resource) { $Tenant = @{ GraphErrorCount = $null LastGraphTokenError = $null + LastGraphError = $null PartitionKey = 'TenantFailed' RowKey = 'Failed' } @@ -305,12 +313,14 @@ function New-TeamsAPIGetRequest($Uri, $tenantID, $Method = 'GET', $Resource = '4 } $Data if ($noPagination) { $nextURL = $null } else { $nextURL = $data.NextLink } - } catch { + } + catch { throw "Failed to make Teams API Get Request $_" } } until ($null -eq $NextURL) return $ReturnedData - } else { + } + else { Write-Error 'Not allowed. You cannot manage your own tenant or tenants not under your scope' } } @@ -332,12 +342,14 @@ function New-ClassicAPIGetRequest($TenantID, $Uri, $Method = 'GET', $Resource = } $Data if ($noPagination) { $nextURL = $null } else { $nextURL = $data.NextLink } - } catch { + } + catch { throw "Failed to make Classic Get Request $_" } } until ($null -eq $NextURL) return $ReturnedData - } else { + } + else { Write-Error 'Not allowed. You cannot manage your own tenant or tenants not under your scope' } } @@ -357,11 +369,13 @@ function New-ClassicAPIPostRequest($TenantID, $Uri, $Method = 'POST', $Resource } - } catch { + } + catch { throw "Failed to make Classic Get Request $_" } return $ReturnedData - } else { + } + else { Write-Error 'Not allowed. You cannot manage your own tenant or tenants not under your scope' } } @@ -382,7 +396,8 @@ function Get-AuthorisedRequest { $SkipList = Get-Tenants -SkipList if (($env:PartnerTenantAvailable -eq $true -and $SkipList.customerId -notcontains $TenantID -and $SkipList.defaultDomainName -notcontains $TenantID) -or (($Tenants.customerId -contains $TenantID -or $Tenants.defaultDomainName -contains $TenantID) -and $TenantID -ne $env:TenantId)) { return $true - } else { + } + else { return $false } } @@ -407,9 +422,11 @@ function Get-Tenants { if ($IncludeAll.IsPresent) { $Filter = "PartitionKey eq 'Tenants'" - } elseif ($IncludeErrors.IsPresent) { + } + elseif ($IncludeErrors.IsPresent) { $Filter = "PartitionKey eq 'Tenants' and Excluded eq false" - } else { + } + else { $Filter = "PartitionKey eq 'Tenants' and Excluded eq false and GraphErrorCount lt 50" } $IncludedTenantsCache = Get-AzDataTableEntity @TenantsTable -Filter $Filter @@ -417,8 +434,10 @@ function Get-Tenants { if (($IncludedTenantsCache | Measure-Object).Count -gt 0) { try { $LastRefresh = ($IncludedTenantsCache | Where-Object { $_.customerId } | Sort-Object LastRefresh -Descending | Select-Object -First 1).LastRefresh | Get-Date -ErrorAction Stop - } catch { $LastRefresh = $false } - } else { + } + catch { $LastRefresh = $false } + } + else { $LastRefresh = $false } if (!$LastRefresh -or $LastRefresh -lt (Get-Date).Addhours(-24).ToUniversalTime()) { @@ -426,7 +445,8 @@ function Get-Tenants { Write-Host "Renewing. Cache not hit. $LastRefresh" $TenantList = (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/tenantRelationships/managedTenants/tenants?`$top=999" -tenantid $env:TenantID ) | Select-Object id, @{l = 'customerId'; e = { $_.tenantId } }, @{l = 'DefaultdomainName'; e = { [string]($_.contract.defaultDomainName) } } , @{l = 'MigratedToNewTenantAPI'; e = { $true } }, DisplayName, domains, @{n = 'delegatedPrivilegeStatus'; exp = { $_.tenantStatusInformation.delegatedPrivilegeStatus } } | Where-Object -Property defaultDomainName -NotIn $SkipListCache.defaultDomainName - } catch { + } + catch { Write-Host "Get-Tenants - Lighthouse Error, using contract/delegatedAdminRelationship calls. Error: $($_.Exception.Message)" [System.Collections.Generic.List[PSCustomObject]]$BulkRequests = @( @{ @@ -529,13 +549,14 @@ function Remove-CIPPCache { } } -function New-ExoRequest ($tenantid, $cmdlet, $cmdParams, $useSystemMailbox, $Anchor) { - if ((Get-AuthorisedRequest -TenantID $tenantid)) { +function New-ExoRequest ($tenantid, $cmdlet, $cmdParams, $useSystemMailbox, $Anchor, $NoAuthCheck) { + if ((Get-AuthorisedRequest -TenantID $tenantid) -or $NoAuthCheck -eq $True) { $token = Get-ClassicAPIToken -resource 'https://outlook.office365.com' -Tenantid $tenantid $tenant = (get-tenants | Where-Object -Property defaultDomainName -EQ $tenantid).customerId if ($cmdParams) { $Params = $cmdParams - } else { + } + else { $Params = @{} } $ExoBody = ConvertTo-Json -Depth 5 -InputObject @{ @@ -550,7 +571,7 @@ function New-ExoRequest ($tenantid, $cmdlet, $cmdParams, $useSystemMailbox, $Anc if ($cmdparams.User) { $Anchor = $cmdparams.User } if (!$Anchor -or $useSystemMailbox) { - $OnMicrosoft = (New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/domains?$top=999' -tenantid $tenantid | Where-Object -Property isInitial -EQ $true).id + $OnMicrosoft = (New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/domains?$top=999' -tenantid $tenantid -NoAuthCheck $NoAuthCheck | Where-Object -Property isInitial -EQ $true).id $anchor = "UPN:SystemMailbox{bb558c35-97f1-4cb9-8ff7-d53741dc928c}@$($OnMicrosoft)" } @@ -565,18 +586,21 @@ function New-ExoRequest ($tenantid, $cmdlet, $cmdParams, $useSystemMailbox, $Anc } try { $ReturnedData = Invoke-RestMethod "https://outlook.office365.com/adminapi/beta/$($tenant)/InvokeCommand" -Method POST -Body $ExoBody -Headers $Headers -ContentType 'application/json; charset=utf-8' - } catch { + } + catch { $ErrorMess = $($_.Exception.Message) $ReportedError = ($_.ErrorDetails | ConvertFrom-Json -ErrorAction SilentlyContinue) $Message = if ($ReportedError.error.details.message) { $ReportedError.error.details.message - } elseif ($ReportedError.error.message) { $ReportedError.error.message } + } + elseif ($ReportedError.error.message) { $ReportedError.error.message } else { $ReportedError.error.innererror.internalException.message } if ($null -eq $Message) { $Message = $ErrorMess } throw $Message } return $ReturnedData.value - } else { + } + else { Write-Error 'Not allowed. You cannot manage your own tenant or tenants not under your scope' } } @@ -659,7 +683,8 @@ function Get-CIPPMSolUsers { if ($null -eq $page) { $Page = (Invoke-RestMethod -Uri 'https://provisioningapi.microsoftonline.com/provisioningwebservice.svc' -Method post -Body $MSOLXML -ContentType 'application/soap+xml; charset=utf-8').envelope.body.ListUsersResponse.listusersresult.returnvalue $Page.results.user - } else { + } + else { $Page = (Invoke-RestMethod -Uri 'https://provisioningapi.microsoftonline.com/provisioningwebservice.svc' -Method post -Body $MSOLXML -ContentType 'application/soap+xml; charset=utf-8').envelope.body.NavigateUserResultsResponse.NavigateUserResultsResult.returnvalue $Page.results.user } @@ -684,14 +709,17 @@ function New-DeviceLogin { if ($TenantID) { $ReturnCode = Invoke-RestMethod -Uri "https://login.microsoftonline.com/$($TenantID)/oauth2/v2.0/devicecode" -Method POST -Body "client_id=$($Clientid)&scope=$encodedscope+offline_access+profile+openid" - } else { + } + else { $ReturnCode = Invoke-RestMethod -Uri 'https://login.microsoftonline.com/organizations/oauth2/v2.0/devicecode' -Method POST -Body "client_id=$($Clientid)&scope=$encodedscope+offline_access+profile+openid" } - } else { + } + else { $Checking = Invoke-RestMethod -SkipHttpErrorCheck -Uri 'https://login.microsoftonline.com/organizations/oauth2/v2.0/token' -Method POST -Body "client_id=$($Clientid)&scope=$encodedscope+offline_access+profile+openid&grant_type=device_code&device_code=$($device_code)" if ($checking.refresh_token) { $ReturnCode = $Checking - } else { + } + else { $returncode = $Checking.error } } @@ -709,7 +737,8 @@ function New-passwordString { if ($PasswordType -eq 'Correct-Battery-Horse') { $Words = Get-Content .\words.txt (Get-Random -InputObject $words -Count 4) -join '-' - } else { + } + else { -join ('abcdefghkmnrstuvwxyzABCDEFGHKLMNPRSTUVWXYZ23456789$%&*#'.ToCharArray() | Get-Random -Count $count) } } @@ -756,7 +785,8 @@ function New-GraphBulkRequest { $MoreData.body.value = $NewValues } - } catch { + } + catch { $Message = ($_.ErrorDetails.Message | ConvertFrom-Json -ErrorAction SilentlyContinue).error.message if ($Message -eq $null) { $Message = $($_.Exception.Message) } if ($Message -ne 'Request not applicable to target tenant.') { @@ -771,7 +801,8 @@ function New-GraphBulkRequest { Update-AzDataTableEntity @TenantsTable -Entity $Tenant return $ReturnedData.responses - } else { + } + else { Write-Error 'Not allowed. You cannot manage your own tenant or tenants not under your scope' } } @@ -779,7 +810,8 @@ function New-GraphBulkRequest { function Get-GraphBulkResultByID ($Results, $ID, [switch]$Value) { if ($Value) { ($Results | Where-Object { $_.id -eq $ID }).body.value - } else { + } + else { ($Results | Where-Object { $_.id -eq $ID }).body } } diff --git a/ListFunctionParameters/function.json b/ListFunctionParameters/function.json new file mode 100644 index 000000000000..bf6c3ef0c49a --- /dev/null +++ b/ListFunctionParameters/function.json @@ -0,0 +1,18 @@ +{ + "scriptFile": "../Modules/CippEntryPoints/CippEntryPoints.psm1", + "entryPoint": "Receive-CippHttpTrigger", + "bindings": [ + { + "authLevel": "anonymous", + "type": "httpTrigger", + "direction": "in", + "name": "Request", + "methods": ["get", "post"] + }, + { + "type": "http", + "direction": "out", + "name": "Response" + } + ] +} diff --git a/Modules/CIPPCore/CIPPCore.psm1 b/Modules/CIPPCore/CIPPCore.psm1 index 34e402aa7101..f69a353414d9 100644 --- a/Modules/CIPPCore/CIPPCore.psm1 +++ b/Modules/CIPPCore/CIPPCore.psm1 @@ -1,5 +1,5 @@ -$Public = @(Get-ChildItem -Path $PSScriptRoot\Public\*.ps1 -ErrorAction SilentlyContinue) -$Private = @(Get-ChildItem -Path $PSScriptRoot\private\*.ps1 -ErrorAction SilentlyContinue) +$Public = @(Get-ChildItem -Path $PSScriptRoot\Public\*.ps1 -Recurse -ErrorAction SilentlyContinue) +$Private = @(Get-ChildItem -Path $PSScriptRoot\private\*.ps1 -Recurse -ErrorAction SilentlyContinue) $Functions = $Public + $Private foreach ($import in @($Functions)) { try { diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListFunctionParameters.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListFunctionParameters.ps1 new file mode 100644 index 000000000000..b4b3382bd237 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListFunctionParameters.ps1 @@ -0,0 +1,59 @@ +using namespace System.Net + +function Invoke-ListFunctionParameters { + # Input bindings are passed in via param block. + param($Request, $TriggerMetadata) + + $APIName = $TriggerMetadata.FunctionName + Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message 'Accessed this API' -Sev 'Debug' + + # Write to the Azure Functions log stream. + Write-Information 'PowerShell HTTP trigger function processed a request.' + + # Interact with query parameters or the body of the request. + $Module = $Request.Query.Module + $Function = $Request.Query.Function + + $CommandQuery = @{} + if ($Module) { + $CommandQuery.Module = $Module + } + if ($Function) { + $CommandQuery.Name = $Function + } + + $CommonParameters = @('Verbose', 'Debug', 'ErrorAction', 'WarningAction', 'InformationAction', 'ErrorVariable', 'WarningVariable', 'InformationVariable', 'OutVariable', 'OutBuffer', 'PipelineVariable', 'TenantFilter', 'APIName', 'ExecutingUser') + #temporary until I clean up the coremodule and move things private. + $TemporaryBlacklist = 'Get-CIPPAuthentication', 'Invoke-CippWebhookProcessing', 'Invoke-ListFunctionParameters', 'New-CIPPAPIConfig', 'New-CIPPGraphSubscription.ps1' + try { + $Functions = Get-Command @CommandQuery + $Results = foreach ($Function in $Functions) { + if ($Function -In $TemporaryBlacklist) { continue } + $Parameters = foreach ($Key in $Function.Parameters.Keys) { + if ($CommonParameters -notcontains $Key) { + $Param = $Function.Parameters.$Key + [PSCustomObject]@{ + Name = $Key + Type = $Param.ParameterType.FullName + } + } + } + [PSCustomObject]@{ + Function = $Function.Name + Parameters = @($Parameters) + } + } + $StatusCode = [HttpStatusCode]::OK + $Results + } + catch { + $Results = "Function Error: $($_.Exception.Message)" + $StatusCode = [HttpStatusCode]::BadRequest + } + # Associate values to output bindings by calling 'Push-OutputBinding'. + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @($Results) + }) + +} \ No newline at end of file diff --git a/Modules/CIPPCore/Public/Entrypoints/Push-AddAlertSubscription_Queue.ps1 b/Modules/CIPPCore/Public/Entrypoints/Push-AddAlertSubscription_Queue.ps1 new file mode 100644 index 000000000000..4651cc31b1cb --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/Push-AddAlertSubscription_Queue.ps1 @@ -0,0 +1,13 @@ +function Push-AddAlertSubscription_Queue { + # Input bindings are passed in via param block. + param($QueueItem, $TriggerMetadata) + + try { + Write-Information ($QueueItem | ConvertTo-Json) + New-CIPPGraphSubscription @QueueItem + Write-Information "Added webhook subscription for $($QueueItem.TenantFilter)" + } catch { + Write-Error "Unable to add webhook subscription $($_.Exception.Message)" + } + +} \ No newline at end of file diff --git a/Modules/CIPPCore/Public/Invoke-CIPPWebhookProcessing.ps1 b/Modules/CIPPCore/Public/Invoke-CIPPWebhookProcessing.ps1 index 3489143ae40d..cb94ab15221e 100644 --- a/Modules/CIPPCore/Public/Invoke-CIPPWebhookProcessing.ps1 +++ b/Modules/CIPPCore/Public/Invoke-CIPPWebhookProcessing.ps1 @@ -7,29 +7,28 @@ function Invoke-CippWebhookProcessing { $Operations, $AllowedLocations, $CIPPPURL, - $APIName = "Process webhook", + $APIName = 'Process webhook', $ExecutingUser ) $LocationTable = Get-CIPPTable -TableName 'knownlocationdb' $AllowedLocations = $AllowedLocations -split ',' - if ($data.userId -eq "Not Available") { $data.userId = $data.userKey } - if ($data.Userkey -eq "Not Available") { $data.Userkey = $data.userId } + if ($data.userId -eq 'Not Available') { $data.userId = $data.userKey } + if ($data.Userkey -eq 'Not Available') { $data.Userkey = $data.userId } if ($data.clientip) { #First we perform a lookup in the knownlocationdb table to see if we have a location for this IP address. $Location = Get-AzDataTableEntity @LocationTable -Filter "RowKey eq '$($data.clientip)'" | Select-Object -Last 1 #If we have a location, we use that. If not, we perform a lookup in the GeoIP database. if ($Location) { - Write-Host "Using known location" + Write-Host 'Using known location' $Country = $Location.CountryOrRegion - $City = $Location.City - } - else { - Write-Host "We have to do a lookup" + $City = $Location.City + } else { + Write-Host 'We have to do a lookup' $Location = Get-CIPPGeoIPLocation -IP $data.clientip - $Country = if ($Location.countryCode) { $Location.CountryCode } else { "Unknown" } - $City = if ($Location.cityName) { $Location.cityName } else { "Unknown" } + $Country = if ($Location.countryCode) { $Location.CountryCode } else { 'Unknown' } + $City = if ($Location.cityName) { $Location.cityName } else { 'Unknown' } } } $TableObj = [PSCustomObject]::new() @@ -38,25 +37,25 @@ function Invoke-CippWebhookProcessing { if ($Data.parameters) { $Data.parameters | ForEach-Object { $TableObj | Add-Member -NotePropertyName $_.Name -NotePropertyValue $_.Value } } $ExtendedPropertiesIgnoreList = @( - "OAuth2:Authorize" - "SAS:EndAuth" - "SAS:ProcessAuth" + 'OAuth2:Authorize' + 'SAS:EndAuth' + 'SAS:ProcessAuth' ) if ($TableObj.RequestType -in $ExtendedPropertiesIgnoreList) { - Write-Host "No need to process this operation." - return "" + Write-Host 'No need to process this operation.' + return '' } switch ($data.operation) { - { "UserLoggedIn" -eq $data.operation -and $Country -notin $AllowedLocations -and $data.ResultStatus -eq "Success" -and $TableObj.ResultStatusDetail -eq "Success" } { $data.operation = "UserLoggedInFromUnknownLocation" } - { "UserloggedIn" -eq $data.operation -and $data.UserType -eq 2 -and $data.ResultStatus -eq "Success" -and $TableObj.ResultStatusDetail -eq "Success" } { $data.operation = "AdminLoggedIn" } + { 'UserLoggedIn' -eq $data.operation -and $Country -notin $AllowedLocations -and $data.ResultStatus -eq 'Success' -and $TableObj.ResultStatusDetail -eq 'Success' } { $data.operation = 'UserLoggedInFromUnknownLocation' } + { 'UserloggedIn' -eq $data.operation -and $data.UserType -eq 2 -and $data.ResultStatus -eq 'Success' -and $TableObj.ResultStatusDetail -eq 'Success' } { $data.operation = 'AdminLoggedIn' } default { break } } #Check if the operation is allowed for this webhook. - if ($data.operation -notin $Operations) { + if ($data.operation -notin $Operations) { Write-Host "No need to process this operation, but we're saving the IP for future" - Write-Host "Add IP and potential location to knownlocation db for this specific user" - + Write-Host 'Add IP and potential location to knownlocation db for this specific user' + if ($data.ClientIP) { $IP = $data.ClientIP if ($IP -match '^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d+$') { @@ -71,105 +70,120 @@ function Invoke-CippWebhookProcessing { } $null = Add-AzDataTableEntity @LocationTable -Entity $LocationInfo -Force } - return "" + return '' } Set-Location (Get-Item $PSScriptRoot).FullName $Appname = '[{"Application Name":"ACOM Azure Website","Application IDs":"23523755-3a2b-41ca-9315-f81f3f566a95"},{"Application Name":"AEM-DualAuth","Application IDs":"69893ee3-dd10-4b1c-832d-4870354be3d8"},{"Application Name":"ASM Campaign Servicing","Application IDs":"0cb7b9ec-5336-483b-bc31-b15b5788de71"},{"Application Name":"Azure Advanced Threat Protection","Application IDs":"7b7531ad-5926-4f2d-8a1d-38495ad33e17"},{"Application Name":"Azure Data Lake","Application IDs":"e9f49c6b-5ce5-44c8-925d-015017e9f7ad"},{"Application Name":"Azure Lab Services Portal","Application IDs":"835b2a73-6e10-4aa5-a979-21dfda45231c"},{"Application Name":"Azure Portal","Application IDs":"c44b4083-3bb0-49c1-b47d-974e53cbdf3c"},{"Application Name":"AzureSupportCenter","Application IDs":"37182072-3c9c-4f6a-a4b3-b3f91cacffce"},{"Application Name":"Bing","Application IDs":"9ea1ad79-fdb6-4f9a-8bc3-2b70f96e34c7"},{"Application Name":"CPIM Service","Application IDs":"bb2a2e3a-c5e7-4f0a-88e0-8e01fd3fc1f4"},{"Application Name":"CRM Power BI Integration","Application IDs":"e64aa8bc-8eb4-40e2-898b-cf261a25954f"},{"Application Name":"Dataverse","Application IDs":"00000007-0000-0000-c000-000000000000"},{"Application Name":"Enterprise Roaming and Backup","Application IDs":"60c8bde5-3167-4f92-8fdb-059f6176dc0f"},{"Application Name":"IAM Supportability","Application IDs":"a57aca87-cbc0-4f3c-8b9e-dc095fdc8978"},{"Application Name":"IrisSelectionFrontDoor","Application IDs":"16aeb910-ce68-41d1-9ac3-9e1673ac9575"},{"Application Name":"MCAPI Authorization Prod","Application IDs":"d73f4b35-55c9-48c7-8b10-651f6f2acb2e"},{"Application Name":"Media Analysis and Transformation Service","Application IDs":"944f0bd1-117b-4b1c-af26-804ed95e767e
0cd196ee-71bf-4fd6-a57c-b491ffd4fb1e"},{"Application Name":"Microsoft 365 Support Service","Application IDs":"ee272b19-4411-433f-8f28-5c13cb6fd407"},{"Application Name":"Microsoft App Access Panel","Application IDs":"0000000c-0000-0000-c000-000000000000"},{"Application Name":"Microsoft Approval Management","Application IDs":"65d91a3d-ab74-42e6-8a2f-0add61688c74
38049638-cc2c-4cde-abe4-4479d721ed44"},{"Application Name":"Microsoft Authentication Broker","Application IDs":"29d9ed98-a469-4536-ade2-f981bc1d605e"},{"Application Name":"Microsoft Azure CLI","Application IDs":"04b07795-8ddb-461a-bbee-02f9e1bf7b46"},{"Application Name":"Microsoft Azure PowerShell","Application IDs":"1950a258-227b-4e31-a9cf-717495945fc2"},{"Application Name":"Microsoft Bing Search","Application IDs":"cf36b471-5b44-428c-9ce7-313bf84528de"},{"Application Name":"Microsoft Bing Search for Microsoft Edge","Application IDs":"2d7f3606-b07d-41d1-b9d2-0d0c9296a6e8"},{"Application Name":"Microsoft Bing Default Search Engine","Application IDs":"1786c5ed-9644-47b2-8aa0-7201292175b6"},{"Application Name":"Microsoft Defender for Cloud Apps","Application IDs":"3090ab82-f1c1-4cdf-af2c-5d7a6f3e2cc7"},{"Application Name":"Microsoft Docs","Application IDs":"18fbca16-2224-45f6-85b0-f7bf2b39b3f3"},{"Application Name":"Microsoft Dynamics ERP","Application IDs":"00000015-0000-0000-c000-000000000000"},{"Application Name":"Microsoft Edge Insider Addons Prod","Application IDs":"6253bca8-faf2-4587-8f2f-b056d80998a7"},{"Application Name":"Microsoft Exchange Online Protection","Application IDs":"00000007-0000-0ff1-ce00-000000000000"},{"Application Name":"Microsoft Forms","Application IDs":"c9a559d2-7aab-4f13-a6ed-e7e9c52aec87"},{"Application Name":"Microsoft Graph","Application IDs":"00000003-0000-0000-c000-000000000000"},{"Application Name":"Microsoft Intune Web Company Portal","Application IDs":"74bcdadc-2fdc-4bb3-8459-76d06952a0e9"},{"Application Name":"Microsoft Intune Windows Agent","Application IDs":"fc0f3af4-6835-4174-b806-f7db311fd2f3"},{"Application Name":"Microsoft Learn","Application IDs":"18fbca16-2224-45f6-85b0-f7bf2b39b3f3"},{"Application Name":"Microsoft Office","Application IDs":"d3590ed6-52b3-4102-aeff-aad2292ab01c"},{"Application Name":"Microsoft Office 365 Portal","Application IDs":"00000006-0000-0ff1-ce00-000000000000"},{"Application Name":"Microsoft Office Web Apps Service","Application IDs":"67e3df25-268a-4324-a550-0de1c7f97287"},{"Application Name":"Microsoft Online Syndication Partner Portal","Application IDs":"d176f6e7-38e5-40c9-8a78-3998aab820e7"},{"Application Name":"Microsoft password reset service","Application IDs":"93625bc8-bfe2-437a-97e0-3d0060024faa"},{"Application Name":"Microsoft Power BI","Application IDs":"871c010f-5e61-4fb1-83ac-98610a7e9110"},{"Application Name":"Microsoft Storefronts","Application IDs":"28b567f6-162c-4f54-99a0-6887f387bbcc"},{"Application Name":"Microsoft Stream Portal","Application IDs":"cf53fce8-def6-4aeb-8d30-b158e7b1cf83"},{"Application Name":"Microsoft Substrate Management","Application IDs":"98db8bd6-0cc0-4e67-9de5-f187f1cd1b41"},{"Application Name":"Microsoft Support","Application IDs":"fdf9885b-dd37-42bf-82e5-c3129ef5a302"},{"Application Name":"Microsoft Teams","Application IDs":"1fec8e78-bce4-4aaf-ab1b-5451cc387264"},{"Application Name":"Microsoft Teams Services","Application IDs":"cc15fd57-2c6c-4117-a88c-83b1d56b4bbe"},{"Application Name":"Microsoft Teams Web Client","Application IDs":"5e3ce6c0-2b1f-4285-8d4b-75ee78787346"},{"Application Name":"Microsoft Whiteboard Services","Application IDs":"95de633a-083e-42f5-b444-a4295d8e9314"},{"Application Name":"O365 Suite UX","Application IDs":"4345a7b9-9a63-4910-a426-35363201d503"},{"Application Name":"Office 365 Exchange Online","Application IDs":"00000002-0000-0ff1-ce00-000000000000"},{"Application Name":"Office 365 Management","Application IDs":"00b41c95-dab0-4487-9791-b9d2c32c80f2"},{"Application Name":"Office 365 Search Service","Application IDs":"66a88757-258c-4c72-893c-3e8bed4d6899"},{"Application Name":"Office 365 SharePoint Online","Application IDs":"00000003-0000-0ff1-ce00-000000000000"},{"Application Name":"Office Delve","Application IDs":"94c63fef-13a3-47bc-8074-75af8c65887a"},{"Application Name":"Office Online Add-in SSO","Application IDs":"93d53678-613d-4013-afc1-62e9e444a0a5"},{"Application Name":"Office Online Client AAD- Augmentation Loop","Application IDs":"2abdc806-e091-4495-9b10-b04d93c3f040"},{"Application Name":"Office Online Client AAD- Loki","Application IDs":"b23dd4db-9142-4734-867f-3577f640ad0c"},{"Application Name":"Office Online Client AAD- Maker","Application IDs":"17d5e35f-655b-4fb0-8ae6-86356e9a49f5"},{"Application Name":"Office Online Client MSA- Loki","Application IDs":"b6e69c34-5f1f-4c34-8cdf-7fea120b8670"},{"Application Name":"Office Online Core SSO","Application IDs":"243c63a3-247d-41c5-9d83-7788c43f1c43"},{"Application Name":"Office Online Search","Application IDs":"a9b49b65-0a12-430b-9540-c80b3332c127"},{"Application Name":"Office.com","Application IDs":"4b233688-031c-404b-9a80-a4f3f2351f90"},{"Application Name":"Office365 Shell WCSS-Client","Application IDs":"89bee1f7-5e6e-4d8a-9f3d-ecd601259da7"},{"Application Name":"OfficeClientService","Application IDs":"0f698dd4-f011-4d23-a33e-b36416dcb1e6"},{"Application Name":"OfficeHome","Application IDs":"4765445b-32c6-49b0-83e6-1d93765276ca"},{"Application Name":"OfficeShredderWacClient","Application IDs":"4d5c2d63-cf83-4365-853c-925fd1a64357"},{"Application Name":"OMSOctopiPROD","Application IDs":"62256cef-54c0-4cb4-bcac-4c67989bdc40"},{"Application Name":"OneDrive SyncEngine","Application IDs":"ab9b8c07-8f02-4f72-87fa-80105867a763"},{"Application Name":"OneNote","Application IDs":"2d4d3d8e-2be3-4bef-9f87-7875a61c29de"},{"Application Name":"Outlook Mobile","Application IDs":"27922004-5251-4030-b22d-91ecd9a37ea4"},{"Application Name":"Partner Customer Delegated Admin Offline Processor","Application IDs":"a3475900-ccec-4a69-98f5-a65cd5dc5306"},{"Application Name":"Password Breach Authenticator","Application IDs":"bdd48c81-3a58-4ea9-849c-ebea7f6b6360"},{"Application Name":"Power BI Service","Application IDs":"00000009-0000-0000-c000-000000000000"},{"Application Name":"SharedWithMe","Application IDs":"ffcb16e8-f789-467c-8ce9-f826a080d987"},{"Application Name":"SharePoint Online Web Client Extensibility","Application IDs":"08e18876-6177-487e-b8b5-cf950c1e598c"},{"Application Name":"Signup","Application IDs":"b4bddae8-ab25-483e-8670-df09b9f1d0ea"},{"Application Name":"Skype for Business Online","Application IDs":"00000004-0000-0ff1-ce00-000000000000"},{"Application Name":"Sway","Application IDs":"905fcf26-4eb7-48a0-9ff0-8dcc7194b5ba"},{"Application Name":"Universal Store Native Client","Application IDs":"268761a2-03f3-40df-8a8b-c3db24145b6b"},{"Application Name":"Vortex [wsfed enabled]","Application IDs":"5572c4c0-d078-44ce-b81c-6cbf8d3ed39e"},{"Application Name":"Windows Azure Active Directory","Application IDs":"00000002-0000-0000-c000-000000000000"},{"Application Name":"Windows Azure Service Management API","Application IDs":"797f4846-ba00-4fd7-ba43-dac1f8f63013"},{"Application Name":"WindowsDefenderATP Portal","Application IDs":"a3b79187-70b2-4139-83f9-6016c58cd27b"},{"Application Name":"Windows Search","Application IDs":"26a7ee05-5602-4d76-a7ba-eae8b7b67941"},{"Application Name":"Windows Spotlight","Application IDs":"1b3c667f-cde3-4090-b60b-3d2abd0117f0"},{"Application Name":"Windows Store for Business","Application IDs":"45a330b1-b1ec-4cc1-9161-9f03992aa49f"},{"Application Name":"Yammer","Application IDs":"00000005-0000-0ff1-ce00-000000000000"},{"Application Name":"Yammer Web","Application IDs":"c1c74fed-04c9-4704-80dc-9f79a2e515cb"},{"Application Name":"Yammer Web Embed","Application IDs":"e1ef36fd-b883-4dbf-97f0-9ece4b576fc6"}]' | ConvertFrom-Json | Where-Object -Property 'Application IDs' -EQ $data.applicationId - $HTML = Get-Content "TemplateEmail.HTML" -Raw | Out-String + $HTML = Get-Content 'TemplateEmail.HTML' -Raw | Out-String switch ($data.Operation) { - "New-InboxRule" { + 'New-InboxRule' { $Title = "$($TenantFilter) - New Rule Detected for $($data.UserId)" $RuleTable = ($TableObj | ConvertTo-Html -Fragment | Out-String).Replace('
', '
') $ParameterName $IntroText = "

A new rule has been created for the user $($data.UserId). You should check if this rule is not malicious. The rule information can be found in the table below.

$RuleTable" $ButtonUrl = "$CIPPPURL/identity/administration/ViewBec?userId=$($data.UserId)&tenantDomain=$($data.OrganizationId)" - $ButtonText = "Start BEC Investigation" - $AfterButtonText = "

If you believe this is a suspect rule, you can click the button above to start the investigation.

" + $ButtonText = 'Start BEC Investigation' + $AfterButtonText = '

If you believe this is a suspect rule, you can click the button above to start the investigation.

' } - "Set-inboxrule" { + 'Set-inboxrule' { $Title = "$($TenantFilter) - Rule Edit Detected for $($data.UserId)" $RuleTable = ($TableObj | ConvertTo-Html -Fragment | Out-String).Replace('
', '
') $ParameterName $IntroText = "

A rule has been edited for the user $($data.UserId). You should check if this rule is not malicious. The rule information can be found in the table below.

$RuleTable" $ButtonUrl = "$CIPPPURL/identity/administration/ViewBec?userId=$($data.UserId)&tenantDomain=$($data.OrganizationId)" - $ButtonText = "Start BEC Investigation" - $AfterButtonText = "

If you believe this is a suspect rule, you can click the button above to start the investigation.

" + $ButtonText = 'Start BEC Investigation' + $AfterButtonText = '

If you believe this is a suspect rule, you can click the button above to start the investigation.

' } - "Add member to role." { + 'Add member to role.' { $Title = "$($TenantFilter) - Role change detected for $($data.ObjectId)" $Table = ($data.ModifiedProperties | ConvertTo-Html -Fragment | Out-String).Replace('
', '
') - $IntroText = "

$($data.UserId) has added $($data.ObjectId) to the $(($data.ModifiedProperties | Where-Object -Property Name -EQ "Role.DisplayName").NewValue) role. The information about the role can be found in the table below.

$Table" + $IntroText = "

$($data.UserId) has added $($data.ObjectId) to the $(($data.ModifiedProperties | Where-Object -Property Name -EQ 'Role.DisplayName').NewValue) role. The information about the role can be found in the table below.

$Table" $ButtonUrl = "$CIPPPURL/identity/administration/roles?customerId=$($data.OrganizationId)" - $ButtonText = "Role Management" - $AfterButtonText = "

If this role is incorrect, or you need more information, use the button to jump to the Role Management page.

" + $ButtonText = 'Role Management' + $AfterButtonText = '

If this role is incorrect, or you need more information, use the button to jump to the Role Management page.

' } - "Disable account." { + 'Disable account.' { $Title = "$($TenantFilter) - $($data.ObjectId) has been disabled" $IntroText = "$($data.ObjectId) has been disabled by $($data.UserId)." $ButtonUrl = "$CIPPPURL/identity/administration/users?customerId=$($data.OrganizationId)" - $ButtonText = "User Management" - $AfterButtonText = "

If this is incorrect, use the user management screen to unblock the users sign-in

" + $ButtonText = 'User Management' + $AfterButtonText = '

If this is incorrect, use the user management screen to unblock the users sign-in

' } - "Enable account." { + 'Enable account.' { $Title = "$($TenantFilter) - $($data.ObjectId) has been enabled" $IntroText = "$($data.ObjectId) has been enabled by $($data.UserId)." $ButtonUrl = "$CIPPPURL/identity/administration/users?customerId=$($data.OrganizationId)" - $ButtonText = "User Management" - $AfterButtonText = "

If this is incorrect, use the user management screen to unblock the users sign-in

" + $ButtonText = 'User Management' + $AfterButtonText = '

If this is incorrect, use the user management screen to unblock the users sign-in

' } - "Update StsRefreshTokenValidFrom Timestamp." { + 'Update StsRefreshTokenValidFrom Timestamp.' { $Title = "$($TenantFilter) - $($data.ObjectId) has had all sessions revoked" $IntroText = "$($data.ObjectId) has had their sessions revoked by $($data.UserId)." - $ButtonUrl = "$CIPPPURL/identity/administration/users?customerId=$($data.OrganizationId)" - $ButtonText = "User Management" - $AfterButtonText = "

If this is incorrect, use the user management screen to unblock the users sign-in

" + $ButtonUrl = "$CIPPPURL/identity/administration/users?customerId=$($data.OrganizationId)" + $ButtonText = 'User Management' + $AfterButtonText = '

If this is incorrect, use the user management screen to unblock the users sign-in

' } - "Disable Strong Authentication." { + 'Disable Strong Authentication.' { $Title = "$($TenantFilter) - $($data.ObjectId) has been MFA disabled" $IntroText = "$($data.ObjectId) MFA has been disabled by $($data.UserId)." - $ButtonUrl = "$CIPPPURL/identity/administration/users?customerId=$($data.OrganizationId)" - $ButtonText = "User Management" - $AfterButtonText = "

If this is incorrect, use the user management screen to reenable MFA

" + $ButtonUrl = "$CIPPPURL/identity/administration/users?customerId=$($data.OrganizationId)" + $ButtonText = 'User Management' + $AfterButtonText = '

If this is incorrect, use the user management screen to reenable MFA

' } - "Remove Member from a role." { + 'Remove Member from a role.' { $Title = "$($TenantFilter) - Role change detected for $($data.ObjectId)" $Table = ($data.ModifiedProperties | ConvertTo-Html -Fragment | Out-String).Replace('
', '
') - $IntroText = "

$($data.UserId) has removed $($data.ObjectId) to the $(($data.ModifiedProperties | Where-Object -Property Name -EQ "Role.DisplayName").NewValue) role. The information about the role can be found in the table below.

$Table" - $ButtonUrl = "$CIPPPURL/identity/administration/roles?customerId=$($data.OrganizationId)" - $ButtonText = "Role Management" - $AfterButtonText = "

If this role change is incorrect, or you need more information, use the button to jump to the Role Management page.

" + $IntroText = "

$($data.UserId) has removed $($data.ObjectId) to the $(($data.ModifiedProperties | Where-Object -Property Name -EQ 'Role.DisplayName').NewValue) role. The information about the role can be found in the table below.

$Table" + $ButtonUrl = "$CIPPPURL/identity/administration/roles?customerId=$($data.OrganizationId)" + $ButtonText = 'Role Management' + $AfterButtonText = '

If this role change is incorrect, or you need more information, use the button to jump to the Role Management page.

' } - "Reset user password." { + 'Reset user password.' { $Title = "$($TenantFilter) - $($data.ObjectId) has had their password reset" $IntroText = "$($data.ObjectId) has had their password reset by $($data.userId)." - $ButtonUrl = "$CIPPPURL/identity/administration/users?customerId=$($data.OrganizationId)" - $ButtonText = "User Management" - $AfterButtonText = "

If this is incorrect, use the user management screen to unblock the users sign-in

" + $ButtonUrl = "$CIPPPURL/identity/administration/users?customerId=$($data.OrganizationId)" + $ButtonText = 'User Management' + $AfterButtonText = '

If this is incorrect, use the user management screen to unblock the users sign-in

' } - "AdminLoggedIn" { + 'AdminLoggedIn' { $Table = ($TableObj | ConvertTo-Html -Fragment -As List | Out-String).Replace('
', '
') if ($Appname) { $AppName = $AppName.'Application Name' } else { $appName = $data.ApplicationId } $Title = "$($TenantFilter) - an admin account has logged on" $IntroText = "$($data.UserId) ($($data.Userkey)) has logged on from IP $($data.ClientIP) to the application $($Appname). See the table below for more information. $Table" $ButtonUrl = "$CIPPPURL/identity/administration/ViewBec?userId=$($data.UserKey)&tenantDomain=$($data.OrganizationId)" - $ButtonText = "User Management" - $AfterButtonText = "

If this is incorrect, use the user management screen to block the user and revoke the sessions

" + $ButtonText = 'User Management' + $AfterButtonText = '

If this is incorrect, use the user management screen to block the user and revoke the sessions

' } - "UserLoggedInFromUnknownLocation" { + 'UserLoggedInFromUnknownLocation' { $Table = ($TableObj | ConvertTo-Html -Fragment -As List | Out-String).Replace('
', '
') if ($Appname) { $AppName = $AppName.'Application Name' } else { $appName = $data.ApplicationId } $Title = "$($TenantFilter) - a user has logged on from a potentially unsafe location" $IntroText = "$($data.UserId) ($($data.Userkey)) has logged on from IP $($data.ClientIP) to the application $($Appname). According to our database this is located in $($Country) - $($City).

You have set up alerts to be notified when this happens. See the table below for more info.$Table" $ButtonUrl = "$CIPPPURL/identity/administration/ViewBec?userId=$($data.ObjectId)&tenantDomain=$($data.OrganizationId)" - $ButtonText = "User Management" - $AfterButtonText = "

If this is incorrect, use the user management screen to block the user and revoke the sessions

" + $ButtonText = 'User Management' + $AfterButtonText = '

If this is incorrect, use the user management screen to block the user and revoke the sessions

' } + 'Add service principal.' { + if ($Appname) { $AppName = $AppName.'Application Name' } else { $appName = $data.ApplicationId } + $Title = "$($TenantFilter) - Service Principal $($data.ObjectId) has been added." + $IntroText = "$($data.ObjectId) has been added by $($data.UserId)." + $ButtonUrl = "$CIPPPURL/tenant/administration/enterprise-apps?customerId=?customerId=$($data.OrganizationId)" + $ButtonText = 'Enterprise Apps' + } + 'Remove service principal.' { + if ($Appname) { $AppName = $AppName.'Application Name' } else { $appName = $data.ApplicationId } + $Title = "$($TenantFilter) - Service Principal $($data.ObjectId) has been removed." + $IntroText = "$($data.ObjectId) has been added by $($data.UserId)." + $ButtonUrl = "$CIPPPURL/tenant/administration/enterprise-apps?customerId=?customerId=$($data.OrganizationId)" + $ButtonText = 'Enterprise Apps' + } + default { break } @@ -177,8 +191,8 @@ function Invoke-CippWebhookProcessing { } $HTML = "$HTML" -f $Title, $IntroText, $ButtonUrl, $ButtonText, $AfterButtonText - Write-Host "Add IP and potential location to knownlocation db for this specific user" - + Write-Host 'Add IP and potential location to knownlocation db for this specific user' + if ($data.ClientIP) { $IP = $data.ClientIP if ($IP -match '^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d+$') { @@ -202,11 +216,11 @@ function Invoke-CippWebhookProcessing { PotentialCity = $City } | ConvertTo-Json -Depth 15 -Compress if ($Title) { - Write-Host "Sending alert to email" + Write-Host 'Sending alert to email' Send-CIPPAlert -Type 'email' -Title $title -HTMLContent $HTML - Write-Host "Sending alert to webhook" + Write-Host 'Sending alert to webhook' Send-CIPPAlert -Type 'webhook' -Title $title -JSONContent $JsonContent - Write-Host "Sending alert to PSA" + Write-Host 'Sending alert to PSA' Send-CIPPAlert -Type 'psa' -Title $title -HTMLContent $HTML } } diff --git a/Modules/CIPPCore/Public/Send-CIPPAlert.ps1 b/Modules/CIPPCore/Public/Send-CIPPAlert.ps1 index 23a640eb4027..be9d5c706381 100644 --- a/Modules/CIPPCore/Public/Send-CIPPAlert.ps1 +++ b/Modules/CIPPCore/Public/Send-CIPPAlert.ps1 @@ -71,7 +71,7 @@ function Send-CIPPAlert { } catch { Write-Host "Could not send alerts to webhook: $($_.Exception.message)" - Write-LogMessage -API 'Webhook Alerts' -message "Could not send alerts to : $($_.Exception.message)" -sev info + Write-LogMessage -API 'Webhook Alerts' -message "Could not send alerts to webhook: $($_.Exception.message)" -sev info } } Write-Host "Trying to send to PSA" @@ -91,7 +91,7 @@ function Send-CIPPAlert { } catch { Write-Host "Could not send alerts to ticketing system: $($_.Exception.message)" - Write-LogMessage -API 'Webhook Alerts' -message "Could not send alerts to : $($_.Exception.message)" -sev info + Write-LogMessage -API 'Webhook Alerts' -message "Could not send alerts to ticketing system: $($_.Exception.message)" -sev info } } } diff --git a/Modules/CIPPCore/Public/Set-CIPPCPVConsent.ps1 b/Modules/CIPPCore/Public/Set-CIPPCPVConsent.ps1 new file mode 100644 index 000000000000..32786d5bd89d --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPCPVConsent.ps1 @@ -0,0 +1,101 @@ +function Set-CIPPCPVConsent { + [CmdletBinding()] + param( + $Tenantfilter, + $APIName = "CPV Consent", + $ExecutingUser + ) + $Results = [System.Collections.ArrayList]@() + Set-Location (Get-Item $PSScriptRoot).Parent.FullName + $ExpectedPermissions = Get-Content '.\Cache_SAMSetup\SAMManifest.json' | ConvertFrom-Json + $Translator = Get-Content '.\Cache_SAMSetup\PermissionsTranslator.json' | ConvertFrom-Json + try { + $ServicePrincipalList = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/servicePrincipals?`$select=AppId,id,displayName&`$top=999" -tenantid $Tenantfilter + $ourSVCPrincipal = $ServicePrincipalList | Where-Object -Property AppId -EQ $env:ApplicationID + } + catch { + + } + if (!$ourSVCPrincipal) { + try { + $AppBody = @" +{ + "ApplicationGrants":[ {"EnterpriseApplicationId":"00000003-0000-0000-c000-000000000000","Scope":"Application.ReadWrite.all,DelegatedPermissionGrant.ReadWrite.All"}], + "ApplicationId": "ed2d757e-dbab-439c-a2d3-1567de12d31f" +} +"@ + $CPVConsent = New-GraphpostRequest -body $AppBody -Type POST -noauthcheck $true -uri "https://api.partnercenter.microsoft.com/v1/customers/$($TenantFilter)/applicationconsents" -scope "https://api.partnercenter.microsoft.com/.default" -tenantid $env:TenantID + $Results.add("Succesfully added CPV Application") + $ServicePrincipalList = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/servicePrincipals?`$select=AppId,id&`$top=999" -tenantid $Tenantfilter + $ourSVCPrincipal = $ServicePrincipalList | Where-Object -Property AppId -EQ $env:ApplicationID + + } + #TODO: after doing this, write to the table that we have done this for current applicationId, so that we don't ever have to do it again when running on a schedule. + + catch { + Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message "Could not add our Service Principal to the client tenant: $($_.Exception.message)" -Sev "Error" -tenant $($Tenantfilter) + return @("Could not add our Service Principal to the client tenant $($Tenantfilter): $($_.Exception.message)") + } + + } + else { + $Results.add("Application Exists, adding permissions") + } + #TODO: Add this as a function so we can use it for more than just our app + $CurrentRoles = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/servicePrincipals/$($ourSVCPrincipal.id)/appRoleAssignments" -tenantid $tenantfilter + + $Grants = foreach ($App in $ExpectedPermissions.requiredResourceAccess) { + $svcPrincipalId = $ServicePrincipalList | Where-Object -Property AppId -EQ $app.resourceAppId + if (!$svcPrincipalId) { continue } #If the app does not exist, we can't add permissions for it. E.g. Defender etc. + foreach ($SingleResource in $app.ResourceAccess | Where-Object -Property Type -EQ "Role") { + if ($singleresource.id -In $currentroles.appRoleId) { continue } + [pscustomobject]@{ + principalId = $($ourSVCPrincipal.id) + resourceId = $($svcPrincipalId.id) + appRoleId = "$($SingleResource.Id)" + } + } + } + + foreach ($Grant in $grants) { + try { + $SettingsRequest = New-GraphPOSTRequest -body ($grant | ConvertTo-Json) -uri "https://graph.microsoft.com/beta/servicePrincipals/$($ourSVCPrincipal.id)/appRoleAssignedTo" -tenantid $tenantfilter -type POST + } + catch { + $Results.add("Failed to grant $($grant.appRoleId) to $($grant.resourceId): $($_.Exception.Message)") + } + } + + #Adding all required Delegated permissions + $CurrentDelegatedScopes = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/servicePrincipals/$($ourSVCPrincipal.id)/oauth2PermissionGrants" -tenantid $tenantfilter + foreach ($App in $ExpectedPermissions.requiredResourceAccess) { + $svcPrincipalId = $ServicePrincipalList | Where-Object -Property AppId -EQ $app.resourceAppId + if (!$svcPrincipalId) { continue } #If the app does not exist, we can't add permissions for it. E.g. Defender etc. + $NewScope = ($Translator | Where-Object { $_.id -in $app.ResourceAccess.id } | Where-Object { $_.value -notin 'profile', 'openid', 'offline_access' }).value -join ' ' + $OldScope = ($CurrentDelegatedScopes | Where-Object -Property Resourceid -EQ $svcPrincipalId.id) + if (!$OldScope) { + $Createbody = @{ + clientId = $ourSVCPrincipal.id + consentType = "AllPrincipals" + resourceId = $svcPrincipalId.id + scope = $NewScope + } | ConvertTo-Json -Compress + $CreateRequest = New-GraphPOSTRequest -uri "https://graph.microsoft.com/v1.0/oauth2PermissionGrants" -tenantid $tenantfilter -body $Createbody -type POST + $Results.add("Succesfully added permissions for $($svcPrincipalId.displayName)") + } + else { + $compare = Compare-Object -ReferenceObject $OldScope.scope.Split(' ') -DifferenceObject $NewScope.Split(' ') + if (!$compare) { + $Results.add("All delegated permissions exist for $($svcPrincipalId.displayName)") + continue + } + $Patchbody = @{ + scope = "$NewScope" + } | ConvertTo-Json -Compress + $Patchrequest = New-GraphPOSTRequest -uri "https://graph.microsoft.com/v1.0/oauth2PermissionGrants/$($Oldscope.id)" -tenantid $tenantfilter -body $Patchbody -type PATCH + $Results.add("Succesfully updated permissions for $($svcPrincipalId.displayName)") + } + + } + return $Results +} \ No newline at end of file diff --git a/Modules/CippEntrypoints/CippEntrypoints.psm1 b/Modules/CippEntrypoints/CippEntrypoints.psm1 index 4f11543eefa0..ffea0fd0021d 100644 --- a/Modules/CippEntrypoints/CippEntrypoints.psm1 +++ b/Modules/CippEntrypoints/CippEntrypoints.psm1 @@ -5,19 +5,7 @@ function Receive-CippHttpTrigger { $APIName = $TriggerMetadata.FunctionName - $FunctionVerbs = @{ - 'Get' = '^(?List.+$)' - 'Update' = '^(?Edit.+$)' - 'New' = '^(?Add.+$)' - 'Invoke' = '^(?Exec.+$)' - } - - foreach ($FunctionVerb in $FunctionVerbs.Keys) { - if ($APIName -match $FunctionVerbs.$FunctionVerb) { - $FunctionName = '{0}-{1}' -f $FunctionVerb, $Matches.APIName - break - } - } + $FunctionName = 'Invoke-{0}' -f $APIName $HttpTrigger = @{ Request = $Request diff --git a/Modules/CippExtensions/CippExtensions.psd1 b/Modules/CippExtensions/CippExtensions.psd1 index 76606b0bd22a..b66d07c0b57a 100644 Binary files a/Modules/CippExtensions/CippExtensions.psd1 and b/Modules/CippExtensions/CippExtensions.psd1 differ diff --git a/Modules/CippExtensions/Private/Get-HaloMapping.ps1 b/Modules/CippExtensions/Private/Get-HaloMapping.ps1 new file mode 100644 index 000000000000..c818c75ad9fa --- /dev/null +++ b/Modules/CippExtensions/Private/Get-HaloMapping.ps1 @@ -0,0 +1,42 @@ +function Get-HaloMapping { + [CmdletBinding()] + param ( + $CIPPMapping + ) + #Get available mappings + $Mappings = [pscustomobject]@{} + $Filter = "PartitionKey eq 'Mapping'" + Get-AzDataTableEntity @CIPPMapping -Filter $Filter | ForEach-Object { + $Mappings | Add-Member -NotePropertyName $_.RowKey -NotePropertyValue @{ label = "$($_.HaloPSAName)"; value = "$($_.HaloPSA)" } + } + #Get Available TEnants + $Tenants = Get-Tenants + #Get available halo clients + $Table = Get-CIPPTable -TableName Extensionsconfig + try { + $Configuration = ((Get-AzDataTableEntity @Table).config | ConvertFrom-Json).HaloPSA + $Token = Get-HaloToken -configuration $Configuration + $i = 1 + $RawHaloClients = do { + $Result = Invoke-RestMethod -Uri "$($Configuration.ResourceURL)/Client?page_no=$i&page_size=999&pageinate=true" -ContentType 'application/json' -Method GET -Headers @{Authorization = "Bearer $($token.access_token)" } + $Result.clients | Select-Object * -ExcludeProperty logo + $i++ + $pagecount = [Math]::Ceiling($Result.record_count / 999) + } while ($i -le $pagecount) + } + catch { $RawHaloClients = @() } + $HaloClients = $RawHaloClients | ForEach-Object { + [PSCustomObject]@{ + name = $_.name + value = "$($_.id)" + } + } + $MappingObj = [PSCustomObject]@{ + Tenants = @($Tenants) + HaloClients = @($HaloClients) + Mappings = $Mappings + } + + return $MappingObj + +} \ No newline at end of file diff --git a/Modules/CippExtensions/Private/Set-HaloMapping.ps1 b/Modules/CippExtensions/Private/Set-HaloMapping.ps1 new file mode 100644 index 000000000000..865034fe6a29 --- /dev/null +++ b/Modules/CippExtensions/Private/Set-HaloMapping.ps1 @@ -0,0 +1,22 @@ +function Set-HaloMapping { + [CmdletBinding()] + param ( + $CIPPMapping, + $APIName, + $Request + ) + + foreach ($Mapping in ([pscustomobject]$Request.body.mappings).psobject.properties) { + $AddObject = @{ + PartitionKey = 'Mapping' + RowKey = "$($mapping.name)" + 'HaloPSA' = "$($mapping.value.value)" + 'HaloPSAName' = "$($mapping.value.label)" + } + Add-AzDataTableEntity @CIPPMapping -Entity $AddObject -Force + Write-LogMessage -API $APINAME -user $request.headers.'x-ms-client-principal' -message "Added mapping for $($mapping.name)." -Sev 'Info' + } + $Result = [pscustomobject]@{'Results' = "Successfully edited mapping table." } + + Return $Result +} \ No newline at end of file diff --git a/Modules/GraphRequests/GraphRequests.psm1 b/Modules/GraphRequests/GraphRequests.psm1 index f584acc34193..12f13762d19c 100644 --- a/Modules/GraphRequests/GraphRequests.psm1 +++ b/Modules/GraphRequests/GraphRequests.psm1 @@ -1,5 +1,5 @@ -$Public = @(Get-ChildItem -Path $PSScriptRoot\Public\*.ps1 -ErrorAction SilentlyContinue) -$Private = @(Get-ChildItem -Path $PSScriptRoot\private\*.ps1 -ErrorAction SilentlyContinue) +$Public = @(Get-ChildItem -Path $PSScriptRoot\Public\*.ps1 -Recurse -ErrorAction SilentlyContinue) +$Private = @(Get-ChildItem -Path $PSScriptRoot\private\*.ps1 -Recurse -ErrorAction SilentlyContinue) $Functions = $Public + $Private foreach ($import in @($Functions)) { try { diff --git a/Modules/GraphRequests/Public/Get-GraphRequestList.ps1 b/Modules/GraphRequests/Public/Core Functions/Get-GraphRequestList.ps1 similarity index 94% rename from Modules/GraphRequests/Public/Get-GraphRequestList.ps1 rename to Modules/GraphRequests/Public/Core Functions/Get-GraphRequestList.ps1 index 85c405cd4bf3..33b0711f43ae 100644 --- a/Modules/GraphRequests/Public/Get-GraphRequestList.ps1 +++ b/Modules/GraphRequests/Public/Core Functions/Get-GraphRequestList.ps1 @@ -25,8 +25,7 @@ function Get-GraphRequestList { if ($QueueNameOverride) { $QueueName = $QueueNameOverride - } - else { + } else { $TextInfo = (Get-Culture).TextInfo $QueueName = $TextInfo.ToTitleCase($DisplayName -csplit '(?=[A-Z])' -ne '' -join ' ') } @@ -47,20 +46,17 @@ function Get-GraphRequestList { $Filter = "QueueId eq '{0}'" -f $QueueId $Rows = Get-AzDataTableEntity @Table -Filter $Filter $Type = 'Queue' - } - elseif ($Tenant -eq 'AllTenants' -or (!$SkipCache.IsPresent -and !$ClearCache.IsPresent -and !$CountOnly.IsPresent)) { + } elseif ($Tenant -eq 'AllTenants' -or (!$SkipCache.IsPresent -and !$ClearCache.IsPresent -and !$CountOnly.IsPresent)) { $Table = Get-CIPPTable -TableName $TableName if ($Tenant -eq 'AllTenants') { $Filter = "PartitionKey eq '{0}' and QueueType eq 'AllTenants'" -f $PartitionKey - } - else { + } else { $Filter = "PartitionKey eq '{0}' and Tenant eq '{1}'" -f $PartitionKey, $Tenant } #Write-Host $Filter $Rows = Get-AzDataTableEntity @Table -Filter $Filter | Where-Object { $_.Timestamp.DateTime -gt (Get-Date).ToUniversalTime().AddHours(-1) } $Type = 'Cache' - } - else { + } else { $Type = 'None' $Rows = @() } @@ -87,16 +83,14 @@ function Get-GraphRequestList { try { Get-GraphRequestList @GraphRequestParams | Select-Object *, @{l = 'Tenant'; e = { $_.defaultDomainName } }, @{l = 'CippStatus'; e = { 'Good' } } - } - catch { + } catch { [PSCustomObject]@{ Tenant = $_.defaultDomainName CippStatus = "Could not connect to tenant. $($_.Exception.message)" } } } - } - else { + } else { if ($RunningQueue) { Write-Host 'Queue currently running' Write-Host ($RunningQueue | ConvertTo-Json) @@ -104,8 +98,7 @@ function Get-GraphRequestList { Tenant = 'Data still processing, please wait' QueueId = $RunningQueue.RowKey } - } - else { + } else { $Queue = New-CippQueueEntry -Name "$QueueName (All Tenants)" -Link $CippLink -Reference $QueueReference [PSCustomObject]@{ QueueMessage = 'Loading data for all tenants. Please check back after the job completes' @@ -132,8 +125,7 @@ function Get-GraphRequestList { Push-OutputBinding -Name QueueItem -Value $QueueTenant } - } - catch { + } catch { Write-Host "QUEUE ERROR: $($_.Exception.Message)" } } @@ -162,6 +154,7 @@ function Get-GraphRequestList { $QueueThresholdExceeded = $false if ($Parameters.'$count' -and !$SkipCache -and !$NoPagination) { $Count = New-GraphGetRequest @GraphRequest -CountOnly -ErrorAction Stop + if ($CountOnly.IsPresent) { return $Count } Write-Host "Total results (`$count): $Count" if ($Count -gt 8000) { $QueueThresholdExceeded = $true @@ -173,8 +166,7 @@ function Get-GraphRequestList { QueueId = $RunningQueue.RowKey Queued = $true } - } - else { + } else { $Queue = New-CippQueueEntry -Name $QueueName -Link $CippLink -Reference $QueueReference $QueueTenant = @{ Tenant = $Tenant @@ -208,20 +200,17 @@ function Get-GraphRequestList { foreach ($Result in $GraphRequestResults) { $Result | Select-Object @{n = 'TenantInfo'; e = { $TenantInfo | Where-Object { $Result.$ReverseTenantLookupProperty -eq $_.tenantId } } }, * } - } - else { + } else { $GraphRequestResults } } - } - catch { + } catch { throw $_.Exception } } } - } - else { + } else { $Rows | ForEach-Object { $_.Data | ConvertFrom-Json } diff --git a/Modules/GraphRequests/Public/Get-ListGraphRequest.ps1 b/Modules/GraphRequests/Public/Entrypoints/Invoke-ListGraphRequest.ps1 similarity index 98% rename from Modules/GraphRequests/Public/Get-ListGraphRequest.ps1 rename to Modules/GraphRequests/Public/Entrypoints/Invoke-ListGraphRequest.ps1 index cfcb4eab2d08..c1ff835fc18d 100644 --- a/Modules/GraphRequests/Public/Get-ListGraphRequest.ps1 +++ b/Modules/GraphRequests/Public/Entrypoints/Invoke-ListGraphRequest.ps1 @@ -1,5 +1,5 @@ -function Get-ListGraphRequest { +function Invoke-ListGraphRequest { # Input bindings are passed in via param block. param($Request, $TriggerMetadata) diff --git a/Modules/GraphRequests/Public/Push-ListGraphRequestQueue.ps1 b/Modules/GraphRequests/Public/Entrypoints/Push-ListGraphRequestQueue.ps1 similarity index 100% rename from Modules/GraphRequests/Public/Push-ListGraphRequestQueue.ps1 rename to Modules/GraphRequests/Public/Entrypoints/Push-ListGraphRequestQueue.ps1 diff --git a/RemoveScheduledItems/function.json b/RemoveScheduledItem/function.json similarity index 94% rename from RemoveScheduledItems/function.json rename to RemoveScheduledItem/function.json index 925eab5aeae1..b0ca1676cc0b 100644 --- a/RemoveScheduledItems/function.json +++ b/RemoveScheduledItem/function.json @@ -1,19 +1,19 @@ -{ - "bindings": [ - { - "authLevel": "anonymous", - "type": "httpTrigger", - "direction": "in", - "name": "Request", - "methods": [ - "get", - "post" - ] - }, - { - "type": "http", - "direction": "out", - "name": "Response" - } - ] +{ + "bindings": [ + { + "authLevel": "anonymous", + "type": "httpTrigger", + "direction": "in", + "name": "Request", + "methods": [ + "get", + "post" + ] + }, + { + "type": "http", + "direction": "out", + "name": "Response" + } + ] } \ No newline at end of file diff --git a/RemoveScheduledItems/run.ps1 b/RemoveScheduledItem/run.ps1 similarity index 58% rename from RemoveScheduledItems/run.ps1 rename to RemoveScheduledItem/run.ps1 index ce7c0a0dcd9c..191c95570b0f 100644 --- a/RemoveScheduledItems/run.ps1 +++ b/RemoveScheduledItem/run.ps1 @@ -1,11 +1,14 @@ -using namespace System.Net -param($Request, $TriggerMetadata) -$APIName = $TriggerMetadata.FunctionName -Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message 'Accessed this API' -Sev 'Debug' -$task = $Request.Body | ConvertFrom-Json -$Table = Get-CIPPTable -TableName 'ScheduledTasks' -Remove-AzDataTableEntity @Table -PartitionKey 'ScheduledTask' -RowKey $task.TaskID -force -Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ - StatusCode = [HttpStatusCode]::OK - Body = 'Task removed successfully.' -}) \ No newline at end of file +using namespace System.Net +param($Request, $TriggerMetadata) +$APIName = $TriggerMetadata.FunctionName +Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message 'Accessed this API' -Sev 'Debug' +$task = @{ + RowKey = $Request.Query.ID + PartitionKey = 'ScheduledTask' +} +$Table = Get-CIPPTable -TableName 'ScheduledTasks' +Remove-AzDataTableEntity @Table -Entity $task +Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = @{ Results = 'Task removed successfully.' } + }) \ No newline at end of file diff --git a/Scheduler_UserTasks/_functions.json b/Scheduler_UserTasks/_functions.json deleted file mode 100644 index a4e981dd588d..000000000000 --- a/Scheduler_UserTasks/_functions.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "bindings": [ - { - "name": "Timer", - "schedule": "0 */15 * * * *", - "direction": "in", - "type": "timerTrigger" - }, - { - "name": "starter", - "type": "durableClient", - "direction": "in" - } - ] - } \ No newline at end of file diff --git a/Scheduler_UserTasks/function.json b/Scheduler_UserTasks/function.json new file mode 100644 index 000000000000..de2a7380d759 --- /dev/null +++ b/Scheduler_UserTasks/function.json @@ -0,0 +1,16 @@ +{ + "bindings": [ + { + "name": "Timer", + "schedule": "0 */15 * * * *", + "direction": "in", + "type": "timerTrigger" + }, + { + "type": "queue", + "direction": "out", + "name": "Msg", + "queueName": "scheduledcommandprocessor" + } + ] +} diff --git a/Scheduler_UserTasks/run.ps1 b/Scheduler_UserTasks/run.ps1 index 91432822b602..af9effa41337 100644 --- a/Scheduler_UserTasks/run.ps1 +++ b/Scheduler_UserTasks/run.ps1 @@ -1,44 +1,51 @@ param($Timer) $Table = Get-CippTable -tablename 'ScheduledTasks' -$Filter = "Results eq 'Not Executed'" +$Filter = "TaskState eq 'Planned' or TaskState eq 'Failed - Planned'" $tasks = Get-AzDataTableEntity @Table -Filter $Filter - foreach ($task in $tasks) { - # Check if task has not been executed yet (i.e., 'Results' is 'Not Executed') - if ((Get-Date) -ge $task.ExpectedRunTime) { + $tenant = $task.Tenant + $currentUnixTime = [int64](([datetime]::UtcNow) - (Get-Date "1/1/1970")).TotalSeconds + if ($currentUnixTime -ge $task.ScheduledTime) { try { Update-AzDataTableEntity @Table -Entity @{ PartitionKey = $task.PartitionKey - RowKey = $task.RowKey - TaskState = 'Running' - # Update other properties as needed + RowKey = $task.RowKey + ExecutedTime = "$currentUnixTime" + TaskState = 'Running' } + $task.Parameters = $task.Parameters | ConvertFrom-Json -AsHashtable - $results = Invoke-Command -ScriptBlock ([ScriptBlock]::Create($task.Command)) -ArgumentList $task.Parameters + if (!$task.Parameters) { $task.Parameters = @{} } + $ScheduledCommand = [pscustomobject]@{ + Command = $task.Command + Parameters = $task.Parameters + TaskInfo = $task + } - Update-AzDataTableEntity @Table -Entity @{ - PartitionKey = $task.PartitionKey - RowKey = $task.RowKey - Results = "$results" - TaskState = 'Completed' - # Update other properties as needed + if ($task.Tenant -eq "AllTenants") { + $Results = Get-Tenants | ForEach-Object { + $ScheduledCommand.Parameters['TenantFilter'] = $_.defaultDomainName + Push-OutputBinding -Name Msg -Value $ScheduledCommand + } + } + else { + $ScheduledCommand.Parameters['TenantFilter'] = $task.Tenant + $Results = Push-OutputBinding -Name Msg -Value $ScheduledCommand } - Write-LogMessage -API "Scheduler_UserTasks" -tenant $tenant -message "Successfully executed task: $($task.RowKey)" -sev Info } catch { $errorMessage = $_.Exception.Message Update-AzDataTableEntity @Table -Entity @{ PartitionKey = $task.PartitionKey - RowKey = $task.RowKey - Results = "$errorMessage" - TaskState = 'Failed' - # Update other properties as needed + RowKey = $task.RowKey + Results = "$errorMessage" + ExecutedTime = "$currentUnixTime" + TaskState = 'Failed' } - - Write-LogMessage -API "Scheduler_UserTasks" -tenant $tenant -message "Failed to execute task: $errorMessage" -sev Error + Write-LogMessage -API "Scheduler_UserTasks" -tenant $tenant -message "Failed to execute task $($task.Name): $errorMessage" -sev Error } } } \ No newline at end of file diff --git a/version_latest.txt b/version_latest.txt index ef8d7569d677..81911389142b 100644 --- a/version_latest.txt +++ b/version_latest.txt @@ -1 +1 @@ -4.2.0 \ No newline at end of file +4.3.0 \ No newline at end of file