diff --git a/AddChocoApp/function.json b/AddChocoApp/function.json deleted file mode 100644 index 3bd167116eae..000000000000 --- a/AddChocoApp/function.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "bindings": [ - { - "authLevel": "anonymous", - "type": "httpTrigger", - "direction": "in", - "name": "Request", - "methods": [ - "get", - "post" - ] - }, - { - "type": "http", - "direction": "out", - "name": "Response" - }, - { - "name": "starter", - "direction": "in", - "type": "durableClient" - } - ] -} \ No newline at end of file diff --git a/AddChocoApp/run.ps1 b/AddChocoApp/run.ps1 deleted file mode 100644 index 13fced9492e5..000000000000 --- a/AddChocoApp/run.ps1 +++ /dev/null @@ -1,58 +0,0 @@ -using namespace System.Net - -# 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" -Set-Location (Get-Item $PSScriptRoot).Parent.FullName - -Write-Host "PowerShell HTTP trigger function processed a request." -$ChocoApp = $request.body -$intuneBody = Get-Content "AddChocoApp\choco.app.json" | ConvertFrom-Json -$assignTo = $Request.body.AssignTo -$intuneBody.description = $ChocoApp.description -$intuneBody.displayName = $chocoapp.ApplicationName -$intuneBody.installExperience.runAsAccount = if ($ChocoApp.InstallAsSystem) { "system" } else { "user" } -$intuneBody.installExperience.deviceRestartBehavior = if ($ChocoApp.DisableRestart) { "suppress" } else { "allow" } -$intuneBody.installCommandLine = "powershell.exe -executionpolicy bypass .\Install.ps1 -InstallChoco -Packagename $($chocoapp.PackageName)" -if ($ChocoApp.customrepo) { - $intuneBody.installCommandLine = $intuneBody.installCommandLine + " -CustomRepo $($chocoapp.CustomRepo)" -} -$intuneBody.UninstallCommandLine = "powershell.exe -executionpolicy bypass .\Uninstall.ps1 -Packagename $($chocoapp.PackageName)" -$intunebody.detectionRules[0].path = "$($ENV:SystemDrive)\programdata\chocolatey\lib" -$intunebody.detectionRules[0].fileOrFolderName = "$($chocoapp.PackageName)" - -$Tenants = ($Request.body | Select-Object Select_*).psobject.properties.value -$Results = foreach ($Tenant in $tenants) { - try { - $CompleteObject = [PSCustomObject]@{ - tenant = $tenant - Applicationname = $ChocoApp.ApplicationName - assignTo = $assignTo - InstallationIntent = $request.body.InstallationIntent - IntuneBody = $intunebody - } | ConvertTo-Json -Depth 15 - $Table = Get-CippTable -tablename 'apps' - $Table.Force = $true - Add-CIPPAzDataTableEntity @Table -Entity @{ - JSON = "$CompleteObject" - RowKey = "$((New-Guid).GUID)" - PartitionKey = "apps" - } - "Successfully added Choco App for $($Tenant) to queue." - Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -tenant $tenant -message "Chocolatey Application $($intunebody.Displayname) queued to add" -Sev "Info" - } - catch { - Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -tenant $tenant -message "Failed to add Chocolatey Application $($intunebody.Displayname) to queue" -Sev "Error" - "Failed added Choco App for $($Tenant) to queue" - } -} - -$body = [pscustomobject]@{"Results" = $results } - -# Associate values to output bindings by calling 'Push-OutputBinding'. -Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ - StatusCode = [HttpStatusCode]::OK - Body = $body - }) diff --git a/AddMSPApp/Immybot.app.json b/AddMSPApp/Immybot.app.json new file mode 100644 index 000000000000..8be12d944925 --- /dev/null +++ b/AddMSPApp/Immybot.app.json @@ -0,0 +1,65 @@ +{ + "displayName": "", + "installCommandLine": "", + "uninstallCommandLine": "", + "description": " ", + "developer": " ", + "owner": " ", + "informationUrl": " ", + "privacyInformationUrl": " ", + "fileName": "ninjarmm.intunewin", + "@odata.type": "#microsoft.graph.win32LobApp", + "applicableArchitectures": "x86, x64", + + "installExperience": { + "runAsAccount": "system", + "deviceRestartBehavior": "suppress", + "@odata.type": "microsoft.graph.win32LobAppInstallExperience" + }, + "detectionRules": [ + { + "@odata.type": "#microsoft.graph.win32LobAppFileSystemDetection", + "path": "%ProgramData%\\Immyboy\\Bin", + "fileOrFolderName": "Immybot.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" + } + ], + "minimumNumberOfProcessors": "1", + "minimumFreeDiskSpaceInMB": "8", + "minimumCpuSpeedInMHz": "4", + "minimumSupportedOperatingSystem": { + "@odata.type": "microsoft.graph.windowsMinimumOperatingSystem", + "v10_1607": true + }, + "notes": "CIPP Uploaded application", + "minimumMemoryInMB": "1", + "setupFilePath": "install.ps1" +} diff --git a/AddMSPApp/automate.app.json b/AddMSPApp/automate.app.json new file mode 100644 index 000000000000..0c3fa83967b8 --- /dev/null +++ b/AddMSPApp/automate.app.json @@ -0,0 +1,64 @@ +{ + "displayName": "", + "installCommandLine": "", + "uninstallCommandLine": "", + "description": " ", + "developer": " ", + "owner": " ", + "informationUrl": " ", + "privacyInformationUrl": " ", + "fileName": "automate.intunewin", + "@odata.type": "#microsoft.graph.win32LobApp", + "applicableArchitectures": "x86, x64", + + "installExperience": { + "runAsAccount": "system", + "deviceRestartBehavior": "suppress", + "@odata.type": "microsoft.graph.win32LobAppInstallExperience" + }, + "detectionRules": [ + { + "@odata.type": "#microsoft.graph.win32LobAppPowerShellScriptDetection", + "enforceSignatureCheck": false, + "runAs32Bit": false, + "scriptContent": "" + } + ], + "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", + "minimumSupportedOperatingSystem": { + "@odata.type": "microsoft.graph.windowsMinimumOperatingSystem", + "v10_1607": true + }, + "notes": "CIPP Uploaded application", + "minimumMemoryInMB": "1", + "setupFilePath": "install.ps1" +} diff --git a/AddMSPApp/automate.app.xml b/AddMSPApp/automate.app.xml new file mode 100644 index 000000000000..8f1a9b75d2a2 --- /dev/null +++ b/AddMSPApp/automate.app.xml @@ -0,0 +1,16 @@ + + install.ps1 + 2117 + automate.intunewin + install.ps1 + + TL5w2kSbhW0+Vb/ngucj1fIa7YfAnFG/d+U3o/qGG24= + NGPnJKKQIPM4yD4dCJ0GVCF0pqFsLX2TCb040bjLBBg= + QGvxYMYrgYovA6uo9XQ60w== + Q8PF4sGPbuxDyoQpmJUGVLvZw9hGhOBX0IhQNeeQEHk= + ProfileVersion1 + 49a2kb03OrNyDt0eZHSpSARq9HzvQL0IrBkcPffwC4M= + SHA256 + + \ No newline at end of file diff --git a/AddMSPApp/automate.detection.ps1 b/AddMSPApp/automate.detection.ps1 new file mode 100644 index 000000000000..be2de760777f --- /dev/null +++ b/AddMSPApp/automate.detection.ps1 @@ -0,0 +1,29 @@ +$ServerAddress = '##SERVER##' +if (Get-Module -ListAvailable -Name ConnectWiseAutomateAgent) { + Import-Module ConnectWiseAutomateAgent +} +else { + Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force + Install-Module -Name PowerShellGet -Force -AllowClobber + Update-Module -Name PowerShellGet + Set-PSRepository -Name 'PSGallery' -InstallationPolicy Trusted + Install-Module ConnectWiseAutomateAgent -MinimumVersion 0.1.2.0 -Confirm:$false -Force +} + +Invoke-CWAACommand -Command 'Send Status' +Start-Sleep -Seconds 20 + +$AgentInfo = Get-CWAAInfo +$ServerPassword = ConvertFrom-CWAASecurity $AgentInfo.ServerPassword +$LastContact = try { Get-Date $AgentInfo.LastSuccessStatus } catch { $null } + + +if ($AgentInfo.ID -gt 0 -and $LastContact -gt (Get-Date).AddDays(-30) -and $AgentInfo.Server -contains $ServerAddress -and $ServerPassword -ne 'Enter the server password here.') { + Write-Output 'SUCCESS: Agent is healthy' + exit 0 +} +else { + Write-Output 'ERROR: Agent is not healthy' + Write-Output ($AgentInfo | Select-Object ID, LocationID, LastSuccessStatus, Server | ConvertTo-Json) + exit 1 +} \ No newline at end of file diff --git a/AddMSPApp/automate.intunewin b/AddMSPApp/automate.intunewin new file mode 100644 index 000000000000..62ef0e225963 Binary files /dev/null and b/AddMSPApp/automate.intunewin differ diff --git a/AddMSPApp/cwcommand.app.json b/AddMSPApp/cwcommand.app.json new file mode 100644 index 000000000000..2d032177b625 --- /dev/null +++ b/AddMSPApp/cwcommand.app.json @@ -0,0 +1,65 @@ +{ + "displayName": "", + "installCommandLine": "", + "uninstallCommandLine": "", + "description": " ", + "developer": " ", + "owner": " ", + "informationUrl": " ", + "privacyInformationUrl": " ", + "fileName": "cwcommand.intunewin", + "@odata.type": "#microsoft.graph.win32LobApp", + "applicableArchitectures": "x86, x64", + + "installExperience": { + "runAsAccount": "system", + "deviceRestartBehavior": "suppress", + "@odata.type": "microsoft.graph.win32LobAppInstallExperience" + }, + "detectionRules": [ + { + "@odata.type": "#microsoft.graph.win32LobAppFileSystemDetection", + "path": "%ProgramFiles(x86)%\\ITSPlatform\\agentcore\\", + "fileOrFolderName": "platform-agent-core.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" + } + ], + "minimumNumberOfProcessors": "1", + "minimumFreeDiskSpaceInMB": "8", + "minimumCpuSpeedInMHz": "4", + "minimumSupportedOperatingSystem": { + "@odata.type": "microsoft.graph.windowsMinimumOperatingSystem", + "v10_1607": true + }, + "notes": "CIPP Uploaded application", + "minimumMemoryInMB": "1", + "setupFilePath": "install.ps1" +} diff --git a/AddMSPApp/cwcommand.app.xml b/AddMSPApp/cwcommand.app.xml new file mode 100644 index 000000000000..cc23edaad6fa --- /dev/null +++ b/AddMSPApp/cwcommand.app.xml @@ -0,0 +1,16 @@ + + install.ps1 + 2717 + cwcommand.intunewin + install.ps1 + + 6bilNd8M34xxoOl/marQi04r0PjYRD0YUuVf5hR/cVY= + QIeCE2WnKhg/yyyN2Vd7WBqy/9Vo22oOY+jN+o7NsM8= + fPjQqWF6INy3aAXKeGIlig== + qWx8/p2CoO2vuP/Dkr7KJw7JoxhmcA4XSj2ictbcC7M= + ProfileVersion1 + fslGge4BZ0F//6xgGCNIIVY5VPr/B2Ms1sGI7RiA9Bo= + SHA256 + + \ No newline at end of file diff --git a/AddMSPApp/cwcommand.intunewin b/AddMSPApp/cwcommand.intunewin new file mode 100644 index 000000000000..3f9387259708 Binary files /dev/null and b/AddMSPApp/cwcommand.intunewin differ diff --git a/AddMSPApp/datto.app.json b/AddMSPApp/datto.app.json new file mode 100644 index 000000000000..cdd10dfd8aeb --- /dev/null +++ b/AddMSPApp/datto.app.json @@ -0,0 +1,65 @@ +{ + "displayName": "", + "installCommandLine": "", + "uninstallCommandLine": "", + "description": " ", + "developer": " ", + "owner": " ", + "informationUrl": " ", + "privacyInformationUrl": " ", + "fileName": "DattoRMM.intunewin", + "@odata.type": "#microsoft.graph.win32LobApp", + "applicableArchitectures": "x86, x64", + + "installExperience": { + "runAsAccount": "system", + "deviceRestartBehavior": "suppress", + "@odata.type": "microsoft.graph.win32LobAppInstallExperience" + }, + "detectionRules": [ + { + "@odata.type": "#microsoft.graph.win32LobAppFileSystemDetection", + "path": "%programfiles(x86)%\\CentraStage", + "fileOrFolderName": "CagService.exe.config", + "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" + } + ], + "minimumNumberOfProcessors": "1", + "minimumFreeDiskSpaceInMB": "8", + "minimumCpuSpeedInMHz": "4", + "minimumSupportedOperatingSystem": { + "@odata.type": "microsoft.graph.windowsMinimumOperatingSystem", + "v10_1607": true + }, + "notes": "CIPP Uploaded application", + "minimumMemoryInMB": "1", + "setupFilePath": "install.ps1" +} diff --git a/AddMSPApp/datto.app.xml b/AddMSPApp/datto.app.xml new file mode 100644 index 000000000000..145d910d8ef3 --- /dev/null +++ b/AddMSPApp/datto.app.xml @@ -0,0 +1,15 @@ + + install.ps1 + 705 + datto.intunewin + install.ps1 + + sL/LP/JZ4F4cBSykm6usgJoV1PMoqd62C6JUwuo2z24= + PEpeqeoX7jAWxb0xHGfCkKFxh4/YRfoMTVXrP+uZWzM= + ulFPA+vYjaxX0pvq0BMAKQ== + 28ZFU4AT1OznwF8pfqO8i+WFUNSf9024H4Jw2H7UJWs= + ProfileVersion1 + YEb+QNQCko/uZyedA+JfcP/RDm+nZOIjFN04CfhwN4c= + SHA256 + + \ No newline at end of file diff --git a/AddMSPApp/datto.intunewin b/AddMSPApp/datto.intunewin new file mode 100644 index 000000000000..607b0725032b Binary files /dev/null and b/AddMSPApp/datto.intunewin differ diff --git a/AddMSPApp/huntress.app.json b/AddMSPApp/huntress.app.json new file mode 100644 index 000000000000..46c8a22057ae --- /dev/null +++ b/AddMSPApp/huntress.app.json @@ -0,0 +1,65 @@ +{ + "displayName": "", + "installCommandLine": "", + "uninstallCommandLine": "", + "description": " ", + "developer": " ", + "owner": " ", + "informationUrl": " ", + "privacyInformationUrl": " ", + "fileName": "huntress.intunewin", + "@odata.type": "#microsoft.graph.win32LobApp", + "applicableArchitectures": "x86, x64", + + "installExperience": { + "runAsAccount": "system", + "deviceRestartBehavior": "suppress", + "@odata.type": "microsoft.graph.win32LobAppInstallExperience" + }, + "detectionRules": [ + { + "@odata.type": "#microsoft.graph.win32LobAppFileSystemDetection", + "path": "%ProgramFiles%\\Huntress", + "fileOrFolderName": "HuntressAgent.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" + } + ], + "minimumNumberOfProcessors": "1", + "minimumFreeDiskSpaceInMB": "8", + "minimumCpuSpeedInMHz": "4", + "minimumSupportedOperatingSystem": { + "@odata.type": "microsoft.graph.windowsMinimumOperatingSystem", + "v10_1607": true + }, + "notes": "CIPP Uploaded application", + "minimumMemoryInMB": "1", + "setupFilePath": "install.ps1" +} diff --git a/AddMSPApp/huntress.app.xml b/AddMSPApp/huntress.app.xml new file mode 100644 index 000000000000..ad4f731a67ff --- /dev/null +++ b/AddMSPApp/huntress.app.xml @@ -0,0 +1,15 @@ + + install.ps1 + 8859 + huntress.intunewin + install.ps1 + + 0wrFiLHex//63XQZEbX535qvhQE5+MiZmfPho1CMrT4= + UOlXFsrh+Pq6ZZNmg2+gzuTCSDAxQNUDVkc6oR5SVAY= + x0cPnMjK6AZARRPhOfC5pg== + z+N/v0mfq8T871kS07/QZ1Lgay2hRabSxwDWRKz3fG4= + ProfileVersion1 + z8JuA/5iCrLM1cRkhL3di5eDysNsab62E812KGsrkbY= + SHA256 + + \ No newline at end of file diff --git a/AddMSPApp/huntress.intunewin b/AddMSPApp/huntress.intunewin new file mode 100644 index 000000000000..12c077f56145 Binary files /dev/null and b/AddMSPApp/huntress.intunewin differ diff --git a/AddMSPApp/immy.app.xml b/AddMSPApp/immy.app.xml new file mode 100644 index 000000000000..33c2b026cf06 --- /dev/null +++ b/AddMSPApp/immy.app.xml @@ -0,0 +1,15 @@ + + install.ps1 + 701 + IntunePackage.intunewin + install.ps1 + + doCFlu6eR0FygwmKEt64S1Yd8DBfok8/l0ophT7m2R4= + 5ai/8f238719rpGrOigf7mjx5u+gF6cBTOLWMBx5lrY= + klnIk9zH1fNeekrILt3tLw== + 9vyw/gMTsEsx3o3TVapXLxUQooNlVMRQj5/cXTo77x0= + ProfileVersion1 + 1eS5ExzbAFPOfI8x9REbwff0RAy5gvjTxYNZdAKUDcc= + SHA256 + + \ No newline at end of file diff --git a/AddMSPApp/immy.intunewin b/AddMSPApp/immy.intunewin new file mode 100644 index 000000000000..5102fd736f75 Binary files /dev/null and b/AddMSPApp/immy.intunewin differ diff --git a/AddMSPApp/ninjarmm.app.json b/AddMSPApp/ninjarmm.app.json new file mode 100644 index 000000000000..5cd567eaf210 --- /dev/null +++ b/AddMSPApp/ninjarmm.app.json @@ -0,0 +1,64 @@ +{ + "displayName": "", + "installCommandLine": "", + "uninstallCommandLine": "", + "description": " ", + "developer": " ", + "owner": " ", + "informationUrl": " ", + "privacyInformationUrl": " ", + "fileName": "ninjarmm.intunewin", + "@odata.type": "#microsoft.graph.win32LobApp", + "applicableArchitectures": "x86, x64", + + "installExperience": { + "runAsAccount": "system", + "deviceRestartBehavior": "suppress", + "@odata.type": "microsoft.graph.win32LobAppInstallExperience" + }, + "detectionRules": [ + { + "@odata.type": "#microsoft.graph.win32LobAppFileSystemDetection", + "path": "%ProgramData%\\Syncro\\Bin", + "fileOrFolderName": "Syncro.Overmind.Service.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" + } + ], + "minimumNumberOfProcessors": "1", + "minimumFreeDiskSpaceInMB": "8", + "minimumCpuSpeedInMHz": "4", + "minimumSupportedOperatingSystem": { + "@odata.type": "microsoft.graph.windowsMinimumOperatingSystem", + "v10_1607": true + }, + "notes": "CIPP Uploaded application", + "minimumMemoryInMB": "1" +} diff --git a/AddMSPApp/ninjarmm.app.xml b/AddMSPApp/ninjarmm.app.xml new file mode 100644 index 000000000000..647d26712e8b --- /dev/null +++ b/AddMSPApp/ninjarmm.app.xml @@ -0,0 +1,15 @@ + + install.ps1 + 728 + syncro.intunewin + install.ps1 + + XsBprXNg7cPsNS7YfRarT1zcSoL6/EF+c2dAjkhfuwk= + c4xkRYZg/r/h1xUD1172Tmr877nDXIoa3wuQnj0dpLQ= + OE7D78wJtXIpVESqaZAiWw== + 2m0nbXMdYK9dQgtjZraz3VsSxQlbO0jabpTaUjPg8I8= + ProfileVersion1 + gPccLH2ZAerHl8aYGYTxWmC8mnpJncTKBw5BX4IpH+g= + SHA256 + + \ No newline at end of file diff --git a/AddMSPApp/syncro.app.json b/AddMSPApp/syncro.app.json new file mode 100644 index 000000000000..1d5f0e01fb4b --- /dev/null +++ b/AddMSPApp/syncro.app.json @@ -0,0 +1,65 @@ +{ + "displayName": "", + "installCommandLine": "", + "uninstallCommandLine": "", + "description": " ", + "developer": " ", + "owner": " ", + "informationUrl": " ", + "privacyInformationUrl": " ", + "fileName": "syncro.intunewin", + "@odata.type": "#microsoft.graph.win32LobApp", + "applicableArchitectures": "x86, x64", + + "installExperience": { + "runAsAccount": "system", + "deviceRestartBehavior": "suppress", + "@odata.type": "microsoft.graph.win32LobAppInstallExperience" + }, + "detectionRules": [ + { + "@odata.type": "#microsoft.graph.win32LobAppFileSystemDetection", + "path": "%ProgramData%\\Syncro\\Bin", + "fileOrFolderName": "Syncro.Overmind.Service.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" + } + ], + "minimumNumberOfProcessors": "1", + "minimumFreeDiskSpaceInMB": "8", + "minimumCpuSpeedInMHz": "4", + "minimumSupportedOperatingSystem": { + "@odata.type": "microsoft.graph.windowsMinimumOperatingSystem", + "v10_1607": true + }, + "notes": "CIPP Uploaded application", + "minimumMemoryInMB": "1", + "setupFilePath": "install.ps1" +} diff --git a/AddMSPApp/syncro.app.xml b/AddMSPApp/syncro.app.xml new file mode 100644 index 000000000000..647d26712e8b --- /dev/null +++ b/AddMSPApp/syncro.app.xml @@ -0,0 +1,15 @@ + + install.ps1 + 728 + syncro.intunewin + install.ps1 + + XsBprXNg7cPsNS7YfRarT1zcSoL6/EF+c2dAjkhfuwk= + c4xkRYZg/r/h1xUD1172Tmr877nDXIoa3wuQnj0dpLQ= + OE7D78wJtXIpVESqaZAiWw== + 2m0nbXMdYK9dQgtjZraz3VsSxQlbO0jabpTaUjPg8I8= + ProfileVersion1 + gPccLH2ZAerHl8aYGYTxWmC8mnpJncTKBw5BX4IpH+g= + SHA256 + + \ No newline at end of file diff --git a/AddMSPApp/syncro.intunewin b/AddMSPApp/syncro.intunewin new file mode 100644 index 000000000000..1cf9f0ef8c66 Binary files /dev/null and b/AddMSPApp/syncro.intunewin differ diff --git a/DomainAnalyser_GetTenantDomains/run.ps1 b/DomainAnalyser_GetTenantDomains/run.ps1 index 2c805c5a0046..4e0fd71f2be8 100644 --- a/DomainAnalyser_GetTenantDomains/run.ps1 +++ b/DomainAnalyser_GetTenantDomains/run.ps1 @@ -5,7 +5,7 @@ $ExcludedTenants = Get-Tenants -SkipList $DomainTable = Get-CippTable -tablename 'Domains' $TenantDomains = $Tenants | ForEach-Object -Parallel { - Import-Module '.\GraphHelper.psm1' + Import-Module CippCore $Tenant = $_ # Get Domains to Lookup try { diff --git a/ExecAlertsListAllTenants/run.ps1 b/ExecAlertsListAllTenants/run.ps1 index bf31544adb5d..4782d15619b9 100644 --- a/ExecAlertsListAllTenants/run.ps1 +++ b/ExecAlertsListAllTenants/run.ps1 @@ -6,7 +6,7 @@ Write-Host "PowerShell queue trigger function processed work item: $QueueItem" Get-Tenants | ForEach-Object -Parallel { $domainName = $_.defaultDomainName - Import-Module '.\GraphHelper.psm1' + Import-Module CippCore $Table = Get-CIPPTable -TableName 'cachealertsandincidents' try { @@ -24,8 +24,7 @@ Get-Tenants | ForEach-Object -Parallel { } - } - catch { + } catch { $GUID = (New-Guid).Guid $AlertText = ConvertTo-Json -InputObject @{ Title = "Could not connect to tenant to retrieve data: $($_.Exception.Message)" @@ -36,8 +35,8 @@ Get-Tenants | ForEach-Object -Parallel { Status = '' userStates = @('None') vendorInformation = @{ - vendor = "CIPP" - provider = "CIPP" + vendor = 'CIPP' + provider = 'CIPP' } } $GraphRequest = @{ diff --git a/ExecGDAPInviteApproved_Timer/function.json b/ExecGDAPInviteApproved_Timer/function.json new file mode 100644 index 000000000000..6b68992375e9 --- /dev/null +++ b/ExecGDAPInviteApproved_Timer/function.json @@ -0,0 +1,16 @@ +{ + "bindings": [ + { + "name": "Timer", + "type": "timerTrigger", + "direction": "in", + "schedule": "0 0 */3 * * *" + }, + { + "type": "queue", + "direction": "out", + "name": "Msg", + "queueName": "gdapinvitequeue" + } + ] +} diff --git a/ExecGDAPInviteApproved_Timer/run.ps1 b/ExecGDAPInviteApproved_Timer/run.ps1 new file mode 100644 index 000000000000..08370014869f --- /dev/null +++ b/ExecGDAPInviteApproved_Timer/run.ps1 @@ -0,0 +1,5 @@ +using namespace System.Net + +param($Timer) + +Set-CIPPGDAPInviteGroups diff --git a/ExecGDAPMigrationQueue/function.json b/ExecGDAPInviteQueue/function.json similarity index 76% rename from ExecGDAPMigrationQueue/function.json rename to ExecGDAPInviteQueue/function.json index e581d4804e4d..e51e66299d6f 100644 --- a/ExecGDAPMigrationQueue/function.json +++ b/ExecGDAPInviteQueue/function.json @@ -4,7 +4,7 @@ "name": "QueueItem", "type": "queueTrigger", "direction": "in", - "queueName": "gdapqueue" + "queueName": "gdapinvitequeue" } ] } diff --git a/ExecGDAPInviteQueue/run.ps1 b/ExecGDAPInviteQueue/run.ps1 new file mode 100644 index 000000000000..78e43c118449 --- /dev/null +++ b/ExecGDAPInviteQueue/run.ps1 @@ -0,0 +1,35 @@ +# Input bindings are passed in via param block. +param( $QueueItem, $TriggerMetadata) + +# Write out the queue message and metadata to the information log. +Write-Host "PowerShell queue trigger function processed work item: $QueueItem" +#$TenantFilter = $env:TenantID + +$Table = Get-CIPPTable -TableName 'GDAPInvites' +$Invite = Get-CIPPAzDataTableEntity @Table -Filter "RowKey eq '$QueueItem'" +$APINAME = 'GDAPInvites' +$RoleMappings = $Invite.RoleMappings | ConvertFrom-Json +Write-Host ($Invite | ConvertTo-Json -Compress) + +foreach ($role in $RoleMappings) { + try { + $Mappingbody = ConvertTo-Json -Depth 10 -InputObject @{ + 'accessContainer' = @{ + 'accessContainerId' = "$($Role.GroupId)" + 'accessContainerType' = 'securityGroup' + } + 'accessDetails' = @{ + 'unifiedRoles' = @(@{ + 'roleDefinitionId' = "$($Role.roleDefinitionId)" + }) + } + } + New-GraphPostRequest -NoAuthCheck $True -uri "https://graph.microsoft.com/beta/tenantRelationships/delegatedAdminRelationships/$($QueueItem)/accessAssignments" -tenantid $env:TenantID -type POST -body $MappingBody -verbose + Start-Sleep -Milliseconds 100 + } catch { + Write-LogMessage -API $APINAME -message "GDAP Group mapping failed - $($role.GroupId): $($_.Exception.Message)" -Sev Error + exit 1 + } + Write-LogMessage -API $APINAME -message "Groups mapped for GDAP Relationship: $($GdapInvite.RowKey)" -Sev Info +} +Remove-AzDataTableEntity @Table -Entity $Invite diff --git a/ExecGDAPMigrationQueue/run.ps1 b/ExecGDAPMigrationQueue/run.ps1 deleted file mode 100644 index a76f06393028..000000000000 --- a/ExecGDAPMigrationQueue/run.ps1 +++ /dev/null @@ -1,92 +0,0 @@ -# Input bindings are passed in via param block. -param( $QueueItem, $TriggerMetadata) - -# Write out the queue message and metadata to the information log. -Write-Host "PowerShell queue trigger function processed work item: $QueueItem" -#$TenantFilter = $env:TenantID -$RoleMappings = $QueueItem.gdapRoles -$tenant = $queueitem.tenant -$Table = Get-CIPPTable -TableName 'gdapmigration' -Write-Host ($QueueItem.tenant | ConvertTo-Json -Compress) -$logRequest = @{ - status = 'Started migration' - tenant = "$($tenant.displayName)" - RowKey = "$($tenant.customerId)" - PartitionKey = 'alert' - startAt = "$((Get-Date).ToString('s'))" -} - -Add-CIPPAzDataTableEntity @Table -Entity $logRequest -Force | Out-Null - -if ($RoleMappings) { - $LogRequest['status'] = 'Step 2: Roles selected, creating new GDAP relationship.' - Add-CIPPAzDataTableEntity @Table -Entity $logRequest -Force | Out-Null -} -else { - $LogRequest['status'] = 'Migration failed at Step 2: No role mappings created.' - Add-CIPPAzDataTableEntity @Table -Entity $logRequest -Force | Out-Null - exit 1 -} -try { - $JSONBody = @{ - 'displayName' = "$((New-Guid).GUID)" - 'partner' = @{ - 'tenantId' = "$env:tenantid" - } - - 'customer' = @{ - 'displayName' = "$($tenant.displayName)" - 'tenantId' = "$($tenant.customerId)" - } - 'accessDetails' = @{ - 'unifiedRoles' = @($RoleMappings | Select-Object roleDefinitionId) - } - 'duration' = 'P730D' - } | ConvertTo-Json -Depth 5 -Compress - Write-Host $JSONBody - $MigrateRequest = New-GraphPostRequest -NoAuthCheck $True -uri 'https://traf-pcsvcadmin-prod.trafficmanager.net/CustomerServiceAdminApi/Web//v1/delegatedAdminRelationships/migrate' -type POST -body $JSONBody -verbose -tenantid $env:TenantID -scope 'https://api.partnercustomeradministration.microsoft.com/.default' - Start-Sleep -Milliseconds 100 - do { - $CheckActive = New-GraphGetRequest -NoAuthCheck $True -uri "https://traf-pcsvcadmin-prod.trafficmanager.net/CustomerServiceAdminApi/Web//v1/delegatedAdminRelationships/$($MigrateRequest.id)" -tenantid $env:TenantID -scope 'https://api.partnercustomeradministration.microsoft.com/.default' - Start-Sleep -Milliseconds 200 - } until ($CheckActive.status -eq 'Active') -} -catch { - $LogRequest['status'] = "Migration Failed. Could not create relationship: $($_.Exception.Message)" - Add-CIPPAzDataTableEntity @Table -Entity $logRequest -Force | Out-Null -} - - -if ($CheckActive.status -eq 'Active') { - $LogRequest['status'] = 'Step 3: GDAP Relationship active. Mapping groups.' - Add-CIPPAzDataTableEntity @Table -Entity $logRequest -Force | Out-Null - foreach ($role in $RoleMappings) { - try { - $Mappingbody = ConvertTo-Json -Depth 10 -InputObject @{ - 'accessContainer' = @{ - 'accessContainerId' = "$($Role.GroupId)" - 'accessContainerType' = 'securityGroup' - } - 'accessDetails' = @{ - 'unifiedRoles' = @(@{ - 'roleDefinitionId' = "$($Role.roleDefinitionId)" - }) - } - } - $RoleActiveID = New-GraphPostRequest -NoAuthCheck $True -uri "https://traf-pcsvcadmin-prod.trafficmanager.net/CustomerServiceAdminApi/Web//v1/delegatedAdminRelationships/$($MigrateRequest.id)/accessAssignments" -tenantid $env:TenantID -type POST -body $MappingBody -verbose -scope 'https://api.partnercustomeradministration.microsoft.com/.default' - Start-Sleep -Milliseconds 400 - $LogRequest['status'] = "Step 3: GDAP Relationship active. Mapping group: $($Role.GroupId)" - Add-CIPPAzDataTableEntity @Table -Entity $logRequest -Force | Out-Null - } - catch { - $LogRequest['status'] = "Migration Failed. Could not create group mapping for group $($role.GroupId): $($_.Exception.Message)" - Add-CIPPAzDataTableEntity @Table -Entity $logRequest -Force | Out-Null - exit 1 - } - #$CheckActiveRole = New-GraphGetRequest -NoAuthCheck $True -uri "https://traf-pcsvcadmin-prod.trafficmanager.net/CustomerServiceAdminApi/Web//v1/delegatedAdminRelationships/$($MigrateRequest.id)/accessAssignments/$($RoleActiveID.id)" -tenantid $env:TenantId -scope 'https://api.partnercustomeradministration.microsoft.com/.default' - } - $LogRequest['status'] = 'Migration Complete' - Add-CIPPAzDataTableEntity @Table -Entity $logRequest -Force | Out-Null - -} - diff --git a/ExecIncidentsListAllTenants/run.ps1 b/ExecIncidentsListAllTenants/run.ps1 index c7222556a39b..480b27d643d9 100644 --- a/ExecIncidentsListAllTenants/run.ps1 +++ b/ExecIncidentsListAllTenants/run.ps1 @@ -6,7 +6,7 @@ Write-Host "PowerShell queue trigger function processed work item: $QueueItem" Get-Tenants | ForEach-Object -Parallel { $domainName = $_.defaultDomainName - Import-Module '.\GraphHelper.psm1' + Import-Module CippCore $Table = Get-CIPPTable -TableName 'cachealertsandincidents' try { @@ -16,26 +16,25 @@ Get-Tenants | ForEach-Object -Parallel { $GraphRequest = @{ Incident = [string]($incident | ConvertTo-Json -Depth 10) RowKey = [string]$GUID - PartitionKey = "Incident" + PartitionKey = 'Incident' Tenant = [string]$domainName } Add-CIPPAzDataTableEntity @Table -Entity $GraphRequest -Force | Out-Null } - } - catch { + } catch { $GUID = (New-Guid).Guid $AlertText = ConvertTo-Json -InputObject @{ Tenant = $domainName displayName = "Could not connect to Tenant: $($_.Exception.Message)" comments = @{ createdDateTime = (Get-Date).ToString('s') - createdbyDisplayName = "CIPP" - comment = "Could not connect" + createdbyDisplayName = 'CIPP' + comment = 'Could not connect' } - classification = "Unknown" - determination = "Unknown" - severity = "CIPP" + classification = 'Unknown' + determination = 'Unknown' + severity = 'CIPP' } $GraphRequest = @{ Incident = [string]$AlertText 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 '', "$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*' { + $Webhook = [PSCustomObject]@{ + 'Tenant' = $tenant + 'TaskInfo' = $QueueItem.TaskInfo + 'Results' = $Results + } + Send-CIPPAlert -Type 'webhook' -Title $title -JSONContent $($Webhook | ConvertTo-Json -Depth 20) + } +} + +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 deleted file mode 100644 index 3f8fe4b97714..000000000000 --- a/GraphHelper.psm1 +++ /dev/null @@ -1,845 +0,0 @@ -function Get-CIPPTable { - [CmdletBinding()] - param ( - $tablename = 'CippLogs' - ) - $Context = New-AzDataTableContext -ConnectionString $env:AzureWebJobsStorage -TableName $tablename - New-AzDataTable -Context $Context | Out-Null - - @{ - Context = $Context - } -} -function Get-NormalizedError { - [CmdletBinding()] - param ( - [string]$message - ) - switch -Wildcard ($message) { - 'Request not applicable to target tenant.' { 'Required license not available for this tenant' } - "Neither tenant is B2C or tenant doesn't have premium license" { 'This feature requires a P1 license or higher' } - 'Response status code does not indicate success: 400 (Bad Request).' { 'Error 400 occured. There is an issue with the token configuration for this tenant. Please perform an access check' } - '*Microsoft.Skype.Sync.Pstn.Tnm.Common.Http.HttpResponseException*' { 'Could not connect to Teams Admin center - Tenant might be missing a Teams license' } - '*Provide valid credential.*' { 'Error 400: There is an issue with your Exchange Token configuration. Please perform an access check for this tenant' } - '*This indicate that a subscription within the tenant has lapsed*' { 'There is no exchange subscription available, or it has lapsed. Check licensing information.' } - '*User was not found.*' { 'The relationship between this tenant and the partner has been dissolved from the tenant side.' } - '*The user or administrator has not consented to use the application*' { 'CIPP cannot access this tenant. Perform a CPV Refresh and Access Check via the settings menu' } - '*AADSTS50020*' { 'AADSTS50020: The user you have used for your Secure Application Model is a guest in this tenant, or your are using GDAP and have not added the user to the correct group. Please delete the guest user to gain access to this tenant' } - '*AADSTS50177' { 'AADSTS50177: The user you have used for your Secure Application Model is a guest in this tenant, or your are using GDAP and have not added the user to the correct group. Please delete the guest user to gain access to this tenant' } - '*invalid or malformed*' { 'The request is malformed. Have you finished the SAM Setup?' } - '*Windows Store repository apps feature is not supported for this tenant*' { 'This tenant does not have WinGet support available' } - '*AADSTS650051*' { 'The application does not exist yet. Try again in 30 seconds.' } - '*AppLifecycle_2210*' { 'Failed to call Intune APIs: Does the tenant have a license available?' } - '*One or more added object references already exist for the following modified properties:*' { 'This user is already a member of this group.' } - '*Microsoft.Exchange.Management.Tasks.MemberAlreadyExistsException*' { 'This user is already a member of this group.' } - '*The property value exceeds the maximum allowed size (64KB)*' { 'One of the values exceeds the maximum allowed size (64KB).' } - Default { $message } - - } -} - -function Get-GraphToken($tenantid, $scope, $AsApp, $AppID, $refreshToken, $ReturnRefresh, $SkipCache) { - if (!$scope) { $scope = 'https://graph.microsoft.com/.default' } - if (!$env:SetFromProfile) { $CIPPAuth = Get-CIPPAuthentication; Write-Host 'Could not get Refreshtoken from environment variable. Reloading token.' } - $AuthBody = @{ - client_id = $env:ApplicationID - client_secret = $env:ApplicationSecret - scope = $Scope - refresh_token = $env:RefreshToken - grant_type = 'refresh_token' - } - if ($asApp -eq $true) { - $AuthBody = @{ - client_id = $env:ApplicationID - client_secret = $env:ApplicationSecret - scope = $Scope - grant_type = 'client_credentials' - } - } - - if ($null -ne $AppID -and $null -ne $refreshToken) { - $AuthBody = @{ - client_id = $appid - refresh_token = $RefreshToken - scope = $Scope - grant_type = 'refresh_token' - } - } - - if (!$tenantid) { $tenantid = $env:TenantID } - - $TokenKey = '{0}-{1}-{2}' -f $tenantid, $scope, $asApp - - try { - if ($script:AccessTokens.$TokenKey -and [int](Get-Date -UFormat %s -Millisecond 0) -lt $script:AccessTokens.$TokenKey.expires_on -and $SkipCache -ne $true) { - Write-Host 'Graph: cached token' - $AccessToken = $script:AccessTokens.$TokenKey - } - 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 - Add-Member -InputObject $AccessToken -NotePropertyName 'expires_on' -NotePropertyValue $ExpiresOn - if (!$script:AccessTokens) { $script:AccessTokens = [HashTable]::Synchronized(@{}) } - $script:AccessTokens.$TokenKey = $AccessToken - } - - if ($ReturnRefresh) { $header = $AccessToken } else { $header = @{ Authorization = "Bearer $($AccessToken.access_token)" } } - return $header - #Write-Host $header['Authorization'] - } - 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 - $Tenant = Get-CIPPAzDataTableEntity @TenantsTable -Filter $Filter - if (!$Tenant.RowKey) { - $donotset = $true - $Tenant = [pscustomobject]@{ - GraphErrorCount = $null - LastGraphTokenError = $null - LastGraphError = $null - PartitionKey = 'TenantFailed' - RowKey = 'Failed' - } - } - $Tenant.LastGraphError = if ( $_.ErrorDetails.Message) { - $msg = $_.ErrorDetails.Message | ConvertFrom-Json - "$($msg.error):$($msg.error_description)" - } - else { - $_.Exception.message - } - $Tenant.GraphErrorCount++ - - if (!$donotset) { Update-AzDataTableEntity @TenantsTable -Entity $Tenant } - throw "Could not get token: $($Tenant.LastGraphError)" - } -} - -function Write-LogMessage ($message, $tenant = 'None', $API = 'None', $tenantId = $null, $user, $sev) { - try { - $username = ([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($user)) | ConvertFrom-Json).userDetails - } - catch { - $username = $user - } - - $Table = Get-CIPPTable -tablename CippLogs - - if (!$tenant) { $tenant = 'None' } - if (!$username) { $username = 'CIPP' } - if ($sev -eq 'Debug' -and $env:DebugMode -ne 'true') { - Write-Information 'Not writing to log file - Debug mode is not enabled.' - return - } - $PartitionKey = (Get-Date -UFormat '%Y%m%d').ToString() - $TableRow = @{ - 'Tenant' = [string]$tenant - 'API' = [string]$API - 'Message' = [string]$message - 'Username' = [string]$username - 'Severity' = [string]$sev - 'SentAsAlert' = $false - 'PartitionKey' = $PartitionKey - 'RowKey' = ([guid]::NewGuid()).ToString() - } - - - if ($tenantId) { - $TableRow.Add('TenantID', [string]$tenantId) - } - - $Table.Entity = $TableRow - Add-CIPPAzDataTableEntity @Table | Out-Null -} - -function New-GraphGetRequest { - Param( - $uri, - $tenantid, - $scope, - $AsApp, - $noPagination, - $NoAuthCheck, - $skipTokenCache, - [switch]$ComplexFilter, - [switch]$CountOnly - ) - - if ($NoAuthCheck -or (Get-AuthorisedRequest -Uri $uri -TenantID $tenantid)) { - if ($scope -eq 'ExchangeOnline') { - $AccessToken = Get-ClassicAPIToken -resource 'https://outlook.office365.com' -Tenantid $tenantid - $headers = @{ Authorization = "Bearer $($AccessToken.access_token)" } - } - else { - $headers = Get-GraphToken -tenantid $tenantid -scope $scope -AsApp $asapp -SkipCache $skipTokenCache - } - - if ($ComplexFilter) { - $headers['ConsistencyLevel'] = 'eventual' - } - $nextURL = $uri - - # Track consecutive Graph API failures - $TenantsTable = Get-CippTable -tablename Tenants - $Filter = "PartitionKey eq 'Tenants' and (defaultDomainName eq '{0}' or customerId eq '{0}')" -f $tenantid - $Tenant = Get-CIPPAzDataTableEntity @TenantsTable -Filter $Filter - if (!$Tenant) { - $Tenant = @{ - GraphErrorCount = 0 - LastGraphError = $null - PartitionKey = 'TenantFailed' - RowKey = 'Failed' - } - } - - $ReturnedData = do { - try { - $Data = (Invoke-RestMethod -Uri $nextURL -Method GET -Headers $headers -ContentType 'application/json; charset=utf-8') - if ($CountOnly) { - $Data.'@odata.count' - $nextURL = $null - } - else { - if ($data.value) { $data.value } else { ($Data) } - if ($noPagination) { $nextURL = $null } else { $nextURL = $data.'@odata.nextLink' } - } - } - 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.' -and $Tenant) { - $Tenant.LastGraphError = $Message - $Tenant.GraphErrorCount++ - Update-AzDataTableEntity @TenantsTable -Entity $Tenant - } - throw $Message - } - } until ($null -eq $NextURL) - $Tenant.LastGraphError = '' - Update-AzDataTableEntity @TenantsTable -Entity $Tenant - return $ReturnedData - } - else { - Write-Error 'Not allowed. You cannot manage your own tenant or tenants not under your scope' - } -} - -function New-GraphPOSTRequest ($uri, $tenantid, $body, $type, $scope, $AsApp, $NoAuthCheck, $skipTokenCache, $AddedHeaders) { - if ($NoAuthCheck -or (Get-AuthorisedRequest -Uri $uri -TenantID $tenantid)) { - $headers = Get-GraphToken -tenantid $tenantid -scope $scope -AsApp $asapp -SkipCache $skipTokenCache - if ($AddedHeaders) { - foreach ($header in $AddedHeaders.getenumerator()) { - $headers.Add($header.Key, $header.Value) - } - } - Write-Verbose "Using $($uri) as url" - if (!$type) { - $type = 'POST' - } - - try { - $ReturnedData = (Invoke-RestMethod -Uri $($uri) -Method $TYPE -Body $body -Headers $headers -ContentType 'application/json; charset=utf-8') - } - catch { - $ErrorMess = $($_.Exception.Message) - $Message = ($_.ErrorDetails.Message | ConvertFrom-Json -ErrorAction SilentlyContinue).error.message - if (!$Message) { $Message = $ErrorMess } - throw $Message - } - return $ReturnedData - } - else { - Write-Error 'Not allowed. You cannot manage your own tenant or tenants not under your scope' - } -} - -function convert-skuname($skuname, $skuID) { - Set-Location (Get-Item $PSScriptRoot).FullName - $ConvertTable = Import-Csv Conversiontable.csv - if ($skuname) { $ReturnedName = ($ConvertTable | Where-Object { $_.String_Id -eq $skuname } | Select-Object -Last 1).'Product_Display_Name' } - if ($skuID) { $ReturnedName = ($ConvertTable | Where-Object { $_.guid -eq $skuid } | Select-Object -Last 1).'Product_Display_Name' } - if ($ReturnedName) { return $ReturnedName } else { return $skuname, $skuID } -} - -function Get-ClassicAPIToken($tenantID, $Resource) { - $TokenKey = '{0}-{1}' -f $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 { - Write-Host 'Using classic' - $uri = "https://login.microsoftonline.com/$($TenantID)/oauth2/token" - $Body = @{ - client_id = $env:ApplicationID - client_secret = $env:ApplicationSecret - resource = $Resource - refresh_token = $env:RefreshToken - grant_type = 'refresh_token' - } - try { - 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 { - # Track consecutive Graph API failures - $TenantsTable = Get-CippTable -tablename Tenants - $Filter = "PartitionKey eq 'Tenants' and (defaultDomainName eq '{0}' or customerId eq '{0}')" -f $tenantid - $Tenant = Get-CIPPAzDataTableEntity @TenantsTable -Filter $Filter - if (!$Tenant) { - $Tenant = @{ - GraphErrorCount = $null - LastGraphTokenError = $null - LastGraphError = $null - PartitionKey = 'TenantFailed' - RowKey = 'Failed' - } - } - $Tenant.LastGraphError = $_.Exception.Message - $Tenant.GraphErrorCount++ - - Update-AzDataTableEntity @TenantsTable -Entity $Tenant - Throw "Failed to obtain Classic API Token for $TenantID - $_" - } - } -} - -function New-TeamsAPIGetRequest($Uri, $tenantID, $Method = 'GET', $Resource = '48ac35b8-9aa8-4d74-927d-1f4a14a0b239', $ContentType = 'application/json') { - - if ((Get-AuthorisedRequest -Uri $uri -TenantID $tenantid)) { - $token = Get-ClassicAPIToken -Tenant $tenantid -Resource $Resource - - $NextURL = $Uri - $ReturnedData = do { - try { - $Data = Invoke-RestMethod -ContentType "$ContentType;charset=UTF-8" -Uri $NextURL -Method $Method -Headers @{ - Authorization = "Bearer $($token.access_token)"; - 'x-ms-client-request-id' = [guid]::NewGuid().ToString(); - 'x-ms-client-session-id' = [guid]::NewGuid().ToString() - 'x-ms-correlation-id' = [guid]::NewGuid() - 'X-Requested-With' = 'XMLHttpRequest' - 'x-ms-tnm-applicationid' = '045268c0-445e-4ac1-9157-d58f67b167d9' - - } - $Data - if ($noPagination) { $nextURL = $null } else { $nextURL = $data.NextLink } - } - catch { - throw "Failed to make Teams API Get Request $_" - } - } until ($null -eq $NextURL) - return $ReturnedData - } - else { - Write-Error 'Not allowed. You cannot manage your own tenant or tenants not under your scope' - } -} - -function New-ClassicAPIGetRequest($TenantID, $Uri, $Method = 'GET', $Resource = 'https://admin.microsoft.com', $ContentType = 'application/json') { - - if ((Get-AuthorisedRequest -Uri $uri -TenantID $tenantid)) { - $token = Get-ClassicAPIToken -Tenant $tenantID -Resource $Resource - - $NextURL = $Uri - $ReturnedData = do { - try { - $Data = Invoke-RestMethod -ContentType "$ContentType;charset=UTF-8" -Uri $NextURL -Method $Method -Headers @{ - Authorization = "Bearer $($token.access_token)"; - 'x-ms-client-request-id' = [guid]::NewGuid().ToString(); - 'x-ms-client-session-id' = [guid]::NewGuid().ToString() - 'x-ms-correlation-id' = [guid]::NewGuid() - 'X-Requested-With' = 'XMLHttpRequest' - } - $Data - if ($noPagination) { $nextURL = $null } else { $nextURL = $data.NextLink } - } - catch { - throw "Failed to make Classic Get Request $_" - } - } until ($null -eq $NextURL) - return $ReturnedData - } - else { - Write-Error 'Not allowed. You cannot manage your own tenant or tenants not under your scope' - } -} - -function New-ClassicAPIPostRequest($TenantID, $Uri, $Method = 'POST', $Resource = 'https://admin.microsoft.com', $Body) { - - if ((Get-AuthorisedRequest -Uri $uri -TenantID $tenantid)) { - $token = Get-ClassicAPIToken -Tenant $tenantID -Resource $Resource - try { - $ReturnedData = Invoke-RestMethod -ContentType 'application/json;charset=UTF-8' -Uri $Uri -Method $Method -Body $Body -Headers @{ - Authorization = "Bearer $($token.access_token)"; - 'x-ms-client-request-id' = [guid]::NewGuid().ToString(); - 'x-ms-client-session-id' = [guid]::NewGuid().ToString() - 'x-ms-correlation-id' = [guid]::NewGuid() - 'X-Requested-With' = 'XMLHttpRequest' - 'X-RequestForceAuthentication' = $true - - } - - } - catch { - throw "Failed to make Classic Get Request $_" - } - return $ReturnedData - } - else { - Write-Error 'Not allowed. You cannot manage your own tenant or tenants not under your scope' - } -} - -function Get-AuthorisedRequest { - [CmdletBinding()] - Param( - [string]$TenantID, - [string]$Uri - ) - if (!$TenantID) { - $TenantID = $env:TenantId - } - if ($Uri -like 'https://graph.microsoft.com/beta/contracts*' -or $Uri -like '*/customers/*' -or $Uri -eq 'https://graph.microsoft.com/v1.0/me/sendMail' -or $Uri -like '*/tenantRelationships/*') { - return $true - } - $Tenants = Get-Tenants -IncludeErrors - $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 { - return $false - } -} - - -function Get-Tenants { - param ( - [Parameter( ParameterSetName = 'Skip', Mandatory = $True )] - [switch]$SkipList, - [Parameter( ParameterSetName = 'Standard')] - [switch]$IncludeAll, - [switch]$IncludeErrors - ) - - $TenantsTable = Get-CippTable -tablename 'Tenants' - $ExcludedFilter = "PartitionKey eq 'Tenants' and Excluded eq true" - - $SkipListCache = Get-CIPPAzDataTableEntity @TenantsTable -Filter $ExcludedFilter - if ($SkipList) { - return $SkipListCache - } - - if ($IncludeAll.IsPresent) { - $Filter = "PartitionKey eq 'Tenants'" - } - elseif ($IncludeErrors.IsPresent) { - $Filter = "PartitionKey eq 'Tenants' and Excluded eq false" - } - else { - $Filter = "PartitionKey eq 'Tenants' and Excluded eq false and GraphErrorCount lt 50" - } - $IncludedTenantsCache = Get-CIPPAzDataTableEntity @TenantsTable -Filter $Filter - - 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 { - $LastRefresh = $false - } - if (!$LastRefresh -or $LastRefresh -lt (Get-Date).Addhours(-24).ToUniversalTime()) { - try { - 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 { $_.defaultDomainName -NotIn $SkipListCache.defaultDomainName -and $_.defaultDomainName -ne $null } - - } - catch { - Write-Host "Get-Tenants - Lighthouse Error, using contract/delegatedAdminRelationship calls. Error: $($_.Exception.Message)" - [System.Collections.Generic.List[PSCustomObject]]$BulkRequests = @( - @{ - id = 'Contracts' - method = 'GET' - url = "/contracts?`$top=999" - }, - @{ - id = 'GDAPRelationships' - method = 'GET' - url = '/tenantRelationships/delegatedAdminRelationships' - } - ) - - $BulkResults = New-GraphBulkRequest -Requests $BulkRequests -tenantid $TenantFilter -NoAuthCheck:$true - $Contracts = Get-GraphBulkResultByID -Results $BulkResults -ID 'Contracts' -Value - $GDAPRelationships = Get-GraphBulkResultByID -Results $BulkResults -ID 'GDAPRelationships' -Value - - $ContractList = $Contracts | Select-Object id, customerId, DefaultdomainName, DisplayName, domains, @{l = 'MigratedToNewTenantAPI'; e = { $true } }, @{ n = 'delegatedPrivilegeStatus'; exp = { $CustomerId = $_.customerId; if (($GDAPRelationships | Where-Object { $_.customer.tenantId -EQ $CustomerId -and $_.status -EQ 'active' } | Measure-Object).Count -gt 0) { 'delegatedAndGranularDelegetedAdminPrivileges' } else { 'delegatedAdminPrivileges' } } } | Where-Object -Property defaultDomainName -NotIn $SkipListCache.defaultDomainName - - $GDAPOnlyList = $GDAPRelationships | Where-Object { $_.status -eq 'active' -and $Contracts.customerId -notcontains $_.customer.tenantId } | Select-Object id, @{l = 'customerId'; e = { $($_.customer.tenantId) } }, @{l = 'defaultDomainName'; e = { (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/tenantRelationships/findTenantInformationByTenantId(tenantId='$($_.customer.tenantId)')" -noauthcheck $true -asApp:$true -tenant $env:TenantId).defaultDomainName } }, @{l = 'MigratedToNewTenantAPI'; e = { $true } }, @{n = 'displayName'; exp = { $_.customer.displayName } }, domains, @{n = 'delegatedPrivilegeStatus'; exp = { 'granularDelegatedAdminPrivileges' } } | Where-Object { $_.defaultDomainName -NotIn $SkipListCache.defaultDomainName -and $_.defaultDomainName -ne $null } | Sort-Object -Property customerId -Unique - - $TenantList = @($ContractList) + @($GDAPOnlyList) - } - <#if (!$TenantList.customerId) { - $TenantList = (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/contracts?`$top=999" -tenantid $env:TenantID ) | Select-Object id, customerId, DefaultdomainName, DisplayName, domains | Where-Object -Property defaultDomainName -NotIn $SkipListCache.defaultDomainName - }#> - $IncludedTenantsCache = [system.collections.generic.list[hashtable]]::new() - if ($env:PartnerTenantAvailable) { - $IncludedTenantsCache.Add(@{ - RowKey = $env:TenantID - PartitionKey = 'Tenants' - customerId = $env:TenantID - defaultDomainName = $env:TenantID - displayName = '*Partner Tenant' - domains = 'PartnerTenant' - Excluded = $false - ExcludeUser = '' - ExcludeDate = '' - GraphErrorCount = 0 - LastGraphError = '' - LastRefresh = (Get-Date).ToUniversalTime() - }) | Out-Null - } - foreach ($Tenant in $TenantList) { - if ($Tenant.defaultDomainName -eq 'Invalid' -or !$Tenant.defaultDomainName) { continue } - $IncludedTenantsCache.Add(@{ - RowKey = [string]$Tenant.customerId - PartitionKey = 'Tenants' - customerId = [string]$Tenant.customerId - defaultDomainName = [string]$Tenant.defaultDomainName - displayName = [string]$Tenant.DisplayName - delegatedPrivilegeStatus = [string]$Tenant.delegatedPrivilegeStatus - domains = '' - Excluded = $false - ExcludeUser = '' - ExcludeDate = '' - GraphErrorCount = 0 - LastGraphError = '' - LastRefresh = (Get-Date).ToUniversalTime() - }) | Out-Null - } - - if ($IncludedTenantsCache) { - $TenantsTable.Force = $true - Add-CIPPAzDataTableEntity @TenantsTable -Entity $IncludedTenantsCache - } - } - return ($IncludedTenantsCache | Where-Object -Property defaultDomainName -NE $null | Sort-Object -Property displayName) - -} - -function Remove-CIPPCache { - param ( - $TenantsOnly - ) - # Remove all tenants except excluded - $TenantsTable = Get-CippTable -tablename 'Tenants' - $Filter = "PartitionKey eq 'Tenants' and Excluded eq false" - $ClearIncludedTenants = Get-CIPPAzDataTableEntity @TenantsTable -Filter $Filter - Remove-AzDataTableEntity @TenantsTable -Entity $ClearIncludedTenants - if ($tenantsonly -eq 'false') { - Write-Host 'Clearing all' - # Remove Domain Analyser cached results - $DomainsTable = Get-CippTable -tablename 'Domains' - $Filter = "PartitionKey eq 'TenantDomains'" - $ClearDomainAnalyserRows = Get-CIPPAzDataTableEntity @DomainsTable -Filter $Filter | ForEach-Object { - $_.DomainAnalyser = '' - $_ - } - Update-AzDataTableEntity @DomainsTable -Entity $ClearDomainAnalyserRows - #Clear BPA - $BPATable = Get-CippTable -tablename 'cachebpa' - $ClearBPARows = Get-CIPPAzDataTableEntity @BPATable - Remove-AzDataTableEntity @BPATable -Entity $ClearBPARows - $ENV:SetFromProfile = $null - $Script:SkipListCache = $Null - $Script:SkipListCacheEmpty = $Null - $Script:IncludedTenantsCache = $Null - } -} - -function New-ExoRequest ($tenantid, $cmdlet, $cmdParams, $useSystemMailbox, $Anchor, $NoAuthCheck, $Select) { - - if ((Get-AuthorisedRequest -TenantID $tenantid) -or $NoAuthCheck -eq $True) { - $token = Get-ClassicAPIToken -resource 'https://outlook.office365.com' -Tenantid $tenantid - $tenant = (get-tenants -IncludeErrors | Where-Object { $_.defaultDomainName -eq $tenantid -or $_.customerId -eq $tenantid }).customerId - if ($cmdParams) { - $Params = $cmdParams - } - else { - $Params = @{} - } - $ExoBody = ConvertTo-Json -Depth 5 -InputObject @{ - CmdletInput = @{ - CmdletName = $cmdlet - Parameters = $Params - } - } - if (!$Anchor) { - if ($cmdparams.Identity) { $Anchor = $cmdparams.Identity } - if ($cmdparams.anr) { $Anchor = $cmdparams.anr } - if ($cmdparams.User) { $Anchor = $cmdparams.User } - - if (!$Anchor -or $useSystemMailbox) { - $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{8cc370d3-822a-4ab8-a926-bb94bd0641a9}@$($OnMicrosoft)" - - - } - } - Write-Host "Using $Anchor" - $Headers = @{ - Authorization = "Bearer $($token.access_token)" - Prefer = 'odata.maxpagesize = 1000' - 'parameter-based-routing' = $true - 'X-AnchorMailbox' = $anchor - - } - try { - if ($Select) { $Select = "`$select=$Select" } - $URL = "https://outlook.office365.com/adminapi/beta/$($tenant)/InvokeCommand?$Select" - - $ReturnedData = - do { - $Return = Invoke-RestMethod $URL -Method POST -Body $ExoBody -Headers $Headers -ContentType 'application/json; charset=utf-8' - $URL = $Return.'@odata.nextLink' - $Return - } until ($null -eq $URL) - - if ($ReturnedData.'@adminapi.warnings' -and $ReturnedData.value -eq $null) { - $ReturnedData.value = $ReturnedData.'@adminapi.warnings' - } - } - 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 } - else { $ReportedError.error.innererror.internalException.message } - if ($null -eq $Message) { $Message = $ErrorMess } - throw $Message - } - return $ReturnedData.value - } - else { - Write-Error 'Not allowed. You cannot manage your own tenant or tenants not under your scope' - } -} - -function Read-JwtAccessDetails { - <# - .SYNOPSIS - Parse Microsoft JWT access tokens - - .DESCRIPTION - Extract JWT access token details for verification - - .PARAMETER Token - Token to get details for - - #> - [cmdletbinding()] - param( - [Parameter(Mandatory = $true)] - [string]$Token - ) - - # Default token object - $TokenDetails = [PSCustomObject]@{ - AppId = '' - AppName = '' - Audience = '' - AuthMethods = '' - IPAddress = '' - Name = '' - Scope = '' - TenantId = '' - UserPrincipalName = '' - } - - if (!$Token.Contains('.') -or !$token.StartsWith('eyJ')) { return $TokenDetails } - - # Get token payload - $tokenPayload = $token.Split('.')[1].Replace('-', '+').Replace('_', '/') - while ($tokenPayload.Length % 4) { - $tokenPayload = '{0}=' -f $tokenPayload - } - - # Convert base64 to json to object - $tokenByteArray = [System.Convert]::FromBase64String($tokenPayload) - $tokenArray = [System.Text.Encoding]::ASCII.GetString($tokenByteArray) - $TokenObj = $tokenArray | ConvertFrom-Json - - # Convert token details to human readable - $TokenDetails.AppId = $TokenObj.appid - $TokenDetails.AppName = $TokenObj.app_displayname - $TokenDetails.Audience = $TokenObj.aud - $TokenDetails.AuthMethods = $TokenObj.amr - $TokenDetails.IPAddress = $TokenObj.ipaddr - $TokenDetails.Name = $TokenObj.name - $TokenDetails.Scope = $TokenObj.scp -split ' ' - $TokenDetails.TenantId = $TokenObj.tid - $TokenDetails.UserPrincipalName = $TokenObj.upn - - return $TokenDetails -} - -function Get-CIPPMSolUsers { - [CmdletBinding()] - param ( - [string]$tenant - ) - $AADGraphtoken = (Get-GraphToken -scope 'https://graph.windows.net/.default') - $tenantid = (get-tenants | Where-Object -Property defaultDomainName -EQ $tenant).customerId - $TrackingGuid = (New-Guid).GUID - $LogonPost = @" -http://provisioning.microsoftonline.com/IProvisioningWebService/MsolConnecturn:uuid:$TrackingGuidhttp://www.w3.org/2005/08/addressing/anonymous$($AADGraphtoken['Authorization'])50afce61-c917-435b-8c6d-60aa5a8b8aa71.2.183.57Version47$($TrackingGuid)https://provisioningapi.microsoftonline.com/provisioningwebservice.svcVersion4 -"@ - $DataBlob = (Invoke-RestMethod -Method POST -Uri 'https://provisioningapi.microsoftonline.com/provisioningwebservice.svc' -ContentType 'application/soap+xml; charset=utf-8' -Body $LogonPost).envelope.header.BecContext.DataBlob.'#text' - - $MSOLXML = @" -http://provisioning.microsoftonline.com/IProvisioningWebService/ListUsersurn:uuid:$TrackingGuidhttp://www.w3.org/2005/08/addressing/anonymous$($AADGraphtoken['Authorization'])$DataBlob250afce61-c917-435b-8c6d-60aa5a8b8aa71.2.183.57Version474e6cb653-c968-4a3a-8a11-2c8919218aebhttps://provisioningapi.microsoftonline.com/provisioningwebservice.svcVersion16$($tenantid)500AscendingNone -"@ - $userlist = do { - 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 { - $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 - } - $MSOLXML = @" -http://provisioning.microsoftonline.com/IProvisioningWebService/NavigateUserResultsurn:uuid:$TrackingGuidhttp://www.w3.org/2005/08/addressing/anonymous$($AADGraphtoken['Authorization'])$DataBlob13050afce61-c917-435b-8c6d-60aa5a8b8aa71.2.183.57Version47$($TrackingGuid)https://provisioningapi.microsoftonline.com/provisioningwebservice.svcVersion16$($tenantid)$($page.listcontext)Next -"@ - } until ($page.IsLastPage -eq $true -or $null -eq $page) - return $userlist -} - -function New-DeviceLogin { - [CmdletBinding()] - param ( - [string]$clientid, - [string]$scope, - [switch]$FirstLogon, - [string]$device_code, - [string]$TenantId - ) - $encodedscope = [uri]::EscapeDataString($scope) - if ($FirstLogon) { - 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 { - $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 { - $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 { - $returncode = $Checking.error - } - } - return $ReturnCode -} - -function New-passwordString { - [CmdletBinding()] - param ( - [int]$count = 12 - ) - Set-Location (Get-Item $PSScriptRoot).FullName - $SettingsTable = Get-CippTable -tablename 'Settings' - $PasswordType = (Get-CIPPAzDataTableEntity @SettingsTable).passwordType - if ($PasswordType -eq 'Correct-Battery-Horse') { - $Words = Get-Content .\words.txt - (Get-Random -InputObject $words -Count 4) -join '-' - } - else { - -join ('abcdefghkmnrstuvwxyzABCDEFGHKLMNPRSTUVWXYZ23456789$%&*#'.ToCharArray() | Get-Random -Count $count) - } -} - -function New-GraphBulkRequest { - Param( - $tenantid, - $NoAuthCheck, - $scope, - $asapp, - $Requests - ) - - if ($NoAuthCheck -or (Get-AuthorisedRequest -Uri $uri -TenantID $tenantid)) { - $headers = Get-GraphToken -tenantid $tenantid -scope $scope -AsApp $asapp - - $URL = 'https://graph.microsoft.com/beta/$batch' - - # Track consecutive Graph API failures - $TenantsTable = Get-CippTable -tablename Tenants - $Filter = "PartitionKey eq 'Tenants' and (defaultDomainName eq '{0}' or customerId eq '{0}')" -f $tenantid - $Tenant = Get-CIPPAzDataTableEntity @TenantsTable -Filter $Filter - if (!$Tenant) { - $Tenant = @{ - GraphErrorCount = 0 - LastGraphError = $null - PartitionKey = 'TenantFailed' - RowKey = 'Failed' - } - } - try { - $ReturnedData = for ($i = 0; $i -lt $Requests.count; $i += 20) { - $req = @{} - # Use select to create hashtables of id, method and url for each call - $req['requests'] = ($Requests[$i..($i + 19)]) - Invoke-RestMethod -Uri $URL -Method POST -Headers $headers -ContentType 'application/json; charset=utf-8' -Body ($req | ConvertTo-Json -Depth 10) - } - - foreach ($MoreData in $ReturnedData.Responses | Where-Object { $_.body.'@odata.nextLink' }) { - Write-Host 'Getting more' - $AdditionalValues = New-GraphGetRequest -ComplexFilter -uri $MoreData.body.'@odata.nextLink' -tenantid $tenantid -NoAuthCheck:$NoAuthCheck - $NewValues = [System.Collections.Generic.List[PSCustomObject]]$MoreData.body.value - $AdditionalValues | ForEach-Object { $NewValues.add($_) } - $MoreData.body.value = $NewValues - } - - } - 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.') { - $Tenant.LastGraphError = $Message - $Tenant.GraphErrorCount++ - Update-AzDataTableEntity @TenantsTable -Entity $Tenant - } - throw $Message - } - - $Tenant.LastGraphError = '' - Update-AzDataTableEntity @TenantsTable -Entity $Tenant - - return $ReturnedData.responses - } - else { - Write-Error 'Not allowed. You cannot manage your own tenant or tenants not under your scope' - } -} - -function Get-GraphBulkResultByID ($Results, $ID, [switch]$Value) { - if ($Value) { - ($Results | Where-Object { $_.id -eq $ID }).body.value - } - else { - ($Results | Where-Object { $_.id -eq $ID }).body - } -} diff --git a/ListGenericAllTenants/run.ps1 b/ListGenericAllTenants/run.ps1 index df23529c61d7..d51627ab7aea 100644 --- a/ListGenericAllTenants/run.ps1 +++ b/ListGenericAllTenants/run.ps1 @@ -12,7 +12,7 @@ Get-CIPPAzDataTableEntity @Table | Remove-AzDataTableEntity @table $RawGraphRequest = Get-Tenants | ForEach-Object -Parallel { $domainName = $_.defaultDomainName - Import-Module '.\GraphHelper.psm1' + Import-Module CippCore try { Write-Host $using:fullUrl New-GraphGetRequest -uri $using:fullUrl -tenantid $_.defaultDomainName -ComplexFilter -ErrorAction Stop | Select-Object *, @{l = 'Tenant'; e = { $domainName } }, @{l = 'CippStatus'; e = { 'Good' } } diff --git a/ListLicensesAllTenants/run.ps1 b/ListLicensesAllTenants/run.ps1 index e7cdf761879e..abc69465c094 100644 --- a/ListLicensesAllTenants/run.ps1 +++ b/ListLicensesAllTenants/run.ps1 @@ -6,7 +6,6 @@ Write-Host "PowerShell queue trigger function processed work item: $QueueItem" $RawGraphRequest = Get-Tenants | ForEach-Object -Parallel { $domainName = $_.defaultDomainName - Import-Module '.\GraphHelper.psm1' Import-Module '.\Modules\AzBobbyTables' Import-Module '.\Modules\CIPPCore' try { diff --git a/ListMFAUsersAllTenants/run.ps1 b/ListMFAUsersAllTenants/run.ps1 index f34fb82921f1..8ab611e514fe 100644 --- a/ListMFAUsersAllTenants/run.ps1 +++ b/ListMFAUsersAllTenants/run.ps1 @@ -13,7 +13,6 @@ try { $GraphRequest = Get-Tenants | ForEach-Object -Parallel { $domainName = $_.defaultDomainName - Import-Module '.\GraphHelper.psm1' Import-Module '.\modules\CippCore' $Table = Get-CIPPTable -TableName cachemfa Try { diff --git a/ListMailboxRulesAllTenants/run.ps1 b/ListMailboxRulesAllTenants/run.ps1 index f47e6b1b105a..28782db5e6cb 100644 --- a/ListMailboxRulesAllTenants/run.ps1 +++ b/ListMailboxRulesAllTenants/run.ps1 @@ -13,7 +13,6 @@ else { } $Tenants | ForEach-Object -Parallel { $domainName = $_.defaultDomainName - Import-Module '.\GraphHelper.psm1' Import-Module '.\Modules\CIPPcore' try { diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecAlertsListAllTenants.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecAlertsListAllTenants.ps1 index 7601d884a0e6..fc14d337dc28 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecAlertsListAllTenants.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecAlertsListAllTenants.ps1 @@ -11,7 +11,6 @@ Function Invoke-ExecAlertsListAllTenants { Get-Tenants | ForEach-Object -Parallel { $domainName = $_.defaultDomainName - Import-Module '.\GraphHelper.psm1' Import-Module '.\Modules\AzBobbyTables' Import-Module '.\Modules\CIPPCore' $Table = Get-CIPPTable -TableName 'cachealertsandincidents' diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecEmailForward.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecEmailForward.ps1 index 70b27c14bfb0..f9b05a8f662a 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecEmailForward.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecEmailForward.ps1 @@ -9,6 +9,7 @@ Function Invoke-ExecEmailForward { param($Request, $TriggerMetadata) $Tenantfilter = $request.body.tenantfilter + $username = $request.body.userid $ForwardingAddress = $request.body.ForwardInternal.value $ForwardingSMTPAddress = $request.body.ForwardExternal $DisableForwarding = $request.body.disableForwarding diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecGraphRequest.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecGraphRequest.ps1 index 684d6f15e987..7ebe9d3aa714 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecGraphRequest.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecGraphRequest.ps1 @@ -79,7 +79,6 @@ Function Invoke-ExecGraphRequest { $RawGraphRequest = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/$($Request.Query.Endpoint)" -tenantid $TenantFilter -NoPagination [boolean]$Request.query.DisablePagination -ComplexFilter } else { $RawGraphRequest = Get-Tenants | ForEach-Object -Parallel { - Import-Module '.\GraphHelper.psm1' Import-Module '.\Modules\AzBobbyTables' Import-Module '.\Modules\CIPPCore' try { diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecIncidentsListAllTenants.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecIncidentsListAllTenants.ps1 index af1f94efd727..25e24aa519ec 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecIncidentsListAllTenants.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecIncidentsListAllTenants.ps1 @@ -11,7 +11,6 @@ Function Invoke-ExecIncidentsListAllTenants { Get-Tenants | ForEach-Object -Parallel { $domainName = $_.defaultDomainName - Import-Module '.\GraphHelper.psm1' Import-Module '.\Modules\AzBobbyTables' Import-Module '.\Modules\CIPPCore' $Table = Get-CIPPTable -TableName 'cachealertsandincidents' diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecOffboardTenant.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecOffboardTenant.ps1 index 5383a20c2087..8a43ad253d09 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecOffboardTenant.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecOffboardTenant.ps1 @@ -45,6 +45,63 @@ Function Invoke-ExecOffboardTenant { } } + if ($request.body.RemoveCSPnotificationContacts) { + Write-Host "DO WE GET HERE?" + # Remove all email adresses that match the CSP tenants domains from the contact properties in /organization + try { + try { + $domains = (New-GraphGETRequest -Uri "https://graph.microsoft.com/v1.0/domains?`$select=id" -tenantid $env:TenantID -NoAuthCheck:$true).id + } catch { + throw "Failed to retrieve CSP domains: $($_.Exception.message)" + } + + try { + # Get /organization data + $orgContacts = New-GraphGETRequest -Uri "https://graph.microsoft.com/v1.0/organization?`$select=id,marketingNotificationEmails,securityComplianceNotificationMails,technicalNotificationMails" -tenantid $TenantFilter + + } catch { + throw "Failed to retrieve CSP domains: $($_.Exception.message)" + } + } catch { + $errors.Add("$($_.Exception.message)") + } + + # foreach through the properties we want to check/update + @('marketingNotificationEmails','securityComplianceNotificationMails','technicalNotificationMails') | ForEach-Object { + $property = $_ + $propertyContacts = $orgContacts.($($property)) + + if ($propertyContacts -AND ($domains -notcontains ($propertyContacts | ForEach-Object { $_.Split("@")[1] }))) { + $newPropertyContent = [System.Collections.Generic.List[object]]($propertyContacts | Where-Object { $domains -notcontains $_.Split("@")[1] }) + + $patchContactBody = if (!($newPropertyContent)) { "{ `"$($property)`" : [] }" } else { [pscustomobject]@{ $property = $newPropertyContent } | ConvertTo-Json } + + try { + New-GraphPostRequest -type PATCH -body $patchContactBody -Uri "https://graph.microsoft.com/v1.0/organization/$($orgContacts.id)" -tenantid $Tenantfilter -ContentType "application/json" + $results.Add("Succesfully removed notification contacts from $($property): $(($propertyContacts | Where-Object { $domains -contains $_.Split("@")[1] }))") + } catch { + $errors.Add("Failed to update property $($property): $($_.Exception.message)") + } + } else { + $results.Add("No notification contacts found in $($property)") + } + } + # Add logic for privacyProfile later - rvdwegen + + } + + if ($request.body.RemoveMSPvendorApps) { + # 9fcfb031-1bf6-4848-8732-5573fd64fc09 - Augmentt + # 9359814a-7403-4af9-9113-d5c8cab020ed - Rewst CSP connector + # 06bfda05-2d5e-4b3b-ac5d-79f07e402973 - Rewst Prod + # c19d36e8-6537-4998-9872-ea8b962bd0b6 - Rewst Azure Integration + # d7db2a1c-c38b-4bd1-a30f-0915167ba928 - Datto Backupify/Saas Protection + # 0c3cdc94-15ba-4b89-9222-29f599727b1c - AutoTask Client Portal SSO + # 62603940-b9b0-454f-b138-eb8d571f21d3 - Eshgro Smarter 365? + # Possible others, Scapmann, PatchMyPC, Datto M365 management, Kaseya crap, Exclaimer(?), HP, Lenovo, Dell, Apple(???), resellers(all region tenants?), Action1, Liquit + # Current idea, do a filtered serviceprincipals request based on the appOwner tenantids of known MSP vendors, load that data into a multi-select on the GUI + } + # All customer tenant specific actions ALWAYS have to be completed before this action! if ($request.body.RemoveMultitenantApps) { # Remove multi-tenant apps with the CSP tenant as origin diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecSetMailboxQuota.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecSetMailboxQuota.ps1 index ceee9e54b069..f2059365139c 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecSetMailboxQuota.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecSetMailboxQuota.ps1 @@ -30,13 +30,13 @@ Function Invoke-ExecSetMailboxQuota { Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message "Changed IssueWarningQuota for $username - $($message)" -Sev 'Info' -tenant $TenantFilter } } catch { - Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message "Could not add OOO for $($username)" -Sev 'Error' -tenant $TenantFilter - "Could not add out of office message for $($username). Error: $($_.Exception.Message)" + Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message "Could not adjust mailbox quota for $($username)" -Sev 'Error' -tenant $TenantFilter + "Could not adjust mailbox quota for $($username). Error: $($_.Exception.Message)" } $body = [pscustomobject]@{'Results' = @($results) } } catch { - $body = [pscustomobject]@{'Results' = @("Could not set Out of Office user: $($_.Exception.message)") } + $body = [pscustomobject]@{'Results' = @("Could not adjust mailbox quota: $($_.Exception.message)") } } # Associate values to output bindings by calling 'Push-OutputBinding'. diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListBasicAuthAllTenants.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListBasicAuthAllTenants.ps1 index f5a00e9231d2..e4be4bafbb02 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListBasicAuthAllTenants.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListBasicAuthAllTenants.ps1 @@ -11,7 +11,6 @@ Function Invoke-ListBasicAuthAllTenants { Get-Tenants | ForEach-Object -Parallel { $domainName = $_.defaultDomainName - Import-Module '.\GraphHelper.psm1' Import-Module '.\Modules\AzBobbyTables' Import-Module '.\Modules\CIPPCore' diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListGenericAllTenants.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListGenericAllTenants.ps1 index c71b75dd3106..00ddc0156cbb 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListGenericAllTenants.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListGenericAllTenants.ps1 @@ -17,7 +17,6 @@ Function Invoke-ListGenericAllTenants { $RawGraphRequest = Get-Tenants | ForEach-Object -Parallel { $domainName = $_.defaultDomainName - Import-Module '.\GraphHelper.psm1' Import-Module '.\Modules\AzBobbyTables' Import-Module '.\Modules\CIPPCore' try { diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListLicensesAllTenants.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListLicensesAllTenants.ps1 index 84264636823a..7d6c25d9daa7 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListLicensesAllTenants.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListLicensesAllTenants.ps1 @@ -12,7 +12,6 @@ Function Invoke-ListLicensesAllTenants { $RawGraphRequest = Get-Tenants | ForEach-Object -Parallel { $domainName = $_.defaultDomainName - Import-Module '.\GraphHelper.psm1' Import-Module '.\Modules\AzBobbyTables' Import-Module '.\Modules\CIPPCore' try { diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListMFAUsersAllTenants.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListMFAUsersAllTenants.ps1 index 5a6a68ee54df..8123a963e1ff 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListMFAUsersAllTenants.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListMFAUsersAllTenants.ps1 @@ -18,7 +18,6 @@ Function Invoke-ListMFAUsersAllTenants { $GraphRequest = Get-Tenants | ForEach-Object -Parallel { $domainName = $_.defaultDomainName - Import-Module '.\GraphHelper.psm1' Import-Module '.\modules\CippCore' Import-Module '.\Modules\AzBobbyTables' diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListMailboxRulesAllTenants.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListMailboxRulesAllTenants.ps1 index f173ab9375be..ee394cb28adc 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListMailboxRulesAllTenants.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListMailboxRulesAllTenants.ps1 @@ -17,14 +17,12 @@ Function Invoke-ListMailboxRulesAllTenants { } $Tenants | ForEach-Object -Parallel { $domainName = $_.defaultDomainName - Import-Module '.\GraphHelper.psm1' Import-Module '.\Modules\CIPPcore' Import-Module '.\Modules\AzBobbyTables' try { $Rules = New-ExoRequest -tenantid $domainName -cmdlet 'Get-Mailbox' | ForEach-Object -Parallel { - Import-Module '.\GraphHelper.psm1' Import-Module '.\Modules\AzBobbyTables' Import-Module '.\Modules\CIPPCore' New-ExoRequest -Anchor $_.UserPrincipalName -tenantid $domainName -cmdlet 'Get-InboxRule' -cmdParams @{Mailbox = $_.GUID } diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListServiceHealth.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListServiceHealth.ps1 index bb25ad57b542..48144aa28668 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListServiceHealth.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListServiceHealth.ps1 @@ -15,7 +15,6 @@ Function Invoke-ListServiceHealth { $ResultHealthSummary = Get-Tenants | ForEach-Object -Parallel { - Import-Module '.\GraphHelper.psm1' Import-Module '.\Modules\AzBobbyTables' Import-Module '.\Modules\CIPPCore' $tenantname = $_.displayName diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListUserConditionalAccessPolicies.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListUserConditionalAccessPolicies.ps1 index d3e6e3295593..1557184a8bb6 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListUserConditionalAccessPolicies.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListUserConditionalAccessPolicies.ps1 @@ -21,7 +21,6 @@ Function Invoke-ListUserConditionalAccessPolicies { try { $json = '{"conditions":{"users":{"allUsers":2,"included":{"userIds":["' + $UserID + '"],"groupIds":[]},"excluded":{"userIds":[],"groupIds":[]}},"servicePrincipals":{"allServicePrincipals":1,"includeAllMicrosoftApps":false,"excludeAllMicrosoftApps":false,"userActions":[],"stepUpTags":[]},"conditions":{"minUserRisk":{"noRisk":false,"lowRisk":false,"mediumRisk":false,"highRisk":false,"applyCondition":false},"minSigninRisk":{"noRisk":false,"lowRisk":false,"mediumRisk":false,"highRisk":false,"applyCondition":false},"servicePrincipalRiskLevels":{"noRisk":false,"lowRisk":false,"mediumRisk":false,"highRisk":false,"applyCondition":false},"devicePlatforms":{"all":2,"included":{"android":false,"ios":false,"windowsPhone":false,"windows":false,"macOs":false,"linux":false},"excluded":null,"applyCondition":false},"locations":{"applyCondition":true,"includeLocationType":2,"excludeAllTrusted":false},"clientApps":{"applyCondition":false,"specificClientApps":false,"webBrowsers":false,"exchangeActiveSync":false,"onlyAllowSupportedPlatforms":false,"mobileDesktop":false},"clientAppsV2":{"applyCondition":false,"webBrowsers":false,"mobileDesktop":false,"modernAuth":false,"exchangeActiveSync":false,"onlyAllowSupportedPlatforms":false,"otherClients":false},"deviceState":{"includeDeviceStateType":1,"excludeDomainJoionedDevice":false,"excludeCompliantDevice":false,"applyCondition":true}}},"country":"","device":{}}' - $UserPolicies = (New-ClassicAPIPostRequest -uri 'https://main.iam.ad.ext.azure.com/api/Policies/Evaluate?' -tenantid $tenantfilter -Method POST -body $json -resource '74658136-14ec-4630-ad9b-26e160ff0fc6' -verbose | Where-Object { $_.applied -eq $true }) $ConditionalAccessPolicyOutput = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/policies' -tenantid $tenantfilter } catch { $ConditionalAccessPolicyOutput = @{} diff --git a/Modules/CIPPCore/Public/GraphHelper/Convert-SKUName.ps1 b/Modules/CIPPCore/Public/GraphHelper/Convert-SKUName.ps1 new file mode 100644 index 000000000000..2bd91fcf7a19 --- /dev/null +++ b/Modules/CIPPCore/Public/GraphHelper/Convert-SKUName.ps1 @@ -0,0 +1,6 @@ +function Convert-SKUname($skuname, $skuID) { + $ConvertTable = Import-Csv Conversiontable.csv + if ($skuname) { $ReturnedName = ($ConvertTable | Where-Object { $_.String_Id -eq $skuname } | Select-Object -Last 1).'Product_Display_Name' } + if ($skuID) { $ReturnedName = ($ConvertTable | Where-Object { $_.guid -eq $skuid } | Select-Object -Last 1).'Product_Display_Name' } + if ($ReturnedName) { return $ReturnedName } else { return $skuname, $skuID } +} diff --git a/Modules/CIPPCore/Public/GraphHelper/Get-AuthorisedRequest.ps1 b/Modules/CIPPCore/Public/GraphHelper/Get-AuthorisedRequest.ps1 new file mode 100644 index 000000000000..89f8684d2e8e --- /dev/null +++ b/Modules/CIPPCore/Public/GraphHelper/Get-AuthorisedRequest.ps1 @@ -0,0 +1,21 @@ + +function Get-AuthorisedRequest { + [CmdletBinding()] + Param( + [string]$TenantID, + [string]$Uri + ) + if (!$TenantID) { + $TenantID = $env:TenantId + } + if ($Uri -like 'https://graph.microsoft.com/beta/contracts*' -or $Uri -like '*/customers/*' -or $Uri -eq 'https://graph.microsoft.com/v1.0/me/sendMail' -or $Uri -like '*/tenantRelationships/*') { + return $true + } + $Tenants = Get-Tenants -IncludeErrors + $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 { + return $false + } +} diff --git a/Modules/CIPPCore/Public/GraphHelper/Get-CIPPTable.ps1 b/Modules/CIPPCore/Public/GraphHelper/Get-CIPPTable.ps1 new file mode 100644 index 000000000000..99567e300e18 --- /dev/null +++ b/Modules/CIPPCore/Public/GraphHelper/Get-CIPPTable.ps1 @@ -0,0 +1,12 @@ +function Get-CIPPTable { + [CmdletBinding()] + param ( + $tablename = 'CippLogs' + ) + $Context = New-AzDataTableContext -ConnectionString $env:AzureWebJobsStorage -TableName $tablename + New-AzDataTable -Context $Context | Out-Null + + @{ + Context = $Context + } +} \ No newline at end of file diff --git a/Modules/CIPPCore/Public/GraphHelper/Get-ClassicAPIToken.ps1 b/Modules/CIPPCore/Public/GraphHelper/Get-ClassicAPIToken.ps1 new file mode 100644 index 000000000000..6568e28b867a --- /dev/null +++ b/Modules/CIPPCore/Public/GraphHelper/Get-ClassicAPIToken.ps1 @@ -0,0 +1,41 @@ +function Get-ClassicAPIToken($tenantID, $Resource) { + $TokenKey = '{0}-{1}' -f $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 { + Write-Host 'Using classic' + $uri = "https://login.microsoftonline.com/$($TenantID)/oauth2/token" + $Body = @{ + client_id = $env:ApplicationID + client_secret = $env:ApplicationSecret + resource = $Resource + refresh_token = $env:RefreshToken + grant_type = 'refresh_token' + } + try { + 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 { + # Track consecutive Graph API failures + $TenantsTable = Get-CippTable -tablename Tenants + $Filter = "PartitionKey eq 'Tenants' and (defaultDomainName eq '{0}' or customerId eq '{0}')" -f $tenantid + $Tenant = Get-CIPPAzDataTableEntity @TenantsTable -Filter $Filter + if (!$Tenant) { + $Tenant = @{ + GraphErrorCount = $null + LastGraphTokenError = $null + LastGraphError = $null + PartitionKey = 'TenantFailed' + RowKey = 'Failed' + } + } + $Tenant.LastGraphError = $_.Exception.Message + $Tenant.GraphErrorCount++ + + Update-AzDataTableEntity @TenantsTable -Entity $Tenant + Throw "Failed to obtain Classic API Token for $TenantID - $_" + } + } +} \ No newline at end of file diff --git a/Modules/CIPPCore/Public/GraphHelper/Get-GraphBulkResultByID.ps1 b/Modules/CIPPCore/Public/GraphHelper/Get-GraphBulkResultByID.ps1 new file mode 100644 index 000000000000..e71d8c8ffb25 --- /dev/null +++ b/Modules/CIPPCore/Public/GraphHelper/Get-GraphBulkResultByID.ps1 @@ -0,0 +1,7 @@ +function Get-GraphBulkResultByID ($Results, $ID, [switch]$Value) { + if ($Value) { + ($Results | Where-Object { $_.id -eq $ID }).body.value + } else { + ($Results | Where-Object { $_.id -eq $ID }).body + } +} diff --git a/Modules/CIPPCore/Public/GraphHelper/Get-GraphToken.ps1 b/Modules/CIPPCore/Public/GraphHelper/Get-GraphToken.ps1 new file mode 100644 index 000000000000..8a654c8e979a --- /dev/null +++ b/Modules/CIPPCore/Public/GraphHelper/Get-GraphToken.ps1 @@ -0,0 +1,74 @@ +function Get-GraphToken($tenantid, $scope, $AsApp, $AppID, $refreshToken, $ReturnRefresh, $SkipCache) { + if (!$scope) { $scope = 'https://graph.microsoft.com/.default' } + if (!$env:SetFromProfile) { $CIPPAuth = Get-CIPPAuthentication; Write-Host 'Could not get Refreshtoken from environment variable. Reloading token.' } + $AuthBody = @{ + client_id = $env:ApplicationID + client_secret = $env:ApplicationSecret + scope = $Scope + refresh_token = $env:RefreshToken + grant_type = 'refresh_token' + } + if ($asApp -eq $true) { + $AuthBody = @{ + client_id = $env:ApplicationID + client_secret = $env:ApplicationSecret + scope = $Scope + grant_type = 'client_credentials' + } + } + + if ($null -ne $AppID -and $null -ne $refreshToken) { + $AuthBody = @{ + client_id = $appid + refresh_token = $RefreshToken + scope = $Scope + grant_type = 'refresh_token' + } + } + + if (!$tenantid) { $tenantid = $env:TenantID } + + $TokenKey = '{0}-{1}-{2}' -f $tenantid, $scope, $asApp + + try { + if ($script:AccessTokens.$TokenKey -and [int](Get-Date -UFormat %s -Millisecond 0) -lt $script:AccessTokens.$TokenKey.expires_on -and $SkipCache -ne $true) { + Write-Host 'Graph: cached token' + $AccessToken = $script:AccessTokens.$TokenKey + } 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 + Add-Member -InputObject $AccessToken -NotePropertyName 'expires_on' -NotePropertyValue $ExpiresOn + if (!$script:AccessTokens) { $script:AccessTokens = [HashTable]::Synchronized(@{}) } + $script:AccessTokens.$TokenKey = $AccessToken + } + + if ($ReturnRefresh) { $header = $AccessToken } else { $header = @{ Authorization = "Bearer $($AccessToken.access_token)" } } + return $header + } 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 + $Tenant = Get-CIPPAzDataTableEntity @TenantsTable -Filter $Filter + if (!$Tenant.RowKey) { + $donotset = $true + $Tenant = [pscustomobject]@{ + GraphErrorCount = $null + LastGraphTokenError = $null + LastGraphError = $null + PartitionKey = 'TenantFailed' + RowKey = 'Failed' + } + } + $Tenant.LastGraphError = if ( $_.ErrorDetails.Message) { + $msg = $_.ErrorDetails.Message | ConvertFrom-Json + "$($msg.error):$($msg.error_description)" + } else { + $_.Exception.message + } + $Tenant.GraphErrorCount++ + + if (!$donotset) { Update-AzDataTableEntity @TenantsTable -Entity $Tenant } + throw "Could not get token: $($Tenant.LastGraphError)" + } +} diff --git a/Modules/CIPPCore/Public/GraphHelper/Get-NormalizedError.ps1 b/Modules/CIPPCore/Public/GraphHelper/Get-NormalizedError.ps1 new file mode 100644 index 000000000000..96d5b0b4533c --- /dev/null +++ b/Modules/CIPPCore/Public/GraphHelper/Get-NormalizedError.ps1 @@ -0,0 +1,27 @@ +function Get-NormalizedError { + [CmdletBinding()] + param ( + [string]$message + ) + switch -Wildcard ($message) { + 'Request not applicable to target tenant.' { 'Required license not available for this tenant' } + "Neither tenant is B2C or tenant doesn't have premium license" { 'This feature requires a P1 license or higher' } + 'Response status code does not indicate success: 400 (Bad Request).' { 'Error 400 occured. There is an issue with the token configuration for this tenant. Please perform an access check' } + '*Microsoft.Skype.Sync.Pstn.Tnm.Common.Http.HttpResponseException*' { 'Could not connect to Teams Admin center - Tenant might be missing a Teams license' } + '*Provide valid credential.*' { 'Error 400: There is an issue with your Exchange Token configuration. Please perform an access check for this tenant' } + '*This indicate that a subscription within the tenant has lapsed*' { 'There is no exchange subscription available, or it has lapsed. Check licensing information.' } + '*User was not found.*' { 'The relationship between this tenant and the partner has been dissolved from the tenant side.' } + '*The user or administrator has not consented to use the application*' { 'CIPP cannot access this tenant. Perform a CPV Refresh and Access Check via the settings menu' } + '*AADSTS50020*' { 'AADSTS50020: The user you have used for your Secure Application Model is a guest in this tenant, or your are using GDAP and have not added the user to the correct group. Please delete the guest user to gain access to this tenant' } + '*AADSTS50177' { 'AADSTS50177: The user you have used for your Secure Application Model is a guest in this tenant, or your are using GDAP and have not added the user to the correct group. Please delete the guest user to gain access to this tenant' } + '*invalid or malformed*' { 'The request is malformed. Have you finished the SAM Setup?' } + '*Windows Store repository apps feature is not supported for this tenant*' { 'This tenant does not have WinGet support available' } + '*AADSTS650051*' { 'The application does not exist yet. Try again in 30 seconds.' } + '*AppLifecycle_2210*' { 'Failed to call Intune APIs: Does the tenant have a license available?' } + '*One or more added object references already exist for the following modified properties:*' { 'This user is already a member of this group.' } + '*Microsoft.Exchange.Management.Tasks.MemberAlreadyExistsException*' { 'This user is already a member of this group.' } + '*The property value exceeds the maximum allowed size (64KB)*' { 'One of the values exceeds the maximum allowed size (64KB).' } + Default { $message } + + } +} diff --git a/Modules/CIPPCore/Public/GraphHelper/Get-Tenants.ps1 b/Modules/CIPPCore/Public/GraphHelper/Get-Tenants.ps1 new file mode 100644 index 000000000000..4f0c1a16cbf8 --- /dev/null +++ b/Modules/CIPPCore/Public/GraphHelper/Get-Tenants.ps1 @@ -0,0 +1,110 @@ +function Get-Tenants { + param ( + [Parameter( ParameterSetName = 'Skip', Mandatory = $True )] + [switch]$SkipList, + [Parameter( ParameterSetName = 'Standard')] + [switch]$IncludeAll, + [switch]$IncludeErrors + ) + + $TenantsTable = Get-CippTable -tablename 'Tenants' + $ExcludedFilter = "PartitionKey eq 'Tenants' and Excluded eq true" + + $SkipListCache = Get-CIPPAzDataTableEntity @TenantsTable -Filter $ExcludedFilter + if ($SkipList) { + return $SkipListCache + } + + if ($IncludeAll.IsPresent) { + $Filter = "PartitionKey eq 'Tenants'" + } elseif ($IncludeErrors.IsPresent) { + $Filter = "PartitionKey eq 'Tenants' and Excluded eq false" + } else { + $Filter = "PartitionKey eq 'Tenants' and Excluded eq false and GraphErrorCount lt 50" + } + $IncludedTenantsCache = Get-CIPPAzDataTableEntity @TenantsTable -Filter $Filter + + 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 { + $LastRefresh = $false + } + if (!$LastRefresh -or $LastRefresh -lt (Get-Date).Addhours(-24).ToUniversalTime()) { + try { + 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 { $_.defaultDomainName -NotIn $SkipListCache.defaultDomainName -and $_.defaultDomainName -ne $null } + + } catch { + Write-Host "Get-Tenants - Lighthouse Error, using contract/delegatedAdminRelationship calls. Error: $($_.Exception.Message)" + [System.Collections.Generic.List[PSCustomObject]]$BulkRequests = @( + @{ + id = 'Contracts' + method = 'GET' + url = "/contracts?`$top=999" + }, + @{ + id = 'GDAPRelationships' + method = 'GET' + url = '/tenantRelationships/delegatedAdminRelationships' + } + ) + + $BulkResults = New-GraphBulkRequest -Requests $BulkRequests -tenantid $TenantFilter -NoAuthCheck:$true + $Contracts = Get-GraphBulkResultByID -Results $BulkResults -ID 'Contracts' -Value + $GDAPRelationships = Get-GraphBulkResultByID -Results $BulkResults -ID 'GDAPRelationships' -Value + + $ContractList = $Contracts | Select-Object id, customerId, DefaultdomainName, DisplayName, domains, @{l = 'MigratedToNewTenantAPI'; e = { $true } }, @{ n = 'delegatedPrivilegeStatus'; exp = { $CustomerId = $_.customerId; if (($GDAPRelationships | Where-Object { $_.customer.tenantId -EQ $CustomerId -and $_.status -EQ 'active' } | Measure-Object).Count -gt 0) { 'delegatedAndGranularDelegetedAdminPrivileges' } else { 'delegatedAdminPrivileges' } } } | Where-Object -Property defaultDomainName -NotIn $SkipListCache.defaultDomainName + + $GDAPOnlyList = $GDAPRelationships | Where-Object { $_.status -eq 'active' -and $Contracts.customerId -notcontains $_.customer.tenantId } | Select-Object id, @{l = 'customerId'; e = { $($_.customer.tenantId) } }, @{l = 'defaultDomainName'; e = { (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/tenantRelationships/findTenantInformationByTenantId(tenantId='$($_.customer.tenantId)')" -noauthcheck $true -asApp:$true -tenant $env:TenantId).defaultDomainName } }, @{l = 'MigratedToNewTenantAPI'; e = { $true } }, @{n = 'displayName'; exp = { $_.customer.displayName } }, domains, @{n = 'delegatedPrivilegeStatus'; exp = { 'granularDelegatedAdminPrivileges' } } | Where-Object { $_.defaultDomainName -NotIn $SkipListCache.defaultDomainName -and $_.defaultDomainName -ne $null } | Sort-Object -Property customerId -Unique + + $TenantList = @($ContractList) + @($GDAPOnlyList) + } + <#if (!$TenantList.customerId) { + $TenantList = (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/contracts?`$top=999" -tenantid $env:TenantID ) | Select-Object id, customerId, DefaultdomainName, DisplayName, domains | Where-Object -Property defaultDomainName -NotIn $SkipListCache.defaultDomainName + }#> + $IncludedTenantsCache = [system.collections.generic.list[hashtable]]::new() + if ($env:PartnerTenantAvailable) { + $IncludedTenantsCache.Add(@{ + RowKey = $env:TenantID + PartitionKey = 'Tenants' + customerId = $env:TenantID + defaultDomainName = $env:TenantID + displayName = '*Partner Tenant' + domains = 'PartnerTenant' + Excluded = $false + ExcludeUser = '' + ExcludeDate = '' + GraphErrorCount = 0 + LastGraphError = '' + LastRefresh = (Get-Date).ToUniversalTime() + }) | Out-Null + } + foreach ($Tenant in $TenantList) { + if ($Tenant.defaultDomainName -eq 'Invalid' -or !$Tenant.defaultDomainName) { continue } + $IncludedTenantsCache.Add(@{ + RowKey = [string]$Tenant.customerId + PartitionKey = 'Tenants' + customerId = [string]$Tenant.customerId + defaultDomainName = [string]$Tenant.defaultDomainName + displayName = [string]$Tenant.DisplayName + delegatedPrivilegeStatus = [string]$Tenant.delegatedPrivilegeStatus + domains = '' + Excluded = $false + ExcludeUser = '' + ExcludeDate = '' + GraphErrorCount = 0 + LastGraphError = '' + LastRefresh = (Get-Date).ToUniversalTime() + }) | Out-Null + } + + if ($IncludedTenantsCache) { + $TenantsTable.Force = $true + Add-CIPPAzDataTableEntity @TenantsTable -Entity $IncludedTenantsCache + } + } + return ($IncludedTenantsCache | Where-Object -Property defaultDomainName -NE $null | Sort-Object -Property displayName) + +} diff --git a/Modules/CIPPCore/Public/GraphHelper/New-ClassicAPIGetRequest.ps1 b/Modules/CIPPCore/Public/GraphHelper/New-ClassicAPIGetRequest.ps1 new file mode 100644 index 000000000000..1528e0e4393d --- /dev/null +++ b/Modules/CIPPCore/Public/GraphHelper/New-ClassicAPIGetRequest.ps1 @@ -0,0 +1,27 @@ + +function New-ClassicAPIGetRequest($TenantID, $Uri, $Method = 'GET', $Resource = 'https://admin.microsoft.com', $ContentType = 'application/json') { + + if ((Get-AuthorisedRequest -Uri $uri -TenantID $tenantid)) { + $token = Get-ClassicAPIToken -Tenant $tenantID -Resource $Resource + + $NextURL = $Uri + $ReturnedData = do { + try { + $Data = Invoke-RestMethod -ContentType "$ContentType;charset=UTF-8" -Uri $NextURL -Method $Method -Headers @{ + Authorization = "Bearer $($token.access_token)" + 'x-ms-client-request-id' = [guid]::NewGuid().ToString() + 'x-ms-client-session-id' = [guid]::NewGuid().ToString() + 'x-ms-correlation-id' = [guid]::NewGuid() + 'X-Requested-With' = 'XMLHttpRequest' + } + $Data + if ($noPagination) { $nextURL = $null } else { $nextURL = $data.NextLink } + } catch { + throw "Failed to make Classic Get Request $_" + } + } until ($null -eq $NextURL) + return $ReturnedData + } else { + Write-Error 'Not allowed. You cannot manage your own tenant or tenants not under your scope' + } +} \ No newline at end of file diff --git a/Modules/CIPPCore/Public/GraphHelper/New-DeviceLogin.ps1 b/Modules/CIPPCore/Public/GraphHelper/New-DeviceLogin.ps1 new file mode 100644 index 000000000000..1d80e47ba069 --- /dev/null +++ b/Modules/CIPPCore/Public/GraphHelper/New-DeviceLogin.ps1 @@ -0,0 +1,27 @@ +function New-DeviceLogin { + [CmdletBinding()] + param ( + [string]$clientid, + [string]$scope, + [switch]$FirstLogon, + [string]$device_code, + [string]$TenantId + ) + $encodedscope = [uri]::EscapeDataString($scope) + if ($FirstLogon) { + 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 { + $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 { + $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 { + $returncode = $Checking.error + } + } + return $ReturnCode +} \ No newline at end of file diff --git a/Modules/CIPPCore/Public/GraphHelper/New-ExoRequest.ps1 b/Modules/CIPPCore/Public/GraphHelper/New-ExoRequest.ps1 new file mode 100644 index 000000000000..d5df999e6a03 --- /dev/null +++ b/Modules/CIPPCore/Public/GraphHelper/New-ExoRequest.ps1 @@ -0,0 +1,66 @@ +function New-ExoRequest ($tenantid, $cmdlet, $cmdParams, $useSystemMailbox, $Anchor, $NoAuthCheck, $Select) { + + if ((Get-AuthorisedRequest -TenantID $tenantid) -or $NoAuthCheck -eq $True) { + $token = Get-ClassicAPIToken -resource 'https://outlook.office365.com' -Tenantid $tenantid + $tenant = (get-tenants -IncludeErrors | Where-Object { $_.defaultDomainName -eq $tenantid -or $_.customerId -eq $tenantid }).customerId + if ($cmdParams) { + $Params = $cmdParams + } else { + $Params = @{} + } + $ExoBody = ConvertTo-Json -Depth 5 -InputObject @{ + CmdletInput = @{ + CmdletName = $cmdlet + Parameters = $Params + } + } + if (!$Anchor) { + if ($cmdparams.Identity) { $Anchor = $cmdparams.Identity } + if ($cmdparams.anr) { $Anchor = $cmdparams.anr } + if ($cmdparams.User) { $Anchor = $cmdparams.User } + + if (!$Anchor -or $useSystemMailbox) { + $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{8cc370d3-822a-4ab8-a926-bb94bd0641a9}@$($OnMicrosoft)" + + + } + } + Write-Host "Using $Anchor" + $Headers = @{ + Authorization = "Bearer $($token.access_token)" + Prefer = 'odata.maxpagesize = 1000' + 'parameter-based-routing' = $true + 'X-AnchorMailbox' = $anchor + + } + try { + if ($Select) { $Select = "`$select=$Select" } + $URL = "https://outlook.office365.com/adminapi/beta/$($tenant)/InvokeCommand?$Select" + + $ReturnedData = + do { + $Return = Invoke-RestMethod $URL -Method POST -Body $ExoBody -Headers $Headers -ContentType 'application/json; charset=utf-8' + $URL = $Return.'@odata.nextLink' + $Return + } until ($null -eq $URL) + + if ($ReturnedData.'@adminapi.warnings' -and $ReturnedData.value -eq $null) { + $ReturnedData.value = $ReturnedData.'@adminapi.warnings' + } + } 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 } + else { $ReportedError.error.innererror.internalException.message } + if ($null -eq $Message) { $Message = $ErrorMess } + throw $Message + } + return $ReturnedData.value + } else { + Write-Error 'Not allowed. You cannot manage your own tenant or tenants not under your scope' + } +} \ No newline at end of file diff --git a/Modules/CIPPCore/Public/GraphHelper/New-GraphBulkRequest.ps1 b/Modules/CIPPCore/Public/GraphHelper/New-GraphBulkRequest.ps1 new file mode 100644 index 000000000000..d328a7518865 --- /dev/null +++ b/Modules/CIPPCore/Public/GraphHelper/New-GraphBulkRequest.ps1 @@ -0,0 +1,61 @@ +function New-GraphBulkRequest { + Param( + $tenantid, + $NoAuthCheck, + $scope, + $asapp, + $Requests + ) + + if ($NoAuthCheck -or (Get-AuthorisedRequest -Uri $uri -TenantID $tenantid)) { + $headers = Get-GraphToken -tenantid $tenantid -scope $scope -AsApp $asapp + + $URL = 'https://graph.microsoft.com/beta/$batch' + + # Track consecutive Graph API failures + $TenantsTable = Get-CippTable -tablename Tenants + $Filter = "PartitionKey eq 'Tenants' and (defaultDomainName eq '{0}' or customerId eq '{0}')" -f $tenantid + $Tenant = Get-CIPPAzDataTableEntity @TenantsTable -Filter $Filter + if (!$Tenant) { + $Tenant = @{ + GraphErrorCount = 0 + LastGraphError = $null + PartitionKey = 'TenantFailed' + RowKey = 'Failed' + } + } + try { + $ReturnedData = for ($i = 0; $i -lt $Requests.count; $i += 20) { + $req = @{} + # Use select to create hashtables of id, method and url for each call + $req['requests'] = ($Requests[$i..($i + 19)]) + Invoke-RestMethod -Uri $URL -Method POST -Headers $headers -ContentType 'application/json; charset=utf-8' -Body ($req | ConvertTo-Json -Depth 10) + } + + foreach ($MoreData in $ReturnedData.Responses | Where-Object { $_.body.'@odata.nextLink' }) { + Write-Host 'Getting more' + $AdditionalValues = New-GraphGetRequest -ComplexFilter -uri $MoreData.body.'@odata.nextLink' -tenantid $tenantid -NoAuthCheck:$NoAuthCheck + $NewValues = [System.Collections.Generic.List[PSCustomObject]]$MoreData.body.value + $AdditionalValues | ForEach-Object { $NewValues.add($_) } + $MoreData.body.value = $NewValues + } + + } 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.') { + $Tenant.LastGraphError = $Message + $Tenant.GraphErrorCount++ + Update-AzDataTableEntity @TenantsTable -Entity $Tenant + } + throw $Message + } + + $Tenant.LastGraphError = '' + Update-AzDataTableEntity @TenantsTable -Entity $Tenant + + return $ReturnedData.responses + } else { + Write-Error 'Not allowed. You cannot manage your own tenant or tenants not under your scope' + } +} diff --git a/Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1 b/Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1 new file mode 100644 index 000000000000..731c09da25b0 --- /dev/null +++ b/Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1 @@ -0,0 +1,67 @@ +function New-GraphGetRequest { + Param( + $uri, + $tenantid, + $scope, + $AsApp, + $noPagination, + $NoAuthCheck, + $skipTokenCache, + [switch]$ComplexFilter, + [switch]$CountOnly + ) + + if ($NoAuthCheck -or (Get-AuthorisedRequest -Uri $uri -TenantID $tenantid)) { + if ($scope -eq 'ExchangeOnline') { + $AccessToken = Get-ClassicAPIToken -resource 'https://outlook.office365.com' -Tenantid $tenantid + $headers = @{ Authorization = "Bearer $($AccessToken.access_token)" } + } else { + $headers = Get-GraphToken -tenantid $tenantid -scope $scope -AsApp $asapp -SkipCache $skipTokenCache + } + + if ($ComplexFilter) { + $headers['ConsistencyLevel'] = 'eventual' + } + $nextURL = $uri + + # Track consecutive Graph API failures + $TenantsTable = Get-CippTable -tablename Tenants + $Filter = "PartitionKey eq 'Tenants' and (defaultDomainName eq '{0}' or customerId eq '{0}')" -f $tenantid + $Tenant = Get-CIPPAzDataTableEntity @TenantsTable -Filter $Filter + if (!$Tenant) { + $Tenant = @{ + GraphErrorCount = 0 + LastGraphError = $null + PartitionKey = 'TenantFailed' + RowKey = 'Failed' + } + } + + $ReturnedData = do { + try { + $Data = (Invoke-RestMethod -Uri $nextURL -Method GET -Headers $headers -ContentType 'application/json; charset=utf-8') + if ($CountOnly) { + $Data.'@odata.count' + $nextURL = $null + } else { + if ($data.value) { $data.value } else { ($Data) } + if ($noPagination) { $nextURL = $null } else { $nextURL = $data.'@odata.nextLink' } + } + } 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.' -and $Tenant) { + $Tenant.LastGraphError = $Message + $Tenant.GraphErrorCount++ + Update-AzDataTableEntity @TenantsTable -Entity $Tenant + } + throw $Message + } + } until ($null -eq $NextURL) + $Tenant.LastGraphError = '' + Update-AzDataTableEntity @TenantsTable -Entity $Tenant + return $ReturnedData + } else { + Write-Error 'Not allowed. You cannot manage your own tenant or tenants not under your scope' + } +} diff --git a/Modules/CIPPCore/Public/GraphHelper/New-GraphPOSTRequest.ps1 b/Modules/CIPPCore/Public/GraphHelper/New-GraphPOSTRequest.ps1 new file mode 100644 index 000000000000..a133d7a93f7d --- /dev/null +++ b/Modules/CIPPCore/Public/GraphHelper/New-GraphPOSTRequest.ps1 @@ -0,0 +1,27 @@ + +function New-GraphPOSTRequest ($uri, $tenantid, $body, $type, $scope, $AsApp, $NoAuthCheck, $skipTokenCache, $AddedHeaders) { + if ($NoAuthCheck -or (Get-AuthorisedRequest -Uri $uri -TenantID $tenantid)) { + $headers = Get-GraphToken -tenantid $tenantid -scope $scope -AsApp $asapp -SkipCache $skipTokenCache + if ($AddedHeaders) { + foreach ($header in $AddedHeaders.getenumerator()) { + $headers.Add($header.Key, $header.Value) + } + } + Write-Verbose "Using $($uri) as url" + if (!$type) { + $type = 'POST' + } + + try { + $ReturnedData = (Invoke-RestMethod -Uri $($uri) -Method $TYPE -Body $body -Headers $headers -ContentType 'application/json; charset=utf-8') + } catch { + $ErrorMess = $($_.Exception.Message) + $Message = ($_.ErrorDetails.Message | ConvertFrom-Json -ErrorAction SilentlyContinue).error.message + if (!$Message) { $Message = $ErrorMess } + throw $Message + } + return $ReturnedData + } else { + Write-Error 'Not allowed. You cannot manage your own tenant or tenants not under your scope' + } +} \ No newline at end of file diff --git a/Modules/CIPPCore/Public/GraphHelper/New-TeamsAPIGetRequest.ps1 b/Modules/CIPPCore/Public/GraphHelper/New-TeamsAPIGetRequest.ps1 new file mode 100644 index 000000000000..7ac373da6ffb --- /dev/null +++ b/Modules/CIPPCore/Public/GraphHelper/New-TeamsAPIGetRequest.ps1 @@ -0,0 +1,28 @@ +function New-TeamsAPIGetRequest($Uri, $tenantID, $Method = 'GET', $Resource = '48ac35b8-9aa8-4d74-927d-1f4a14a0b239', $ContentType = 'application/json') { + + if ((Get-AuthorisedRequest -Uri $uri -TenantID $tenantid)) { + $token = Get-ClassicAPIToken -Tenant $tenantid -Resource $Resource + + $NextURL = $Uri + $ReturnedData = do { + try { + $Data = Invoke-RestMethod -ContentType "$ContentType;charset=UTF-8" -Uri $NextURL -Method $Method -Headers @{ + Authorization = "Bearer $($token.access_token)" + 'x-ms-client-request-id' = [guid]::NewGuid().ToString() + 'x-ms-client-session-id' = [guid]::NewGuid().ToString() + 'x-ms-correlation-id' = [guid]::NewGuid() + 'X-Requested-With' = 'XMLHttpRequest' + 'x-ms-tnm-applicationid' = '045268c0-445e-4ac1-9157-d58f67b167d9' + + } + $Data + if ($noPagination) { $nextURL = $null } else { $nextURL = $data.NextLink } + } catch { + throw "Failed to make Teams API Get Request $_" + } + } until ($null -eq $NextURL) + return $ReturnedData + } else { + Write-Error 'Not allowed. You cannot manage your own tenant or tenants not under your scope' + } +} \ No newline at end of file diff --git a/Modules/CIPPCore/Public/GraphHelper/New-passwordString.ps1 b/Modules/CIPPCore/Public/GraphHelper/New-passwordString.ps1 new file mode 100644 index 000000000000..4e6e27befdc8 --- /dev/null +++ b/Modules/CIPPCore/Public/GraphHelper/New-passwordString.ps1 @@ -0,0 +1,14 @@ +function New-passwordString { + [CmdletBinding()] + param ( + [int]$count = 12 + ) + $SettingsTable = Get-CippTable -tablename 'Settings' + $PasswordType = (Get-CIPPAzDataTableEntity @SettingsTable).passwordType + if ($PasswordType -eq 'Correct-Battery-Horse') { + $Words = Get-Content .\words.txt + (Get-Random -InputObject $words -Count 4) -join '-' + } else { + -join ('abcdefghkmnrstuvwxyzABCDEFGHKLMNPRSTUVWXYZ23456789$%&*#'.ToCharArray() | Get-Random -Count $count) + } +} diff --git a/Modules/CIPPCore/Public/GraphHelper/Read-JwtAccessDetails.ps1 b/Modules/CIPPCore/Public/GraphHelper/Read-JwtAccessDetails.ps1 new file mode 100644 index 000000000000..b789fe87e298 --- /dev/null +++ b/Modules/CIPPCore/Public/GraphHelper/Read-JwtAccessDetails.ps1 @@ -0,0 +1,57 @@ +function Read-JwtAccessDetails { + <# + .SYNOPSIS + Parse Microsoft JWT access tokens + + .DESCRIPTION + Extract JWT access token details for verification + + .PARAMETER Token + Token to get details for + + #> + [cmdletbinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Token + ) + + # Default token object + $TokenDetails = [PSCustomObject]@{ + AppId = '' + AppName = '' + Audience = '' + AuthMethods = '' + IPAddress = '' + Name = '' + Scope = '' + TenantId = '' + UserPrincipalName = '' + } + + if (!$Token.Contains('.') -or !$token.StartsWith('eyJ')) { return $TokenDetails } + + # Get token payload + $tokenPayload = $token.Split('.')[1].Replace('-', '+').Replace('_', '/') + while ($tokenPayload.Length % 4) { + $tokenPayload = '{0}=' -f $tokenPayload + } + + # Convert base64 to json to object + $tokenByteArray = [System.Convert]::FromBase64String($tokenPayload) + $tokenArray = [System.Text.Encoding]::ASCII.GetString($tokenByteArray) + $TokenObj = $tokenArray | ConvertFrom-Json + + # Convert token details to human readable + $TokenDetails.AppId = $TokenObj.appid + $TokenDetails.AppName = $TokenObj.app_displayname + $TokenDetails.Audience = $TokenObj.aud + $TokenDetails.AuthMethods = $TokenObj.amr + $TokenDetails.IPAddress = $TokenObj.ipaddr + $TokenDetails.Name = $TokenObj.name + $TokenDetails.Scope = $TokenObj.scp -split ' ' + $TokenDetails.TenantId = $TokenObj.tid + $TokenDetails.UserPrincipalName = $TokenObj.upn + + return $TokenDetails +} \ No newline at end of file diff --git a/Modules/CIPPCore/Public/GraphHelper/Remove-CIPPCache.ps1 b/Modules/CIPPCore/Public/GraphHelper/Remove-CIPPCache.ps1 new file mode 100644 index 000000000000..b2da4587453c --- /dev/null +++ b/Modules/CIPPCore/Public/GraphHelper/Remove-CIPPCache.ps1 @@ -0,0 +1,29 @@ +function Remove-CIPPCache { + param ( + $TenantsOnly + ) + # Remove all tenants except excluded + $TenantsTable = Get-CippTable -tablename 'Tenants' + $Filter = "PartitionKey eq 'Tenants' and Excluded eq false" + $ClearIncludedTenants = Get-CIPPAzDataTableEntity @TenantsTable -Filter $Filter + Remove-AzDataTableEntity @TenantsTable -Entity $ClearIncludedTenants + if ($tenantsonly -eq 'false') { + Write-Host 'Clearing all' + # Remove Domain Analyser cached results + $DomainsTable = Get-CippTable -tablename 'Domains' + $Filter = "PartitionKey eq 'TenantDomains'" + $ClearDomainAnalyserRows = Get-CIPPAzDataTableEntity @DomainsTable -Filter $Filter | ForEach-Object { + $_.DomainAnalyser = '' + $_ + } + Update-AzDataTableEntity @DomainsTable -Entity $ClearDomainAnalyserRows + #Clear BPA + $BPATable = Get-CippTable -tablename 'cachebpa' + $ClearBPARows = Get-CIPPAzDataTableEntity @BPATable + Remove-AzDataTableEntity @BPATable -Entity $ClearBPARows + $ENV:SetFromProfile = $null + $Script:SkipListCache = $Null + $Script:SkipListCacheEmpty = $Null + $Script:IncludedTenantsCache = $Null + } +} diff --git a/Modules/CIPPCore/Public/GraphHelper/Write-LogMessage.ps1 b/Modules/CIPPCore/Public/GraphHelper/Write-LogMessage.ps1 new file mode 100644 index 000000000000..2c5ea7d00ba7 --- /dev/null +++ b/Modules/CIPPCore/Public/GraphHelper/Write-LogMessage.ps1 @@ -0,0 +1,35 @@ +function Write-LogMessage ($message, $tenant = 'None', $API = 'None', $tenantId = $null, $user, $sev) { + try { + $username = ([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($user)) | ConvertFrom-Json).userDetails + } catch { + $username = $user + } + + $Table = Get-CIPPTable -tablename CippLogs + + if (!$tenant) { $tenant = 'None' } + if (!$username) { $username = 'CIPP' } + if ($sev -eq 'Debug' -and $env:DebugMode -ne 'true') { + Write-Information 'Not writing to log file - Debug mode is not enabled.' + return + } + $PartitionKey = (Get-Date -UFormat '%Y%m%d').ToString() + $TableRow = @{ + 'Tenant' = [string]$tenant + 'API' = [string]$API + 'Message' = [string]$message + 'Username' = [string]$username + 'Severity' = [string]$sev + 'SentAsAlert' = $false + 'PartitionKey' = $PartitionKey + 'RowKey' = ([guid]::NewGuid()).ToString() + } + + + if ($tenantId) { + $TableRow.Add('TenantID', [string]$tenantId) + } + + $Table.Entity = $TableRow + Add-CIPPAzDataTableEntity @Table | Out-Null +} \ No newline at end of file diff --git a/Modules/CIPPCore/Public/GraphRequests/Get-GraphRequestList.ps1 b/Modules/CIPPCore/Public/GraphRequests/Get-GraphRequestList.ps1 index fc9235272edb..3b3b4660a0bf 100644 --- a/Modules/CIPPCore/Public/GraphRequests/Get-GraphRequestList.ps1 +++ b/Modules/CIPPCore/Public/GraphRequests/Get-GraphRequestList.ps1 @@ -117,7 +117,6 @@ function Get-GraphRequestList { 'AllTenants' { if ($SkipCache) { Get-Tenants -IncludeErrors | ForEach-Object -Parallel { - Import-Module '.\GraphHelper.psm1' Import-Module '.\Modules\AzBobbyTables' Import-Module '.\Modules\CIPPCore' diff --git a/Modules/CIPPCore/Public/Invoke-RemoveApp.ps1 b/Modules/CIPPCore/Public/Invoke-RemoveApp.ps1 index 1a80a02c9e30..f0ca41db08a0 100644 --- a/Modules/CIPPCore/Public/Invoke-RemoveApp.ps1 +++ b/Modules/CIPPCore/Public/Invoke-RemoveApp.ps1 @@ -32,6 +32,5 @@ Function Invoke-RemoveApp { Body = $body }) - #@{ Name = 'LicJoined'; Expression = { ($_.assignedLicenses | ForEach-Object { convert-skuname -skuID $_.skuid }) -join ", " } }, @{ Name = 'Aliases'; Expression = { $_.Proxyaddresses -join ", " } }, @{ Name = 'primDomain'; Expression = { $_.userPrincipalName -split "@" | Select-Object -Last 1 } } } diff --git a/Modules/CIPPCore/Public/Invoke-RemovePolicy.ps1 b/Modules/CIPPCore/Public/Invoke-RemovePolicy.ps1 index 22ae4c5d7db5..58cc90823d16 100644 --- a/Modules/CIPPCore/Public/Invoke-RemovePolicy.ps1 +++ b/Modules/CIPPCore/Public/Invoke-RemovePolicy.ps1 @@ -34,6 +34,5 @@ Function Invoke-RemovePolicy { Body = $body }) - #@{ Name = 'LicJoined'; Expression = { ($_.assignedLicenses | ForEach-Object { convert-skuname -skuID $_.skuid }) -join ", " } }, @{ Name = 'Aliases'; Expression = { $_.Proxyaddresses -join ", " } }, @{ Name = 'primDomain'; Expression = { $_.userPrincipalName -split "@" | Select-Object -Last 1 } } } diff --git a/Modules/CIPPCore/Public/Invoke-RemoveUser.ps1 b/Modules/CIPPCore/Public/Invoke-RemoveUser.ps1 index 96b42b933ba6..386ab3e4be6b 100644 --- a/Modules/CIPPCore/Public/Invoke-RemoveUser.ps1 +++ b/Modules/CIPPCore/Public/Invoke-RemoveUser.ps1 @@ -32,6 +32,5 @@ Function Invoke-RemoveUser { Body = $body }) - #@{ Name = 'LicJoined'; Expression = { ($_.assignedLicenses | ForEach-Object { convert-skuname -skuID $_.skuid }) -join ", " } }, @{ Name = 'Aliases'; Expression = { $_.Proxyaddresses -join ", " } }, @{ Name = 'primDomain'; Expression = { $_.userPrincipalName -split "@" | Select-Object -Last 1 } } } diff --git a/Modules/CIPPCore/Public/Remove-CIPPGroups.ps1 b/Modules/CIPPCore/Public/Remove-CIPPGroups.ps1 index 14aa12d54089..aa9eeed5b90c 100644 --- a/Modules/CIPPCore/Public/Remove-CIPPGroups.ps1 +++ b/Modules/CIPPCore/Public/Remove-CIPPGroups.ps1 @@ -14,7 +14,6 @@ function Remove-CIPPGroups { $AllGroups = (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/groups/?`$select=displayName,mailEnabled,id,groupTypes" -tenantid $tenantFilter) $Returnval = (New-GraphPostRequest -uri "https://graph.microsoft.com/beta/users/$($userid)/GetMemberGroups" -tenantid $tenantFilter -type POST -body '{"securityEnabledOnly": false}').value | ForEach-Object -Parallel { - Import-Module '.\GraphHelper.psm1' Import-Module '.\Modules\AzBobbyTables' Import-Module '.\Modules\CIPPCore' $group = $_ diff --git a/Modules/CIPPCore/Public/Remove-CIPPMailboxPermissions.ps1 b/Modules/CIPPCore/Public/Remove-CIPPMailboxPermissions.ps1 index 7495e989b44d..7035a083ff0f 100644 --- a/Modules/CIPPCore/Public/Remove-CIPPMailboxPermissions.ps1 +++ b/Modules/CIPPCore/Public/Remove-CIPPMailboxPermissions.ps1 @@ -13,7 +13,6 @@ function Remove-CIPPMailboxPermissions { if ($userid -eq 'AllUsers') { $Mailboxes = New-ExoRequest -tenantid $TenantFilter -cmdlet 'get-mailbox' $Mailboxes | ForEach-Object -Parallel { - Import-Module '.\GraphHelper.psm1' Import-Module '.\Modules\AzBobbyTables' Import-Module '.\Modules\CIPPCore' Write-Host "Removing permissions from mailbox $($_.UserPrincipalName)" diff --git a/Modules/CIPPCore/Public/Set-CIPPOutOfoffice.ps1 b/Modules/CIPPCore/Public/Set-CIPPOutOfoffice.ps1 index c9b4f66240d3..8365e4b64a90 100644 --- a/Modules/CIPPCore/Public/Set-CIPPOutOfoffice.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPOutOfoffice.ps1 @@ -14,7 +14,6 @@ function Set-CIPPOutOfOffice { try { if (-not $StartTime) { - $State = "Enabled" $StartTime = (Get-Date).ToString("yyyy-MM-dd HH:mm") } if (-not $EndTime) { @@ -23,7 +22,7 @@ function Set-CIPPOutOfOffice { if ($State -ne "Scheduled") { $OutOfOffice = New-ExoRequest -tenantid $TenantFilter -cmdlet "Set-MailboxAutoReplyConfiguration" -cmdParams @{Identity = $userid; AutoReplyState = $State; InternalMessage = $InternalMessage; ExternalMessage = $ExternalMessage } -Anchor $userid Write-LogMessage -user $ExecutingUser -API $APIName -message "Set Out-of-office for $($userid) to $state" -Sev "Info" -tenant $TenantFilter - return "Set Out-of-office for $($userid) to $state. Internal Message is $InternalMessage | External Message is $ExternalMessage" + return "Set Out-of-office for $($userid) to $state." } else { $OutOfOffice = New-ExoRequest -tenantid $TenantFilter -cmdlet "Set-MailboxAutoReplyConfiguration" -cmdParams @{Identity = $userid; AutoReplyState = $State; InternalMessage = $InternalMessage; ExternalMessage = $ExternalMessage; StartTime = $StartTime; EndTime = $EndTime } -Anchor $userid diff --git a/Modules/CippEntrypoints/CippEntrypoints.psm1 b/Modules/CippEntrypoints/CippEntrypoints.psm1 index 44c7ebc5443c..7e376917303d 100644 --- a/Modules/CippEntrypoints/CippEntrypoints.psm1 +++ b/Modules/CippEntrypoints/CippEntrypoints.psm1 @@ -20,7 +20,7 @@ function Receive-CippHttpTrigger { function Receive-CippQueueTrigger { Param($QueueItem, $TriggerMetadata) $APIName = $TriggerMetadata.FunctionName - + Set-Location (Get-Item $PSScriptRoot).Parent.Parent.FullName $FunctionName = 'Push-{0}' -f $APIName $QueueTrigger = @{ QueueItem = $QueueItem diff --git a/Modules/CippExtensions/Private/New-GradientServiceSyncRun.ps1 b/Modules/CippExtensions/Private/New-GradientServiceSyncRun.ps1 index 38637ebe9ffd..3200c46782c5 100644 --- a/Modules/CippExtensions/Private/New-GradientServiceSyncRun.ps1 +++ b/Modules/CippExtensions/Private/New-GradientServiceSyncRun.ps1 @@ -37,7 +37,6 @@ function New-GradientServiceSyncRun { $RawGraphRequest = $Tenants | ForEach-Object -Parallel { $domainName = $_.defaultDomainName - Import-Module '.\GraphHelper.psm1' Import-Module '.\Modules\AzBobbyTables' Import-Module '.\Modules\CIPPCore' Write-Host "Doing $domainName" diff --git a/Standards_EnableAppConsentRequests/run.ps1 b/Standards_EnableAppConsentRequests/run.ps1 index 8f5c751752bc..03d3e12bc621 100644 --- a/Standards_EnableAppConsentRequests/run.ps1 +++ b/Standards_EnableAppConsentRequests/run.ps1 @@ -1,32 +1,66 @@ param($tenant) try { + + $ConfigTable = Get-CippTable -tablename 'standards' + $Setting = ((Get-CIPPAzDataTableEntity @ConfigTable -Filter "PartitionKey eq 'standards' and RowKey eq '$tenant'").JSON | ConvertFrom-Json).standards.EnableAppConsentRequests + if (!$Setting) { + $Setting = ((Get-CIPPAzDataTableEntity @ConfigTable -Filter "PartitionKey eq 'standards' and RowKey eq 'AllTenants'").JSON | ConvertFrom-Json).standards.EnableAppConsentRequests + } + # Get current state $CurrentInfo = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies/adminConsentRequestPolicy' -tenantid $Tenant - + # Change state to enabled with default settings $CurrentInfo.isEnabled = 'true' $CurrentInfo.notifyReviewers = 'true' $CurrentInfo.remindersEnabled = 'true' $CurrentInfo.requestDurationInDays = 30 - # Get Global Admin role ID TODO: change to be able to chose role - $Role = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/roleManagement/directory/roleDefinitions?`$filter=(displayName eq 'Global Administrator')&`$select=displayName,id" -tenantid $Tenant - $RoleReviewers = @(@{ - query = "/beta/roleManagement/directory/roleAssignments?`$filter=roleDefinitionId eq `'$($Role.id)`'" + # Roles from standards table + $RolesToAdd = $Setting.ReviewerRoles.value + $RoleNames = $Setting.ReviewerRoles.label -join ', ' + + # Set default if no roles are selected + if (!$RolesToAdd) { + $RolesToAdd = @('62e90394-69f5-4237-9190-012177145e10') + $RoleNames = '(Default) Global Administrator' + } + + $NewReviewers = foreach ($Role in $RolesToAdd) { + @{ + query = "/beta/roleManagement/directory/roleAssignments?`$filter=roleDefinitionId eq '$Role'" queryType = 'MicrosoftGraph' queryRoot = 'null' - }) - # Set reviewers to Global Admins if not already set, this avoids overwriting existing reviewers and duplication of reviewers objects - $CurrentInfo.reviewers = if ($CurrentInfo.reviewers.query -notlike "*$($Role.id)*") { - $RoleReviewers + } + } + + # Add existing reviewers + $Reviewers = [System.Collections.Generic.List[object]]::new() + foreach ($Reviewer in $CurrentInfo.reviewers) { + $RoleFound = $false + foreach ($Role in $RolesToAdd) { + if ($Reviewer.query -match $Role -or $Reviewers.query -contains $Reviewer.query) { + $RoleFound = $true + } + } + if (!$RoleFound) { + $Reviewers.add($Reviewer) + } } - $body = (ConvertTo-Json -Depth 10 -InputObject $CurrentInfo) - (New-GraphPostRequest -tenantid $tenant -Uri 'https://graph.microsoft.com/beta/policies/adminConsentRequestPolicy' -Type put -Body $body -ContentType 'application/json') + # Add new reviewer roles + foreach ($NewReviewer in $NewReviewers) { + $Reviewers.add($NewReviewer) + } + + # Update reviewer list + $CurrentInfo.reviewers = @($Reviewers) + $body = (ConvertTo-Json -Compress -Depth 10 -InputObject $CurrentInfo) + + New-GraphPostRequest -tenantid $tenant -Uri 'https://graph.microsoft.com/beta/policies/adminConsentRequestPolicy' -Type put -Body $body -ContentType 'application/json' + Write-LogMessage -API 'Standards' -tenant $tenant -message "Enabled App consent admin requests for the following roles: $RoleNames" -sev Info - Write-LogMessage -API 'Standards' -tenant $tenant -message 'Enabled App consent admin requests' -sev Info - } catch { Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to enable App consent admin requests. Error: $($_.exception.message)" -sev Error } diff --git a/Tools/Initialize-DevEnvironment.ps1 b/Tools/Initialize-DevEnvironment.ps1 index 46d6088ea4ce..4f4f8f55aa58 100644 --- a/Tools/Initialize-DevEnvironment.ps1 +++ b/Tools/Initialize-DevEnvironment.ps1 @@ -9,7 +9,6 @@ ForEach ($Key in $CIPPSettings.PSObject.Properties.Name) { } } -Import-Module "$CippRoot\GraphHelper.psm1" Import-Module "$CippRoot\Modules\AzBobbyTables" Import-Module "$CippRoot\Modules\DNSHealth" Import-Module "$CippRoot\Modules\CippQueue" diff --git a/profile.ps1 b/profile.ps1 index 056111747b68..66e78cec4b1c 100644 --- a/profile.ps1 +++ b/profile.ps1 @@ -11,7 +11,8 @@ # Authenticate with Azure PowerShell using MSI. # Remove this if you are not planning on using MSI or Azure PowerShell. -Import-Module .\GraphHelper.psm1 +Import-Module CippCore + try { Import-Module Az.KeyVault -ErrorAction Stop } catch { $_.Exception.Message } @@ -19,7 +20,6 @@ try { Import-Module Az.Accounts } catch { $_.Exception.Message } Import-Module CippExtensions -Import-Module CippCore try { Disable-AzContextAutosave -Scope Process | Out-Null diff --git a/version_latest.txt b/version_latest.txt index cfacfe40809c..5ca7df98c441 100644 --- a/version_latest.txt +++ b/version_latest.txt @@ -1 +1 @@ -4.7.1 \ No newline at end of file +4.7.4 \ No newline at end of file