diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000000..c2795fa0dd8c --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "editorconfig.editorconfig" + ] +} diff --git a/Cache_SAMSetup/PermissionsTranslator.json b/Cache_SAMSetup/PermissionsTranslator.json index 27c5a7e6463f..ecc57bd2649d 100644 --- a/Cache_SAMSetup/PermissionsTranslator.json +++ b/Cache_SAMSetup/PermissionsTranslator.json @@ -1004,8 +1004,15 @@ "description": "Allows the app to create, read, update, and delete events of all calendars without a signed-in user.", "displayName": "Read and write calendars in all mailboxes", "id": "ef54d2bf-783f-4e0f-bca1-3210c0444d99", - "origin": "Application", - "value": "Calendars.ReadWrite" + "origin": "Application (Office 365 Exchange Online)", + "value": "Calendars.ReadWrite.All" + }, + { + "description": "Allows the app to create, read, update, and delete user's mailbox settings without a signed-in user. Does not include permission to send mail.", + "displayName": "Read and write all user mailbox settings", + "id": "f9156939-25cd-4ba8-abfe-7fabcf003749", + "origin": "Application (Office 365 Exchange Online)", + "value": "Mailbox.Settings.ReadWrite" }, { "description": "Allows the app to read your organization's user flows, without a signed-in user.", @@ -5286,6 +5293,24 @@ "userConsentDisplayName": "Read Threat and Vulnerability Management vulnerability information", "value": "Exchange.Manage" }, + { + "description": "Allows the app to create, read, update and delete events in all calendars in the organization user has permissions to access. This includes delegate and shared calendars", + "displayName": "Read and write user and shared calendars", + "id": "bbd1ca91-75e0-4814-ad94-9c5dbbae3415", + "Origin": "Delegated (Office 365 Exchange Online)", + "userConsentDescription": "Allows the app to read, update, create and delete events in all calendars in your organization you have permissions to access. This includes delegate and shared calendars", + "userConsentDisplayName": "Read and write to your and shared calendars", + "value": "Calendars.ReadWrite.All" + }, + { + "description": "Allows the app to create, read, update, and delete user's mailbox settings. Does not include permission to send mail.", + "displayName": "Read and write user mailbox settings", + "id": "2e83d72d-8895-4b66-9eea-abb43449ab8b", + "Origin": "Delegated (Office 365 Exchange Online)", + "userConsentDescription": "Allows the app to read, update, create, and delete your mailbox settings.", + "userConsentDisplayName": "Read and write to your mailbox settings", + "value": "MailboxSettings.ReadWrite" + }, { "description": "Allows the app to have full control of all site collections on behalf of the signed-in user.", "displayName": "Manage Sharepoint Online", @@ -5312,5 +5337,14 @@ "userConsentDescription": "Access Microsoft Teams and Skype for Business data as the signed in user", "userConsentDisplayName": "Access Microsoft Teams and Skype for Business data based on the user's role membership", "value": "user_impersonation" + }, + { + "description": "Read and write all on-premises directory synchronization information", + "displayName": "Read and write all on-premises directory synchronization information", + "id": "c2d95988-7604-4ba1-aaed-38a5f82a51c7", + "Origin": "Delegated", + "userConsentDescription": "Access Microsoft Teams and Skype for Business data as the signed in user", + "userConsentDisplayName": "Access Microsoft Teams and Skype for Business data based on the user's role membership", + "value": "OnPremDirectorySynchronization.ReadWrite.All" } ] diff --git a/Cache_SAMSetup/SAMManifest.json b/Cache_SAMSetup/SAMManifest.json index 6b1f6429af88..f94959dc3ac6 100644 --- a/Cache_SAMSetup/SAMManifest.json +++ b/Cache_SAMSetup/SAMManifest.json @@ -11,6 +11,12 @@ ] }, "requiredResourceAccess": [ + { + "resourceAppId": "aeb86249-8ea3-49e2-900b-54cc8e308f85", + "resourceAccess": [ + { "id": "fc946a4f-bc4d-413b-a090-b2c86113ec4f", "type": "Scope" } + ] + }, { "resourceAppId": "fa3d9a0c-3fb0-42cc-9193-47c7ecd2edbd", "resourceAccess": [ @@ -159,7 +165,11 @@ "resourceAppId": "00000002-0000-0ff1-ce00-000000000000", "resourceAccess": [ { "id": "ab4f2b77-0b06-4fc1-a9de-02113fc2ab7c", "type": "Scope" }, - { "id": "dc50a0fb-09a3-484d-be87-e023b12c6440", "type": "Role" } + { "id": "bbd1ca91-75e0-4814-ad94-9c5dbbae3415", "type": "Scope" }, + { "id": "2e83d72d-8895-4b66-9eea-abb43449ab8b", "type": "Scope" }, + { "id": "dc50a0fb-09a3-484d-be87-e023b12c6440", "type": "Role" }, + { "id": "ef54d2bf-783f-4e0f-bca1-3210c0444d99", "type": "Role" }, + { "id": "f9156939-25cd-4ba8-abfe-7fabcf003749", "type": "Role" } ] }, { diff --git a/Durable_BECRun/run.ps1 b/Durable_BECRun/run.ps1 index 377ca2c5533b..44eecff33d2f 100644 --- a/Durable_BECRun/run.ps1 +++ b/Durable_BECRun/run.ps1 @@ -10,7 +10,7 @@ Write-Host "Working on $UserName" try { $startDate = (Get-Date).AddDays(-7) $endDate = (Get-Date) - $auditLog = (New-ExoRequest -tenantid $Tenantfilter -cmdlet 'Get-AdminAuditLogConfig').UnifiedAuditLogIngestionEnabled + $auditLog = (New-ExoRequest -tenantid $Tenantfilter -cmdlet 'Get-AdminAuditLogConfig').UnifiedAuditLogIngestionEnabled $7dayslog = if ($auditLog -eq $false) { $ExtractResult = 'AuditLog is disabled. Cannot perform full analysis' } else { @@ -40,10 +40,10 @@ try { Write-Host "Retrieved $($logsTenant.count) logs" -ForegroundColor Yellow $logsTenant } while ($LogsTenant.count % 5000 -eq 0 -and $LogsTenant.count -ne 0) - $ExtractResult = 'Succesfully extracted logs from auditlog' + $ExtractResult = 'Successfully extracted logs from auditlog' } Try { - $URI = "https://graph.microsoft.com/beta/auditLogs/signIns?`$filter=(userId eq '$SuspectUser')&`$top=1&`$orderby=createdDateTime desc" + $URI = "https://graph.microsoft.com/beta/auditLogs/signIns?`$filter=(userId eq '$SuspectUser')&`$top=1&`$orderby=createdDateTime desc" $LastSignIn = New-GraphGetRequest -uri $URI -tenantid $TenantFilter -noPagination $true -verbose | Select-Object @{ Name = 'CreatedDateTime'; Expression = { $(($_.createdDateTime | Out-String) -replace '\r\n') } }, id, @{ Name = 'AppDisplayName'; Expression = { $_.resourceDisplayName } }, diff --git a/Modules/CIPPCore/Public/Add-CIPPApplicationPermission.ps1 b/Modules/CIPPCore/Public/Add-CIPPApplicationPermission.ps1 index f0f4c6badf6d..5ec28d3c2e7a 100644 --- a/Modules/CIPPCore/Public/Add-CIPPApplicationPermission.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPApplicationPermission.ps1 @@ -6,24 +6,25 @@ function Add-CIPPApplicationPermission { $Tenantfilter ) if ($ApplicationId -eq $ENV:ApplicationID -and $Tenantfilter -eq $env:TenantID) { - return @('Cannot modify application permissions for CIPP-SAM on partner tenant') + #return @('Cannot modify application permissions for CIPP-SAM on partner tenant') + $RequiredResourceAccess = 'CIPPDefaults' } Set-Location (Get-Item $PSScriptRoot).FullName if ($RequiredResourceAccess -eq 'CIPPDefaults') { $RequiredResourceAccess = (Get-Content '.\SAMManifest.json' | ConvertFrom-Json).requiredResourceAccess } - $ServicePrincipalList = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/servicePrincipals?`$select=AppId,id,displayName&`$top=999" -skipTokenCache $true -tenantid $Tenantfilter + $ServicePrincipalList = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/servicePrincipals?`$select=AppId,id,displayName&`$top=999" -skipTokenCache $true -tenantid $Tenantfilter -NoAuthCheck $true $ourSVCPrincipal = $ServicePrincipalList | Where-Object -Property AppId -EQ $ApplicationId if (!$ourSVCPrincipal) { #Our Service Principal isn't available yet. We do a sleep and reexecute after 3 seconds. Start-Sleep -Seconds 5 - $ServicePrincipalList = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/servicePrincipals?`$select=AppId,id,displayName&`$top=999" -skipTokenCache $true -tenantid $Tenantfilter + $ServicePrincipalList = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/servicePrincipals?`$select=AppId,id,displayName&`$top=999" -skipTokenCache $true -tenantid $Tenantfilter -NoAuthCheck $true $ourSVCPrincipal = $ServicePrincipalList | Where-Object -Property AppId -EQ $ApplicationId } $Results = [System.Collections.ArrayList]@() - $CurrentRoles = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/servicePrincipals/$($ourSVCPrincipal.id)/appRoleAssignments" -tenantid $Tenantfilter -skipTokenCache $true + $CurrentRoles = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/servicePrincipals/$($ourSVCPrincipal.id)/appRoleAssignments" -tenantid $Tenantfilter -skipTokenCache $true -NoAuthCheck $true $Grants = foreach ($App in $RequiredResourceAccess) { $svcPrincipalId = $ServicePrincipalList | Where-Object -Property AppId -EQ $App.resourceAppId @@ -40,7 +41,7 @@ function Add-CIPPApplicationPermission { $counter = 0 foreach ($Grant in $Grants) { try { - $SettingsRequest = New-GraphPOSTRequest -body ($Grant | ConvertTo-Json) -uri "https://graph.microsoft.com/beta/servicePrincipals/$($ourSVCPrincipal.id)/appRoleAssignedTo" -tenantid $Tenantfilter -type POST + $SettingsRequest = New-GraphPOSTRequest -body ($Grant | ConvertTo-Json) -uri "https://graph.microsoft.com/beta/servicePrincipals/$($ourSVCPrincipal.id)/appRoleAssignedTo" -tenantid $Tenantfilter -type POST -NoAuthCheck $true $counter++ } catch { $Results.add("Failed to grant $($Grant.appRoleId) to $($Grant.resourceId): $($_.Exception.Message)") | Out-Null @@ -48,4 +49,4 @@ function Add-CIPPApplicationPermission { } "Added $counter Application permissions to $($ourSVCPrincipal.displayName)" return $Results -} \ No newline at end of file +} diff --git a/Modules/CIPPCore/Public/Add-CIPPAzDataTableEntity.ps1 b/Modules/CIPPCore/Public/Add-CIPPAzDataTableEntity.ps1 index befa8155df6c..6e2e0dd618d5 100644 --- a/Modules/CIPPCore/Public/Add-CIPPAzDataTableEntity.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPAzDataTableEntity.ps1 @@ -6,54 +6,138 @@ function Add-CIPPAzDataTableEntity { [switch]$Force, [switch]$CreateTableIfNotExists ) - + + $MaxRowSize = 500000 - 100 # Maximum size of an entity + $MaxSize = 30kb # Maximum size of a property value + foreach ($SingleEnt in $Entity) { try { - Add-AzDataTableEntity -context $Context -force:$Force -CreateTableIfNotExists:$CreateTableIfNotExists -Entity $SingleEnt -ErrorAction Stop + Add-AzDataTableEntity -Context $Context -Force:$Force -CreateTableIfNotExists:$CreateTableIfNotExists -Entity $SingleEnt -ErrorAction Stop } catch [System.Exception] { - if ($_.Exception.ErrorCode -eq 'PropertyValueTooLarge' -or $_.Exception.ErrorCode -eq 'EntityTooLarge') { + if ($_.Exception.ErrorCode -eq 'PropertyValueTooLarge' -or $_.Exception.ErrorCode -eq 'EntityTooLarge' -or $_.Exception.ErrorCode -eq 'RequestBodyTooLarge') { try { - $MaxSize = 30kb - $largePropertyName = $null + $largePropertyNames = [System.Collections.ArrayList]::new() + $entitySize = 0 foreach ($key in $SingleEnt.Keys) { - if ($SingleEnt[$key].Length -gt $MaxSize) { - $largePropertyName = $key - break + $propertySize = [System.Text.Encoding]::UTF8.GetByteCount($SingleEnt[$key].ToString()) + $entitySize = $entitySize + $propertySize + if ($propertySize -gt $MaxSize) { + $largePropertyNames.Add($key) } + } - if ($largePropertyName) { - $dataString = $SingleEnt[$largePropertyName] - $splitCount = [math]::Ceiling($dataString.Length / $MaxSize) - $splitData = 0..($splitCount - 1) | ForEach-Object { - $start = $_ * $MaxSize - $dataString.Substring($start, [Math]::Min($MaxSize, $dataString.Length - $start)) - } + if ($largePropertyNames.Count -gt 0) { + $splitInfoList = [System.Collections.ArrayList]@() + foreach ($largePropertyName in $largePropertyNames) { + $dataString = $SingleEnt[$largePropertyName] + $splitCount = [math]::Ceiling($dataString.Length / $MaxSize) + $splitData = [System.Collections.ArrayList]@() + for ($i = 0; $i -lt $splitCount; $i++) { + $start = $i * $MaxSize + $splitData.Add($dataString.Substring($start, [Math]::Min($MaxSize, $dataString.Length - $start))) > $null + } + + $splitPropertyNames = [System.Collections.ArrayList]@() + for ($i = 0; $i -lt $splitData.Count; $i++) { + $splitPropertyNames.Add("${largePropertyName}_Part$i") > $null + } - $splitPropertyNames = 1..$splitData.Count | ForEach-Object { - "${largePropertyName}_Part$_" + $splitInfo = @{ + OriginalHeader = $largePropertyName + SplitHeaders = $splitPropertyNames + } + $splitInfoList.Add($splitInfo) > $null + $SingleEnt.Remove($largePropertyName) + + for ($i = 0; $i -lt $splitData.Count; $i++) { + $SingleEnt[$splitPropertyNames[$i]] = $splitData[$i] + } } - $splitInfo = @{ - OriginalHeader = $largePropertyName - SplitHeaders = $splitPropertyNames + $SingleEnt['SplitOverProps'] = ($splitInfoList | ConvertTo-Json -Compress).ToString() + } + + # Check if the entity is still too large + $entitySize = [System.Text.Encoding]::UTF8.GetByteCount($($SingleEnt | ConvertTo-Json)) + if ($entitySize -gt $MaxRowSize) { + $rows = [System.Collections.ArrayList]@() + $originalPartitionKey = $SingleEnt.PartitionKey + $originalRowKey = $SingleEnt.RowKey + $entityIndex = 0 + + while ($entitySize -gt $MaxRowSize) { + Write-Information "Entity size is $entitySize. Splitting entity into multiple parts." + $newEntity = @{} + $newEntity['PartitionKey'] = $originalPartitionKey + if ($entityIndex -eq 0) { + $newEntity['RowKey'] = $originalRowKey + } else { + $newEntity['RowKey'] = "$($originalRowKey)-part$entityIndex" + } + $newEntity['OriginalEntityId'] = $originalRowKey + $newEntity['PartIndex'] = $entityIndex + $entityIndex++ + + $propertiesToRemove = [System.Collections.ArrayList]@() + foreach ($key in $SingleEnt.Keys) { + $newEntitySize = [System.Text.Encoding]::UTF8.GetByteCount($($newEntity | ConvertTo-Json)) + if ($newEntitySize -lt $MaxRowSize) { + $propertySize = [System.Text.Encoding]::UTF8.GetByteCount($SingleEnt[$key].ToString()) + if ($propertySize -gt $MaxRowSize) { + $dataString = $SingleEnt[$key] + $splitCount = [math]::Ceiling($dataString.Length / $MaxSize) + $splitData = [System.Collections.ArrayList]@() + for ($i = 0; $i -lt $splitCount; $i++) { + $start = $i * $MaxSize + $splitData.Add($dataString.Substring($start, [Math]::Min($MaxSize, $dataString.Length - $start))) > $null + } + + $splitPropertyNames = [System.Collections.ArrayList]@() + for ($i = 0; $i -lt $splitData.Count; $i++) { + $splitPropertyNames.Add("${key}_Part$i") > $null + } + + for ($i = 0; $i -lt $splitData.Count; $i++) { + $newEntity[$splitPropertyNames[$i]] = $splitData[$i] + } + } else { + $newEntity[$key] = $SingleEnt[$key] + } + $propertiesToRemove.Add($key) > $null + } + } + + foreach ($prop in $propertiesToRemove) { + $SingleEnt.Remove($prop) + } + + $rows.Add($newEntity) > $null + $entitySize = [System.Text.Encoding]::UTF8.GetByteCount($($SingleEnt | ConvertTo-Json)) } - $SingleEnt['SplitOverProps'] = ($splitInfo | ConvertTo-Json).ToString() - $SingleEnt.Remove($largePropertyName) - for ($i = 0; $i -lt $splitData.Count; $i++) { - $SingleEnt[$splitPropertyNames[$i]] = $splitData[$i] + if ($SingleEnt.Count -gt 0) { + $SingleEnt['RowKey'] = "$($originalRowKey)-part$entityIndex" + $SingleEnt['OriginalEntityId'] = $originalRowKey + $SingleEnt['PartIndex'] = $entityIndex + $SingleEnt['PartitionKey'] = $originalPartitionKey + + $rows.Add($SingleEnt) > $null } - Add-AzDataTableEntity -context $Context -force:$Force -CreateTableIfNotExists:$CreateTableIfNotExists -Entity $SingleEnt + foreach ($row in $rows) { + Write-Information "current entity is $($row.RowKey) with $($row.PartitionKey). Our size is $([System.Text.Encoding]::UTF8.GetByteCount($($row | ConvertTo-Json)))" + Add-AzDataTableEntity -Context $Context -Force:$Force -CreateTableIfNotExists:$CreateTableIfNotExists -Entity $row + } + } else { + Add-AzDataTableEntity -Context $Context -Force:$Force -CreateTableIfNotExists:$CreateTableIfNotExists -Entity $SingleEnt } } catch { - throw "Error processing entity: $($_.Exception.Message)." + throw "Error processing entity: $($_.Exception.Message) Linenumner: $($_.InvocationInfo.ScriptLineNumber)" } } else { - Write-Host "THE ERROR IS $($_.Exception.ErrorCode)" - + Write-Information "THE ERROR IS $($_.Exception.ErrorCode). The size of the entity is $entitySize." throw $_ } } diff --git a/Modules/CIPPCore/Public/Add-CIPPDelegatedPermission.ps1 b/Modules/CIPPCore/Public/Add-CIPPDelegatedPermission.ps1 index 921488c45f08..df1e80e1de37 100644 --- a/Modules/CIPPCore/Public/Add-CIPPDelegatedPermission.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPDelegatedPermission.ps1 @@ -3,29 +3,51 @@ function Add-CIPPDelegatedPermission { param( $RequiredResourceAccess, $ApplicationId, + $NoTranslateRequired, $Tenantfilter ) Write-Host 'Adding Delegated Permissions' Set-Location (Get-Item $PSScriptRoot).FullName if ($ApplicationId -eq $ENV:ApplicationID -and $Tenantfilter -eq $env:TenantID) { - return @('Cannot modify delgated permissions for CIPP-SAM on partner tenant') + #return @('Cannot modify delgated permissions for CIPP-SAM on partner tenant') + $RequiredResourceAccess = 'CIPPDefaults' } if ($RequiredResourceAccess -eq 'CIPPDefaults') { $RequiredResourceAccess = (Get-Content '.\SAMManifest.json' | ConvertFrom-Json).requiredResourceAccess + $AdditionalPermissions = Get-Content '.\AdditionalPermissions.json' | ConvertFrom-Json + + if ($Tenantfilter -eq $env:TenantID) { + $RequiredResourceAccess = $RequiredResourceAccess + ($AdditionalPermissions | Where-Object { $RequiredResourceAccess.resourceAppId -notcontains $_.resourceAppId }) + } else { + # remove the partner center permission if not pushing to partner tenant + $RequiredResourceAccess = $RequiredResourceAccess | Where-Object { $_.resourceAppId -ne 'fa3d9a0c-3fb0-42cc-9193-47c7ecd2edbd' } + } + $RequiredResourceAccess = $RequiredResourceAccess + ($AdditionalPermissions | Where-Object { $RequiredResourceAccess.resourceAppId -notcontains $_.resourceAppId }) } $Translator = Get-Content '.\PermissionsTranslator.json' | ConvertFrom-Json - $ServicePrincipalList = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/servicePrincipals?`$select=AppId,id,displayName&`$top=999" -tenantid $Tenantfilter -skipTokenCache $true + $ServicePrincipalList = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/servicePrincipals?`$select=AppId,id,displayName&`$top=999" -tenantid $Tenantfilter -skipTokenCache $true -NoAuthCheck $true $ourSVCPrincipal = $ServicePrincipalList | Where-Object -Property AppId -EQ $ApplicationId $Results = [System.Collections.ArrayList]@() - $CurrentDelegatedScopes = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/servicePrincipals/$($ourSVCPrincipal.id)/oauth2PermissionGrants" -skipTokenCache $true -tenantid $Tenantfilter + $CurrentDelegatedScopes = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/servicePrincipals/$($ourSVCPrincipal.id)/oauth2PermissionGrants" -skipTokenCache $true -tenantid $Tenantfilter -NoAuthCheck $true - foreach ($App in $requiredResourceAccess) { + foreach ($App in $RequiredResourceAccess) { $svcPrincipalId = $ServicePrincipalList | Where-Object -Property AppId -EQ $App.resourceAppId + $AdditionalScopes = ($AdditionalPermissions | Where-Object -Property resourceAppId -EQ $App.resourceAppId).resourceAccess if (!$svcPrincipalId) { continue } - $NewScope = ($Translator | Where-Object { $_.id -in $App.ResourceAccess.id }).value -join ' ' + if ($AdditionalScopes) { + $NewScope = (@(($Translator | Where-Object { $_.id -in $App.ResourceAccess.id }).value) + @($AdditionalScopes.id | Select-Object -Unique)) -join ' ' + } else { + if ($NoTranslateRequired) { + $NewScope = $App.resourceAccess | ForEach-Object { $_.id } -join ' ' + } else { + $NewScope = ($Translator | Where-Object { $_.id -in $App.resourceAccess.id }).value -join ' ' + } + $NewScope = ($Translator | Where-Object { $_.id -in $App.ResourceAccess.id }).value -join ' ' + } + $OldScope = ($CurrentDelegatedScopes | Where-Object -Property Resourceid -EQ $svcPrincipalId.id) if (!$OldScope) { @@ -35,7 +57,7 @@ function Add-CIPPDelegatedPermission { resourceId = $svcPrincipalId.id scope = $NewScope } | ConvertTo-Json -Compress - $CreateRequest = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/v1.0/oauth2PermissionGrants' -tenantid $Tenantfilter -body $Createbody -type POST + $CreateRequest = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/v1.0/oauth2PermissionGrants' -tenantid $Tenantfilter -body $Createbody -type POST -NoAuthCheck $true $Results.add("Successfully added permissions for $($svcPrincipalId.displayName)") | Out-Null } else { $compare = Compare-Object -ReferenceObject $OldScope.scope.Split(' ') -DifferenceObject $NewScope.Split(' ') @@ -46,7 +68,7 @@ function Add-CIPPDelegatedPermission { $Patchbody = @{ scope = "$NewScope" } | ConvertTo-Json -Compress - $Patchrequest = New-GraphPOSTRequest -uri "https://graph.microsoft.com/v1.0/oauth2PermissionGrants/$($OldScope.id)" -tenantid $Tenantfilter -body $Patchbody -type PATCH + $Patchrequest = New-GraphPOSTRequest -uri "https://graph.microsoft.com/v1.0/oauth2PermissionGrants/$($OldScope.id)" -tenantid $Tenantfilter -body $Patchbody -type PATCH -NoAuthCheck $true $Results.add("Successfully updated permissions for $($svcPrincipalId.displayName): $($NewScope)") | Out-Null } } diff --git a/Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 b/Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 index 952e3a68bc93..d1ecc6f090a8 100644 --- a/Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 @@ -2,10 +2,20 @@ function Add-CIPPScheduledTask { [CmdletBinding()] param( [pscustomobject]$Task, - [bool]$Hidden + [bool]$Hidden, + $DisallowDuplicateName = $false, + [string]$SyncType = $null ) $Table = Get-CIPPTable -TableName 'ScheduledTasks' + if ($DisallowDuplicateName) { + $Filter = "PartitionKey eq 'ScheduledTask' and Name eq '$($Task.Name)'" + $ExistingTask = (Get-CIPPAzDataTableEntity @Table -Filter $Filter) + if ($ExistingTask) { + return "Task with name $($Task.Name) already exists" + } + } + $propertiesToCheck = @('Webhook', 'Email', 'PSA') $PostExecution = ($propertiesToCheck | Where-Object { $task.PostExecution.$_ -eq $true }) -join ',' $Parameters = [System.Collections.Hashtable]@{} @@ -34,6 +44,13 @@ function Add-CIPPScheduledTask { } else { $RowKey = $Task.RowKey } + + $Recurrence = if ([string]::IsNullOrEmpty($task.Recurrence.value)) { + $task.Recurrence + } else { + $task.Recurrence.value + } + $entity = @{ PartitionKey = [string]'ScheduledTask' TaskState = [string]'Planned' @@ -43,16 +60,19 @@ function Add-CIPPScheduledTask { Command = [string]$task.Command.value Parameters = [string]$Parameters ScheduledTime = [string]$task.ScheduledTime - Recurrence = [string]$task.Recurrence.value + Recurrence = [string]$Recurrence PostExecution = [string]$PostExecution AdditionalProperties = [string]$AdditionalProperties Hidden = [bool]$Hidden Results = 'Planned' } + if ($SyncType) { + $entity.SyncType = $SyncType + } try { Add-CIPPAzDataTableEntity @Table -Entity $entity -Force } catch { return "Could not add task: $($_.Exception.Message)" } return "Successfully added task: $($entity.Name)" -} \ No newline at end of file +} diff --git a/Modules/CIPPCore/Public/AdditionalPermissions.json b/Modules/CIPPCore/Public/AdditionalPermissions.json new file mode 100644 index 000000000000..4983c6f5fd03 --- /dev/null +++ b/Modules/CIPPCore/Public/AdditionalPermissions.json @@ -0,0 +1,15 @@ +[ + { + "resourceAppId": "00000003-0000-0ff1-ce00-000000000000", + "resourceAccess": [{ "id": "AllProfiles.Manage", "type": "Scope" }] + }, + { + "resourceAppId": "fb78d390-0c51-40cd-8e17-fdbfab77341b", + "resourceAccess": [ + { "id": "AdminApi.AccessAsUser.All", "type": "Scope" }, + { "id": "FfoPowerShell.AccessAsUser.All", "type": "Scope" }, + { "id": "RemotePowerShell.AccessAsUser.All", "type": "Scope" }, + { "id": "VivaFeatureAccessPolicy.Manage.All", "type": "Scope" } + ] + } +] diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSoftDeletedMailboxes.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSoftDeletedMailboxes.ps1 new file mode 100644 index 000000000000..68070c18497e --- /dev/null +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSoftDeletedMailboxes.ps1 @@ -0,0 +1,28 @@ +function Get-CIPPAlertSoftDeletedMailboxes { + <# + .FUNCTIONALITY + Entrypoint + #> + [CmdletBinding()] + Param ( + [Parameter(Mandatory = $false)] + [Alias('input')] + $InputValue, + $TenantFilter + ) + + $Select = 'ExchangeGuid,ArchiveGuid,WhenSoftDeleted,UserPrincipalName,IsInactiveMailbox' + + try { + $SoftDeletedMailBoxes = New-ExoRequest -tenantid $TenantFilter -cmdlet 'get-mailbox' -cmdParams @{SoftDeletedMailbox = $true} -Select $Select | Select-Object ExchangeGuid, ArchiveGuid, WhenSoftDeleted, @{ Name = 'UPN'; Expression = { $_.'UserPrincipalName' } }, IsInactiveMailbox + + # Filter out the mailboxes where IsInactiveMailbox is $true + $AlertData = $SoftDeletedMailBoxes | Where-Object { $_.IsInactiveMailbox -ne $true } + + # Write the alert trace with the filtered data + Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData + + } catch { + Write-AlertMessage -tenant $($TenantFilter) -message "Failed to check for soft deleted mailboxes in $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)" + } +} \ No newline at end of file diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecAddMultiTenantApp.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecAddMultiTenantApp.ps1 index 62e04ac8ba5b..3f2009a0a950 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecAddMultiTenantApp.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecAddMultiTenantApp.ps1 @@ -18,4 +18,4 @@ function Push-ExecAddMultiTenantApp($QueueItem, $TriggerMetadata) { } catch { Write-LogMessage -message "Error adding application to tenant $($Queueitem.Tenant) - $($_.Exception.Message)" -tenant $Queueitem.Tenant -API 'Add Multitenant App' -sev Error } -} \ No newline at end of file +} diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecApplicationCopy.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecApplicationCopy.ps1 new file mode 100644 index 000000000000..6437940809db --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecApplicationCopy.ps1 @@ -0,0 +1,13 @@ +function Push-ExecApplicationCopy($QueueItem, $TriggerMetadata) { + <# + .FUNCTIONALITY + Entrypoint + #> + try { + $Queueitem = $QueueItem | ConvertTo-Json -Depth 10 | ConvertFrom-Json + Write-Host "$($Queueitem | ConvertTo-Json -Depth 10)" + New-CIPPApplicationCopy -App $queueitem.AppId -Tenant $Queueitem.Tenant + } catch { + Write-LogMessage -message "Error adding application to tenant $($Queueitem.Tenant) - $($_.Exception.Message)" -tenant $Queueitem.Tenant -API 'Add Multitenant App' -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecOnboardTenantQueue.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecOnboardTenantQueue.ps1 index 3e597fab6553..0e3d211d6718 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecOnboardTenantQueue.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecOnboardTenantQueue.ps1 @@ -264,14 +264,14 @@ Function Push-ExecOnboardTenantQueue { $TenantOnboarding.Logs = [string](ConvertTo-Json -InputObject @($Logs) -Compress) Add-CIPPAzDataTableEntity @OnboardTable -Entity $TenantOnboarding -Force -ErrorAction Stop - $IsExcluded = (Get-Tenants -SkipList | Where-Object { $_.customerId -eq $Relationship.customer.tenantId } | Measure-Object).Count -gt 0 + $ExcludedTenant = Get-Tenants -SkipList | Where-Object { $_.customerId -eq $Relationship.customer.tenantId } + $IsExcluded = ($ExcludedTenant | Measure-Object).Count -gt 0 if ($IsExcluded) { - $Logs.Add([PSCustomObject]@{ Date = Get-Date -UFormat $DateFormat; Log = 'Tenant is excluded from CIPP, onboarding cannot continue.' }) + $Logs.Add([PSCustomObject]@{ Date = Get-Date -UFormat $DateFormat; Log = ('Tenant is excluded from CIPP, onboarding cannot continue. Remove the exclusion from "{0}" ({1})' -f $ExcludedTenant.displayName, $ExcludedTenant.customerId) }) $TenantOnboarding.Status = 'failed' $OnboardingSteps.Step4.Status = 'failed' $OnboardingSteps.Step4.Message = 'Tenant excluded from CIPP, remove the exclusion and retry onboarding.' } else { - $Logs.Add([PSCustomObject]@{ Date = Get-Date -UFormat $DateFormat; Log = 'Clearing tenant cache' }) $y = 0 do { diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 index e94a5d904bf1..f157dd0884b9 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 @@ -14,7 +14,7 @@ function Push-ExecScheduledCommand { Write-Host "Started Task: $($Item.Command) for tenant: $tenant" try { try { - Write-Host "Starting task: $($Item.Command) with parameters: " + Write-Host "Starting task: $($Item.Command) with parameters: $($commandParameters | ConvertTo-Json)" $results = & $Item.Command @commandParameters } catch { $results = "Task Failed: $($_.Exception.Message)" @@ -112,4 +112,4 @@ function Push-ExecScheduledCommand { if ($TaskType -ne 'Alert') { Write-LogMessage -API 'Scheduler_UserTasks' -tenant $tenant -message "Successfully executed task: $($task.Name)" -sev Info } -} \ No newline at end of file +} diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Webhooks/Push-AuditLogTenant.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Webhooks/Push-AuditLogTenant.ps1 index 0f4c03e1c833..873af42e2e43 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Webhooks/Push-AuditLogTenant.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Webhooks/Push-AuditLogTenant.ps1 @@ -7,9 +7,17 @@ function Push-AuditLogTenant { $WebhookTable = Get-CippTable -tablename 'webhookTable' $Webhooks = Get-CIPPAzDataTableEntity @WebhookTable -Filter "PartitionKey eq '$($Item.TenantFilter)' and Version eq '3'" | Where-Object { $_.Resource -match '^Audit' } $ExistingBundles = Get-CIPPAzDataTableEntity @AuditBundleTable -Filter "PartitionKey eq '$($Item.TenantFilter)' and ContentType eq '$ContentType'" + $ConfigTable = Get-CIPPTable -TableName 'WebhookRules' + $ConfigEntries = Get-CIPPAzDataTableEntity @ConfigTable $NewBundles = [System.Collections.Generic.List[object]]::new() foreach ($Webhook in $Webhooks) { + # only process webhooks that are configured in the webhookrules table + $Configuration = $ConfigEntries | Where-Object { ($_.Tenants -match $TenantFilter -or $_.Tenants -match 'AllTenants') } + if ($Configuration.Type -notcontains $Webhook.Resource) { + continue + } + $TenantFilter = $Webhook.PartitionKey $LogType = $Webhook.Resource Write-Information "Querying for $LogType on $TenantFilter" diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecListBackup.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecListBackup.ps1 index 90b7e41bc4c9..f04ea258bba7 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecListBackup.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecListBackup.ps1 @@ -10,7 +10,10 @@ Function Invoke-ExecListBackup { [CmdletBinding()] param($Request, $TriggerMetadata) - $Result = Get-CIPPBackup -type $Request.body.Type -TenantFilter $Request.body.TenantFilter + $Result = Get-CIPPBackup -type $Request.query.Type -TenantFilter $Request.query.TenantFilter + if ($request.query.NameOnly) { + $Result = $Result | Select-Object RowKey, timestamp + } Write-LogMessage -user $request.headers.'x-ms-client-principal' -API 'Alerts' -message $request.body.text -Sev $request.body.Severity # Associate values to output bindings by calling 'Push-OutputBinding'. Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-AddScheduledItem.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-AddScheduledItem.ps1 index 50860f6e034b..f5885143a196 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-AddScheduledItem.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-AddScheduledItem.ps1 @@ -14,7 +14,7 @@ Function Invoke-AddScheduledItem { } else { $hidden = $true } - $Result = Add-CIPPScheduledTask -Task $Request.body -hidden $hidden + $Result = Add-CIPPScheduledTask -Task $Request.body -hidden $hidden -DisallowDuplicateName $Request.query.DisallowDuplicateName Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message $Result -Sev 'Info' Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-ListScheduledItems.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-ListScheduledItems.ps1 index 81231ca9df96..4a3869b56176 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-ListScheduledItems.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-ListScheduledItems.ps1 @@ -19,6 +19,11 @@ Function Invoke-ListScheduledItems { $HiddenTasks = $true } $Tasks = Get-CIPPAzDataTableEntity @Table -Filter "PartitionKey eq 'ScheduledTask'" | Where-Object { $_.Hidden -ne $HiddenTasks } + if ($Request.Query.Type) { + $tasks.Command + $Tasks = $Tasks | Where-Object { $_.command -eq $Request.Query.Type } + } + $AllowedTenants = Test-CIPPAccess -Request $Request -TenantList if ($AllowedTenants -notcontains 'AllTenants') { $Tasks = $Tasks | Where-Object -Property TenantId -In $AllowedTenants diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExtensionMapping.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExtensionMapping.ps1 index 77b8e277c780..32a21c2119f4 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExtensionMapping.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExtensionMapping.ps1 @@ -20,17 +20,20 @@ Function Invoke-ExecExtensionMapping { if ($Request.Query.List) { switch ($Request.Query.List) { - 'Halo' { + 'HaloPSA' { $body = Get-HaloMapping -CIPPMapping $Table } - - 'NinjaOrgs' { + 'NinjaOne' { $Body = Get-NinjaOneOrgMapping -CIPPMapping $Table } - - 'NinjaFields' { + 'NinjaOneFields' { $Body = Get-NinjaOneFieldMapping -CIPPMapping $Table - + } + 'Hudu' { + $Body = Get-HuduMapping -CIPPMapping $Table + } + 'HuduFields' { + $Body = Get-HuduFieldMapping -CIPPMapping $Table } } } @@ -38,15 +41,23 @@ Function Invoke-ExecExtensionMapping { try { if ($Request.Query.AddMapping) { switch ($Request.Query.AddMapping) { - 'Halo' { + 'HaloPSA' { $body = Set-HaloMapping -CIPPMapping $Table -APIName $APIName -Request $Request } - 'NinjaOrgs' { + 'NinjaOne' { $Body = Set-NinjaOneOrgMapping -CIPPMapping $Table -APIName $APIName -Request $Request } - 'NinjaFields' { + 'NinjaOneFields' { $Body = Set-NinjaOneFieldMapping -CIPPMapping $Table -APIName $APIName -Request $Request -TriggerMetadata $TriggerMetadata } + 'Hudu' { + $Body = Set-HuduMapping -CIPPMapping $Table -APIName $APIName -Request $Request + Register-CIPPExtensionScheduledTasks + } + 'HuduFields' { + $Body = Set-ExtensionFieldMapping -CIPPMapping $Table -APIName $APIName -Request $Request -Extension 'Hudu' + Register-CIPPExtensionScheduledTasks + } } } } catch { diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExtensionSync.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExtensionSync.ps1 index 2f069988996a..911ce71d7e14 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExtensionSync.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExtensionSync.ps1 @@ -13,42 +13,60 @@ Function Invoke-ExecExtensionSync { $APIName = $TriggerMetadata.FunctionName Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message 'Accessed this API' -Sev 'Debug' - if ($Request.Query.Extension -eq 'Gradient') { - try { - Write-LogMessage -API 'Scheduler_Billing' -tenant 'none' -message 'Starting billing processing.' -sev Info - $Table = Get-CIPPTable -TableName Extensionsconfig - $Configuration = (Get-CIPPAzDataTableEntity @Table).config | ConvertFrom-Json -Depth 10 - foreach ($ConfigItem in $Configuration.psobject.properties.name) { - switch ($ConfigItem) { - 'Gradient' { - If ($Configuration.Gradient.enabled -and $Configuration.Gradient.BillingEnabled) { - Push-OutputBinding -Name gradientqueue -Value 'LetsGo' - $Results = [pscustomobject]@{'Results' = 'Succesfully started Gradient Sync' } + switch ($Request.Query.Extension) { + 'Gradient' { + try { + Write-LogMessage -API 'Scheduler_Billing' -tenant 'none' -message 'Starting billing processing.' -sev Info + $Table = Get-CIPPTable -TableName Extensionsconfig + $Configuration = (Get-CIPPAzDataTableEntity @Table).config | ConvertFrom-Json -Depth 10 + foreach ($ConfigItem in $Configuration.psobject.properties.name) { + switch ($ConfigItem) { + 'Gradient' { + If ($Configuration.Gradient.enabled -and $Configuration.Gradient.BillingEnabled) { + Push-OutputBinding -Name gradientqueue -Value 'LetsGo' + $Results = [pscustomobject]@{'Results' = 'Successfully started Gradient Sync' } + } } } } - } - } catch { - $Results = [pscustomobject]@{'Results' = "Could not start Gradient Sync: $($_.Exception.Message)" } + } catch { + $Results = [pscustomobject]@{'Results' = "Could not start Gradient Sync: $($_.Exception.Message)" } - Write-LogMessage -API 'Scheduler_Billing' -tenant 'none' -message "Could not start billing processing $($_.Exception.Message)" -sev Error + Write-LogMessage -API 'Scheduler_Billing' -tenant 'none' -message "Could not start billing processing $($_.Exception.Message)" -sev Error + } } - } - if ($Request.Query.Extension -eq 'NinjaOne') { - try { - $Table = Get-CIPPTable -TableName NinjaOneSettings + 'NinjaOne' { + try { + $Table = Get-CIPPTable -TableName NinjaOneSettings + + $CIPPMapping = Get-CIPPTable -TableName CippMapping + $Filter = "PartitionKey eq 'NinjaOneMapping'" + $TenantsToProcess = Get-AzDataTableEntity @CIPPMapping -Filter $Filter | Where-Object { $Null -ne $_.IntegrationId -and $_.IntegrationId -ne '' } + + if ($Request.Query.TenantID) { + $Tenant = $TenantsToProcess | Where-Object { $_.RowKey -eq $Request.Query.TenantID } + if (($Tenant | Measure-Object).count -eq 1) { + $Batch = [PSCustomObject]@{ + 'NinjaAction' = 'SyncTenant' + 'MappedTenant' = $Tenant + 'FunctionName' = 'NinjaOneQueue' + } + $InputObject = [PSCustomObject]@{ + OrchestratorName = 'NinjaOneOrchestrator' + Batch = @($Batch) + } + #Write-Host ($InputObject | ConvertTo-Json) + $InstanceId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) - $CIPPMapping = Get-CIPPTable -TableName CippMapping - $Filter = "PartitionKey eq 'NinjaOrgsMapping'" - $TenantsToProcess = Get-AzDataTableEntity @CIPPMapping -Filter $Filter | Where-Object { $Null -ne $_.NinjaOne -and $_.NinjaOne -ne '' } + $Results = [pscustomobject]@{'Results' = "NinjaOne Synchronization Queued for $($Tenant.IntegrationName)" } + } else { + $Results = [pscustomobject]@{'Results' = 'Tenant was not found.' } + } - if ($Request.Query.TenantID) { - $Tenant = $TenantsToProcess | Where-Object { $_.RowKey -eq $Request.Query.TenantID } - if (($Tenant | Measure-Object).count -eq 1) { + } else { $Batch = [PSCustomObject]@{ - 'NinjaAction' = 'SyncTenant' - 'MappedTenant' = $Tenant + 'NinjaAction' = 'SyncTenants' 'FunctionName' = 'NinjaOneQueue' } $InputObject = [PSCustomObject]@{ @@ -58,32 +76,17 @@ Function Invoke-ExecExtensionSync { #Write-Host ($InputObject | ConvertTo-Json) $InstanceId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) Write-Host "Started permissions orchestration with ID = '$InstanceId'" + $Results = [pscustomobject]@{'Results' = "NinjaOne Synchronization Queuing $(($TenantsToProcess | Measure-Object).count) Tenants" } - $Results = [pscustomobject]@{'Results' = "NinjaOne Synchronization Queued for $($Tenant.NinjaOneName)" } - } else { - $Results = [pscustomobject]@{'Results' = 'Tenant was not found.' } } - - } else { - $Batch = [PSCustomObject]@{ - 'NinjaAction' = 'SyncTenants' - 'FunctionName' = 'NinjaOneQueue' - } - $InputObject = [PSCustomObject]@{ - OrchestratorName = 'NinjaOneOrchestrator' - Batch = @($Batch) - } - #Write-Host ($InputObject | ConvertTo-Json) - $InstanceId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) - Write-Host "Started permissions orchestration with ID = '$InstanceId'" - $Results = [pscustomobject]@{'Results' = "NinjaOne Synchronization Queuing $(($TenantsToProcess | Measure-Object).count) Tenants" } - + } catch { + $Results = [pscustomobject]@{'Results' = "Could not start NinjaOne Sync: $($_.Exception.Message)" } + Write-LogMessage -API 'Scheduler_Billing' -tenant 'none' -message "Could not start NinjaOne Sync $($_.Exception.Message)" -sev Error } - - - } catch { - $Results = [pscustomobject]@{'Results' = "Could not start NinjaOne Sync: $($_.Exception.Message)" } - Write-LogMessage -API 'Scheduler_Billing' -tenant 'none' -message "Could not start NinjaOne Sync $($_.Exception.Message)" -sev Error + } + 'Hudu' { + Register-CIPPExtensionScheduledTasks -Reschedule + $Results = [pscustomobject]@{'Results' = 'Extension sync tasks have been rescheduled and will start within 15 minutes' } } } @@ -92,6 +95,6 @@ Function Invoke-ExecExtensionSync { Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ StatusCode = [HttpStatusCode]::OK Body = $Results - }) -clobber + }) } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExtensionTest.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExtensionTest.ps1 index 78e82e78121a..e9a6465c4ff0 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExtensionTest.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExtensionTest.ps1 @@ -27,7 +27,7 @@ Function Invoke-ExecExtensionTest { if ($ExistingIntegrations.Status -ne 'active') { $ActivateRequest = Invoke-RestMethod -Uri 'https://app.usegradient.com/api/vendor-api/organization/status/active' -Method PATCH -Headers $GradientToken } - $Results = [pscustomobject]@{'Results' = 'Succesfully Connected to Gradient' } + $Results = [pscustomobject]@{'Results' = 'Successfully Connected to Gradient' } } 'CIPP-API' { @@ -35,17 +35,23 @@ Function Invoke-ExecExtensionTest { } 'NinjaOne' { $token = Get-NinjaOneToken -configuration $Configuration.NinjaOne - $Results = [pscustomobject]@{'Results' = 'Succesfully Connected to NinjaOne' } + $Results = [pscustomobject]@{'Results' = 'Successfully Connected to NinjaOne' } } 'PWPush' { $Payload = 'This is a test from CIPP' $PasswordLink = New-PwPushLink -Payload $Payload if ($PasswordLink) { - $Results = [pscustomobject]@{'Results' = 'Succesfully generated PWPush'; 'Link' = $PasswordLink } + $Results = [pscustomobject]@{'Results' = 'Successfully generated PWPush'; 'Link' = $PasswordLink } } else { $Results = [pscustomobject]@{'Results' = 'PWPush is not enabled' } } } + 'Hudu' { + Connect-HuduAPI -configuration $Configuration.Hudu + $Version = Get-HuduAppInfo + Write-Host ($Version | ConvertTo-Json) + $Results = [pscustomobject]@{'Results' = ('Successfully Connected to Hudu, version: {0}' -f $Version.version) } + } } } catch { $Results = [pscustomobject]@{'Results' = "Failed to connect: $($_.Exception.Message) $($_.InvocationInfo.ScriptLineNumber)" } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExtensionsConfig.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExtensionsConfig.ps1 index fd3b8764959d..bc19a2b3940a 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExtensionsConfig.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExtensionsConfig.ps1 @@ -7,25 +7,26 @@ Function Invoke-ExecExtensionsConfig { .ROLE CIPP.Extension.ReadWrite #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '', Scope = 'Function')] [CmdletBinding()] param($Request, $TriggerMetadata) $APIName = $TriggerMetadata.FunctionName - Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message 'Accessed this API' -Sev 'Debug' + Write-LogMessage -user $Request.Headers.'x-ms-client-principal' -API $APINAME -message 'Accessed this API' -Sev 'Debug' #Connect-AzAccount -UseDeviceAuthentication # Write to the Azure Functions log stream. - Write-Host 'PowerShell HTTP trigger function processed a request.' + Write-Information 'PowerShell HTTP trigger function processed a request.' $results = try { - if ($Request.body.CIPPAPI.Enabled) { - $APIConfig = New-CIPPAPIConfig -ExecutingUser $request.headers.'x-ms-client-principal' -resetpassword $request.body.CIPPAPI.ResetPassword + if ($Request.Body.CIPPAPI.Enabled) { + $APIConfig = New-CIPPAPIConfig -ExecutingUser $Request.Headers.'x-ms-client-principal' -resetpassword $Request.Body.CIPPAPI.ResetPassword $AddedText = $APIConfig.Results } - # Check if NinjaOne URL is set correctly and the intance has at least version 5.6 - if ($request.body.NinjaOne) { + # Check if NinjaOne URL is set correctly and the instance has at least version 5.6 + if ($Request.Body.NinjaOne) { try { - [version]$Version = (Invoke-WebRequest -Method GET -Uri "https://$(($request.body.NinjaOne.Instance -replace '/ws','') -replace 'https://','')/app-version.txt" -ea stop).content + [version]$Version = (Invoke-WebRequest -Method GET -Uri "https://$(($Request.Body.NinjaOne.Instance -replace '/ws','') -replace 'https://','')/app-version.txt" -ea stop).content } catch { throw "Failed to connect to NinjaOne check your Instance is set correctly eg 'app.ninjarmmm.com'" } @@ -35,30 +36,31 @@ Function Invoke-ExecExtensionsConfig { } $Table = Get-CIPPTable -TableName Extensionsconfig - foreach ($APIKey in ([pscustomobject]$request.body).psobject.properties.name) { - Write-Host "Working on $apikey" - if ($request.body.$APIKey.APIKey -eq 'SentToKeyVault' -or $request.body.$APIKey.APIKey -eq '') { - Write-Host 'Not sending to keyvault. Key previously set or left blank.' + foreach ($APIKey in ([pscustomobject]$Request.Body).psobject.properties.name) { + Write-Information "Working on $apikey" + if ($Request.Body.$APIKey.APIKey -eq 'SentToKeyVault' -or $Request.Body.$APIKey.APIKey -eq '') { + Write-Information 'Not sending to keyvault. Key previously set or left blank.' } else { - Write-Host 'writing API Key to keyvault, and clearing.' - Write-Host "$ENV:WEBSITE_DEPLOYMENT_ID" - if ($request.body.$APIKey.APIKey) { + Write-Information 'writing API Key to keyvault, and clearing.' + Write-Information "$ENV:WEBSITE_DEPLOYMENT_ID" + if ($Request.Body.$APIKey.APIKey) { if ($env:AzureWebJobsStorage -eq 'UseDevelopmentStorage=true') { $DevSecretsTable = Get-CIPPTable -tablename 'DevSecrets' $Secret = [PSCustomObject]@{ 'PartitionKey' = $APIKey 'RowKey' = $APIKey - 'APIKey' = $request.body.$APIKey.APIKey + 'APIKey' = $Request.Body.$APIKey.APIKey } Add-CIPPAzDataTableEntity @DevSecretsTable -Entity $Secret -Force } else { - $null = Set-AzKeyVaultSecret -VaultName $ENV:WEBSITE_DEPLOYMENT_ID -Name $APIKey -SecretValue (ConvertTo-SecureString -String $request.body.$APIKey.APIKey -AsPlainText -Force) + $null = Set-AzKeyVaultSecret -VaultName $ENV:WEBSITE_DEPLOYMENT_ID -Name $APIKey -SecretValue (ConvertTo-SecureString -AsPlainText -Force -String $Request.Body.$APIKey.APIKey) } } - $request.body.$APIKey.APIKey = 'SentToKeyVault' + $Request.Body.$APIKey.APIKey = 'SentToKeyVault' } + $Request.Body.$APIKey = $Request.Body.$APIKey | Select-Object * -ExcludeProperty ResetPassword } - $body = $request.body | Select-Object * -ExcludeProperty APIKey, Enabled | ConvertTo-Json -Depth 10 -Compress + $body = $Request.Body | Select-Object * -ExcludeProperty APIKey, Enabled | ConvertTo-Json -Depth 10 -Compress $Config = @{ 'PartitionKey' = 'CippExtensions' 'RowKey' = 'Config' @@ -66,6 +68,18 @@ Function Invoke-ExecExtensionsConfig { } Add-CIPPAzDataTableEntity @Table -Entity $Config -Force | Out-Null + + #Write-Information ($Request.Headers | ConvertTo-Json) + $AddObject = @{ + PartitionKey = 'InstanceProperties' + RowKey = 'CIPPURL' + Value = [string]([System.Uri]$Request.Headers.'x-ms-original-url').Host + } + Write-Information ($AddObject | ConvertTo-Json -Compress) + $ConfigTable = Get-CIPPTable -tablename 'Config' + Add-AzDataTableEntity @ConfigTable -Entity $AddObject -Force + + Register-CIPPExtensionScheduledTasks "Successfully set the configuration. $AddedText" } catch { "Failed to set configuration: $($_.Exception.message) Linenumber: $($_.InvocationInfo.ScriptLineNumber)" diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecRestoreBackup.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecRestoreBackup.ps1 index b9820352bfdd..476fffa02389 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecRestoreBackup.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecRestoreBackup.ps1 @@ -25,7 +25,7 @@ Function Invoke-ExecRestoreBackup { Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message 'Created backup' -Sev 'Debug' $body = [pscustomobject]@{ - 'Results' = 'Succesfully restored backup.' + 'Results' = 'Successfully restored backup.' } } catch { Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message "Failed to create backup: $($_.Exception.Message)" -Sev 'Error' diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecRunBackup.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecRunBackup.ps1 index ef8d564acdca..afdd570d8910 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecRunBackup.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecRunBackup.ps1 @@ -15,7 +15,7 @@ Function Invoke-ExecRunBackup { $CSVfile = New-CIPPBackup -BackupType 'CIPP' $body = [pscustomobject]@{ 'Results' = 'Created backup' - backup = $CSVfile + backup = $CSVfile.BackupData } # Associate values to output bindings by calling 'Push-OutputBinding'. Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Invoke-ExecDisableEmailForward.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Invoke-ExecDisableEmailForward.ps1 deleted file mode 100644 index d2894b583ee4..000000000000 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Invoke-ExecDisableEmailForward.ps1 +++ /dev/null @@ -1,34 +0,0 @@ -using namespace System.Net - -Function Invoke-ExecDisableEmailForward { - <# - .FUNCTIONALITY - Entrypoint - .ROLE - Exchange.Mailbox.ReadWrite - #> - [CmdletBinding()] - param($Request, $TriggerMetadata) - try { - $APIName = $TriggerMetadata.FunctionName - Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message 'Accessed this API' -Sev 'Debug' - $Username = $request.body.user - $Tenantfilter = $request.body.tenantfilter - $Results = try { - Set-CIPPForwarding -userid $Request.body.user -tenantFilter $TenantFilter -APIName $APINAME -ExecutingUser $request.headers.'x-ms-client-principal' -Forward $null -keepCopy $false -ForwardingSMTPAddress $null -Disable $true - } catch { - "Could not disable forwarding message for $($username). Error: $($_.Exception.Message)" - } - - $body = [pscustomobject]@{'Results' = @($results) } - } catch { - $body = [pscustomobject]@{'Results' = @("Could not disable forwarding user: $($_.Exception.message)") } - } - - # Associate values to output bindings by calling 'Push-OutputBinding'. - Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ - StatusCode = [HttpStatusCode]::OK - Body = $Body - }) - -} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Invoke-ExecEmailForward.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Invoke-ExecEmailForward.ps1 index 9ddf5aa7b523..e64c821acd68 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Invoke-ExecEmailForward.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Invoke-ExecEmailForward.ps1 @@ -16,15 +16,14 @@ Function Invoke-ExecEmailForward { $ForwardingSMTPAddress = $request.body.ForwardExternal $DisableForwarding = $request.body.disableForwarding $APIName = $TriggerMetadata.FunctionName + [bool]$KeepCopy = if ($request.body.keepCopy -eq 'true') { $true } else { $false } if ($ForwardingAddress) { try { - New-ExoRequest -tenantid $TenantFilter -cmdlet 'Set-mailbox' -cmdParams @{Identity = $Username; ForwardingAddress = $ForwardingAddress ; DeliverToMailboxAndForward = [bool]$request.body.keepCopy } -Anchor $username + Set-CIPPForwarding -userid $username -tenantFilter $TenantFilter -APIName $APINAME -ExecutingUser $request.headers.'x-ms-client-principal' -Forward $ForwardingAddress -keepCopy $KeepCopy if (-not $request.body.KeepCopy) { - Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message "Set Forwarding for $($username) to $($ForwardingAddress) and not keeping a copy" -Sev 'Info' -tenant $TenantFilter $results = "Forwarding all email for $($username) to $($ForwardingAddress) and not keeping a copy" - } elseif ($request.body.KeepCopy) { - Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message "Set Forwarding for $($username) to $($ForwardingAddress) and keeping a copy" -Sev 'Info' -tenant $TenantFilter + } else { $results = "Forwarding all email for $($username) to $($ForwardingAddress) and keeping a copy" } } catch { @@ -34,14 +33,12 @@ Function Invoke-ExecEmailForward { } } - elseif ($ForwardingSMTPAddress) { + if ($ForwardingSMTPAddress) { try { - New-ExoRequest -tenantid $TenantFilter -cmdlet 'Set-mailbox' -cmdParams @{Identity = $Username; ForwardingSMTPAddress = $ForwardingSMTPAddress ; DeliverToMailboxAndForward = [bool]$request.body.keepCopy } -Anchor $username + Set-CIPPForwarding -userid $username -tenantFilter $TenantFilter -APIName $APINAME -ExecutingUser $request.headers.'x-ms-client-principal' -forwardingSMTPAddress $ForwardingSMTPAddress -keepCopy $KeepCopy if (-not $request.body.KeepCopy) { - Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message "Set forwarding for $($username) to $($ForwardingSMTPAddress) and not keeping a copy" -Sev 'Info' -tenant $TenantFilter $results = "Forwarding all email for $($username) to $($ForwardingSMTPAddress) and not keeping a copy" - } elseif ($request.body.KeepCopy) { - Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message "Set forwarding for $($username) to $($ForwardingSMTPAddress) and keeping a copy" -Sev 'Info' -tenant $TenantFilter + } else { $results = "Forwarding all email for $($username) to $($ForwardingSMTPAddress) and keeping a copy" } } catch { @@ -52,10 +49,9 @@ Function Invoke-ExecEmailForward { } - elseif ($DisableForwarding -eq 'True') { + if ($DisableForwarding -eq 'True') { try { - New-ExoRequest -tenantid $TenantFilter -cmdlet 'Set-Mailbox' -cmdParams @{Identity = $Username; ForwardingAddress = $null; ForwardingSMTPAddress = $null; DeliverToMailboxAndForward = $false } - Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message "Disabled Email forwarding for $($username)" -Sev 'Info' -tenant $TenantFilter + Set-CIPPForwarding -userid $username -username $username -tenantFilter $Tenantfilter -ExecutingUser $ExecutingUser -APIName $APIName -Disable $true $results = "Disabled Email Forwarding for $($username)" } catch { Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message "Could not disable Email forwarding for $($username)" -Sev 'Error' -tenant $TenantFilter diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Invoke-ExecMailboxRestore.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Invoke-ExecMailboxRestore.ps1 index fb66074d979d..7e787acacfc8 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Invoke-ExecMailboxRestore.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Invoke-ExecMailboxRestore.ps1 @@ -46,7 +46,7 @@ function Invoke-ExecMailboxRestore { $TenantFilter = $Request.Body.TenantFilter $RequestName = $Request.Body.RequestName $SourceMailbox = $Request.Body.SourceMailbox - $TargetMailbox = $Request.Body.TargetMailbox + $TargetMailbox = if (!$Request.Body.input) {$Request.Body.TargetMailbox} else {$Request.Body.input} $ExoRequest = @{ tenantid = $TenantFilter diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddWinGetApp.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddWinGetApp.ps1 index 8097c6d328e2..f80645694331 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddWinGetApp.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddWinGetApp.ps1 @@ -24,7 +24,7 @@ Function Invoke-AddWinGetApp { 'packageIdentifier' = "$($WinGetApp.PackageName)" 'installExperience' = @{ '@odata.type' = 'microsoft.graph.winGetAppInstallExperience' - 'runAsAccount' = 'user' + 'runAsAccount' = 'system' } } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddDefenderDeployment.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddDefenderDeployment.ps1 index 7bb3f446bd11..bc94e7cf5851 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddDefenderDeployment.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddDefenderDeployment.ps1 @@ -33,9 +33,9 @@ Function Invoke-AddDefenderDeployment { allowPartnerToCollectIOSPersonalApplicationMetadata = [bool]$Compliance.ConnectIosCompliance androidMobileApplicationManagementEnabled = [bool]$Compliance.ConnectAndroidCompliance iosMobileApplicationManagementEnabled = [bool]$Compliance.appSync - microsoftDefenderForEndpointAttachEnabled = [bool]$compliance.AllowMEMEnforceCompliance + microsoftDefenderForEndpointAttachEnabled = [bool]$true } | ConvertTo-Json -Compress - $SettingsRequest = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/mobileThreatDefenseConnectors/' -tenantid $tenant -type POST -body $SettingsObj + $SettingsRequest = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/mobileThreatDefenseConnectors/' -tenantid $tenant -type POST -body $SettingsObj -AsApp $true "$($Tenant): Successfully set Defender Compliance and Reporting settings" $Settings = switch ($PolicySettings) { @@ -79,8 +79,7 @@ Function Invoke-AddDefenderDeployment { Write-Host ($CheckExististing | ConvertTo-Json) if ('Default AV Policy' -in $CheckExististing.Name) { "$($Tenant): AV Policy already exists. Skipping" - } - else { + } else { $PolBody = ConvertTo-Json -Depth 10 -Compress -InputObject @{ name = 'Default AV Policy' description = '' @@ -138,8 +137,7 @@ Function Invoke-AddDefenderDeployment { $CheckExististingASR = New-GraphGETRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/configurationPolicies' -tenantid $tenant if ('ASR Default rules' -in $CheckExististingASR.Name) { "$($Tenant): ASR Policy already exists. Skipping" - } - else { + } else { Write-Host $ASRbody $ASRRequest = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/configurationPolicies' -tenantid $tenant -type POST -body $ASRbody Write-Host ($ASRRequest.id) @@ -215,9 +213,8 @@ Function Invoke-AddDefenderDeployment { $CheckExististingEDR = New-GraphGETRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/configurationPolicies' -tenantid $tenant if ('EDR Configuration' -in $CheckExististingEDR.Name) { "$($Tenant): EDR Policy already exists. Skipping" - } - else { - $EDRRequest = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/configurationPolicies' -tenantid $tenant -type POST -body $EDRbody + } else { + #$EDRRequest = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/configurationPolicies' -tenantid $tenant -type POST -body $EDRbody if ($ASR.AssignTo -ne 'none') { $AssignBody = if ($ASR.AssignTo -ne 'AllDevicesAndUsers') { '{"assignments":[{"id":"","target":{"@odata.type":"#microsoft.graph.' + $($asr.AssignTo) + 'AssignmentTarget"}}]}' } else { '{"assignments":[{"id":"","target":{"@odata.type":"#microsoft.graph.allDevicesAssignmentTarget"}},{"id":"","target":{"@odata.type":"#microsoft.graph.allLicensedUsersAssignmentTarget"}}]}' } $assign = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/configurationPolicies('$($EDRRequest.id)')/assign" -tenantid $tenant -type POST -body $AssignBody @@ -226,8 +223,7 @@ Function Invoke-AddDefenderDeployment { "$($Tenant): Successfully added EDR Settings" } - } - catch { + } catch { "Failed to add policy for $($Tenant): $($_.Exception.Message)" Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -tenant $($Tenant) -message "Failed adding policy $($Displayname). Error: $($_.Exception.Message)" -Sev 'Error' continue diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddIntuneTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddIntuneTemplate.ps1 index 498441852f6f..847c5f1174c7 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddIntuneTemplate.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddIntuneTemplate.ps1 @@ -41,82 +41,13 @@ Function Invoke-AddIntuneTemplate { $TenantFilter = $Request.Query.TenantFilter $URLName = $Request.Query.URLName $ID = $Request.Query.id - switch ($URLName) { - 'deviceCompliancePolicies' { - $Type = 'deviceCompliancePolicies' - $Template = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$($urlname)/$($ID)?`$expand=scheduledActionsForRule(`$expand=scheduledActionConfigurations)" -tenantid $tenantfilter - $DisplayName = $Template.displayName - $TemplateJson = ConvertTo-Json -InputObject $Template -Depth 100 -Compress - } - 'managedAppPolicies' { - $Type = 'AppProtection' - $Template = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceAppManagement/$($urlname)('$($ID)')" -tenantid $tenantfilter - $DisplayName = $Template.displayName - $TemplateJson = ConvertTo-Json -InputObject $Template -Depth 100 -Compress - } - 'configurationPolicies' { - $Type = 'Catalog' - $Template = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$($urlname)('$($ID)')?`$expand=settings" -tenantid $tenantfilter | Select-Object name, description, settings, platforms, technologies, templateReference - $TemplateJson = $Template | ConvertTo-Json -Depth 100 - $DisplayName = $Template.name - - } - 'windowsDriverUpdateProfiles' { - $Type = 'windowsDriverUpdateProfiles' - $Template = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$($urlname)/$($ID)" -tenantid $tenantfilter | Select-Object * -ExcludeProperty id, lastModifiedDateTime, '@odata.context', 'ScopeTagIds', 'supportsScopeTags', 'createdDateTime' - Write-Host ($Template | ConvertTo-Json) - $DisplayName = $Template.displayName - $TemplateJson = ConvertTo-Json -InputObject $Template -Depth 100 -Compress - } - 'deviceConfigurations' { - $Type = 'Device' - $Template = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$($urlname)/$($ID)" -tenantid $tenantfilter | Select-Object * -ExcludeProperty id, lastModifiedDateTime, '@odata.context', 'ScopeTagIds', 'supportsScopeTags', 'createdDateTime' - Write-Host ($Template | ConvertTo-Json) - $DisplayName = $Template.displayName - $TemplateJson = ConvertTo-Json -InputObject $Template -Depth 100 -Compress - } - 'groupPolicyConfigurations' { - $Type = 'Admin' - $Template = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$($urlname)('$($ID)')" -tenantid $tenantfilter - $DisplayName = $Template.displayName - $TemplateJsonItems = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$($urlname)('$($ID)')/definitionValues?`$expand=definition" -tenantid $tenantfilter - $TemplateJsonSource = foreach ($TemplateJsonItem in $TemplateJsonItems) { - $presentationValues = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$($urlname)('$($ID)')/definitionValues('$($TemplateJsonItem.id)')/presentationValues?`$expand=presentation" -tenantid $tenantfilter | ForEach-Object { - $obj = $_ - if ($obj.id) { - $PresObj = @{ - id = $obj.id - 'presentation@odata.bind' = "https://graph.microsoft.com/beta/deviceManagement/groupPolicyDefinitions('$($TemplateJsonItem.definition.id)')/presentations('$($obj.presentation.id)')" - } - if ($obj.values) { $PresObj['values'] = $obj.values } - if ($obj.value) { $PresObj['value'] = $obj.value } - if ($obj.'@odata.type') { $PresObj['@odata.type'] = $obj.'@odata.type' } - [pscustomobject]$PresObj - } - } - [PSCustomObject]@{ - 'definition@odata.bind' = "https://graph.microsoft.com/beta/deviceManagement/groupPolicyDefinitions('$($TemplateJsonItem.definition.id)')" - enabled = $TemplateJsonItem.enabled - presentationValues = @($presentationValues) - } - } - $inputvar = [pscustomobject]@{ - added = @($TemplateJsonSource) - updated = @() - deletedIds = @() - - } - - - $TemplateJson = (ConvertTo-Json -InputObject $inputvar -Depth 100 -Compress) - } - } + $Template = New-CIPPIntuneTemplate -TenantFilter $TenantFilter -URLName $URLName -ID $ID $object = [PSCustomObject]@{ - Displayname = $DisplayName + Displayname = $Template.DisplayName Description = $Template.Description - RAWJson = $TemplateJson - Type = $Type + RAWJson = $Template.TemplateJson + Type = $Template.Type GUID = $GUID } | ConvertTo-Json $Table = Get-CippTable -tablename 'templates' diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddPolicy.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddPolicy.ps1 index aca7c04ca269..dea49aa6c1ab 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddPolicy.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddPolicy.ps1 @@ -25,83 +25,9 @@ Function Invoke-AddPolicy { ([pscustomobject]$Request.Body.replacemap.$tenant).psobject.properties | ForEach-Object { $RawJson = $RawJson -replace $_.name, $_.value } } try { - switch ($Request.Body.TemplateType) { - 'AppProtection' { - $PlatformType = 'deviceAppManagement' - $TemplateType = ($RawJSON | ConvertFrom-Json).'@odata.type' -replace '#microsoft.graph.', '' - $TemplateTypeURL = "$($TemplateType)s" - $CheckExististing = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/deviceAppManagement/$TemplateTypeURL" -tenantid $tenant - if ($displayname -in $CheckExististing.displayName) { - Throw "Policy with Display Name $($Displayname) Already exists" - } - $CreateRequest = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceAppManagement/$TemplateTypeURL" -tenantid $tenant -type POST -body $RawJSON - } - 'deviceCompliancePolicies' { - $TemplateTypeURL = 'deviceCompliancePolicies' - $CheckExististing = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$TemplateTypeURL" -tenantid $tenant - if ($displayname -in $CheckExististing.displayName) { - Throw "Policy with Display Name $($Displayname) Already exists" - } - $JSON = $RawJSON | ConvertFrom-Json | Select-Object * -ExcludeProperty id, createdDateTime, lastModifiedDateTime, version, 'scheduledActionsForRule@odata.context', '@odata.context' - $JSON.scheduledActionsForRule = @($JSON.scheduledActionsForRule | Select-Object * -ExcludeProperty 'scheduledActionConfigurations@odata.context') - $RawJSON = ConvertTo-Json -InputObject $JSON -Depth 20 -Compress - Write-Host $RawJSON - $CreateRequest = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$TemplateTypeURL" -tenantid $tenant -type POST -body $RawJson - } - 'Admin' { - $TemplateTypeURL = 'groupPolicyConfigurations' - $CreateBody = '{"description":"' + $description + '","displayName":"' + $displayname + '","roleScopeTagIds":["0"]}' - $CheckExististing = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$TemplateTypeURL" -tenantid $tenant - if ($displayname -in $CheckExististing.displayName) { - Throw "Policy with Display Name $($Displayname) Already exists" - } - $CreateRequest = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$TemplateTypeURL" -tenantid $tenant -type POST -body $CreateBody - $UpdateRequest = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$TemplateTypeURL('$($CreateRequest.id)')/updateDefinitionValues" -tenantid $tenant -type POST -body $RawJSON - } - 'Device' { - $TemplateTypeURL = 'deviceConfigurations' - $PolicyName = ($RawJSON | ConvertFrom-Json).displayName - $CheckExististing = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$TemplateTypeURL" -tenantid $tenant - Write-Host $PolicyName - if ($PolicyName -in $CheckExististing.displayName) { - Throw "Policy with Display Name $($Displayname) Already exists" - } - $PolicyFile = $RawJSON | ConvertFrom-Json - $Null = $PolicyFile | Add-Member -MemberType NoteProperty -Name 'description' -Value $description -Force - $null = $PolicyFile | Add-Member -MemberType NoteProperty -Name 'displayName' -Value $displayname -Force - $RawJSON = ConvertTo-Json -InputObject $PolicyFile -Depth 20 - $CreateRequest = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$TemplateTypeURL" -tenantid $tenant -type POST -body $RawJSON - } - 'Catalog' { - $TemplateTypeURL = 'configurationPolicies' - $CheckExististing = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$TemplateTypeURL" -tenantid $tenant - $PolicyName = ($RawJSON | ConvertFrom-Json).Name - $CheckExististing = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$TemplateTypeURL" -tenantid $tenant - if ($PolicyName -in $CheckExististing.name) { - Throw "Policy with Display Name $($Displayname) Already exists" - } - $CreateRequest = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$TemplateTypeURL" -tenantid $tenant -type POST -body $RawJSON - } - 'windowsDriverUpdateProfiles' { - $TemplateTypeURL = 'windowsDriverUpdateProfiles' - $PolicyName = ($RawJSON | ConvertFrom-Json).Name - $CheckExististing = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$TemplateTypeURL" -tenantid $tenant - if ($PolicyName -in $CheckExististing.name) { - $ExistingID = $CheckExististing | Where-Object -Property Name -EQ $PolicyName - $CreateRequest = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$TemplateTypeURL/$($ExistingID.Id)" -tenantid $tenant -type PUT -body $RawJSON - - } else { - $CreateRequest = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$TemplateTypeURL" -tenantid $tenant -type POST -body $RawJSON - Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -tenant $($Tenant) -message "Added policy $($PolicyName) via template" -Sev 'info' - } - } - - } + Write-Host 'Calling Adding policy' + Set-CIPPIntunePolicy -TemplateType $Request.body.TemplateType -Description $description -DisplayName $displayname -RawJSON $RawJSON -AssignTo $AssignTo -tenantFilter $Tenant Write-LogMessage -user $Request.headers.'x-ms-client-principal' -API $APINAME -tenant $($Tenant) -message "Added policy $($Displayname)" -Sev 'Info' - if ($AssignTo) { - Set-CIPPAssignedPolicy -GroupName $AssignTo -PolicyId $CreateRequest.id -Type $TemplateTypeURL -TenantFilter $tenant -PlatformType $PlatformType - } - "Successfully added policy for $($Tenant)" } catch { "Failed to add policy for $($Tenant): $($_.Exception.Message)" Write-LogMessage -user $Request.headers.'x-ms-client-principal' -API $APINAME -tenant $($Tenant) -message "Failed adding policy $($Displayname). Error: $($_.Exception.Message)" -Sev 'Error' diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroupSenderAuthentication.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroupSenderAuthentication.ps1 new file mode 100644 index 000000000000..97ca6fe52147 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroupSenderAuthentication.ps1 @@ -0,0 +1,46 @@ +using namespace System.Net + +Function Invoke-ListGroupSenderAuthentication { + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $TriggerMetadata.FunctionName + Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message 'Accessed this API' -Sev 'Debug' + # Write to the Azure Functions log stream. + Write-Host 'PowerShell HTTP trigger function processed a request.' + + # Interact with query parameters or the body of the request. + + $TenantFilter = $Request.Query.TenantFilter + $groupid = $Request.query.groupid + $GroupType = $Request.query.Type + + $params = @{ + Identity = $groupid + } + + + try { + switch ($GroupType) { + 'Distribution List' { + Write-Host 'Checking DL' + $State = (New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-DistributionGroup' -cmdParams $params -UseSystemMailbox $true).RequireSenderAuthenticationEnabled + } + 'Microsoft 365' { + Write-Host 'Checking M365 Group' + $State = (New-ExoRequest -tenantid $TenantFilter -cmdlet 'get-unifiedgroup' -cmdParams $params -UseSystemMailbox $true).RequireSenderAuthenticationEnabled + + } + default { $state = $true } + } + + } catch { + $state = $true + } + + # We flip the value because the API is asking if the group is allowed to receive external mail + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = @{ allowedToReceiveExternal = !$state } + }) +} \ No newline at end of file diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecJITAdmin.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecJITAdmin.ps1 index f9dbca2ab349..458656041088 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecJITAdmin.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecJITAdmin.ps1 @@ -156,7 +156,7 @@ Function Invoke-ExecJITAdmin { $DisableTaskBody = @{ TenantFilter = $Request.Body.TenantFilter - Name = "JIT Admin (disable): $Username" + Name = "JIT Admin ($($Request.Body.ExpireAction)): $Username" Command = @{ value = 'Set-CIPPUserJITAdmin' label = 'Set-CIPPUserJITAdmin' @@ -177,7 +177,7 @@ Function Invoke-ExecJITAdmin { ScheduledTime = $Request.Body.EndDate } Add-CIPPScheduledTask -Task $DisableTaskBody -hidden $false - $Results.Add("Scheduling JIT Admin disable task for $Username") + $Results.Add("Scheduling JIT Admin $($Request.Body.ExpireAction) task for $Username") $Body = @{ Results = @($Results) } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSites.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSites.ps1 index 7b5ac0cd37b7..d94c6b0ce4bd 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSites.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSites.ps1 @@ -29,7 +29,7 @@ Function Invoke-ListSites { } else { $ParsedRequest = $Result } - $GraphRequest = $ParsedRequest | Select-Object @{ Name = 'UPN'; Expression = { $_.'Owner Principal Name' } }, + $GraphRequest = $ParsedRequest | Select-Object AutoMapUrl, @{ Name = 'UPN'; Expression = { $_.'Owner Principal Name' } }, @{ Name = 'displayName'; Expression = { $_.'Owner Display Name' } }, @{ Name = 'LastActive'; Expression = { $_.'Last Activity Date' } }, @{ Name = 'FileCount'; Expression = { [int]$_.'File Count' } }, @@ -41,14 +41,28 @@ Function Invoke-ListSites { #Temporary workaround for url as report is broken. #This API is so stupid its great. - $URLs = (New-GraphGetRequest -uri 'https://graph.microsoft.com/v1.0/sites/getAllSites?$select=SharePointIds' -asapp $true -tenantid $TenantFilter).SharePointIds - + $URLs = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/sites/getAllSites?$select=SharePointIds,name,webUrl,displayName,siteCollection' -asapp $true -tenantid $TenantFilter + $int = 0 + if ($Type -eq 'SharePointSiteUsage') { + $Requests = foreach ($url in $URLs) { + @{ + id = $int++ + method = 'GET' + url = "sites/$($url.sharepointIds.siteId)/lists?`$select=id,name,list,parentReference" + } + } + $Requests = (New-GraphBulkRequest -tenantid $TenantFilter -scope 'https://graph.microsoft.com/.default' -Requests @($Requests) -asapp $true).body.value | Where-Object { $_.list.template -eq 'DocumentLibrary' } + } $GraphRequest = foreach ($site in $GraphRequest) { - $site.URL = ($URLs | Where-Object { $_.siteId -eq $site.SiteId }).siteUrl + $SiteURLs = ($URLs.SharePointIds | Where-Object { $_.siteId -eq $site.SiteId }) + $site.URL = $SiteURLs.siteUrl + $ListId = ($Requests | Where-Object { $_.parentReference.siteId -like "*$($SiteURLs.siteId)*" }).id + $site.AutoMapUrl = "tenantId=$($SiteUrls.tenantId)&webId={$($SiteUrls.webId)}&siteid={$($SiteURLs.siteId)}&webUrl=$($SiteURLs.siteUrl)&listId={$($ListId)}" $site } $StatusCode = [HttpStatusCode]::OK + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message $StatusCode = [HttpStatusCode]::Forbidden diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ListAlertsQueue.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ListAlertsQueue.ps1 index 9b0e5aab48bb..0b90937f4feb 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ListAlertsQueue.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ListAlertsQueue.ps1 @@ -20,7 +20,7 @@ Function Invoke-ListAlertsQueue { $WebhookRules = Get-CIPPAzDataTableEntity @WebhookTable $ScheduledTasks = Get-CIPPTable -TableName 'ScheduledTasks' - $ScheduledTasks = Get-CIPPAzDataTableEntity @ScheduledTasks | Where-Object { $_.hidden -eq $true } + $ScheduledTasks = Get-CIPPAzDataTableEntity @ScheduledTasks | Where-Object { $_.hidden -eq $true -and $_.command -like 'Get-CippAlert*' } $AllowedTenants = Test-CIPPAccess -Request $Request -TenantList $TenantList = Get-Tenants -IncludeErrors diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecAddMultiTenantApp.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecAddMultiTenantApp.ps1 index 08124ad41683..4cb38d9f9dc8 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecAddMultiTenantApp.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecAddMultiTenantApp.ps1 @@ -18,15 +18,10 @@ function Invoke-ExecAddMultiTenantApp { $Results = try { if ($request.body.CopyPermissions -eq $true) { - try { - $ExistingApp = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/applications(appId='$($Request.body.AppId)')" -tenantid $ENV:tenantid -NoAuthCheck $true - $DelegateResourceAccess = $Existingapp.requiredResourceAccess - $ApplicationResourceAccess = $Existingapp.requiredResourceAccess - } catch { - 'Failed to get existing permissions. The app does not exist in the partner tenant.' - } + $Command = 'ExecApplicationCopy' + } else { + $Command = 'ExecAddMultiTenantApp' } - #This needs to be moved to a queue. if ('allTenants' -in $Request.body.SelectedTenants.defaultDomainName) { $TenantFilter = (Get-Tenants).defaultDomainName } else { @@ -36,7 +31,7 @@ function Invoke-ExecAddMultiTenantApp { foreach ($Tenant in $TenantFilter) { try { Push-OutputBinding -Name QueueItem -Value ([pscustomobject]@{ - FunctionName = 'ExecAddMultiTenantApp' + FunctionName = $Command Tenant = $tenant appId = $Request.body.appid applicationResourceAccess = $ApplicationResourceAccess @@ -59,4 +54,4 @@ function Invoke-ExecAddMultiTenantApp { Body = @{ Results = @($Results) } }) -} \ No newline at end of file +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecOffboardTenant.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecOffboardTenant.ps1 index adcb580e45cf..230816f2f58f 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecOffboardTenant.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecOffboardTenant.ps1 @@ -41,7 +41,7 @@ Function Invoke-ExecOffboardTenant { $BulkResults = New-GraphBulkRequest -Requests $BulkRequests -tenantid $TenantFilter - $results.Add('Succesfully removed guest users') + $results.Add('Successfully removed guest users') Write-LogMessage -user $ExecutingUser -API $APIName -message "CSP Guest users were removed" -Sev "Info" -tenant $TenantFilter } else { $results.Add('No guest users found to remove') @@ -83,7 +83,7 @@ Function Invoke-ExecOffboardTenant { 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] }))") + $results.Add("Successfully removed notification contacts from $($property): $(($propertyContacts | Where-Object { $domains -contains $_.Split("@")[1] }))") Write-LogMessage -user $ExecutingUser -API $APIName -message "Contacts were removed from $($property)" -Sev "Info" -tenant $TenantFilter } catch { $errors.Add("Failed to update property $($property): $($_.Exception.message)") @@ -100,7 +100,7 @@ Function Invoke-ExecOffboardTenant { $request.body.RemoveVendorApps | ForEach-Object { try { $delete = (New-GraphPostRequest -type 'DELETE' -Uri "https://graph.microsoft.com/v1.0/serviceprincipals/$($_.value)" -tenantid $Tenantfilter) - $results.Add("Succesfully removed app $($_.label)") + $results.Add("Successfully removed app $($_.label)") Write-LogMessage -user $ExecutingUser -API $APIName -message "App $($_.label) was removed" -Sev "Info" -tenant $TenantFilter } catch { #$results.Add("Failed to removed app $($_.displayName)") @@ -118,7 +118,7 @@ Function Invoke-ExecOffboardTenant { $sortedArray | ForEach-Object { try { $delete = (New-GraphPostRequest -type 'DELETE' -Uri "https://graph.microsoft.com/v1.0/serviceprincipals/$($_.id)" -tenantid $Tenantfilter) - $results.Add("Succesfully removed app $($_.displayName)") + $results.Add("Successfully removed app $($_.displayName)") Write-LogMessage -user $ExecutingUser -API $APIName -message "App $($_.displayName) was removed" -Sev "Info" -tenant $TenantFilter } catch { #$results.Add("Failed to removed app $($_.displayName)") @@ -141,7 +141,7 @@ Function Invoke-ExecOffboardTenant { $delegatedAdminRelationships | ForEach-Object { try { $terminate = (New-GraphPostRequest -type 'POST' -Uri "https://graph.microsoft.com/v1.0/tenantRelationships/delegatedAdminRelationships/$($_.id)/requests" -body '{"action":"terminate"}' -ContentType 'application/json' -tenantid $env:TenantID) - $results.Add("Succesfully terminated GDAP relationship $($_.displayName) from tenant $TenantFilter") + $results.Add("Successfully terminated GDAP relationship $($_.displayName) from tenant $TenantFilter") Write-LogMessage -user $ExecutingUser -API $APIName -message "GDAP Relationship $($_.displayName) has been terminated" -Sev "Info" -tenant $TenantFilter } catch { $($_.Exception.message) @@ -160,7 +160,7 @@ Function Invoke-ExecOffboardTenant { # Terminate contract relationship try { $terminate = (New-GraphPostRequest -type 'PATCH' -body '{ "relationshipToPartner": "none" }' -Uri "https://api.partnercenter.microsoft.com/v1/customers/$TenantFilter" -ContentType 'application/json' -scope 'https://api.partnercenter.microsoft.com/user_impersonation' -tenantid $env:TenantID) - $results.Add('Succesfully terminated contract relationship') + $results.Add('Successfully terminated contract relationship') Write-LogMessage -user $ExecutingUser -API $APIName -message "Contract relationship terminated" -Sev "Info" -tenant $TenantFilter } catch { #$results.Add("Failed to terminate contract relationship: $($_.Exception.message)") diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecUpdateSecureScore.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecUpdateSecureScore.ps1 index 26bc7332a928..237b5f3415e5 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecUpdateSecureScore.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecUpdateSecureScore.ps1 @@ -21,7 +21,7 @@ Function Invoke-ExecUpdateSecureScore { } try { $GraphRequest = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/security/secureScoreControlProfiles/$($Request.body.ControlName)" -tenantid $Request.body.TenantFilter -type PATCH -Body $($Body | ConvertTo-Json -Compress) - $Results = [pscustomobject]@{'Results' = "Succesfully set control to $($body.state) " } + $Results = [pscustomobject]@{'Results' = "Successfully set control to $($body.state) " } } catch { $Results = [pscustomobject]@{'Results' = "Failed to set Control to $($body.state) $($_.Exception.Message)" } } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-SetAuthMethod.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-SetAuthMethod.ps1 new file mode 100644 index 000000000000..2b00af589c58 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-SetAuthMethod.ps1 @@ -0,0 +1,32 @@ +function Invoke-SetAuthMethod { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Tenant.Administration.ReadWrite + #> + Param( + $Request, + $TriggerMetadata + ) + + $APIName = "Set Authentication Policy" + $state = if ($Request.Body.state -eq 'enabled') { $true } else { $false } + $Tenantfilter = $Request.Body.TenantFilter + + try { + Set-CIPPAuthenticationPolicy -Tenant $Tenantfilter -APIName $APIName -AuthenticationMethodId $($Request.Body.Id) -Enabled $state + $StatusCode = [HttpStatusCode]::OK + $SuccessMessage = "Authentication Policy for $($Request.Body.Id) has been set to $state" + } catch { + $ErrorMsg = Get-NormalizedError -message $($_.Exception.Message) + $SuccessMessage = "Function Error: $($_.InvocationInfo.ScriptLineNumber) - $ErrorMsg" + $StatusCode = [HttpStatusCode]::BadRequest + } + + # Associate values to output bindings by calling 'Push-OutputBinding'. + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = [pscustomobject]@{'Results' = "$SuccessMessage" } + }) +} \ No newline at end of file diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-AddCATemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-AddCATemplate.ps1 index b0f64582c5c4..339cbf077481 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-AddCATemplate.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-AddCATemplate.ps1 @@ -16,67 +16,7 @@ Function Invoke-AddCATemplate { $TenantFilter = $Request.Query.TenantFilter try { $GUID = (New-Guid).GUID - $JSON = if ($request.body.rawjson) { - ConvertFrom-Json -InputObject ([pscustomobject]$request.body.rawjson) - } else { - ([pscustomobject]$Request.body) | ForEach-Object { - $NonEmptyProperties = $_.psobject.Properties | Where-Object { $null -ne $_.Value } | Select-Object -ExpandProperty Name - $_ | Select-Object -Property $NonEmptyProperties - } - } - - $includelocations = New-Object System.Collections.ArrayList - $IncludeJSON = foreach ($Location in $JSON.conditions.locations.includeLocations) { - $locationinfo = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/namedLocations' -tenantid $TenantFilter | Where-Object -Property id -EQ $location | Select-Object * -ExcludeProperty id, *time* - $null = if ($locationinfo) { $includelocations.add($locationinfo.displayName) } else { $includelocations.add($location) } - $locationinfo - } - if ($includelocations) { $JSON.conditions.locations.includeLocations = $includelocations } - - - $excludelocations = New-Object System.Collections.ArrayList - $ExcludeJSON = foreach ($Location in $JSON.conditions.locations.excludeLocations) { - $locationinfo = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/namedLocations' -tenantid $TenantFilter | Where-Object -Property id -EQ $location | Select-Object * -ExcludeProperty id, *time* - $null = if ($locationinfo) { $excludelocations.add($locationinfo.displayName) } else { $excludelocations.add($location) } - $locationinfo - } - - if ($excludelocations) { $JSON.conditions.locations.excludeLocations = $excludelocations } - if ($JSON.conditions.users.includeUsers) { - $JSON.conditions.users.includeUsers = @($JSON.conditions.users.includeUsers | ForEach-Object { - if ($_ -in 'All', 'None', 'GuestOrExternalUsers') { return $_ } - (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$($_)" -tenantid $TenantFilter).displayName - }) - } - - if ($JSON.conditions.users.excludeUsers) { - $JSON.conditions.users.excludeUsers = @($JSON.conditions.users.excludeUsers | ForEach-Object { - if ($_ -in 'All', 'None', 'GuestOrExternalUsers') { return $_ } - (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$($_)" -tenantid $TenantFilter).displayName - }) - } - - # Function to check if a string is a GUID - function Test-IsGuid($string) { - return [guid]::tryparse($string, [ref][guid]::Empty) - } - - if ($JSON.conditions.users.includeGroups) { - $JSON.conditions.users.includeGroups = @($JSON.conditions.users.includeGroups | ForEach-Object { - if ($_ -in 'All', 'None', 'GuestOrExternalUsers' -or -not (Test-IsGuid $_)) { return $_ } - (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/groups/$($_)" -tenantid $TenantFilter).displayName - }) - } - if ($JSON.conditions.users.excludeGroups) { - $JSON.conditions.users.excludeGroups = @($JSON.conditions.users.excludeGroups | ForEach-Object { - if ($_ -in 'All', 'None', 'GuestOrExternalUsers' -or -not (Test-IsGuid $_)) { return $_ } - (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/groups/$($_)" -tenantid $TenantFilter).displayName - }) - } - - $JSON | Add-Member -NotePropertyName 'LocationInfo' -NotePropertyValue @($IncludeJSON, $ExcludeJSON) - - $JSON = (ConvertTo-Json -Depth 100 -InputObject $JSON ) + $JSON = New-CIPPCATemplate -TenantFilter $TenantFilter -JSON $request.body $Table = Get-CippTable -tablename 'templates' $Table.Force = $true Add-CIPPAzDataTableEntity @Table -Entity @{ diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ListConditionalAccessPolicies.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ListConditionalAccessPolicies.ps1 index 9c46ceae1ec4..39dd529a4ceb 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ListConditionalAccessPolicies.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ListConditionalAccessPolicies.ps1 @@ -19,8 +19,6 @@ Function Invoke-ListConditionalAccessPolicies { param ( [Parameter()] $ID, - - [Parameter(Mandatory = $true)] $Locations ) if ($id -eq 'All') { @@ -39,8 +37,6 @@ Function Invoke-ListConditionalAccessPolicies { param ( [Parameter()] $ID, - - [Parameter(Mandatory = $true)] $RoleDefinitions ) if ($id -eq 'All') { @@ -59,8 +55,6 @@ Function Invoke-ListConditionalAccessPolicies { param ( [Parameter()] $ID, - - [Parameter(Mandatory = $true)] $Users ) if ($id -eq 'All') { @@ -78,8 +72,6 @@ Function Invoke-ListConditionalAccessPolicies { param ( [Parameter()] $ID, - - [Parameter(Mandatory = $true)] $Groups ) if ($id -eq 'All') { @@ -98,8 +90,6 @@ Function Invoke-ListConditionalAccessPolicies { param ( [Parameter()] $ID, - - [Parameter(Mandatory = $true)] $Applications ) if ($id -eq 'All') { diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-AddTenantAllowBlockList.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-AddTenantAllowBlockList.ps1 index 00c2cffc02e7..ff1464ea8e3b 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Invoke-AddTenantAllowBlockList.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-AddTenantAllowBlockList.ps1 @@ -14,40 +14,42 @@ Function Invoke-AddTenantAllowBlockList { Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APIName -message 'Accessed this API' -Sev 'Debug' $blocklistobj = $Request.body - + if ($Request.body.tenantId -eq 'AllTenants') { $Tenants = (Get-Tenants).defaultDomainName } else { $Tenants = @($Request.body.tenantId) } # Write to the Azure Functions log stream. Write-Host 'PowerShell HTTP trigger function processed a request.' - try { - $ExoRequest = @{ - tenantid = $Request.body.tenantid - cmdlet = 'New-TenantAllowBlockListItems' - cmdParams = @{ - Entries = [string[]]$blocklistobj.entries - ListType = [string]$blocklistobj.listType - Notes = [string]$blocklistobj.notes - $blocklistobj.listMethod = [bool]$true + $Results = [System.Collections.Generic.List[string]]::new() + foreach ($Tenant in $Tenants) { + try { + $ExoRequest = @{ + tenantid = $Tenant + cmdlet = 'New-TenantAllowBlockListItems' + cmdParams = @{ + Entries = [string[]]$blocklistobj.entries + ListType = [string]$blocklistobj.listType + Notes = [string]$blocklistobj.notes + $blocklistobj.listMethod = [bool]$true + } } - } - if ($blocklistobj.NoExpiration -eq $true) { - $ExoRequest.cmdParams.NoExpiration = $true - } + if ($blocklistobj.NoExpiration -eq $true) { + $ExoRequest.cmdParams.NoExpiration = $true + } - New-ExoRequest @ExoRequest + New-ExoRequest @ExoRequest - $result = "Successfully added $($blocklistobj.Entries) as type $($blocklistobj.ListType) to the $($blocklistobj.listMethod) list" - Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APIName -tenant $Request.body.tenantid -message $result -Sev 'Info' - } catch { - $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - $result = "Failed to create blocklist. Error: $ErrorMessage" - Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APIName -tenant $Request.body.tenantid -message $result -Sev 'Error' + $results.add("Successfully added $($blocklistobj.Entries) as type $($blocklistobj.ListType) to the $($blocklistobj.listMethod) list for $tenant") + Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APIName -tenant $Tenant -message $result -Sev 'Info' + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + $results.add("Failed to create blocklist. Error: $ErrorMessage") + Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APIName -tenant $Tenant -message $result -Sev 'Error' + } } - # Associate values to output bindings by calling 'Push-OutputBinding'. Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ StatusCode = [HttpStatusCode]::OK Body = @{ - 'Results' = $result + 'Results' = $results 'Request' = $ExoRequest } }) diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListIntuneTemplates.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListIntuneTemplates.ps1 index c94431612970..a11384cf8e85 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListIntuneTemplates.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListIntuneTemplates.ps1 @@ -31,7 +31,7 @@ Function Invoke-ListIntuneTemplates { $Templates = (Get-CIPPAzDataTableEntity @Table -Filter $Filter).JSON | ConvertFrom-Json if ($Request.query.View) { $Templates = $Templates | ForEach-Object { - $data = $_.RAWJson | ConvertFrom-Json + $data = $_.RAWJson | ConvertFrom-Json -Depth 100 $data | Add-Member -NotePropertyName 'displayName' -NotePropertyValue $_.Displayname -Force $data | Add-Member -NotePropertyName 'description' -NotePropertyValue $_.Description -Force $data | Add-Member -NotePropertyName 'Type' -NotePropertyValue $_.Type -Force @@ -46,7 +46,7 @@ Function Invoke-ListIntuneTemplates { # Associate values to output bindings by calling 'Push-OutputBinding'. Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ StatusCode = [HttpStatusCode]::OK - Body = ($Templates | ConvertTo-Json -Depth 10) + Body = ($Templates | ConvertTo-Json -Depth 100) }) } diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListMailboxes.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListMailboxes.ps1 index 7a125550426a..f136d38cbd5e 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListMailboxes.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListMailboxes.ps1 @@ -20,12 +20,12 @@ Function Invoke-ListMailboxes { # Interact with query parameters or the body of the request. $TenantFilter = $Request.Query.TenantFilter try { - $Select = 'id,ExchangeGuid,ArchiveGuid,UserPrincipalName,DisplayName,PrimarySMTPAddress,RecipientType,RecipientTypeDetails,EmailAddresses' + $Select = 'id,ExchangeGuid,ArchiveGuid,UserPrincipalName,DisplayName,PrimarySMTPAddress,RecipientType,RecipientTypeDetails,EmailAddresses,WhenSoftDeleted,IsInactiveMailbox' $ExoRequest = @{ tenantid = $TenantFilter cmdlet = 'Get-Mailbox' - cmdParams = @{resultsize = 'unlimited' } - Select = $select + cmdParams = @{} + Select = $Select } $AllowedParameters = @( @@ -57,7 +57,7 @@ Function Invoke-ListMailboxes { } Write-Host ($ExoRequest | ConvertTo-Json) - $GraphRequest = (New-ExoRequest @ExoRequest) | Select-Object id, ExchangeGuid, ArchiveGuid, @{ Name = 'UPN'; Expression = { $_.'UserPrincipalName' } }, + $GraphRequest = (New-ExoRequest @ExoRequest) | Select-Object id, ExchangeGuid, ArchiveGuid, WhenSoftDeleted, @{ Name = 'UPN'; Expression = { $_.'UserPrincipalName' } }, @{ Name = 'displayName'; Expression = { $_.'DisplayName' } }, @{ Name = 'primarySmtpAddress'; Expression = { $_.'PrimarySMTPAddress' } }, diff --git a/Modules/CIPPCore/Public/Get-CIPPAzDatatableEntity.ps1 b/Modules/CIPPCore/Public/Get-CIPPAzDatatableEntity.ps1 index 4ce96415966d..60ee9464ab93 100644 --- a/Modules/CIPPCore/Public/Get-CIPPAzDatatableEntity.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPAzDatatableEntity.ps1 @@ -7,23 +7,71 @@ function Get-CIPPAzDataTableEntity { $First, $Skip, $Sort, - $Count + $Count ) + $Results = Get-AzDataTableEntity @PSBoundParameters - $Results = $Results | ForEach-Object { - $entity = $_ - if ($entity.SplitOverProps) { - $splitInfo = $entity.SplitOverProps | ConvertFrom-Json - $mergedData = -join ($splitInfo.SplitHeaders | ForEach-Object { $entity.$_ }) - $entity | Add-Member -NotePropertyName $splitInfo.OriginalHeader -NotePropertyValue $mergedData -Force - $propsToRemove = $splitInfo.SplitHeaders + "SplitOverProps" - $entity = $entity | Select-Object * -ExcludeProperty $propsToRemove - $entity + $mergedResults = @{} + + # First pass: Collect all parts and complete entities + foreach ($entity in $Results) { + if ($entity.OriginalEntityId) { + $entityId = $entity.OriginalEntityId + if (-not $mergedResults.ContainsKey($entityId)) { + $mergedResults[$entityId] = @{ + Parts = New-Object 'System.Collections.ArrayList' + } + } + $mergedResults[$entityId]['Parts'].Add($entity) > $null + } else { + $mergedResults[$entity.RowKey] = @{ + Entity = $entity + Parts = New-Object 'System.Collections.ArrayList' + } + } + } + + # Second pass: Reassemble entities from parts + $finalResults = @() + foreach ($entityId in $mergedResults.Keys) { + $entityData = $mergedResults[$entityId] + if ($entityData.Parts.Count -gt 0) { + $fullEntity = [PSCustomObject]@{} + $parts = $entityData.Parts | Sort-Object PartIndex + foreach ($part in $parts) { + foreach ($key in $part.PSObject.Properties.Name) { + if ($key -notin @('OriginalEntityId', 'PartIndex', 'PartitionKey', 'RowKey', 'Timestamp')) { + if ($fullEntity.PSObject.Properties[$key]) { + $fullEntity | Add-Member -MemberType NoteProperty -Name $key -Value ($fullEntity.$key + $part.$key) -Force + } else { + $fullEntity | Add-Member -MemberType NoteProperty -Name $key -Value $part.$key + } + } + } + } + $fullEntity | Add-Member -MemberType NoteProperty -Name 'PartitionKey' -Value $parts[0].PartitionKey -Force + $fullEntity | Add-Member -MemberType NoteProperty -Name 'RowKey' -Value $entityId -Force + $finalResults = $finalResults + @($fullEntity) + } else { + $finalResults = $finalResults + @($entityData.Entity) } - else { - $entity + } + + # Third pass: Process split properties and remerge them + foreach ($entity in $finalResults) { + if ($entity.SplitOverProps) { + $splitInfoList = $entity.SplitOverProps | ConvertFrom-Json + foreach ($splitInfo in $splitInfoList) { + $mergedData = [string]::Join('', ($splitInfo.SplitHeaders | ForEach-Object { $entity.$_ })) + $entity | Add-Member -NotePropertyName $splitInfo.OriginalHeader -NotePropertyValue $mergedData -Force + $propsToRemove = $splitInfo.SplitHeaders + foreach ($prop in $propsToRemove) { + $entity.PSObject.Properties.Remove($prop) + } + } + $entity.PSObject.Properties.Remove('SplitOverProps') } } - - return $Results + + return $finalResults } diff --git a/Modules/CIPPCore/Public/Get-CIPPBackup.ps1 b/Modules/CIPPCore/Public/Get-CIPPBackup.ps1 index e463202983a1..c172f40f1c90 100644 --- a/Modules/CIPPCore/Public/Get-CIPPBackup.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPBackup.ps1 @@ -4,6 +4,7 @@ function Get-CIPPBackup { [string]$Type, [string]$TenantFilter ) + Write-Host "Getting backup for $Type with TenantFilter $TenantFilter" $Table = Get-CippTable -tablename "$($Type)Backup" if ($TenantFilter) { $Filter = "PartitionKey eq '$($Type)Backup' and TenantFilter eq '$($TenantFilter)'" diff --git a/Modules/CIPPCore/Public/Get-CIPPSPOTenant.ps1 b/Modules/CIPPCore/Public/Get-CIPPSPOTenant.ps1 new file mode 100644 index 000000000000..80f6e83453aa --- /dev/null +++ b/Modules/CIPPCore/Public/Get-CIPPSPOTenant.ps1 @@ -0,0 +1,27 @@ +function Get-CIPPSPOTenant { + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter, + [string]$SharepointPrefix + ) + + if (!$SharepointPrefix) { + # get sharepoint admin site + $tenantName = (New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/sites/root' -asApp $true -tenantid $TenantFilter).id.Split('.')[0] + } else { + $tenantName = $SharepointPrefix + } + $AdminUrl = "https://$($tenantName)-admin.sharepoint.com" + + # Query tenant settings + $XML = @' + +'@ + $AdditionalHeaders = @{ + 'Accept' = 'application/json;odata=verbose' + } + $Results = New-GraphPostRequest -scope "$AdminURL/.default" -tenantid $TenantFilter -Uri "$AdminURL/_vti_bin/client.svc/ProcessQuery" -Type POST -Body $XML -ContentType 'text/xml' -AddedHeaders $AdditionalHeaders + + $Results | Select-Object -Last 1 *, @{n = 'SharepointPrefix'; e = { $tenantName } }, @{n = 'TenantFilter'; e = { $TenantFilter } } +} \ No newline at end of file diff --git a/Modules/CIPPCore/Public/GraphHelper/Convert-SKUName.ps1 b/Modules/CIPPCore/Public/GraphHelper/Convert-SKUName.ps1 index 7d0d5e874aea..61e8e0acd4b6 100644 --- a/Modules/CIPPCore/Public/GraphHelper/Convert-SKUName.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/Convert-SKUName.ps1 @@ -1,9 +1,17 @@ -function Convert-SKUname($skuname, $skuID) { +function Convert-SKUname { <# .FUNCTIONALITY Internal #> - $ConvertTable = Import-Csv Conversiontable.csv + param( + $skuname, + $skuID, + $ConvertTable + ) + if (!$ConvertTable) { + Set-Location (Get-Item $PSScriptRoot).Parent.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 } diff --git a/Modules/CIPPCore/Public/GraphHelper/New-GraphBulkRequest.ps1 b/Modules/CIPPCore/Public/GraphHelper/New-GraphBulkRequest.ps1 index f37be378e39d..ae2777fa2ac7 100644 --- a/Modules/CIPPCore/Public/GraphHelper/New-GraphBulkRequest.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/New-GraphBulkRequest.ps1 @@ -33,10 +33,15 @@ function New-GraphBulkRequest { $req = @{} # Use select to create hashtables of id, method and url for each call $req['requests'] = ($Requests[$i..($i + 19)]) - $ReqBody = ($req | ConvertTo-Json -Depth 10) - Invoke-RestMethod -Uri $URL -Method POST -Headers $headers -ContentType 'application/json; charset=utf-8' -Body $ReqBody + $ReqBody = (ConvertTo-Json -InputObject $req -Compress -Depth 100) + $Return = Invoke-RestMethod -Uri $URL -Method POST -Headers $headers -ContentType 'application/json; charset=utf-8' -Body $ReqBody + if ($Return.headers.'retry-after') { + #Revist this when we are pushing this data into our custom schema instead. + $headers = Get-GraphToken -tenantid $tenantid -scope $scope -AsApp $asapp + Invoke-RestMethod -Uri $URL -Method POST -Headers $headers -ContentType 'application/json; charset=utf-8' -Body $ReqBody + } + $Return } - 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 @@ -47,7 +52,7 @@ function New-GraphBulkRequest { } catch { $Message = ($_.ErrorDetails.Message | ConvertFrom-Json -ErrorAction SilentlyContinue).error.message - if ($Message -eq $null) { $Message = $($_.Exception.Message) } + if ($null -eq $Message) { $Message = $($_.Exception.Message) } if ($Message -ne 'Request not applicable to target tenant.') { $Tenant.LastGraphError = $Message $Tenant.GraphErrorCount++ diff --git a/Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1 b/Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1 index 7c4eb8927b35..e5fe77f2e484 100644 --- a/Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1 @@ -88,6 +88,7 @@ function New-GraphGetRequest { } } until ([string]::IsNullOrEmpty($NextURL) -or $NextURL -is [object[]] -or ' ' -eq $NextURL) $Tenant.LastGraphError = '' + $Tenant.GraphErrorCount = 0 Update-AzDataTableEntity @TenantsTable -Entity $Tenant return $ReturnedData } else { diff --git a/Modules/CIPPCore/Public/Invoke-CIPPOffboardingJob.ps1 b/Modules/CIPPCore/Public/Invoke-CIPPOffboardingJob.ps1 index e051a2a0144b..70244fc0ff3b 100644 --- a/Modules/CIPPCore/Public/Invoke-CIPPOffboardingJob.ps1 +++ b/Modules/CIPPCore/Public/Invoke-CIPPOffboardingJob.ps1 @@ -1,4 +1,4 @@ - + function Invoke-CIPPOffboardingJob { [CmdletBinding()] param ( @@ -18,13 +18,13 @@ function Invoke-CIPPOffboardingJob { { $_.'ConvertToShared' -eq 'true' } { Set-CIPPMailboxType -ExecutingUser $ExecutingUser -tenantFilter $tenantFilter -userid $username -username $username -MailboxType 'Shared' -APIName $APIName } - { $_.RevokeSessions -eq 'true' } { + { $_.RevokeSessions -eq 'true' } { Revoke-CIPPSessions -tenantFilter $tenantFilter -username $username -userid $userid -ExecutingUser $ExecutingUser -APIName $APIName } - { $_.ResetPass -eq 'true' } { + { $_.ResetPass -eq 'true' } { Set-CIPPResetPassword -tenantFilter $tenantFilter -userid $username -ExecutingUser $ExecutingUser -APIName $APIName } - { $_.RemoveGroups -eq 'true' } { + { $_.RemoveGroups -eq 'true' } { Remove-CIPPGroups -userid $userid -tenantFilter $Tenantfilter -ExecutingUser $ExecutingUser -APIName $APIName -Username "$Username" } @@ -35,22 +35,26 @@ function Invoke-CIPPOffboardingJob { Set-CIPPSignInState -TenantFilter $tenantFilter -userid $username -AccountEnabled $false -ExecutingUser $ExecutingUser -APIName $APIName } - { $_.'OnedriveAccess' -ne '' } { + { $_.'OnedriveAccess' -ne '' } { $Options.OnedriveAccess | ForEach-Object { Set-CIPPSharePointPerms -tenantFilter $tenantFilter -userid $username -OnedriveAccessUser $_.value -ExecutingUser $ExecutingUser -APIName $APIName } } - { $_.'AccessNoAutomap' -ne '' } { + { $_.'AccessNoAutomap' -ne '' } { $Options.AccessNoAutomap | ForEach-Object { Set-CIPPMailboxAccess -tenantFilter $tenantFilter -userid $username -AccessUser $_.value -Automap $false -AccessRights @('FullAccess') -ExecutingUser $ExecutingUser -APIName $APIName } } - { $_.'AccessAutomap' -ne '' } { + { $_.'AccessAutomap' -ne '' } { $Options.AccessAutomap | ForEach-Object { Set-CIPPMailboxAccess -tenantFilter $tenantFilter -userid $username -AccessUser $_.value -Automap $true -AccessRights @('FullAccess') -ExecutingUser $ExecutingUser -APIName $APIName } } - - { $_.'OOO' -ne '' } { + + { $_.'OOO' -ne '' } { Set-CIPPOutOfOffice -tenantFilter $tenantFilter -userid $username -InternalMessage $Options.OOO -ExternalMessage $Options.OOO -ExecutingUser $ExecutingUser -APIName $APIName -state 'Enabled' } - { $_.'forward' -ne '' } { - Set-CIPPForwarding -userid $userid -username $username -tenantFilter $Tenantfilter -Forward $Options.forward -KeepCopy [bool]$Options.keepCopy -ExecutingUser $ExecutingUser -APIName $APIName + { $_.'forward' -ne '' } { + if (!$options.keepcopy) { + Set-CIPPForwarding -userid $userid -username $username -tenantFilter $Tenantfilter -Forward $Options.forward -ExecutingUser $ExecutingUser -APIName $APIName + } else { + Set-CIPPForwarding -userid $userid -username $username -tenantFilter $Tenantfilter -Forward $Options.forward -KeepCopy [boolean]$Options.keepCopy -ExecutingUser $ExecutingUser -APIName $APIName + } } { $_.'RemoveLicenses' -eq 'true' } { Remove-CIPPLicense -userid $userid -username $Username -tenantFilter $Tenantfilter -ExecutingUser $ExecutingUser -APIName $APIName @@ -68,6 +72,9 @@ function Invoke-CIPPOffboardingJob { { $_.'removeMobile' -eq 'true' } { Remove-CIPPMobileDevice -userid $userid -username $Username -tenantFilter $Tenantfilter -ExecutingUser $ExecutingUser -APIName $APIName } + { $_.'removeCalendarInvites' -eq 'true' } { + Remove-CIPPCalendarInvites -userid $userid -username $Username -tenantFilter $Tenantfilter -ExecutingUser $ExecutingUser -APIName $APIName + } { $_.'removePermissions' } { if ($RunScheduled) { Remove-CIPPMailboxPermissions -PermissionsLevel @('FullAccess', 'SendAs', 'SendOnBehalf') -userid 'AllUsers' -AccessUser $UserName -TenantFilter $TenantFilter -APIName $APINAME -ExecutingUser $ExecutingUser @@ -82,8 +89,8 @@ function Invoke-CIPPOffboardingJob { "Removal of permissions queued. This task will run in the background and send it's results to the logbook." } } - + } return $Return -} \ No newline at end of file +} diff --git a/Modules/CIPPCore/Public/New-CIPPAPIConfig.ps1 b/Modules/CIPPCore/Public/New-CIPPAPIConfig.ps1 index 58231273000e..47d111209e0f 100644 --- a/Modules/CIPPCore/Public/New-CIPPAPIConfig.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPAPIConfig.ps1 @@ -51,7 +51,7 @@ function New-CIPPAPIConfig { Write-Host "writing to Azure" $SetAPIAuth = New-GraphPOSTRequest -type "PUT" -uri "https://management.azure.com/subscriptions/$($subscription)/resourceGroups/$ENV:WEBSITE_RESOURCE_GROUP/providers/Microsoft.Web/sites/$ENV:WEBSITE_SITE_NAME/Config/authsettingsV2?api-version=2018-11-01" -scope "https://management.azure.com/.default" -NoAuthCheck $true -body $currentBody $null = Set-AzKeyVaultSecret -VaultName $ENV:WEBSITE_DEPLOYMENT_ID -Name 'CIPPAPIAPP' -SecretValue (ConvertTo-SecureString -String $APIApp.AppID -AsPlainText -Force) - Write-LogMessage -user $ExecutingUser -API $APINAME -tenant 'None '-message "Succesfully setup CIPP-API Access." -Sev "info" + Write-LogMessage -user $ExecutingUser -API $APINAME -tenant 'None '-message "Successfully setup CIPP-API Access." -Sev "info" } return @{ ApplicationID = $APIApp.AppId diff --git a/Modules/CIPPCore/Public/New-CIPPApplicationCopy.ps1 b/Modules/CIPPCore/Public/New-CIPPApplicationCopy.ps1 new file mode 100644 index 000000000000..da8b584954f8 --- /dev/null +++ b/Modules/CIPPCore/Public/New-CIPPApplicationCopy.ps1 @@ -0,0 +1,46 @@ +function New-CIPPApplicationCopy { + [CmdletBinding()] + param( + $App, + $Tenant + ) + $CurrentInfo = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/servicePrincipals?$top=999' -tenantid $env:TenantID -NoAuthCheck $true + try { + $ExistingApp = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/Applications(appId='$($app)')" -tenantid $ENV:tenantid -NoAuthCheck $true + $Type = 'Application' + } catch { + $ExistingApp = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/servicePrincipals(appId='$($app)')/oauth2PermissionGrants" -tenantid $ENV:tenantid -NoAuthCheck $true + $ExistingAppRoleAssignments = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/servicePrincipals(appId='$($app)')/appRoleAssignments" -tenantid $ENV:tenantid -NoAuthCheck $true + $Type = 'ServicePrincipal' + } + if (!$ExistingApp) { + Write-LogMessage -message "Failed to add $App to tenant. This app does not exist." -tenant $tenant -API 'Application Copy' -sev error + continue + } + if ($Type -eq 'Application') { + $DelegateResourceAccess = $Existingapp.requiredResourceAccess + $ApplicationResourceAccess = $Existingapp.requiredResourceAccess + $NoTranslateRequired = $false + } else { + $DelegateResourceAccess = $ExistingApp | Group-Object -Property resourceId | ForEach-Object { + [pscustomobject]@{ resourceAppId = ($CurrentInfo | Where-Object -Property id -EQ $_.Name).appId; resourceAccess = @($_.Group | ForEach-Object { [pscustomobject]@{ id = $_.scope; type = 'Scope' } } ) + } + } + $ApplicationResourceAccess = $ExistingappRoleAssignments | Group-Object -Property ResourceId | ForEach-Object { + [pscustomobject]@{ resourceAppId = ($CurrentInfo | Where-Object -Property id -EQ $_.Name).appId; resourceAccess = @($_.Group | ForEach-Object { [pscustomobject]@{ id = $_.appRoleId; type = 'Role' } } ) + } + } + $NoTranslateRequired = $true + } + $TenantInfo = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/servicePrincipals?$top=999' -tenantid $Tenant -NoAuthCheck $true + + if ($App -Notin $TenantInfo.appId) { + $PostResults = New-GraphPostRequest 'https://graph.microsoft.com/beta/servicePrincipals' -type POST -tenantid $Tenant -body "{ `"appId`": `"$($App)`" }" + Write-LogMessage -message "Added $App as a service principal" -tenant $tenant -API 'Application Copy' -sev Info + } + Add-CIPPApplicationPermission -RequiredResourceAccess $ApplicationResourceAccess -ApplicationId $App -Tenantfilter $Tenant + Add-CIPPDelegatedPermission -RequiredResourceAccess $DelegateResourceAccess -ApplicationId $App -Tenantfilter $Tenant -NoTranslateRequired $NoTranslateRequired + Write-LogMessage -message "Added permissions to $app" -tenant $tenant -API 'Application Copy' -sev Info + + return $Results +} diff --git a/Modules/CIPPCore/Public/New-CIPPBackup.ps1 b/Modules/CIPPCore/Public/New-CIPPBackup.ps1 index 477ea7c2e690..722a1f09a18f 100644 --- a/Modules/CIPPCore/Public/New-CIPPBackup.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPBackup.ps1 @@ -4,13 +4,14 @@ function New-CIPPBackup { $backupType, $StorageOutput = 'default', $TenantFilter, + $ScheduledBackupValues, $APIName = 'CIPP Backup', $ExecutingUser ) $BackupData = switch ($backupType) { #If backup type is CIPP, create CIPP backup. - 'CIPP' { + 'CIPP' { try { $BackupTables = @( 'bpa' @@ -26,7 +27,7 @@ function New-CIPPBackup { Get-CIPPAzDataTableEntity @Table | Select-Object *, @{l = 'table'; e = { $CSVTable } } } Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message 'Created backup' -Sev 'Debug' - $CSVfile + $CSVfile $RowKey = 'CIPPBackup' + '_' + (Get-Date).ToString('yyyy-MM-dd-HHmm') $entity = [PSCustomObject]@{ PartitionKey = 'CIPPBackup' @@ -42,7 +43,7 @@ function New-CIPPBackup { Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message "Failed to create backup for CIPP: $($_.Exception.Message)" -Sev 'Error' [pscustomobject]@{'Results' = "Backup Creation failed: $($_.Exception.Message)" } } - + } catch { Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message "Failed to create backup: $($_.Exception.Message)" -Sev 'Error' [pscustomobject]@{'Results' = "Backup Creation failed: $($_.Exception.Message)" } @@ -50,40 +51,38 @@ function New-CIPPBackup { } #If Backup type is ConditionalAccess, create Conditional Access backup. - 'ConditionalAccess' { - $ConditionalAccessPolicyOutput = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/policies' -tenantid $tenantfilter - $AllNamedLocations = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/namedLocations' -tenantid $tenantfilter - switch ($StorageOutput) { - 'default' { - [PSCustomObject]@{ - ConditionalAccessPolicies = $ConditionalAccessPolicyOutput - NamedLocations = $AllNamedLocations - } - } - 'table' { - #Store output in tablestorage for Recovery - $RowKey = $TenantFilter + '_' + (Get-Date).ToString('yyyy-MM-dd-HHmm') - $entity = [PSCustomObject]@{ - PartitionKey = 'ConditionalAccessBackup' - RowKey = $RowKey - TenantFilter = $TenantFilter - Policies = [string]($ConditionalAccessPolicyOutput | ConvertTo-Json -Compress -Depth 10) - NamedLocations = [string]($AllNamedLocations | ConvertTo-Json -Compress -Depth 10) - } - $Table = Get-CippTable -tablename 'ConditionalAccessBackup' - try { - $Result = Add-CIPPAzDataTableEntity @Table -entity $entity -Force - Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message 'Created backup for Conditional Access Policies' -Sev 'Debug' - $Result - } catch { - Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message "Failed to create backup for Conditional Access Policies: $($_.Exception.Message)" -Sev 'Error' - [pscustomobject]@{'Results' = "Backup Creation failed: $($_.Exception.Message)" } - } - } + 'Scheduled' { + #Do a sub switch here based on the ScheduledBackupValues? + #Store output in tablestorage for Recovery + $RowKey = $TenantFilter + '_' + (Get-Date).ToString('yyyy-MM-dd-HHmm') + $entity = @{ + PartitionKey = 'ScheduledBackup' + RowKey = $RowKey + TenantFilter = $TenantFilter + } + Write-Host "Scheduled backup value psproperties: $(([pscustomobject]$ScheduledBackupValues).psobject.Properties)" + foreach ($ScheduledBackup in ([pscustomobject]$ScheduledBackupValues).psobject.Properties.Name) { + $BackupResult = New-CIPPBackupTask -Task $ScheduledBackup -TenantFilter $TenantFilter | ConvertTo-Json -Depth 100 -Compress | Out-String + $entity[$ScheduledBackup] = "$BackupResult" + } + $Table = Get-CippTable -tablename 'ScheduledBackup' + try { + $Result = Add-CIPPAzDataTableEntity @Table -entity $entity -Force + Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message 'Created backup' -Sev 'Debug' + $State = 'Backup finished succesfully' + $Result + } catch { + $State = 'Failed to write backup to table storage' + Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message "Failed to create backup for Conditional Access Policies: $($_.Exception.Message)" -Sev 'Error' + [pscustomobject]@{'Results' = "Backup Creation failed: $($_.Exception.Message)" } } } } - return $BackupData + return [pscustomobject]@{ + BackupName = $RowKey + BackupState = $State + BackupData = $BackupData + } } diff --git a/Modules/CIPPCore/Public/New-CIPPBackupTask.ps1 b/Modules/CIPPCore/Public/New-CIPPBackupTask.ps1 new file mode 100644 index 000000000000..a82bd4d1ac3a --- /dev/null +++ b/Modules/CIPPCore/Public/New-CIPPBackupTask.ps1 @@ -0,0 +1,97 @@ +function New-CIPPBackupTask { + [CmdletBinding()] + param ( + $Task, + $TenantFilter + ) + + $BackupData = switch ($Task) { + 'users' { + Write-Host "Backup users for $TenantFilter" + $Users = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/users?$top=999' -tenantid $TenantFilter | Select-Object * -ExcludeProperty mail, provisionedPlans, onPrem*, *passwordProfile*, *serviceProvisioningErrors*, isLicenseReconciliationNeeded, isManagementRestricted, isResourceAccount, *date*, *external*, identities, deletedDateTime, isSipEnabled, assignedPlans, cloudRealtimeCommunicationInfo, deviceKeys, provisionedPlan, securityIdentifier + #remove the property if the value is $null + $Users | ForEach-Object { + $_.psobject.properties | Where-Object { $_.Value -eq $null } | ForEach-Object { + $_.psobject.properties.Remove($_.Name) + } + } + $Users + } + 'groups' { + Write-Host "Backup groups for $TenantFilter" + New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/groups?$top=999' -tenantid $TenantFilter + } + 'ca' { + Write-Host "Backup Conditional Access Policies for $TenantFilter" + $Policies = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/conditionalAccess/policies?$top=999' -tenantid $TenantFilter + Write-Host 'Creating templates for found Conditional Access Policies' + foreach ($policy in $policies) { + try { + New-CIPPCATemplate -TenantFilter $TenantFilter -JSON $policy + } catch { + "Failed to create a template of the Conditional Access Policy with ID: $($policy.id). Error: $($_.Exception.Message)" + } + } + } + 'intuneconfig' { + Write-Host "Backup Intune Configuration Policies for $TenantFilter" + $GraphURLS = @("https://graph.microsoft.com/beta/deviceManagement/deviceConfigurations?`$select=id,displayName,lastModifiedDateTime,roleScopeTagIds,microsoft.graph.unsupportedDeviceConfiguration/originalEntityTypeName&`$expand=assignments&top=1000" + 'https://graph.microsoft.com/beta/deviceManagement/windowsDriverUpdateProfiles' + "https://graph.microsoft.com/beta/deviceManagement/groupPolicyConfigurations?`$expand=assignments&top=999" + "https://graph.microsoft.com/beta/deviceAppManagement/mobileAppConfigurations?`$expand=assignments&`$filter=microsoft.graph.androidManagedStoreAppConfiguration/appSupportsOemConfig%20eq%20true" + 'https://graph.microsoft.com/beta/deviceManagement/configurationPolicies' + ) + + $Policies = foreach ($url in $GraphURLS) { + try { + $Policies = New-GraphGetRequest -uri "$($url)" -tenantid $TenantFilter + $URLName = (($url).split('?') | Select-Object -First 1) -replace 'https://graph.microsoft.com/beta/deviceManagement/', '' + foreach ($Policy in $Policies) { + try { + New-CIPPIntuneTemplate -TenantFilter $TenantFilter -URLName $URLName -ID $Policy.ID + } catch { + "Failed to create a template of the Intune Configuration Policy with ID: $($Policy.id). Error: $($_.Exception.Message)" + } + } + } catch { + Write-Host "Failed to backup $url" + } + } + } + 'intunecompliance' { + Write-Host "Backup Intune Configuration Policies for $TenantFilter" + + New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/deviceCompliancePolicies?$top=999' -tenantid $TenantFilter | ForEach-Object { + New-CIPPIntuneTemplate -TenantFilter $TenantFilter -URLName 'deviceCompliancePolicies' -ID $_.ID + } + } + + 'intuneprotection' { + Write-Host "Backup Intune Configuration Policies for $TenantFilter" + + New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/deviceAppManagement/managedAppPolicies?$top=999' -tenantid $TenantFilter | ForEach-Object { + New-CIPPIntuneTemplate -TenantFilter $TenantFilter -URLName 'managedAppPolicies' -ID $_.ID + } + } + + 'CippWebhookAlerts' { + Write-Host "Backup Webhook Alerts for $TenantFilter" + $WebhookTable = Get-CIPPTable -TableName 'WebhookRules' + Get-CIPPAzDataTableEntity @WebhookTable | Where-Object { $TenantFilter -in ($_.Tenants | ConvertFrom-Json).fullvalue.defaultDomainName } + } + 'CippScriptedAlerts' { + Write-Host "Backup Scripted Alerts for $TenantFilter" + $ScheduledTasks = Get-CIPPTable -TableName 'ScheduledTasks' + Get-CIPPAzDataTableEntity @ScheduledTasks | Where-Object { $_.hidden -eq $true -and $_.command -like 'Get-CippAlert*' -and $TenantFilter -in $_.Tenant } + } + 'CippStandards' { + Write-Host "Backup Standards for $TenantFilter" + $Table = Get-CippTable -tablename 'standards' + $Filter = "PartitionKey eq 'standards' and RowKey eq '$($TenantFilter)'" + (Get-CIPPAzDataTableEntity @Table -Filter $Filter) + } + + } + return $BackupData +} + diff --git a/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 b/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 index 10f92773ee22..26f9046c4d21 100644 --- a/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 @@ -91,11 +91,10 @@ function New-CIPPCAPolicy { name = ($CheckExististing | Where-Object -Property displayName -EQ $Location.displayName).displayName } Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message "Matched a CA policy with the existing Named Location: $($location.displayName)" -Sev 'Info' - + } else { if ($location.countriesAndRegions) { $location.countriesAndRegions = @($location.countriesAndRegions) } $Body = ConvertTo-Json -InputObject $Location - Write-Host "Trying to create named location with: $body" $GraphRequest = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/namedLocations' -body $body -Type POST -tenantid $tenantfilter Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message "Created new Named Location: $($location.displayName)" -Sev 'Info' [pscustomobject]@{ @@ -138,10 +137,10 @@ function New-CIPPCAPolicy { Write-Host 'Replacement pattern for inclusions and exclusions is displayName.' $users = New-GraphGETRequest -uri 'https://graph.microsoft.com/beta/users?$select=id,displayName' -tenantid $TenantFilter $groups = New-GraphGETRequest -uri 'https://graph.microsoft.com/beta/groups?$select=id,displayName' -tenantid $TenantFilter - + if ($JSONObj.conditions.users.includeUsers -and $JSONObj.conditions.users.includeUsers -notin 'All', 'None', 'GuestOrExternalUsers') { $JSONObj.conditions.users.includeUsers = @(($users | Where-Object -Property displayName -In $JSONObj.conditions.users.includeUsers).id) } if ($JSONObj.conditions.users.excludeUsers) { $JSONObj.conditions.users.excludeUsers = @(($users | Where-Object -Property displayName -In $JSONObj.conditions.users.excludeUsers).id) } - + # Check the included and excluded groups foreach ($groupType in 'includeGroups', 'excludeGroups') { if ($JSONObj.conditions.users.PSObject.Properties.Name -contains $groupType) { @@ -152,7 +151,7 @@ function New-CIPPCAPolicy { throw "Failed to replace displayNames for conditional access rule $($JSONObj.displayName): $($_.exception.message)" Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to replace displayNames for conditional access rule $($JSONObj.displayName)" -sev 'Error' } - } + } } $JsonObj.PSObject.Properties.Remove('LocationInfo') $RawJSON = $JSONObj | ConvertTo-Json -Depth 10 -Compress @@ -177,7 +176,6 @@ function New-CIPPCAPolicy { return "Created policy $displayname for $tenantfilter" } } catch { - Write-Host "$($_.exception | ConvertTo-Json)" throw "Failed to create or update conditional access rule $($JSONObj.displayName): $($_.exception.message)" Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to create or update conditional access rule $($JSONObj.displayName): $($_.exception.message) " -sev 'Error' } diff --git a/Modules/CIPPCore/Public/New-CIPPCATemplate.ps1 b/Modules/CIPPCore/Public/New-CIPPCATemplate.ps1 new file mode 100644 index 000000000000..37577fd35f72 --- /dev/null +++ b/Modules/CIPPCore/Public/New-CIPPCATemplate.ps1 @@ -0,0 +1,85 @@ +function New-CIPPCATemplate { + [CmdletBinding()] + param ( + $TenantFilter, + $JSON, + $APIName = 'Add CIPP CA Template', + $ExecutingUser + ) + + $JSON = ([pscustomobject]$JSON) | ForEach-Object { + $NonEmptyProperties = $_.psobject.Properties | Where-Object { $null -ne $_.Value } | Select-Object -ExpandProperty Name + $_ | Select-Object -Property $NonEmptyProperties + } + + $includelocations = New-Object System.Collections.ArrayList + $IncludeJSON = foreach ($Location in $JSON.conditions.locations.includeLocations) { + $locationinfo = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/namedLocations' -tenantid $TenantFilter | Where-Object -Property id -EQ $location | Select-Object * -ExcludeProperty id, *time* + $null = if ($locationinfo) { $includelocations.add($locationinfo.displayName) } else { $includelocations.add($location) } + $locationinfo + } + if ($includelocations) { $JSON.conditions.locations.includeLocations = $includelocations } + + + $excludelocations = New-Object System.Collections.ArrayList + $ExcludeJSON = foreach ($Location in $JSON.conditions.locations.excludeLocations) { + $locationinfo = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/namedLocations' -tenantid $TenantFilter | Where-Object -Property id -EQ $location | Select-Object * -ExcludeProperty id, *time* + $null = if ($locationinfo) { $excludelocations.add($locationinfo.displayName) } else { $excludelocations.add($location) } + $locationinfo + } + + if ($excludelocations) { $JSON.conditions.locations.excludeLocations = $excludelocations } + if ($JSON.conditions.users.includeUsers) { + $JSON.conditions.users.includeUsers = @($JSON.conditions.users.includeUsers | ForEach-Object { + if ($_ -in 'All', 'None', 'GuestOrExternalUsers') { return $_ } + try { + (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$($_)" -tenantid $TenantFilter).displayName + } catch { + return $_ + } + }) + } + + if ($JSON.conditions.users.excludeUsers) { + $JSON.conditions.users.excludeUsers = @($JSON.conditions.users.excludeUsers | ForEach-Object { + if ($_ -in 'All', 'None', 'GuestOrExternalUsers') { return $_ } + try { + (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$($_)" -tenantid $TenantFilter).displayName + } catch { + return $_ + } + }) + } + + # Function to check if a string is a GUID + function Test-IsGuid($string) { + return [guid]::tryparse($string, [ref][guid]::Empty) + } + + if ($JSON.conditions.users.includeGroups) { + $JSON.conditions.users.includeGroups = @($JSON.conditions.users.includeGroups | ForEach-Object { + if ($_ -in 'All', 'None', 'GuestOrExternalUsers' -or -not (Test-IsGuid $_)) { return $_ } + try { + (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/groups/$($_)" -tenantid $TenantFilter).displayName + } catch { + return $_ + } + }) + } + if ($JSON.conditions.users.excludeGroups) { + $JSON.conditions.users.excludeGroups = @($JSON.conditions.users.excludeGroups | ForEach-Object { + if ($_ -in 'All', 'None', 'GuestOrExternalUsers' -or -not (Test-IsGuid $_)) { return $_ } + try { + (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/groups/$($_)" -tenantid $TenantFilter).displayName + } catch { + return $_ + } + }) + } + + $JSON | Add-Member -NotePropertyName 'LocationInfo' -NotePropertyValue @($IncludeJSON, $ExcludeJSON) + + $JSON = (ConvertTo-Json -Compress -Depth 100 -InputObject $JSON) + return $JSON +} + diff --git a/Modules/CIPPCore/Public/New-CIPPIntuneTemplate.ps1 b/Modules/CIPPCore/Public/New-CIPPIntuneTemplate.ps1 new file mode 100644 index 000000000000..0707b9824400 --- /dev/null +++ b/Modules/CIPPCore/Public/New-CIPPIntuneTemplate.ps1 @@ -0,0 +1,83 @@ +function New-CIPPIntuneTemplate { + param( + $urlname, + $id, + $TenantFilter, + $ActionResults, + $CIPPURL + ) + switch ($URLName) { + 'deviceCompliancePolicies' { + $Type = 'deviceCompliancePolicies' + $Template = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$($urlname)/$($ID)?`$expand=scheduledActionsForRule(`$expand=scheduledActionConfigurations)" -tenantid $tenantfilter + $DisplayName = $Template.displayName + $TemplateJson = ConvertTo-Json -InputObject $Template -Depth 100 -Compress + } + 'managedAppPolicies' { + $Type = 'AppProtection' + $Template = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceAppManagement/$($urlname)('$($ID)')" -tenantid $tenantfilter + $DisplayName = $Template.displayName + $TemplateJson = ConvertTo-Json -InputObject $Template -Depth 100 -Compress + } + 'configurationPolicies' { + $Type = 'Catalog' + $Template = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$($urlname)('$($ID)')?`$expand=settings" -tenantid $tenantfilter | Select-Object name, description, settings, platforms, technologies, templateReference + $TemplateJson = $Template | ConvertTo-Json -Depth 100 + $DisplayName = $Template.name + + } + 'windowsDriverUpdateProfiles' { + $Type = 'windowsDriverUpdateProfiles' + $Template = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$($urlname)/$($ID)" -tenantid $tenantfilter | Select-Object * -ExcludeProperty id, lastModifiedDateTime, '@odata.context', 'ScopeTagIds', 'supportsScopeTags', 'createdDateTime' + $DisplayName = $Template.displayName + $TemplateJson = ConvertTo-Json -InputObject $Template -Depth 100 -Compress + } + 'deviceConfigurations' { + $Type = 'Device' + $Template = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$($urlname)/$($ID)" -tenantid $tenantfilter | Select-Object * -ExcludeProperty id, lastModifiedDateTime, '@odata.context', 'ScopeTagIds', 'supportsScopeTags', 'createdDateTime' + $DisplayName = $Template.displayName + $TemplateJson = ConvertTo-Json -InputObject $Template -Depth 100 -Compress + } + 'groupPolicyConfigurations' { + $Type = 'Admin' + $Template = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$($urlname)('$($ID)')" -tenantid $tenantfilter + $DisplayName = $Template.displayName + $TemplateJsonItems = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$($urlname)('$($ID)')/definitionValues?`$expand=definition" -tenantid $tenantfilter + $TemplateJsonSource = foreach ($TemplateJsonItem in $TemplateJsonItems) { + $presentationValues = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$($urlname)('$($ID)')/definitionValues('$($TemplateJsonItem.id)')/presentationValues?`$expand=presentation" -tenantid $tenantfilter | ForEach-Object { + $obj = $_ + if ($obj.id) { + $PresObj = @{ + id = $obj.id + 'presentation@odata.bind' = "https://graph.microsoft.com/beta/deviceManagement/groupPolicyDefinitions('$($TemplateJsonItem.definition.id)')/presentations('$($obj.presentation.id)')" + } + if ($obj.values) { $PresObj['values'] = $obj.values } + if ($obj.value) { $PresObj['value'] = $obj.value } + if ($obj.'@odata.type') { $PresObj['@odata.type'] = $obj.'@odata.type' } + [pscustomobject]$PresObj + } + } + [PSCustomObject]@{ + 'definition@odata.bind' = "https://graph.microsoft.com/beta/deviceManagement/groupPolicyDefinitions('$($TemplateJsonItem.definition.id)')" + enabled = $TemplateJsonItem.enabled + presentationValues = @($presentationValues) + } + } + $inputvar = [pscustomobject]@{ + added = @($TemplateJsonSource) + updated = @() + deletedIds = @() + + } + + + $TemplateJson = (ConvertTo-Json -InputObject $inputvar -Depth 100 -Compress) + } + } + return [PSCustomObject]@{ + TemplateJson = $TemplateJson + DisplayName = $DisplayName + Description = $Template.description + Type = $Type + } +} diff --git a/Modules/CIPPCore/Public/New-CIPPRestore.ps1 b/Modules/CIPPCore/Public/New-CIPPRestore.ps1 new file mode 100644 index 000000000000..f3dd2ca21cbd --- /dev/null +++ b/Modules/CIPPCore/Public/New-CIPPRestore.ps1 @@ -0,0 +1,18 @@ +function New-CIPPRestore { + [CmdletBinding()] + param ( + $TenantFilter, + $Type = 'Scheduled', + $RestoreValues, + $APIName = 'CIPP Restore', + $ExecutingUser + ) + + Write-Host "Scheduled Restore psproperties: $(([pscustomobject]$RestoreValues).psobject.Properties)" + Write-LogMessage -user $ExecutingUser -API $APINAME -message 'Restored backup' -Sev 'Debug' + $RestoreData = foreach ($ScheduledBackup in ([pscustomobject]$RestoreValues).psobject.Properties.Name | Where-Object { $_ -notin 'email', 'webhook', 'psa', 'backup', 'overwrite' }) { + New-CIPPRestoreTask -Task $ScheduledBackup -TenantFilter $TenantFilter -backup $RestoreValues.backup.value -overwrite $RestoreValues.overwrite + } + return $RestoreData +} + diff --git a/Modules/CIPPCore/Public/New-CIPPRestoreTask.ps1 b/Modules/CIPPCore/Public/New-CIPPRestoreTask.ps1 new file mode 100644 index 000000000000..8d48960d1899 --- /dev/null +++ b/Modules/CIPPCore/Public/New-CIPPRestoreTask.ps1 @@ -0,0 +1,167 @@ +function New-CIPPRestoreTask { + [CmdletBinding()] + param ( + $Task, + $TenantFilter, + $backup, + $overwrite + ) + $Table = Get-CippTable -tablename 'ScheduledBackup' + $BackupData = Get-CIPPAzDataTableEntity @Table -Filter "RowKey eq '$backup'" + $RestoreData = switch ($Task) { + 'users' { + $currentUsers = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/users?$top=999&select=id,userPrincipalName' -tenantid $TenantFilter + $backupUsers = $BackupData.users | ConvertFrom-Json + $BackupUsers | ForEach-Object { + try { + $JSON = $_ | ConvertTo-Json -Depth 100 -Compress + $DisplayName = $_.displayName + $UPN = $_.userPrincipalName + if ($overwrite) { + if ($_.id -in $currentUsers.id) { + New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/users/$($_.id)" -tenantid $TenantFilter -body $JSON -type PATCH + Write-LogMessage -message "Restored $($UPN) from backup by patching the existing object." -Sev 'info' + "The user existed. Restored $($UPN) from backup" + } else { + New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/users' -tenantid $TenantFilter -body $JSON -type POST + Write-LogMessage -message "Restored $($UPN) from backup by creating a new object." -Sev 'info' + "The user did not exist. Restored $($UPN) from backup" + } + } + if (!$overwrite) { + if ($_.id -notin $backupUsers.id) { + New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/users' -tenantid $TenantFilter -body $JSON -type POST + Write-LogMessage -message "Restored $($UPN) from backup" -Sev 'info' + "Restored $($UPN) from backup" + } else { + Write-LogMessage -message "User $($UPN) already exists in tenant $TenantFilter and overwrite is disabled" -Sev 'info' + "User $($UPN) already exists in tenant $TenantFilter and overwrite is disabled" + } + } + } catch { + "Could not restore user $($UPN): $($_.Exception.Message) " + Write-LogMessage -user $ExecutingUser -API $APINAME -message "Could not restore user $($UPN): $($_.Exception.Message) " -Sev 'error' + } + } + } + 'groups' { + Write-Host "Restore groups for $TenantFilter" + $backupGroups = $BackupData.groups | ConvertFrom-Json + $Groups = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/groups?$top=999' -tenantid $TenantFilter + $BackupGroups | ForEach-Object { + try { + $JSON = $_ | ConvertTo-Json -Depth 100 -Compress + $DisplayName = $_.displayName + if ($overwrite) { + if ($_.id -in $Groups.id) { + New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/groups/$($_.id)" -tenantid $TenantFilter -body $JSON -type PATCH + Write-LogMessage -message "Restored $DisplayName from backup by patching the existing object." -Sev 'info' + "The group existed. Restored $DisplayName from backup" + } else { + New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/groups' -tenantid $TenantFilter -body $JSON -type POST + Write-LogMessage -message "Restored $DisplayName from backup" -Sev 'info' + "Restored $DisplayName from backup" + } + } + if (!$overwrite) { + if ($_.id -notin $Groups.id) { + New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/groups' -tenantid $TenantFilter -body $JSON -type POST + Write-LogMessage -message "Restored $DisplayName from backup" -Sev 'info' + "Restored $DisplayName from backup" + } else { + Write-LogMessage -message "Group $DisplayName already exists in tenant $TenantFilter and overwrite is disabled" -Sev 'info' + "Group $DisplayName already exists in tenant $TenantFilter and overwrite is disabled" + } + } + } catch { + "Could not restore group $DisplayName $($_.Exception.Message) " + Write-LogMessage -user $ExecutingUser -API $APINAME -message "Could not restore group $DisplayName $($_.Exception.Message) " -Sev 'error' + } + } + } + 'ca' { + Write-Host "Restore Conditional Access Policies for $TenantFilter" + $BackupCAPolicies = $BackupData.ca | ConvertFrom-Json + $BackupCAPolicies | ForEach-Object { + $JSON = $_ + try { + New-CIPPCAPolicy -replacePattern 'displayName' -Overwrite $overwrite -TenantFilter $TenantFilter -state 'donotchange' -RawJSON $JSON -APIName 'CIPP Restore' -ErrorAction SilentlyContinue + } catch { + "Could not restore Conditional Access Policy $DisplayName $($_.Exception.Message) " + Write-LogMessage -user $ExecutingUser -API $APINAME -message "Could not restore Conditional Access Policy $DisplayName $($_.Exception.Message) " -Sev 'error' + } + } + } + 'intuneconfig' { + $BackupConfig = $BackupData.intuneconfig | ConvertFrom-Json + foreach ($backup in $backupConfig) { + try { + Set-CIPPIntunePolicy -TemplateType $backup.Type -TenantFilter $TenantFilter -DisplayName $backup.DisplayName -Description $backup.Description -RawJSON ($backup.TemplateJson) -ErrorAction SilentlyContinue + } catch { + "Could not restore Intune Configuration $DisplayName $($_.Exception.Message) " + Write-LogMessage -user $ExecutingUser -API $APINAME -message "Could not restore Intune Configuration $DisplayName $($_.Exception.Message) " -Sev 'error' + } + } + #Convert the manual method to a function + } + 'intunecompliance' { + $BackupConfig = $BackupData.intunecompliance | ConvertFrom-Json + foreach ($backup in $backupConfig) { + try { + Set-CIPPIntunePolicy -TemplateType $backup.Type -TenantFilter $TenantFilter -DisplayName $backup.DisplayName -Description $backup.Description -RawJSON ($backup.TemplateJson) -ErrorAction SilentlyContinue + } catch { + "Could not restore Intune Compliance $DisplayName $($_.Exception.Message) " + Write-LogMessage -user $ExecutingUser -API $APINAME -message "Could not restore Intune Configuration $DisplayName $($_.Exception.Message) " -Sev 'error' + } + } + + } + + 'intuneprotection' { + $BackupConfig = $BackupData.intuneprotection | ConvertFrom-Json + foreach ($backup in $backupConfig) { + try { + Set-CIPPIntunePolicy -TemplateType $backup.Type -TenantFilter $TenantFilter -DisplayName $backup.DisplayName -Description $backup.Description -RawJSON ($backup.TemplateJson) -ErrorAction SilentlyContinue + } catch { + "Could not restore Intune Protection $DisplayName $($_.Exception.Message) " + Write-LogMessage -user $ExecutingUser -API $APINAME -message "Could not restore Intune Configuration $DisplayName $($_.Exception.Message) " -Sev 'error' + } + } + + } + + 'CippWebhookAlerts' { + Write-Host "Restore Webhook Alerts for $TenantFilter" + $WebhookTable = Get-CIPPTable -TableName 'WebhookRules' + $Backup = $BackupData.CippWebhookAlerts | ConvertFrom-Json + try { + Add-CIPPAzDataTableEntity @WebhookTable -Entity $Backup -Force + } catch { + "Could not restore Webhook Alerts $($_.Exception.Message)" + } + } + 'CippScriptedAlerts' { + Write-Host "Restore Scripted Alerts for $TenantFilter" + $ScheduledTasks = Get-CIPPTable -TableName 'ScheduledTasks' + $Backup = $BackupData.CippScriptedAlerts | ConvertFrom-Json + try { + Add-CIPPAzDataTableEntity @ScheduledTasks -Entity $Backup -Force + } catch { + "Could not restore Scripted Alerts $($_.Exception.Message) " + } + } + 'CippStandards' { + Write-Host "Restore Standards for $TenantFilter" + $Table = Get-CippTable -tablename 'standards' + $StandardsBackup = $BackupData.CippStandards | ConvertFrom-Json + try { + Add-CIPPAzDataTableEntity @Table -Entity $StandardsBackup -Force + } catch { + "Could not restore Standards $($_.Exception.Message) " + } + } + + } + return $RestoreData +} + diff --git a/Modules/CIPPCore/Public/New-CIPPSharepointSite.ps1 b/Modules/CIPPCore/Public/New-CIPPSharepointSite.ps1 new file mode 100644 index 000000000000..ccf2e8b81b22 --- /dev/null +++ b/Modules/CIPPCore/Public/New-CIPPSharepointSite.ps1 @@ -0,0 +1,145 @@ +function New-CIPPSharepointSite { + <# + .SYNOPSIS + Create a new SharePoint site + + .DESCRIPTION + Create a new SharePoint site using the Modern REST API + + .PARAMETER SiteName + The name of the site + + .PARAMETER SiteDescription + The description of the site + + .PARAMETER SiteOwner + The username of the site owner + + .PARAMETER TemplateName + The template to use for the site. Default is Communication + + .PARAMETER SiteDesign + The design to use for the site. Default is Topic + + .PARAMETER WebTemplateExtensionId + The web template extension ID to use + + .PARAMETER SensitivityLabel + The Purview sensitivity label to apply to the site + + .PARAMETER TenantFilter + The tenant associated with the site + + #> + [CmdletBinding(SupportsShouldProcess = $true)] + Param( + [Parameter(Mandatory = $true)] + [string]$SiteName, + + [Parameter(Mandatory = $true)] + [string]$SiteDescription, + + [Parameter(Mandatory = $true)] + [string]$SiteOwner, + + [Parameter(Mandatory = $false)] + [ValidateSet('Communication', 'Team')] + [string]$TemplateName = 'Communication', + + [Parameter(Mandatory = $false)] + [ValidateSet('Topic', 'Showcase', 'Blank', 'Custom')] + [string]$SiteDesign = 'Showcase', + + [Parameter(Mandatory = $false)] + [ValidatePattern('(\{|\()?[A-Za-z0-9]{4}([A-Za-z0-9]{4}\-?){4}[A-Za-z0-9]{12}(\}|\()?')] + [string]$WebTemplateExtensionId, + + [Parameter(Mandatory = $false)] + [ValidatePattern('(\{|\()?[A-Za-z0-9]{4}([A-Za-z0-9]{4}\-?){4}[A-Za-z0-9]{12}(\}|\()?')] + [string]$SensitivityLabel, + + [string]$Classification, + + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + $tenantName = (New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/sites/root' -asApp $true -tenantid $TenantFilter).id.Split('.')[0] + $AdminUrl = "https://$($tenantName)-admin.sharepoint.com" + $SitePath = $SiteName -replace ' ' -replace '[^A-Za-z0-9-]' + $SiteUrl = "https://$tenantName.sharepoint.com/sites/$SitePath" + + + + + switch ($TemplateName) { + 'Communication' { + $WebTemplate = 'SITEPAGEPUBLISHING#0' + } + 'Team' { + $WebTemplate = 'STS#0' + } + } + + $WebTemplateExtensionId = '00000000-0000-0000-0000-000000000000' + $DefaultSiteDesignIds = @( '96c933ac-3698-44c7-9f4a-5fd17d71af9e', '6142d2a0-63a5-4ba0-aede-d9fefca2c767', 'f6cc5403-0d63-442e-96c0-285923709ffc') + + switch ($SiteDesign) { + 'Topic' { + $SiteDesignId = '96c933ac-3698-44c7-9f4a-5fd17d71af9e' + } + 'Showcase' { + $SiteDesignId = '6142d2a0-63a5-4ba0-aede-d9fefca2c767' + } + 'Blank' { + $SiteDesignId = 'f6cc5403-0d63-442e-96c0-285923709ffc' + } + 'Custom' { + if ($WebTemplateExtensionId -match '^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$') { + if ($WebTemplateExtensionId -notin $DefaultSiteDesignIds) { + $WebTemplateExtensionId = $SiteDesign + $SiteDesignId = '00000000-0000-0000-0000-000000000000' + } else { + $SiteDesignId = $WebTemplateExtensionId + } + } else { + $SiteDesignId = '96c933ac-3698-44c7-9f4a-5fd17d71af9e' + } + } + } + + # Create the request body + $Request = @{ + Title = $SiteName + Url = $SiteUrl + Lcid = 1033 + ShareByEmailEnabled = $false + Description = $SiteDescription + WebTemplate = $WebTemplate + SiteDesignId = $SiteDesignId + Owner = $SiteOwner + WebTemplateExtensionId = $WebTemplateExtensionId + } + + # Set the sensitivity label if provided + if ($SensitivityLabel) { + $Request.SensitivityLabel = $SensitivityLabel + } + if ($Classification) { + $Request.Classification = $Classification + } + + Write-Verbose ($Request | ConvertTo-Json -Compress -Depth 10) + + $body = @{ + request = $Request + } + + # Create the site + if ($PSCmdlet.ShouldProcess($SiteName, 'Create new SharePoint site')) { + $AddedHeaders = @{ + 'accept' = 'application/json;odata.metadata=none' + 'odata-version' = '4.0' + } + New-GraphPostRequest -scope "$AdminUrl/.default" -uri "$AdminUrl/_api/SPSiteManager/create" -Body ($body | ConvertTo-Json -Compress -Depth 10) -tenantid $TenantFilter -ContentType 'application/json' -AddedHeaders $AddedHeaders + } +} diff --git a/Modules/CIPPCore/Public/PermissionsTranslator.json b/Modules/CIPPCore/Public/PermissionsTranslator.json index a0ba05d3dc02..1a88f6dbf708 100644 --- a/Modules/CIPPCore/Public/PermissionsTranslator.json +++ b/Modules/CIPPCore/Public/PermissionsTranslator.json @@ -1,4 +1,11 @@ [ + { + "description": "Allows the app to impersonate the signed-in user to access the Partner Center API.", + "displayName": "Partner Center as User", + "id": "1cebfa2a-fb4d-419e-b5f9-839b4383e05a", + "origin": "Delegated (Microsoft Partner Center)", + "value": "user_impersonation" + }, { "description": "Allows Exchange Management as app", "displayName": "Manage Exchange As Application ", @@ -1004,8 +1011,15 @@ "description": "Allows the app to create, read, update, and delete events of all calendars without a signed-in user.", "displayName": "Read and write calendars in all mailboxes", "id": "ef54d2bf-783f-4e0f-bca1-3210c0444d99", - "origin": "Application", - "value": "Calendars.ReadWrite" + "origin": "Application (Office 365 Exchange Online)", + "value": "Calendars.ReadWrite.All" + }, + { + "description": "Allows the app to create, read, update, and delete user's mailbox settings without a signed-in user. Does not include permission to send mail.", + "displayName": "Read and write all user mailbox settings", + "id": "f9156939-25cd-4ba8-abfe-7fabcf003749", + "origin": "Application (Office 365 Exchange Online)", + "value": "Mailbox.Settings.ReadWrite" }, { "description": "Allows the app to read your organization's user flows, without a signed-in user.", @@ -5286,6 +5300,24 @@ "userConsentDisplayName": "Read Threat and Vulnerability Management vulnerability information", "value": "Exchange.Manage" }, + { + "description": "Allows the app to create, read, update and delete events in all calendars in the organization user has permissions to access. This includes delegate and shared calendars", + "displayName": "Read and write user and shared calendars", + "id": "bbd1ca91-75e0-4814-ad94-9c5dbbae3415", + "Origin": "Delegated (Office 365 Exchange Online)", + "userConsentDescription": "Allows the app to read, update, create and delete events in all calendars in your organization you have permissions to access. This includes delegate and shared calendars", + "userConsentDisplayName": "Read and write to your and shared calendars", + "value": "Calendars.ReadWrite.All" + }, + { + "description": "Allows the app to create, read, update, and delete user's mailbox settings. Does not include permission to send mail.", + "displayName": "Read and write user mailbox settings", + "id": "2e83d72d-8895-4b66-9eea-abb43449ab8b", + "Origin": "Delegated (Office 365 Exchange Online)", + "userConsentDescription": "Allows the app to read, update, create, and delete your mailbox settings.", + "userConsentDisplayName": "Read and write to your mailbox settings", + "value": "MailboxSettings.ReadWrite" + }, { "description": "Allows the app to have full control of all site collections on behalf of the signed-in user.", "displayName": "Manage Sharepoint Online", diff --git a/Modules/CIPPCore/Public/Remove-CIPPCalendarInvites.ps1 b/Modules/CIPPCore/Public/Remove-CIPPCalendarInvites.ps1 new file mode 100644 index 000000000000..22e57c2acff8 --- /dev/null +++ b/Modules/CIPPCore/Public/Remove-CIPPCalendarInvites.ps1 @@ -0,0 +1,21 @@ +function Remove-CIPPCalendarInvites { + [CmdletBinding()] + param( + $userid, + $tenantFilter, + $username, + $APIName = 'Remove Calendar Invites', + $ExecutingUser + ) + + try { + + New-ExoRequest -tenantid $tenantFilter -cmdlet 'Remove-CalendarEvents' -Anchor $username -cmdParams @{Identity = $username; QueryWindowInDays = 730 ; CancelOrganizedMeetings = $true ; Confirm = $false} + Write-LogMessage -user $ExecutingUser -API $APIName -message "Cancelled all calendar invites for $($username)" -Sev 'Info' -tenant $tenantFilter + "Cancelled all calendar invites for $($username)" + + } catch { + Write-LogMessage -user $ExecutingUser -API $APIName -message "Could not cancel calendar invites for $($username): $($_.Exception.Message)" -Sev 'Error' -tenant $tenantFilter + return "Could not cancel calendar invites for $($username). Error: $($_.Exception.Message)" + } +} diff --git a/Modules/CIPPCore/Public/Request-CIPPSPOPersonalSite.ps1 b/Modules/CIPPCore/Public/Request-CIPPSPOPersonalSite.ps1 new file mode 100644 index 000000000000..44f1764b674c --- /dev/null +++ b/Modules/CIPPCore/Public/Request-CIPPSPOPersonalSite.ps1 @@ -0,0 +1,51 @@ +function Request-CIPPSPOPersonalSite { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter, + [Parameter(Mandatory = $true)] + [string[]]$UserEmails, + [string]$ExecutingUser = 'CIPP', + [string]$APIName = 'Request-CIPPSPOPersonalSite' + ) + $UserList = [System.Collections.Generic.List[string]]::new() + foreach ($User in $UserEmails) { + $UserList.Add("$User") + } + + $XML = @" + + + + + + + + + + + + + + + + $($UserList -join '') + + + + + +"@ + $tenantName = (New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/sites/root' -asApp $true -tenantid $TenantFilter).id.Split('.')[0] + $AdminUrl = "https://$($tenantName)-admin.sharepoint.com" + + try { + $Request = New-GraphPostRequest -scope "$AdminURL/.default" -tenantid $TenantFilter -Uri "$AdminURL/_vti_bin/client.svc/ProcessQuery" -Type POST -Body $XML -ContentType 'text/xml' + if (!$Request.IsComplete) { throw } + Write-LogMessage -user $ExecutingUser -API $APIName -message "Requested personal site for $($Users -join ', ')" -Sev 'Info' -tenant $TenantFilter + return "Requested personal site for $($Users -join ', ')" + } catch { + Write-LogMessage -user $ExecutingUser -API $APIName -message "Could not request personal site for $($Users -join ', ')" -Sev 'Error' -tenant $TenantFilter + return "Could not request personal site for $($Users -join ', '). Error: $($_.Exception.Message)" + } +} \ No newline at end of file diff --git a/Modules/CIPPCore/Public/SAMManifest.json b/Modules/CIPPCore/Public/SAMManifest.json index 6b1f6429af88..50a03c019af1 100644 --- a/Modules/CIPPCore/Public/SAMManifest.json +++ b/Modules/CIPPCore/Public/SAMManifest.json @@ -11,6 +11,12 @@ ] }, "requiredResourceAccess": [ + { + "resourceAppId": "aeb86249-8ea3-49e2-900b-54cc8e308f85", + "resourceAccess": [ + { "id": "fc946a4f-bc4d-413b-a090-b2c86113ec4f", "type": "Scope" } + ] + }, { "resourceAppId": "fa3d9a0c-3fb0-42cc-9193-47c7ecd2edbd", "resourceAccess": [ @@ -145,7 +151,8 @@ { "id": "b6890674-9dd5-4e42-bb15-5af07f541ae1", "type": "Role" }, { "id": "9e4862a5-b68f-479e-848a-4e07e25c9916", "type": "Scope" }, { "id": "bb6f654c-d7fd-4ae3-85c3-fc380934f515", "type": "Scope" }, - { "id": "e0a7cdbb-08b0-4697-8264-0069786e9674", "type": "Scope" } + { "id": "e0a7cdbb-08b0-4697-8264-0069786e9674", "type": "Scope" }, + { "id": "19da66cb-0fb0-4390-b071-ebc76a349482", "type": "Role" } ] }, { @@ -159,7 +166,11 @@ "resourceAppId": "00000002-0000-0ff1-ce00-000000000000", "resourceAccess": [ { "id": "ab4f2b77-0b06-4fc1-a9de-02113fc2ab7c", "type": "Scope" }, - { "id": "dc50a0fb-09a3-484d-be87-e023b12c6440", "type": "Role" } + { "id": "bbd1ca91-75e0-4814-ad94-9c5dbbae3415", "type": "Scope" }, + { "id": "2e83d72d-8895-4b66-9eea-abb43449ab8b", "type": "Scope" }, + { "id": "dc50a0fb-09a3-484d-be87-e023b12c6440", "type": "Role" }, + { "id": "ef54d2bf-783f-4e0f-bca1-3210c0444d99", "type": "Role" }, + { "id": "f9156939-25cd-4ba8-abfe-7fabcf003749", "type": "Role" } ] }, { diff --git a/Modules/CIPPCore/Public/Set-CIPPDefaultAPDeploymentProfile.ps1 b/Modules/CIPPCore/Public/Set-CIPPDefaultAPDeploymentProfile.ps1 index 07f2646a7243..e685491b262d 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDefaultAPDeploymentProfile.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDefaultAPDeploymentProfile.ps1 @@ -59,6 +59,8 @@ function Set-CIPPDefaultAPDeploymentProfile { Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APIName -tenant $($tenantfilter) -message "Added Autopilot profile $($displayname)" -Sev 'Info' } } else { + #patch the profile + $null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeploymentProfiles/$($Profiles.id)" -tenantid $tenantfilter -body $body -type PATCH $GraphRequest = $Profiles } diff --git a/Modules/CIPPCore/Public/Set-CIPPForwarding.ps1 b/Modules/CIPPCore/Public/Set-CIPPForwarding.ps1 index 130ff65ff430..d8ebfe422845 100644 --- a/Modules/CIPPCore/Public/Set-CIPPForwarding.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPForwarding.ps1 @@ -43,19 +43,25 @@ function Set-CIPPForwarding { [string]$ExecutingUser, [string]$APIName = 'Forwarding', [string]$Forward, - [bool]$KeepCopy, + $KeepCopy, [bool]$Disable ) + try { if (!$username) { $username = $userid } if ($PSCmdlet.ShouldProcess($username, 'Set forwarding')) { - $null = New-ExoRequest -tenantid $tenantFilter -cmdlet 'Set-mailbox' -cmdParams @{Identity = $userid; ForwardingSMTPAddress = $forwardingSMTPAddress; ForwardingAddress = $Forward ; DeliverToMailboxAndForward = [bool]$KeepCopy } -Anchor $username - } - if (!$Disable) { - $Message = "Forwarding all email for $username to $Forward" - } else { - $Message = "Disabled forwarding for $username" + if ($Disable -eq $true) { + Write-Output "Disabling forwarding for $username" + $null = New-ExoRequest -tenantid $tenantFilter -cmdlet 'Set-mailbox' -cmdParams @{Identity = $userid; ForwardingSMTPAddress = $null; ForwardingAddress = $null ; DeliverToMailboxAndForward = $false } -Anchor $username + $Message = "Disabled forwarding for $username" + } elseif ($Forward) { + $null = New-ExoRequest -tenantid $tenantFilter -cmdlet 'Set-mailbox' -cmdParams @{Identity = $userid; ForwardingSMTPAddress = $null; ForwardingAddress = $Forward ; DeliverToMailboxAndForward = $KeepCopy } -Anchor $username + $Message = "Forwarding all email for $username to Internal Address $Forward and keeping a copy set to $KeepCopy" + } elseif ($forwardingSMTPAddress) { + $null = New-ExoRequest -tenantid $tenantFilter -cmdlet 'Set-mailbox' -cmdParams @{Identity = $userid; ForwardingSMTPAddress = $forwardingSMTPAddress; ForwardingAddress = $null ; DeliverToMailboxAndForward = $KeepCopy } -Anchor $username + $Message = "Forwarding all email for $username to External Address $ForwardingSMTPAddress and keeping a copy set to $KeepCopy" + } } Write-LogMessage -user $ExecutingUser -API $APIName -message $Message -Sev 'Info' -tenant $TenantFilter return $Message diff --git a/Modules/CIPPCore/Public/Set-CIPPIntunePolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPIntunePolicy.ps1 new file mode 100644 index 000000000000..45ffd9fad2e1 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPIntunePolicy.ps1 @@ -0,0 +1,130 @@ +function Set-CIPPIntunePolicy { + param ( + [Parameter(Mandatory = $true)] + $TemplateType, + $Description, + $DisplayName, + $RawJSON, + $AssignTo, + $ExecutingUser, + $tenantFilter + ) + $ReturnValue = try { + switch ($TemplateType) { + 'AppProtection' { + $TemplateType = ($RawJSON | ConvertFrom-Json).'@odata.type' -replace '#microsoft.graph.', '' + $TemplateTypeURL = "$($TemplateType)s" + $CheckExististing = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/deviceAppManagement/$TemplateTypeURL" -tenantid $tenantFilter + if ($displayname -in $CheckExististing.displayName) { + $PostType = 'edited' + $ExistingID = $CheckExististing | Where-Object -Property displayName -EQ $PolicyName + $CreateRequest = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceAppManagement/$TemplateTypeURL/$($ExistingID.Id)" -tenantid $tenantFilter -type PATCH -body $RawJSON + } else { + $PostType = 'added' + $CreateRequest = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceAppManagement/$TemplateTypeURL" -tenantid $tenantFilter -type POST -body $RawJSON + } + } + 'deviceCompliancePolicies' { + $TemplateTypeURL = 'deviceCompliancePolicies' + $CheckExististing = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$TemplateTypeURL" -tenantid $tenantFilter + $JSON = $RawJSON | ConvertFrom-Json | Select-Object * -ExcludeProperty id, createdDateTime, lastModifiedDateTime, version, 'scheduledActionsForRule@odata.context', '@odata.context' + $JSON.scheduledActionsForRule = @($JSON.scheduledActionsForRule | Select-Object * -ExcludeProperty 'scheduledActionConfigurations@odata.context') + $RawJSON = ConvertTo-Json -InputObject $JSON -Depth 20 -Compress + Write-Host $RawJSON + if ($displayname -in $CheckExististing.displayName) { + $PostType = 'edited' + $ExistingID = $CheckExististing | Where-Object -Property displayName -EQ $PolicyName + $CreateRequest = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$TemplateTypeURL/$($ExistingID.Id)" -tenantid $tenantFilter -type PATCH -body $RawJSON + Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -tenant $($tenantFilter) -message "Updated policy $($PolicyName) to template defaults" -Sev 'info' + } else { + $PostType = 'added' + $CreateRequest = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$TemplateTypeURL" -tenantid $tenantFilter -type POST -body $RawJSON + Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -tenant $($tenantFilter) -message "Added policy $($PolicyName) via template" -Sev 'info' + } + } + 'Admin' { + $TemplateTypeURL = 'groupPolicyConfigurations' + $CreateBody = '{"description":"' + $description + '","displayName":"' + $displayname + '","roleScopeTagIds":["0"]}' + $CheckExististing = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$TemplateTypeURL" -tenantid $tenantFilter + if ($displayname -in $CheckExististing.displayName) { + $ExistingID = $CheckExististing | Where-Object -Property displayName -EQ $displayname + $ExistingData = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$TemplateTypeURL('$($existingId.id)')/definitionValues" -tenantid $tenantFilter + $DeleteJson = $RawJSON | ConvertFrom-Json -Depth 10 + $DeleteJson.deletedIds = @($ExistingData.id) + $DeleteJson.added = @() + $DeleteJson = ConvertTo-Json -Depth 10 -InputObject $DeleteJson + $DeleteRequest = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$TemplateTypeURL('$($existingId.id)')/updateDefinitionValues" -tenantid $tenantFilter -type POST -body $DeleteJson + $CreateRequest = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$TemplateTypeURL('$($existingId.id)')/updateDefinitionValues" -tenantid $tenantFilter -type POST -body $RawJSON + Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -tenant $($tenantFilter) -message "Updated policy $($Displayname) to template defaults" -Sev 'info' + $PostType = 'edited' + } else { + $PostType = 'added' + $CreateRequest = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$TemplateTypeURL" -tenantid $tenantFilter -type POST -body $CreateBody + $UpdateRequest = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$TemplateTypeURL('$($CreateRequest.id)')/updateDefinitionValues" -tenantid $tenantFilter -type POST -body $RawJSON + Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -tenant $($tenantFilter) -message "Added policy $($Displayname) to template defaults" -Sev 'info' + + } + } + 'Device' { + $TemplateTypeURL = 'deviceConfigurations' + + $PolicyName = ($RawJSON | ConvertFrom-Json).displayName + $CheckExististing = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$TemplateTypeURL" -tenantid $tenantFilter + if ($PolicyName -in $CheckExististing.displayName) { + $PostType = 'edited' + $ExistingID = $CheckExististing | Where-Object -Property displayName -EQ $PolicyName + $CreateRequest = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$TemplateTypeURL/$($ExistingID.Id)" -tenantid $tenantFilter -type PATCH -body $RawJSON + $CreateRequest = $CheckExististing | Where-Object -Property displayName -EQ $PolicyName + Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -tenant $($tenantFilter) -message "Updated policy $($PolicyName) to template defaults" -Sev 'info' + } else { + $PostType = 'added' + $CreateRequest = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$TemplateTypeURL" -tenantid $tenantFilter -type POST -body $RawJSON + Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -tenant $($tenantFilter) -message "Added policy $($PolicyName) via template" -Sev 'info' + + } + } + 'Catalog' { + $TemplateTypeURL = 'configurationPolicies' + $PolicyName = ($RawJSON | ConvertFrom-Json).Name + $CheckExististing = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$TemplateTypeURL" -tenantid $tenantFilter + if ($PolicyName -in $CheckExististing.name) { + $ExistingID = $CheckExististing | Where-Object -Property Name -EQ $PolicyName + $CreateRequest = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$TemplateTypeURL/$($ExistingID.Id)" -tenantid $tenantFilter -type PUT -body $RawJSON + $CreateRequest = $CheckExististing | Where-Object -Property Name -EQ $PolicyName + $PostType = 'edited' + } else { + $PostType = 'added' + $CreateRequest = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$TemplateTypeURL" -tenantid $tenantFilter -type POST -body $RawJSON + Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -tenant $($tenantFilter) -message "Added policy $($PolicyName) via template" -Sev 'info' + } + } + 'windowsDriverUpdateProfiles' { + $TemplateTypeURL = 'windowsDriverUpdateProfiles' + $PolicyName = ($RawJSON | ConvertFrom-Json).Name + $CheckExististing = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$TemplateTypeURL" -tenantid $tenantFilter + if ($PolicyName -in $CheckExististing.name) { + $PostType = 'edited' + $ExistingID = $CheckExististing | Where-Object -Property Name -EQ $PolicyName + $CreateRequest = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$TemplateTypeURL/$($ExistingID.Id)" -tenantid $tenantFilter -type PUT -body $RawJSON + } else { + $PostType = 'added' + $CreateRequest = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$TemplateTypeURL" -tenantid $tenantFilter -type POST -body $RawJSON + Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -tenant $($tenantFilter) -message "Added policy $($PolicyName) via template" -Sev 'info' + } + } + + } + Write-LogMessage -user $ExecutingUser -API $APINAME -tenant $($tenantFilter) -message "$($PostType) policy $($Displayname)" -Sev 'Info' + if ($AssignTo) { + Write-Host "Assigning policy to $($AssignTo) with ID $($CreateRequest.id) and type $TemplateTypeURL for tenant $tenantFilter" + Set-CIPPAssignedPolicy -GroupName $AssignTo -PolicyId $CreateRequest.id -Type $TemplateTypeURL -TenantFilter $tenantFilter + } + "Successfully $($PostType) policy for $($tenantFilter) with display name $($Displayname)" + } catch { + "Failed to add or set policy for $($tenantFilter) with display name $($Displayname): $($_.Exception.Message)" + Write-LogMessage -user $ExecutingUser -API $APINAME -tenant $($tenantFilter) -message "Failed $($PostType) policy $($Displayname). Error: $($_.Exception.Message)" -Sev 'Error' + continue + } + + return $ReturnValue +} diff --git a/Modules/CIPPCore/Public/Set-CIPPSPOTenant.ps1 b/Modules/CIPPCore/Public/Set-CIPPSPOTenant.ps1 new file mode 100644 index 000000000000..ad6a9b115321 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPSPOTenant.ps1 @@ -0,0 +1,92 @@ +function Set-CIPPSPOTenant { + <# + .SYNOPSIS + Set Sharepoint Tenant properties + + .DESCRIPTION + Set Sharepoint Tenant properties via SPO API + + .PARAMETER TenantFilter + Tenant to apply settings to + + .PARAMETER Identity + Tenant Identity (Get from Get-CIPPSPOTenant) + + .PARAMETER Properties + Hashtable of tenant properties to change + + .PARAMETER SharepointPrefix + Prefix for the sharepoint tenant + + .EXAMPLE + $Properties = @{ + 'EnableAIPIntegration' = $true + } + Get-CippSPOTenant -TenantFilter 'contoso.onmicrosoft.com' | Set-CIPPSPOTenant -Properties $Properties + + .FUNCTIONALITY + Internal + + #> + [CmdletBinding(SupportsShouldProcess = $true)] + Param( + [Parameter(ValueFromPipelineByPropertyName = $true, Mandatory = $true)] + [string]$TenantFilter, + [Parameter(ValueFromPipelineByPropertyName = $true, Mandatory = $true)] + [Alias('_ObjectIdentity_')] + [string]$Identity, + [Parameter(Mandatory = $true)] + [hashtable]$Properties, + [Parameter(ValueFromPipelineByPropertyName = $true)] + [string]$SharepointPrefix + ) + + process { + if (!$SharepointPrefix) { + # get sharepoint admin site + $tenantName = (New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/sites/root' -asApp $true -tenantid $TenantFilter).id.Split('.')[0] + } else { + $tenantName = $SharepointPrefix + } + $Identity = $Identity -replace "`n", ' ' + $AdminUrl = "https://$($tenantName)-admin.sharepoint.com" + $AllowedTypes = @('Boolean', 'String', 'Int32') + $SetProperty = [System.Collections.Generic.List[string]]::new() + $x = 114 + foreach ($Property in $Properties.Keys) { + # Get property type + $PropertyType = $Properties[$Property].GetType().Name + if ($PropertyType -in $AllowedTypes) { + if ($PropertyType -eq 'Boolean') { + $PropertyToSet = $Properties[$Property].ToString().ToLower() + } else { + $PropertyToSet = $Properties[$Property] + } + $xml = @" + + $($PropertyToSet) + +"@ + $SetProperty.Add($xml) + $x++ + } + } + + if (($SetProperty | Measure-Object).Count -eq 0) { + Write-Error 'No valid properties found' + return + } + + # Query tenant settings + $XML = @" + $($SetProperty -join '') +"@ + $AdditionalHeaders = @{ + 'Accept' = 'application/json;odata=verbose' + } + + if ($PSCmdlet.ShouldProcess(($Properties.Keys -join ', '), 'Set Tenant Properties')) { + New-GraphPostRequest -scope "$AdminURL/.default" -tenantid $TenantFilter -Uri "$AdminURL/_vti_bin/client.svc/ProcessQuery" -Type POST -Body $XML -ContentType 'text/xml' -AddedHeaders $AdditionalHeaders + } + } +} diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAPConfig.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAPConfig.ps1 index 4f168c0e6d2b..59f5203229fa 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAPConfig.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAPConfig.ps1 @@ -20,7 +20,7 @@ function Invoke-CIPPStandardAPConfig { DeploymentMode = $DeploymentMode assignto = $settings.Assignto devicenameTemplate = $Settings.DeviceNameTemplate - allowWhiteGlove = $Settings.allowWhiteGlove + allowWhiteGlove = $Settings.allowWhiteglove CollectHash = $Settings.CollectHash hideChangeAccount = $Settings.HideChangeAccount hidePrivacy = $Settings.HidePrivacy diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardActivityBasedTimeout.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardActivityBasedTimeout.ps1 index ebf92d21e199..a09e427fbbf8 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardActivityBasedTimeout.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardActivityBasedTimeout.ps1 @@ -2,7 +2,35 @@ function Invoke-CIPPStandardActivityBasedTimeout { <# .FUNCTIONALITY Internal + .APINAME + ActivityBasedTimeout + .CAT + Global Standards + .TAG + "mediumimpact" + "CIS" + "spo_idle_session_timeout" + .HELPTEXT + Enables and sets Idle session timeout for Microsoft 365 to 1 hour. This policy affects most M365 web apps + .ADDEDCOMPONENT + {"type":"Select","label":"Select value","name":"standards.ActivityBasedTimeout.timeout","values":[{"label":"1 Hour","value":"01:00:00"},{"label":"3 Hours","value":"03:00:00"},{"label":"6 Hours","value":"06:00:00"},{"label":"12 Hours","value":"12:00:00"},{"label":"24 Hours","value":"1.00:00:00"}]} + .LABEL + Enable Activity based Timeout + .IMPACT + Medium Impact + .POWERSHELLEQUIVALENT + Portal or Graph API + .RECOMMENDEDBY + "CIS" + .DOCSDESCRIPTION + Enables and sets Idle session timeout for Microsoft 365 to 1 hour. This policy affects most M365 web apps + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) # Input validation @@ -64,3 +92,7 @@ function Invoke-CIPPStandardActivityBasedTimeout { } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDKIM.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDKIM.ps1 index d4b8bd35166c..5cb6a387ad32 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDKIM.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDKIM.ps1 @@ -2,7 +2,33 @@ function Invoke-CIPPStandardAddDKIM { <# .FUNCTIONALITY Internal + .APINAME + AddDKIM + .CAT + Exchange Standards + .TAG + "lowimpact" + "CIS" + .HELPTEXT + Enables DKIM for all domains that currently support it + .ADDEDCOMPONENT + .LABEL + Enables DKIM for all domains that currently support it + .IMPACT + Low Impact + .POWERSHELLEQUIVALENT + New-DkimSigningConfig and Set-DkimSigningConfig + .RECOMMENDEDBY + "CIS" + .DOCSDESCRIPTION + Enables DKIM for all domains that currently support it + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $AllDomains = (New-GraphGetRequest -uri 'https://graph.microsoft.com/v1.0/domains?$top=999' -tenantid $Tenant | Where-Object { $_.supportedServices -contains 'Email' -or $_.id -like '*mail.onmicrosoft.com' }).id @@ -81,3 +107,7 @@ function Invoke-CIPPStandardAddDKIM { Add-CIPPBPAField -FieldName 'DKIM' -FieldValue $DKIMState -StoreAs bool -Tenant $tenant } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAnonReportDisable.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAnonReportDisable.ps1 index 411342e5ab3e..9255be3c1bff 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAnonReportDisable.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAnonReportDisable.ps1 @@ -2,7 +2,33 @@ function Invoke-CIPPStandardAnonReportDisable { <# .FUNCTIONALITY Internal + .APINAME + AnonReportDisable + .CAT + Global Standards + .TAG + "lowimpact" + .HELPTEXT + Shows usernames instead of pseudo anonymised names in reports. This standard is required for reporting to work correctly. + .DOCSDESCRIPTION + Microsoft announced some APIs and reports no longer return names, to comply with compliance and legal requirements in specific countries. This proves an issue for a lot of MSPs because those reports are often helpful for engineers. This standard applies a setting that shows usernames in those API calls / reports. + .ADDEDCOMPONENT + .LABEL + Enable Usernames instead of pseudo anonymised names in reports + .IMPACT + Low Impact + .POWERSHELLEQUIVALENT + Update-MgBetaAdminReportSetting -BodyParameter @{displayConcealedNames = $true} + .RECOMMENDEDBY + .DOCSDESCRIPTION + Shows usernames instead of pseudo anonymised names in reports. This standard is required for reporting to work correctly. + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $CurrentInfo = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/admin/reportSettings' -tenantid $Tenant -AsApp $true @@ -32,3 +58,7 @@ function Invoke-CIPPStandardAnonReportDisable { Add-CIPPBPAField -FieldName 'AnonReport' -FieldValue $CurrentInfo.displayConcealedNames -StoreAs bool -Tenant $tenant } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAntiPhishPolicy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAntiPhishPolicy.ps1 index dc7695e459c1..3a245863f65b 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAntiPhishPolicy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAntiPhishPolicy.ps1 @@ -1,15 +1,60 @@ function Invoke-CIPPStandardAntiPhishPolicy { - <# - .FUNCTIONALITY - Internal - #> + <# + .FUNCTIONALITY + Internal + .APINAME + AntiPhishPolicy + .CAT + Defender Standards + .TAG + "lowimpact" + "CIS" + "mdo_safeattachments" + "mdo_highconfidencespamaction" + "mdo_highconfidencephishaction" + "mdo_phisspamacation" + "mdo_spam_notifications_only_for_admins" + "mdo_antiphishingpolicies" + .HELPTEXT + This creates a Anti-Phishing policy that automatically enables Mailbox Intelligence and spoofing, optional switches for Mailtips. + .ADDEDCOMPONENT + {"type":"number","label":"Phishing email threshold. (Default 1)","name":"standards.AntiPhishPolicy.PhishThresholdLevel","default":1} + {"type":"boolean","label":"Show first contact safety tip","name":"standards.AntiPhishPolicy.EnableFirstContactSafetyTips","default":true} + {"type":"boolean","label":"Show user impersonation safety tip","name":"standards.AntiPhishPolicy.EnableSimilarUsersSafetyTips","default":true} + {"type":"boolean","label":"Show domain impersonation safety tip","name":"standards.AntiPhishPolicy.EnableSimilarDomainsSafetyTips","default":true} + {"type":"boolean","label":"Show user impersonation unusual characters safety tip","name":"standards.AntiPhishPolicy.EnableUnusualCharactersSafetyTips","default":true} + {"type":"Select","label":"If the message is detected as spoof by spoof intelligence","name":"standards.AntiPhishPolicy.AuthenticationFailAction","values":[{"label":"Quarantine the message","value":"Quarantine"},{"label":"Move to Junk Folder","value":"MoveToJmf"}]} + {"type":"Select","label":"Quarantine policy for Spoof","name":"standards.AntiPhishPolicy.SpoofQuarantineTag","values":[{"label":"AdminOnlyAccessPolicy","value":"AdminOnlyAccessPolicy"},{"label":"DefaultFullAccessPolicy","value":"DefaultFullAccessPolicy"},{"label":"DefaultFullAccessWithNotificationPolicy","value":"DefaultFullAccessWithNotificationPolicy"}]} + {"type":"Select","label":"If a message is detected as user impersonation","name":"standards.AntiPhishPolicy.TargetedUserProtectionAction","values":[{"label":"Move to Junk Folder","value":"MoveToJmf"},{"label":"Delete the message before its delivered","value":"Delete"},{"label":"Quarantine the message","value":"Quarantine"}]} + {"type":"Select","label":"Quarantine policy for user impersonation","name":"standards.AntiPhishPolicy.TargetedUserQuarantineTag","values":[{"label":"AdminOnlyAccessPolicy","value":"AdminOnlyAccessPolicy"},{"label":"DefaultFullAccessPolicy","value":"DefaultFullAccessPolicy"},{"label":"DefaultFullAccessWithNotificationPolicy","value":"DefaultFullAccessWithNotificationPolicy"}]} + {"type":"Select","label":"If a message is detected as domain impersonation","name":"standards.AntiPhishPolicy.TargetedDomainProtectionAction","values":[{"label":"Move to Junk Folder","value":"MoveToJmf"},{"label":"Delete the message before its delivered","value":"Delete"},{"label":"Quarantine the message","value":"Quarantine"}]} + {"type":"Select","label":"Quarantine policy for domain impersonation","name":"standards.AntiPhishPolicy.TargetedDomainQuarantineTag","values":[{"label":"DefaultFullAccessWithNotificationPolicy","value":"DefaultFullAccessWithNotificationPolicy"},{"label":"AdminOnlyAccessPolicy","value":"AdminOnlyAccessPolicy"},{"label":"DefaultFullAccessPolicy","value":"DefaultFullAccessPolicy"}]} + {"type":"Select","label":"If Mailbox Intelligence detects an impersonated user","name":"standards.AntiPhishPolicy.MailboxIntelligenceProtectionAction","values":[{"label":"Move to Junk Folder","value":"MoveToJmf"},{"label":"Delete the message before its delivered","value":"Delete"},{"label":"Quarantine the message","value":"Quarantine"}]} + {"type":"Select","label":"Apply quarantine policy","name":"standards.AntiPhishPolicy.MailboxIntelligenceQuarantineTag","values":[{"label":"AdminOnlyAccessPolicy","value":"AdminOnlyAccessPolicy"},{"label":"DefaultFullAccessPolicy","value":"DefaultFullAccessPolicy"},{"label":"DefaultFullAccessWithNotificationPolicy","value":"DefaultFullAccessWithNotificationPolicy"}]} + .LABEL + Default Anti-Phishing Policy + .IMPACT + Low Impact + .POWERSHELLEQUIVALENT + Set-AntiphishPolicy or New-AntiphishPolicy + .RECOMMENDEDBY + "CIS" + .DOCSDESCRIPTION + This creates a Anti-Phishing policy that automatically enables Mailbox Intelligence and spoofing, optional switches for Mailtips. + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + #> + + + + param($Tenant, $Settings) $PolicyName = 'Default Anti-Phishing Policy' $CurrentState = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-AntiPhishPolicy' | Where-Object -Property Name -EQ $PolicyName | - Select-Object Name, Enabled, PhishThresholdLevel, EnableMailboxIntelligence, EnableMailboxIntelligenceProtection, EnableSpoofIntelligence, EnableFirstContactSafetyTips, EnableSimilarUsersSafetyTips, EnableSimilarDomainsSafetyTips, EnableUnusualCharactersSafetyTips, EnableUnauthenticatedSender, EnableViaTag, MailboxIntelligenceProtectionAction, MailboxIntelligenceQuarantineTag + Select-Object Name, Enabled, PhishThresholdLevel, EnableMailboxIntelligence, EnableMailboxIntelligenceProtection, EnableSpoofIntelligence, EnableFirstContactSafetyTips, EnableSimilarUsersSafetyTips, EnableSimilarDomainsSafetyTips, EnableUnusualCharactersSafetyTips, EnableUnauthenticatedSender, EnableViaTag, AuthenticationFailAction, SpoofQuarantineTag, MailboxIntelligenceProtectionAction, MailboxIntelligenceQuarantineTag, TargetedUserProtectionAction, TargetedUserQuarantineTag, TargetedDomainProtectionAction, TargetedDomainQuarantineTag, EnableOrganizationDomainsProtection $StateIsCorrect = ($CurrentState.Name -eq $PolicyName) -and ($CurrentState.Enabled -eq $true) -and @@ -23,10 +68,14 @@ function Invoke-CIPPStandardAntiPhishPolicy { ($CurrentState.EnableUnusualCharactersSafetyTips -eq $Settings.EnableUnusualCharactersSafetyTips) -and ($CurrentState.EnableUnauthenticatedSender -eq $true) -and ($CurrentState.EnableViaTag -eq $true) -and + ($CurrentState.AuthenticationFailAction -eq $Settings.AuthenticationFailAction) -and + ($CurrentState.SpoofQuarantineTag -eq $Settings.SpoofQuarantineTag) -and ($CurrentState.MailboxIntelligenceProtectionAction -eq $Settings.MailboxIntelligenceProtectionAction) -and ($CurrentState.MailboxIntelligenceQuarantineTag -eq $Settings.MailboxIntelligenceQuarantineTag) -and ($CurrentState.TargetedUserProtectionAction -eq $Settings.TargetedUserProtectionAction) -and + ($CurrentState.TargetedUserQuarantineTag -eq $Settings.TargetedUserQuarantineTag) -and ($CurrentState.TargetedDomainProtectionAction -eq $Settings.TargetedDomainProtectionAction) -and + ($CurrentState.TargetedDomainQuarantineTag -eq $Settings.TargetedDomainQuarantineTag) -and ($CurrentState.EnableOrganizationDomainsProtection -eq $true) $AcceptedDomains = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-AcceptedDomain' @@ -56,10 +105,14 @@ function Invoke-CIPPStandardAntiPhishPolicy { EnableUnusualCharactersSafetyTips = $Settings.EnableUnusualCharactersSafetyTips EnableUnauthenticatedSender = $true EnableViaTag = $true + AuthenticationFailAction = $Settings.AuthenticationFailAction + SpoofQuarantineTag = $Settings.SpoofQuarantineTag MailboxIntelligenceProtectionAction = $Settings.MailboxIntelligenceProtectionAction MailboxIntelligenceQuarantineTag = $Settings.MailboxIntelligenceQuarantineTag TargetedUserProtectionAction = $Settings.TargetedUserProtectionAction + TargetedUserQuarantineTag = $Settings.TargetedUserQuarantineTag TargetedDomainProtectionAction = $Settings.TargetedDomainProtectionAction + TargetedDomainQuarantineTag = $Settings.TargetedDomainQuarantineTag EnableOrganizationDomainsProtection = $true } @@ -127,3 +180,7 @@ function Invoke-CIPPStandardAntiPhishPolicy { } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAppDeploy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAppDeploy.ps1 new file mode 100644 index 000000000000..0b5ba7f47945 --- /dev/null +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAppDeploy.ps1 @@ -0,0 +1,46 @@ +function Invoke-CIPPStandardAppDeploy { + <# + .FUNCTIONALITY + Internal + .COMPONENT + (APIName) AppDeploy + .SYNOPSIS + Deploy Application + .DESCRIPTION + (Helptext) Deploys selected applications to the tenant. Use a comma separated list of application IDs to deploy multiple applications. Permissions will be copied from the source application. + (DocsDescription) Uses the CIPP functionality that deploys applications across an entire tenant base as a standard. + .NOTES + CAT + Entra (AAD) Standards + TAG + "lowimpact" + ADDEDCOMPONENT + {"type":"input","name":"standards.AppDeploy.appids","label":"Application IDs, comma separated"} + LABEL + Deploy Application + IMPACT + Low Impact + POWERSHELLEQUIVALENT + Portal or Graph API + RECOMMENDEDBY + UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + .LINK + https://docs.cipp.app/user-documentation/tenant/standards/edit-standards + #> + + param($Tenant, $Settings) + + If ($Settings.remediate -eq $true) { + $AppsToAdd = $Settings.appids -split ',' + foreach ($App In $AppsToAdd) { + try { + New-CIPPApplicationCopy -App $App -Tenant $Tenant + Write-LogMessage -API 'Standards' -tenant $tenant -message "Added $App to $Tenant and update it's permissions" -sev Info + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to add app $App. Error: $ErrorMessage" -sev Error + } + } + } +} diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAtpPolicyForO365.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAtpPolicyForO365.ps1 index 4538eef5aed7..3d09454aaaf1 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAtpPolicyForO365.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAtpPolicyForO365.ps1 @@ -1,8 +1,35 @@ function Invoke-CIPPStandardAtpPolicyForO365 { - <# - .FUNCTIONALITY - Internal - #> + <# + .FUNCTIONALITY + Internal + .APINAME + AtpPolicyForO365 + .CAT + Defender Standards + .TAG + "lowimpact" + "CIS" + .HELPTEXT + This creates a Atp policy that enables Defender for Office 365 for Sharepoint, OneDrive and Microsoft Teams. + .ADDEDCOMPONENT + {"type":"boolean","label":"Allow people to click through Protected View even if Safe Documents identified the file as malicious","name":"standards.AtpPolicyForO365.AllowSafeDocsOpen","default":false} + .LABEL + Default Atp Policy For O365 + .IMPACT + Low Impact + .POWERSHELLEQUIVALENT + Set-AtpPolicyForO365 + .RECOMMENDEDBY + "CIS" + .DOCSDESCRIPTION + This creates a Atp policy that enables Defender for Office 365 for Sharepoint, OneDrive and Microsoft Teams. + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + #> + + + + param($Tenant, $Settings) $CurrentState = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-AtpPolicyForO365' | @@ -46,3 +73,7 @@ function Invoke-CIPPStandardAtpPolicyForO365 { } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAuditLog.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAuditLog.ps1 index acb6bf9834a1..17d7c440b840 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAuditLog.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAuditLog.ps1 @@ -2,7 +2,34 @@ function Invoke-CIPPStandardAuditLog { <# .FUNCTIONALITY Internal + .APINAME + AuditLog + .CAT + Global Standards + .TAG + "lowimpact" + "CIS" + "mip_search_auditlog" + .HELPTEXT + Enables the Unified Audit Log for tracking and auditing activities. Also runs Enable-OrganizationCustomization if necessary. + .ADDEDCOMPONENT + .LABEL + Enable the Unified Audit Log + .IMPACT + Low Impact + .POWERSHELLEQUIVALENT + Enable-OrganizationCustomization + .RECOMMENDEDBY + "CIS" + .DOCSDESCRIPTION + Enables the Unified Audit Log for tracking and auditing activities. Also runs Enable-OrganizationCustomization if necessary. + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) Write-Host ($Settings | ConvertTo-Json) $AuditLogEnabled = (New-ExoRequest -tenantid $Tenant -cmdlet 'Get-AdminAuditLogConfig' -Select UnifiedAuditLogIngestionEnabled).UnifiedAuditLogIngestionEnabled @@ -48,3 +75,7 @@ function Invoke-CIPPStandardAuditLog { Add-CIPPBPAField -FieldName 'AuditLog' -FieldValue $AuditLogEnabled -StoreAs bool -Tenant $tenant } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutoExpandArchive.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutoExpandArchive.ps1 index 53d29e442822..432923a068d1 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutoExpandArchive.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutoExpandArchive.ps1 @@ -2,7 +2,33 @@ function Invoke-CIPPStandardAutoExpandArchive { <# .FUNCTIONALITY Internal + .APINAME + AutoExpandArchive + .CAT + Exchange Standards + .TAG + "lowimpact" + .HELPTEXT + Enables auto-expanding archives for the tenant + .DOCSDESCRIPTION + Enables auto-expanding archives for the tenant. Does not enable archives for users. + .ADDEDCOMPONENT + .LABEL + Enable Auto-expanding archives + .IMPACT + Low Impact + .POWERSHELLEQUIVALENT + Set-OrganizationConfig -AutoExpandingArchive + .RECOMMENDEDBY + .DOCSDESCRIPTION + Enables auto-expanding archives for the tenant + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $CurrentState = (New-ExoRequest -tenantid $Tenant -cmdlet 'Get-OrganizationConfig').AutoExpandingArchiveEnabled @@ -36,3 +62,7 @@ function Invoke-CIPPStandardAutoExpandArchive { Add-CIPPBPAField -FieldName 'AutoExpandingArchive' -FieldValue $CurrentState -StoreAs bool -Tenant $tenant } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardBookings.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardBookings.ps1 index 406f0c06bd84..b42cf95556c3 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardBookings.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardBookings.ps1 @@ -2,7 +2,34 @@ function Invoke-CIPPStandardBookings { <# .FUNCTIONALITY Internal + .APINAME + Bookings + .CAT + Exchange Standards + .TAG + "mediumimpact" + .HELPTEXT + Sets the state of Bookings on the tenant. Bookings is a scheduling tool that allows users to book appointments with others both internal and external. + .DOCSDESCRIPTION + Sets the state of Bookings on the tenant. Bookings is a scheduling tool that allows users to book appointments with others both internal and external. + .ADDEDCOMPONENT + {"type":"Select","label":"Select value","name":"standards.Bookings.state","values":[{"label":"Enabled","value":"true"},{"label":"Disabled","value":"false"}]} + .LABEL + Set Bookings state + .IMPACT + Medium Impact + .POWERSHELLEQUIVALENT + Set-OrganizationConfig -BookingsEnabled + .RECOMMENDEDBY + .DOCSDESCRIPTION + Sets the state of Bookings on the tenant. Bookings is a scheduling tool that allows users to book appointments with others both internal and external. + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $CurrentState = (New-ExoRequest -tenantid $Tenant -cmdlet 'Get-OrganizationConfig').BookingsEnabled @@ -47,3 +74,7 @@ function Invoke-CIPPStandardBookings { } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardBranding.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardBranding.ps1 index 2f3841588d28..bd53e1c635e7 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardBranding.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardBranding.ps1 @@ -2,8 +2,38 @@ function Invoke-CIPPStandardBranding { <# .FUNCTIONALITY Internal + .APINAME + Branding + .CAT + Global Standards + .TAG + "lowimpact" + .HELPTEXT + Sets the branding for the tenant. This includes the login page, and the Office 365 portal. + .ADDEDCOMPONENT + {"type":"input","name":"standards.Branding.signInPageText","label":"Sign-in page text"} + {"type":"input","name":"standards.Branding.usernameHintText","label":"Username hint Text"} + {"type":"boolean","name":"standards.Branding.hideAccountResetCredentials","label":"Hide self-service password reset"} + {"type":"Select","label":"Visual Template","name":"standards.Branding.layoutTemplateType","values":[{"label":"Full-screen background","value":"default"},{"label":"Parial-screen background","value":"verticalSplit"}]} + {"type":"boolean","name":"standards.Branding.isHeaderShown","label":"Show header"} + {"type":"boolean","name":"standards.Branding.isFooterShown","label":"Show footer"} + .LABEL + Set branding for the tenant + .IMPACT + Low Impact + .POWERSHELLEQUIVALENT + Portal only + .RECOMMENDEDBY + .DOCSDESCRIPTION + Sets the branding for the tenant. This includes the login page, and the Office 365 portal. + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $TenantId = Get-Tenants | Where-Object -Property defaultDomainName -EQ $Tenant @@ -68,3 +98,7 @@ function Invoke-CIPPStandardBranding { Add-CIPPBPAField -FieldName 'Branding' -FieldValue [bool]$StateIsCorrect -StoreAs bool -Tenant $Tenant } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardCloudMessageRecall.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardCloudMessageRecall.ps1 index 19c6926d5af4..0c2fcedfcca8 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardCloudMessageRecall.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardCloudMessageRecall.ps1 @@ -2,7 +2,34 @@ function Invoke-CIPPStandardCloudMessageRecall { <# .FUNCTIONALITY Internal + .APINAME + CloudMessageRecall + .CAT + Exchange Standards + .TAG + "lowimpact" + .HELPTEXT + Sets the Cloud Message Recall state for the tenant. This allows users to recall messages from the cloud. + .DOCSDESCRIPTION + Sets the default state for Cloud Message Recall for the tenant. By default this is enabled. You can read more about the feature [here.](https://techcommunity.microsoft.com/t5/exchange-team-blog/cloud-based-message-recall-in-exchange-online/ba-p/3744714) + .ADDEDCOMPONENT + {"type":"Select","label":"Select value","name":"standards.CloudMessageRecall.state","values":[{"label":"Enabled","value":"true"},{"label":"Disabled","value":"false"}]} + .LABEL + Set Cloud Message Recall state + .IMPACT + Low Impact + .POWERSHELLEQUIVALENT + Set-OrganizationConfig -MessageRecallEnabled + .RECOMMENDEDBY + .DOCSDESCRIPTION + Sets the Cloud Message Recall state for the tenant. This allows users to recall messages from the cloud. + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $CurrentState = (New-ExoRequest -tenantid $Tenant -cmdlet 'Get-OrganizationConfig').MessageRecallEnabled @@ -48,3 +75,7 @@ function Invoke-CIPPStandardCloudMessageRecall { } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDelegateSentItems.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDelegateSentItems.ps1 index 81cbdc6e5167..819ba429fa25 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDelegateSentItems.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDelegateSentItems.ps1 @@ -2,7 +2,33 @@ function Invoke-CIPPStandardDelegateSentItems { <# .FUNCTIONALITY Internal + .APINAME + DelegateSentItems + .CAT + Exchange Standards + .TAG + "mediumimpact" + .HELPTEXT + Sets emails sent as and on behalf of shared mailboxes to also be stored in the shared mailbox sent items folder + .DOCSDESCRIPTION + This makes sure that e-mails sent from shared mailboxes or delegate mailboxes, end up in the mailbox of the shared/delegate mailbox instead of the sender, allowing you to keep replies in the same mailbox as the original e-mail. + .ADDEDCOMPONENT + .LABEL + Set mailbox Sent Items delegation (Sent items for shared mailboxes) + .IMPACT + Medium Impact + .POWERSHELLEQUIVALENT + Set-Mailbox + .RECOMMENDEDBY + .DOCSDESCRIPTION + Sets emails sent as and on behalf of shared mailboxes to also be stored in the shared mailbox sent items folder + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $Mailboxes = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-Mailbox' -cmdParams @{ RecipientTypeDetails = @('UserMailbox', 'SharedMailbox') } | Where-Object { $_.MessageCopyForSendOnBehalfEnabled -eq $false -or $_.MessageCopyForSentAsEnabled -eq $false } @@ -51,3 +77,7 @@ function Invoke-CIPPStandardDelegateSentItems { Add-CIPPBPAField -FieldName 'DelegateSentItems' -FieldValue $Filtered -StoreAs json -Tenant $tenant } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeletedUserRentention.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeletedUserRentention.ps1 index 157bf5fbf690..012a8555188e 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeletedUserRentention.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeletedUserRentention.ps1 @@ -2,40 +2,83 @@ function Invoke-CIPPStandardDeletedUserRentention { <# .FUNCTIONALITY Internal + .APINAME + DeletedUserRentention + .CAT + SharePoint Standards + .TAG + "lowimpact" + .HELPTEXT + Sets the retention period for deleted users OneDrive to 1 year/365 days + .DOCSDESCRIPTION + When a OneDrive user gets deleted, the personal SharePoint site is saved for 1 year and data can be retrieved from it. + .ADDEDCOMPONENT + .LABEL + Retain a deleted user OneDrive for 1 year + .IMPACT + Low Impact + .POWERSHELLEQUIVALENT + Update-MgBetaAdminSharepointSetting + .RECOMMENDEDBY + .DOCSDESCRIPTION + Sets the retention period for deleted users OneDrive to 1 year/365 days + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + param($Tenant, $Settings) $CurrentInfo = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/admin/sharepoint/settings' -tenantid $Tenant -AsApp $true - $StateSetCorrectly = if ($CurrentInfo.deletedUserPersonalSiteRetentionPeriodInDays -eq 365) { $true } else { $false } + + if ($Settings.report -eq $true) { + Add-CIPPBPAField -FieldName 'DeletedUserRentention' -FieldValue $CurrentInfo.deletedUserPersonalSiteRetentionPeriodInDays -StoreAs string -Tenant $tenant + } + + # Input validation + if (($Settings.Days -eq 'Select a value') -and ($Settings.remediate -eq $true -or $Settings.alert -eq $true)) { + Write-LogMessage -API 'Standards' -tenant $tenant -message 'DeletedUserRententio: Invalid Days parameter set' -sev Error + Return + } + + # Backwards compatibility for pre v5.10.0 + if ($null -eq $Settings.Days) { + $WantedState = 365 + } else { + $WantedState = [int]$Settings.Days + } + + $StateSetCorrectly = if ($CurrentInfo.deletedUserPersonalSiteRetentionPeriodInDays -eq $WantedState) { $true } else { $false } + $RetentionInYears = $WantedState / 365 If ($Settings.remediate -eq $true) { Write-Host 'Time to remediate' if ($StateSetCorrectly -eq $false) { try { - $body = '{"deletedUserPersonalSiteRetentionPeriodInDays": 365}' + $body = [PSCustomObject]@{ + deletedUserPersonalSiteRetentionPeriodInDays = $Settings.Days + } + $body = ConvertTo-Json -InputObject $body -Depth 5 -Compress New-GraphPostRequest -tenantid $tenant -Uri 'https://graph.microsoft.com/beta/admin/sharepoint/settings' -AsApp $true -Type PATCH -Body $body -ContentType 'application/json' - Write-LogMessage -API 'Standards' -tenant $tenant -message 'Set deleted user rentention of OneDrive to 1 year' -sev Info + Write-LogMessage -API 'Standards' -tenant $tenant -message "Set deleted user rentention of OneDrive to $RetentionInYears year(s)" -sev Info } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to set deleted user rentention of OneDrive to 1 year. Error: $ErrorMessage" -sev Error + Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to set deleted user rentention of OneDrive to $RetentionInYears year(s). Error: $ErrorMessage" -sev Error } } else { - Write-LogMessage -API 'Standards' -tenant $tenant -message 'Deleted user rentention of OneDrive is already set to 1 year' -sev Info - + Write-LogMessage -API 'Standards' -tenant $tenant -message "Deleted user rentention of OneDrive is already set to $RetentionInYears year(s)" -sev Info } } if ($Settings.alert -eq $true) { - if ($StateSetCorrectly) { - Write-LogMessage -API 'Standards' -tenant $tenant -message 'Deleted user rentention of OneDrive is set to 1 year' -sev Info + if ($StateSetCorrectly -eq $true) { + Write-LogMessage -API 'Standards' -tenant $tenant -message "Deleted user rentention of OneDrive is set to $RetentionInYears year(s)" -sev Info } else { - Write-LogMessage -API 'Standards' -tenant $tenant -message 'Deleted user rentention of OneDrive is not set to 1 year' -sev Alert + Write-LogMessage -API 'Standards' -tenant $tenant -message "Deleted user rentention of OneDrive is not set to $RetentionInYears year(s). Value is: $($CurrentInfo.deletedUserPersonalSiteRetentionPeriodInDays) " -sev Alert } } +} + + - if ($Settings.report -eq $true) { - Add-CIPPBPAField -FieldName 'DeletedUserRentention' -FieldValue $StateSetCorrectly -StoreAs bool -Tenant $tenant - } -} diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableAddShortcutsToOneDrive.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableAddShortcutsToOneDrive.ps1 index 485fe370c59d..1cfb91402bca 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableAddShortcutsToOneDrive.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableAddShortcutsToOneDrive.ps1 @@ -2,7 +2,33 @@ function Invoke-CIPPStandardDisableAddShortcutsToOneDrive { <# .FUNCTIONALITY Internal + .APINAME + DisableAddShortcutsToOneDrive + .CAT + SharePoint Standards + .TAG + "mediumimpact" + .HELPTEXT + When the feature is disabled the option Add shortcut to OneDrive will be removed. Any folders that have already been added will remain on the user's computer. + .DISABLEDFEATURES + + .ADDEDCOMPONENT + .LABEL + Disable Add Shortcuts To OneDrive + .IMPACT + Medium Impact + .POWERSHELLEQUIVALENT + Graph API or Portal + .RECOMMENDEDBY + .DOCSDESCRIPTION + When the feature is disabled the option Add shortcut to OneDrive will be removed. Any folders that have already been added will remain on the user's computer. + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) If ($Settings.remediate -eq $true) { @@ -95,3 +121,7 @@ function Invoke-CIPPStandardDisableAddShortcutsToOneDrive { Write-LogMessage @log } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableAdditionalStorageProviders.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableAdditionalStorageProviders.ps1 index 6612d7090240..242a4fa64d3c 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableAdditionalStorageProviders.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableAdditionalStorageProviders.ps1 @@ -2,7 +2,36 @@ function Invoke-CIPPStandardDisableAdditionalStorageProviders { <# .FUNCTIONALITY Internal + .APINAME + DisableAdditionalStorageProviders + .CAT + Exchange Standards + .TAG + "lowimpact" + "CIS" + "exo_storageproviderrestricted" + .HELPTEXT + Disables the ability for users to open files in Outlook on the Web, from other providers such as Box, Dropbox, Facebook, Google Drive, OneDrive Personal, etc. + .DOCSDESCRIPTION + Disables additional storage providers in OWA. This is to prevent users from using personal storage providers like Dropbox, Google Drive, etc. Usually this has little user impact. + .ADDEDCOMPONENT + .LABEL + Disable additional storage providers in OWA + .IMPACT + Low Impact + .POWERSHELLEQUIVALENT + Get-OwaMailboxPolicy | Set-OwaMailboxPolicy -AdditionalStorageProvidersEnabled $False + .RECOMMENDEDBY + "CIS" + .DOCSDESCRIPTION + Disables the ability for users to open files in Outlook on the Web, from other providers such as Box, Dropbox, Facebook, Google Drive, OneDrive Personal, etc. + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $AdditionalStorageProvidersState = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-OwaMailboxPolicy' -cmdParams @{Identity = 'OwaMailboxPolicy-Default' } @@ -35,3 +64,7 @@ function Invoke-CIPPStandardDisableAdditionalStorageProviders { Add-CIPPBPAField -FieldName 'AdditionalStorageProvidersEnabled' -FieldValue $AdditionalStorageProvidersState.AdditionalStorageProvidersEnabled -StoreAs bool -Tenant $tenant } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableAppCreation.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableAppCreation.ps1 index 7204971fae4e..624f7d20f1f1 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableAppCreation.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableAppCreation.ps1 @@ -2,7 +2,35 @@ function Invoke-CIPPStandardDisableAppCreation { <# .FUNCTIONALITY Internal + .APINAME + DisableAppCreation + .CAT + Entra (AAD) Standards + .TAG + "lowimpact" + "CIS" + .HELPTEXT + Disables the ability for users to create App registrations in the tenant. + .DOCSDESCRIPTION + Disables the ability for users to create applications in Entra. Done to prevent breached accounts from creating an app to maintain access to the tenant, even after the breached account has been secured. + .ADDEDCOMPONENT + .LABEL + Disable App creation by users + .IMPACT + Low Impact + .POWERSHELLEQUIVALENT + Update-MgPolicyAuthorizationPolicy + .RECOMMENDEDBY + "CIS" + .DOCSDESCRIPTION + Disables the ability for users to create App registrations in the tenant. + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $CurrentInfo = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/policies/authorizationPolicy/authorizationPolicy?$select=defaultUserRolePermissions' -tenantid $Tenant @@ -36,3 +64,7 @@ function Invoke-CIPPStandardDisableAppCreation { Add-CIPPBPAField -FieldName 'UserAppCreationDisabled' -FieldValue $State -StoreAs bool -Tenant $tenant } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 index ed8a7b256ff7..ea9a2c14fc1d 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 @@ -2,7 +2,33 @@ function Invoke-CIPPStandardDisableBasicAuthSMTP { <# .FUNCTIONALITY Internal + .APINAME + DisableBasicAuthSMTP + .CAT + Global Standards + .TAG + "mediumimpact" + .HELPTEXT + Disables SMTP AUTH for the organization and all users. This is the default for new tenants. + .DOCSDESCRIPTION + Disables SMTP basic authentication for the tenant and all users with it explicitly enabled. + .ADDEDCOMPONENT + .LABEL + Disable SMTP Basic Authentication + .IMPACT + Medium Impact + .POWERSHELLEQUIVALENT + Set-TransportConfig -SmtpClientAuthenticationDisabled $true + .RECOMMENDEDBY + .DOCSDESCRIPTION + Disables SMTP AUTH for the organization and all users. This is the default for new tenants. + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $CurrentInfo = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-TransportConfig' $SMTPusers = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-CASMailbox' -cmdParams @{ ResultSize = 'Unlimited' } | Where-Object { ($_.SmtpClientAuthenticationDisabled -eq $false) } @@ -35,18 +61,19 @@ function Invoke-CIPPStandardDisableBasicAuthSMTP { } } + $LogMessage = [System.Collections.Generic.List[string]]::new() if ($Settings.alert -eq $true -or $Settings.report -eq $true) { # Build the log message for use in the alert and report if ($CurrentInfo.SmtpClientAuthenticationDisabled) { - $LogMessage = 'SMTP Basic Authentication for tenant is disabled. ' + $LogMessage.add('SMTP Basic Authentication for tenant is disabled. ') } else { - $LogMessage = 'SMTP Basic Authentication for tenant is not disabled. ' + $LogMessage.add('SMTP Basic Authentication for tenant is not disabled. ') } if ($SMTPusers.Count -eq 0) { - $LogMessage += 'SMTP Basic Authentication for all users is disabled' + $LogMessage.add('SMTP Basic Authentication for all users is disabled') } else { - $LogMessage += "SMTP Basic Authentication for the following $($SMTPusers.Count) users is not disabled: $($SMTPusers.PrimarySmtpAddress -join ',')" + $LogMessage.add("SMTP Basic Authentication for the following $($SMTPusers.Count) users is not disabled: $($SMTPusers.PrimarySmtpAddress -join ',')") } if ($Settings.alert -eq $true) { @@ -68,3 +95,7 @@ function Invoke-CIPPStandardDisableBasicAuthSMTP { } } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableEmail.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableEmail.ps1 index 43eb1f36db3a..09b6fa9ca8d3 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableEmail.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableEmail.ps1 @@ -2,7 +2,31 @@ function Invoke-CIPPStandardDisableEmail { <# .FUNCTIONALITY Internal + .APINAME + DisableEmail + .CAT + Entra (AAD) Standards + .TAG + "highimpact" + .HELPTEXT + This blocks users from using email as an MFA method. This disables the email OTP option for guest users, and instead promts them to create a Microsoft account. + .ADDEDCOMPONENT + .LABEL + Disables Email as an MFA method + .IMPACT + High Impact + .POWERSHELLEQUIVALENT + Update-MgBetaPolicyAuthenticationMethodPolicyAuthenticationMethodConfiguration + .RECOMMENDEDBY + .DOCSDESCRIPTION + This blocks users from using email as an MFA method. This disables the email OTP option for guest users, and instead promts them to create a Microsoft account. + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $CurrentInfo = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/policies/authenticationmethodspolicy/authenticationMethodConfigurations/Email' -tenantid $Tenant $State = if ($CurrentInfo.state -eq 'enabled') { $true } else { $false } @@ -27,3 +51,7 @@ function Invoke-CIPPStandardDisableEmail { Add-CIPPBPAField -FieldName 'DisableEmail' -FieldValue $State -StoreAs bool -Tenant $tenant } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableExternalCalendarSharing.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableExternalCalendarSharing.ps1 index 2393e7c3994d..1646b7b36dc1 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableExternalCalendarSharing.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableExternalCalendarSharing.ps1 @@ -2,7 +2,36 @@ function Invoke-CIPPStandardDisableExternalCalendarSharing { <# .FUNCTIONALITY Internal + .APINAME + DisableExternalCalendarSharing + .CAT + Exchange Standards + .TAG + "lowimpact" + "CIS" + "exo_individualsharing" + .HELPTEXT + Disables the ability for users to share their calendar with external users. Only for the default policy, so exclusions can be made if needed. + .DOCSDESCRIPTION + Disables external calendar sharing for the entire tenant. This is not a widely used feature, and it's therefore unlikely that this will impact users. Only for the default policy, so exclusions can be made if needed by making a new policy and assigning it to users. + .ADDEDCOMPONENT + .LABEL + Disable external calendar sharing + .IMPACT + Low Impact + .POWERSHELLEQUIVALENT + Get-SharingPolicy | Set-SharingPolicy -Enabled $False + .RECOMMENDEDBY + "CIS" + .DOCSDESCRIPTION + Disables the ability for users to share their calendar with external users. Only for the default policy, so exclusions can be made if needed. + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $CurrentInfo = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-SharingPolicy' | Where-Object { $_.Default -eq $true } @@ -37,3 +66,7 @@ function Invoke-CIPPStandardDisableExternalCalendarSharing { Add-CIPPBPAField -FieldName 'ExternalCalendarSharingDisabled' -FieldValue $CurrentInfo.Enabled -StoreAs bool -Tenant $tenant } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableGuestDirectory.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableGuestDirectory.ps1 index 8fbcb35110d5..62b17aef5b46 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableGuestDirectory.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableGuestDirectory.ps1 @@ -2,7 +2,33 @@ function Invoke-CIPPStandardDisableGuestDirectory { <# .FUNCTIONALITY Internal + .APINAME + DisableGuestDirectory + .CAT + Global Standards + .TAG + "lowimpact" + .HELPTEXT + Disables Guest access to enumerate directory objects. This prevents guest users from seeing other users or guests in the directory. + .DOCSDESCRIPTION + Sets it so guests can view only their own user profile. Permission to view other users isn't allowed. Also restricts guest users from seeing the membership of groups they're in. See exactly what get locked down in the [Microsoft documentation.](https://learn.microsoft.com/en-us/entra/fundamentals/users-default-permissions) + .ADDEDCOMPONENT + .LABEL + Restrict guest user access to directory objects + .IMPACT + Low Impact + .POWERSHELLEQUIVALENT + Set-AzureADMSAuthorizationPolicy -GuestUserRoleId '2af84b1e-32c8-42b7-82bc-daa82404023b' + .RECOMMENDEDBY + .DOCSDESCRIPTION + Disables Guest access to enumerate directory objects. This prevents guest users from seeing other users or guests in the directory. + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $CurrentInfo = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/policies/authorizationPolicy/authorizationPolicy' -tenantid $Tenant @@ -36,3 +62,7 @@ function Invoke-CIPPStandardDisableGuestDirectory { Add-CIPPBPAField -FieldName 'DisableGuestDirectory' -FieldValue $CurrentInfo.guestUserRoleId -StoreAs bool -Tenant $tenant } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableGuests.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableGuests.ps1 index 7b13fffd147f..e654cd9b5dc7 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableGuests.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableGuests.ps1 @@ -2,7 +2,31 @@ function Invoke-CIPPStandardDisableGuests { <# .FUNCTIONALITY Internal + .APINAME + DisableGuests + .CAT + Entra (AAD) Standards + .TAG + "mediumimpact" + .HELPTEXT + Blocks login for guest users that have not logged in for 90 days + .ADDEDCOMPONENT + .LABEL + Disable Guest accounts that have not logged on for 90 days + .IMPACT + Medium Impact + .POWERSHELLEQUIVALENT + Graph API + .RECOMMENDEDBY + .DOCSDESCRIPTION + Blocks login for guest users that have not logged in for 90 days + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $Lookup = (Get-Date).AddDays(-90).ToUniversalTime().ToString('o') $GraphRequest = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users?`$filter=(signInActivity/lastNonInteractiveSignInDateTime le $Lookup)&`$select=id,UserPrincipalName,signInActivity,mail,userType,accountEnabled" -scope 'https://graph.microsoft.com/.default' -tenantid $Tenant | Where-Object { $_.userType -EQ 'Guest' -and $_.AccountEnabled -EQ $true } @@ -37,3 +61,7 @@ function Invoke-CIPPStandardDisableGuests { Add-CIPPBPAField -FieldName 'DisableGuests' -FieldValue $filtered -StoreAs json -Tenant $tenant } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableM365GroupUsers.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableM365GroupUsers.ps1 index a173eaba6759..558ce91bf77f 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableM365GroupUsers.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableM365GroupUsers.ps1 @@ -2,7 +2,33 @@ function Invoke-CIPPStandardDisableM365GroupUsers { <# .FUNCTIONALITY Internal + .APINAME + DisableM365GroupUsers + .CAT + Entra (AAD) Standards + .TAG + "lowimpact" + .HELPTEXT + Restricts M365 group creation to certain admin roles. This disables the ability to create Teams, Sharepoint sites, Planner, etc + .DOCSDESCRIPTION + Users by default are allowed to create M365 groups. This restricts M365 group creation to certain admin roles. This disables the ability to create Teams, SharePoint sites, Planner, etc + .ADDEDCOMPONENT + .LABEL + Disable M365 Group creation by users + .IMPACT + Low Impact + .POWERSHELLEQUIVALENT + Update-MgBetaDirectorySetting + .RECOMMENDEDBY + .DOCSDESCRIPTION + Restricts M365 group creation to certain admin roles. This disables the ability to create Teams, Sharepoint sites, Planner, etc + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $CurrentState = (New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/settings' -tenantid $tenant) | Where-Object -Property displayname -EQ 'Group.unified' @@ -53,3 +79,7 @@ function Invoke-CIPPStandardDisableM365GroupUsers { } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableOutlookAddins.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableOutlookAddins.ps1 index 230920781a6a..39b2b91a764e 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableOutlookAddins.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableOutlookAddins.ps1 @@ -2,7 +2,36 @@ function Invoke-CIPPStandardDisableOutlookAddins { <# .FUNCTIONALITY Internal + .APINAME + DisableOutlookAddins + .CAT + Exchange Standards + .TAG + "mediumimpact" + "CIS" + "exo_outlookaddins" + .HELPTEXT + Disables the ability for users to install add-ins in Outlook. This is to prevent users from installing malicious add-ins. + .DOCSDESCRIPTION + Disables users from being able to install add-ins in Outlook. Only admins are able to approve add-ins for the users. This is done to reduce the threat surface for data exfiltration. + .ADDEDCOMPONENT + .LABEL + Disable users from installing add-ins in Outlook + .IMPACT + Medium Impact + .POWERSHELLEQUIVALENT + Get-ManagementRoleAssignment | Remove-ManagementRoleAssignment + .RECOMMENDEDBY + "CIS" + .DOCSDESCRIPTION + Disables the ability for users to install add-ins in Outlook. This is to prevent users from installing malicious add-ins. + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $CurrentInfo = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-RoleAssignmentPolicy' | Where-Object { $_.IsDefault -eq $true } @@ -53,3 +82,7 @@ function Invoke-CIPPStandardDisableOutlookAddins { Add-CIPPBPAField -FieldName 'DisabledOutlookAddins' -FieldValue $State -StoreAs bool -Tenant $tenant } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableReshare.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableReshare.ps1 index 0893d8fda2d2..26db5c11c88a 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableReshare.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableReshare.ps1 @@ -2,7 +2,35 @@ function Invoke-CIPPStandardDisableReshare { <# .FUNCTIONALITY Internal + .APINAME + DisableReshare + .CAT + SharePoint Standards + .TAG + "highimpact" + "CIS" + .HELPTEXT + Disables the ability for external users to share files they don't own. Sharing links can only be made for People with existing access + .DOCSDESCRIPTION + Disables the ability for external users to share files they don't own. Sharing links can only be made for People with existing access. This is a tenant wide setting and overrules any settings set on the site level + .ADDEDCOMPONENT + .LABEL + Disable Resharing by External Users + .IMPACT + High Impact + .POWERSHELLEQUIVALENT + Update-MgBetaAdminSharepointSetting + .RECOMMENDEDBY + "CIS" + .DOCSDESCRIPTION + Disables the ability for external users to share files they don't own. Sharing links can only be made for People with existing access + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $CurrentInfo = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/admin/sharepoint/settings' -tenantid $Tenant -AsApp $true @@ -34,3 +62,7 @@ function Invoke-CIPPStandardDisableReshare { Add-CIPPBPAField -FieldName 'DisableReshare' -FieldValue $CurrentInfo.isResharingByExternalUsersEnabled -StoreAs bool -Tenant $tenant } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSMS.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSMS.ps1 index 43dd0198d1b3..30454df4cba6 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSMS.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSMS.ps1 @@ -2,7 +2,33 @@ function Invoke-CIPPStandardDisableSMS { <# .FUNCTIONALITY Internal + .APINAME + DisableSMS + .CAT + Entra (AAD) Standards + .TAG + "highimpact" + .HELPTEXT + This blocks users from using SMS as an MFA method. If a user only has SMS as a MFA method, they will be unable to log in. + .DOCSDESCRIPTION + Disables SMS as an MFA method for the tenant. If a user only has SMS as a MFA method, they will be unable to sign in. + .ADDEDCOMPONENT + .LABEL + Disables SMS as an MFA method + .IMPACT + High Impact + .POWERSHELLEQUIVALENT + Update-MgBetaPolicyAuthenticationMethodPolicyAuthenticationMethodConfiguration + .RECOMMENDEDBY + .DOCSDESCRIPTION + This blocks users from using SMS as an MFA method. If a user only has SMS as a MFA method, they will be unable to log in. + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $CurrentInfo = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/policies/authenticationmethodspolicy/authenticationMethodConfigurations/SMS' -tenantid $Tenant $State = if ($CurrentInfo.state -eq 'enabled') { $true } else { $false } @@ -27,3 +53,7 @@ function Invoke-CIPPStandardDisableSMS { Add-CIPPBPAField -FieldName 'DisableSMS' -FieldValue $State -StoreAs bool -Tenant $tenant } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSecurityGroupUsers.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSecurityGroupUsers.ps1 index edaf91dfde7d..df6a6d327447 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSecurityGroupUsers.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSecurityGroupUsers.ps1 @@ -2,7 +2,31 @@ function Invoke-CIPPStandardDisableSecurityGroupUsers { <# .FUNCTIONALITY Internal + .APINAME + DisableSecurityGroupUsers + .CAT + Entra (AAD) Standards + .TAG + "mediumimpact" + .HELPTEXT + Completely disables the creation of security groups by users. This also breaks the ability to manage groups themselves, or create Teams + .ADDEDCOMPONENT + .LABEL + Disable Security Group creation by users + .IMPACT + Medium Impact + .POWERSHELLEQUIVALENT + Update-MgBetaPolicyAuthorizationPolicy + .RECOMMENDEDBY + .DOCSDESCRIPTION + Completely disables the creation of security groups by users. This also breaks the ability to manage groups themselves, or create Teams + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $CurrentInfo = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/policies/authorizationPolicy/authorizationPolicy' -tenantid $Tenant @@ -35,3 +59,7 @@ function Invoke-CIPPStandardDisableSecurityGroupUsers { Add-CIPPBPAField -FieldName 'DisableSecurityGroupUsers' -FieldValue $CurrentInfo.defaultUserRolePermissions.allowedToCreateSecurityGroups -StoreAs bool -Tenant $tenant } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSelfServiceLicenses.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSelfServiceLicenses.ps1 index 6431f6053dc5..fa8b5cb537e2 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSelfServiceLicenses.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSelfServiceLicenses.ps1 @@ -2,9 +2,96 @@ function Invoke-CIPPStandardDisableSelfServiceLicenses { <# .FUNCTIONALITY Internal + .APINAME + DisableSelfServiceLicenses + .CAT + Entra (AAD) Standards + .TAG + "mediumimpact" + .HELPTEXT + This standard disables all self service licenses and enables all exclusions + .ADDEDCOMPONENT + .LABEL + Disable Self Service Licensing + .IMPACT + Medium Impact + .POWERSHELLEQUIVALENT + Update-MSCommerceProductPolicy -PolicyId AllowSelfServicePurchase -ProductId {productId} -Value "Disabled" + .RECOMMENDEDBY + .DOCSDESCRIPTION + This standard disables all self service licenses and enables all exclusions + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + param($Tenant, $Settings) - Write-LogMessage -API 'Standards' -tenant $tenant -message 'Self Service Licenses cannot be disabled' -sev Error + #Write-LogMessage -API 'Standards' -tenant $tenant -message 'Self Service Licenses cannot be disabled' -sev Error + try { + $selfServiceItems = (New-GraphGETRequest -scope "aeb86249-8ea3-49e2-900b-54cc8e308f85/.default" -uri "https://licensing.m365.microsoft.com/v1.0/policies/AllowSelfServicePurchase/products" -tenantid $Tenant).items + #$selfServiceItems = (Invoke-RestMethod -Method GET -Uri "https://licensing.m365.microsoft.com/v1.0/policies/AllowSelfServicePurchase/products" -Headers $header).items + } catch { + Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to retrieve self service products: $($_.Exception.Message)" -sev Error + throw "Failed to retrieve self service products: $($_.Exception.Message)" + } + + if ($settings.remediate) { + if ($settings.exclusions -like "*;*") { + $exclusions = $settings.Exclusions -split(';') + } else { + $exclusions = $settings.Exclusions -split(',') + } + + $selfServiceItems | ForEach-Object { + $body = $null + + if ($_.policyValue -eq "Enabled" -AND ($_.productId -in $exclusions)) { + # Self service is enabled on product and productId is in exclusions, skip + } + if ($_.policyValue -eq "Disabled" -AND ($_.productId -in $exclusions)) { + # Self service is disabled on product and productId is in exclusions, enable + $body = '{ "policyValue": "Enabled" }' + } + if ($_.policyValue -eq "Enabled" -AND ($_.productId -notin $exclusions)) { + # Self service is enabled on product and productId is NOT in exclusions, disable + $body = '{ "policyValue": "Disabled" }' + } + if ($_.policyValue -eq "Disabled" -AND ($_.productId -notin $exclusions)) { + # Self service is disabled on product and productId is NOT in exclusions, skip + } + + try { + if ($body) { + $product = $_ + New-GraphPOSTRequest -scope "aeb86249-8ea3-49e2-900b-54cc8e308f85/.default" -uri "https://licensing.m365.microsoft.com/v1.0/policies/AllowSelfServicePurchase/products/$($product.productId)" -tenantid $Tenant -body $body -type PUT + } + } catch { + Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to set product status for $($product.productId) with body $($body) for reason: $($_.Exception.Message)" -sev Error + #Write-Error "Failed to disable product $($product.productName):$($_.Exception.Message)" + } + } + + if (!$exclusions) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'No exclusions set for self-service licenses, disabled all not excluded licenses for self-service.' -sev Info + } else { + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Exclusions present for self-service licenses, disabled all not excluded licenses for self-service.' -sev Info + } + } + if ($Settings.alert) { + $selfServiceItemsToAlert = $selfServiceItems | Where-Object { $_.policyValue -eq "Enabled"} + if (!$selfServiceItemsToAlert) { + Write-LogMessage -API 'Standards' -tenant $tenant -message 'All self-service licenses are disabled' -sev Info + } else { + Write-LogMessage -API 'Standards' -tenant $tenant -message 'One or more self-service licenses are enabled' -sev Alert + } + } + + if ($Settings.report -eq $true) { + #Add-CIPPBPAField -FieldName '????' -FieldValue "????" -StoreAs bool -Tenant $tenant + } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSharePointLegacyAuth.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSharePointLegacyAuth.ps1 index 1e109b41a3aa..b562d10dbd3e 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSharePointLegacyAuth.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSharePointLegacyAuth.ps1 @@ -2,7 +2,35 @@ function Invoke-CIPPStandardDisableSharePointLegacyAuth { <# .FUNCTIONALITY Internal + .APINAME + DisableSharePointLegacyAuth + .CAT + SharePoint Standards + .TAG + "mediumimpact" + "CIS" + .HELPTEXT + Disables the ability to authenticate with SharePoint using legacy authentication methods. Any applications that use legacy authentication will need to be updated to use modern authentication. + .DOCSDESCRIPTION + Disables the ability for users and applications to access SharePoint via legacy basic authentication. This will likely not have any user impact, but will block systems/applications depending on basic auth or the SharePointOnlineCredentials class. + .ADDEDCOMPONENT + .LABEL + Disable legacy basic authentication for SharePoint + .IMPACT + Medium Impact + .POWERSHELLEQUIVALENT + Set-SPOTenant -LegacyAuthProtocolsEnabled $false + .RECOMMENDEDBY + "CIS" + .DOCSDESCRIPTION + Disables the ability to authenticate with SharePoint using legacy authentication methods. Any applications that use legacy authentication will need to be updated to use modern authentication. + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $CurrentInfo = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/admin/sharepoint/settings?$select=isLegacyAuthProtocolsEnabled' -tenantid $Tenant -AsApp $true @@ -36,3 +64,7 @@ function Invoke-CIPPStandardDisableSharePointLegacyAuth { Add-CIPPBPAField -FieldName 'SharePointLegacyAuthEnabled' -FieldValue $CurrentInfo.isLegacyAuthProtocolsEnabled -StoreAs bool -Tenant $tenant } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSharedMailbox.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSharedMailbox.ps1 index d9d3356eba00..c60ffd274664 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSharedMailbox.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSharedMailbox.ps1 @@ -2,7 +2,35 @@ function Invoke-CIPPStandardDisableSharedMailbox { <# .FUNCTIONALITY Internal + .APINAME + DisableSharedMailbox + .CAT + Exchange Standards + .TAG + "mediumimpact" + "CIS" + .HELPTEXT + Blocks login for all accounts that are marked as a shared mailbox. This is Microsoft best practice to prevent direct logons to shared mailboxes. + .DOCSDESCRIPTION + Shared mailboxes can be directly logged into if the password is reset, this presents a security risk as do all shared login credentials. Microsoft's recommendation is to disable the user account for shared mailboxes. It would be a good idea to review the sign-in reports to establish potential impact. + .ADDEDCOMPONENT + .LABEL + Disable Shared Mailbox AAD accounts + .IMPACT + Medium Impact + .POWERSHELLEQUIVALENT + Get-Mailbox & Update-MgUser + .RECOMMENDEDBY + "CIS" + .DOCSDESCRIPTION + Blocks login for all accounts that are marked as a shared mailbox. This is Microsoft best practice to prevent direct logons to shared mailboxes. + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $UserList = New-GraphGetRequest -uri 'https://graph.microsoft.com/v1.0/users?$top=999&$filter=accountEnabled eq true' -Tenantid $tenant -scope 'https://graph.microsoft.com/.default' $SharedMailboxList = (New-GraphGetRequest -uri "https://outlook.office365.com/adminapi/beta/$($Tenant)/Mailbox" -Tenantid $tenant -scope ExchangeOnline | Where-Object { $_.RecipientTypeDetails -EQ 'SharedMailbox' -or $_.RecipientTypeDetails -eq 'SchedulingMailbox' -and $_.UserPrincipalName -in $UserList.UserPrincipalName }) @@ -37,3 +65,7 @@ function Invoke-CIPPStandardDisableSharedMailbox { Add-CIPPBPAField -FieldName 'DisableSharedMailbox' -FieldValue $SharedMailboxList -StoreAs json -Tenant $tenant } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableTNEF.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableTNEF.ps1 index 022f21807864..1db22507ede0 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableTNEF.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableTNEF.ps1 @@ -2,8 +2,34 @@ function Invoke-CIPPStandardDisableTNEF { <# .FUNCTIONALITY Internal + .APINAME + DisableTNEF + .CAT + Exchange Standards + .TAG + "lowimpact" + .HELPTEXT + Disables Transport Neutral Encapsulation Format (TNEF)/winmail.dat for the tenant. TNEF can cause issues if the recipient is not using a client supporting TNEF. + .DOCSDESCRIPTION + Disables Transport Neutral Encapsulation Format (TNEF)/winmail.dat for the tenant. TNEF can cause issues if the recipient is not using a client supporting TNEF. Cannot be overridden by the user. For more information, see [Microsoft's documentation.](https://learn.microsoft.com/en-us/exchange/mail-flow/content-conversion/tnef-conversion?view=exchserver-2019) + .ADDEDCOMPONENT + .LABEL + Disable TNEF/winmail.dat + .IMPACT + Low Impact + .POWERSHELLEQUIVALENT + Set-RemoteDomain -Identity 'Default' -TNEFEnabled $false + .RECOMMENDEDBY + .DOCSDESCRIPTION + Disables Transport Neutral Encapsulation Format (TNEF)/winmail.dat for the tenant. TNEF can cause issues if the recipient is not using a client supporting TNEF. + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param ($Tenant, $Settings) $CurrentState = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-RemoteDomain' -cmdParams @{Identity = 'Default' } @@ -37,3 +63,7 @@ function Invoke-CIPPStandardDisableTNEF { } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableTenantCreation.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableTenantCreation.ps1 index 97616ca367ea..30eef38a9254 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableTenantCreation.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableTenantCreation.ps1 @@ -2,7 +2,35 @@ function Invoke-CIPPStandardDisableTenantCreation { <# .FUNCTIONALITY Internal + .APINAME + DisableTenantCreation + .CAT + Entra (AAD) Standards + .TAG + "lowimpact" + "CIS" + .HELPTEXT + Restricts creation of M365 tenants to the Global Administrator or Tenant Creator roles. + .DOCSDESCRIPTION + Users by default are allowed to create M365 tenants. This disables that so only admins can create new M365 tenants. + .ADDEDCOMPONENT + .LABEL + Disable M365 Tenant creation by users + .IMPACT + Low Impact + .POWERSHELLEQUIVALENT + Update-MgPolicyAuthorizationPolicy + .RECOMMENDEDBY + "CIS" + .DOCSDESCRIPTION + Restricts creation of M365 tenants to the Global Administrator or Tenant Creator roles. + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $CurrentInfo = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/policies/authorizationPolicy/authorizationPolicy' -tenantid $Tenant $State = $CurrentInfo.defaultUserRolePermissions.allowedToCreateTenants @@ -35,3 +63,7 @@ function Invoke-CIPPStandardDisableTenantCreation { Add-CIPPBPAField -FieldName 'DisableTenantCreation' -FieldValue $State -StoreAs bool -Tenant $tenant } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableUserSiteCreate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableUserSiteCreate.ps1 index 97bac09d7668..b47e17c3ccc6 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableUserSiteCreate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableUserSiteCreate.ps1 @@ -2,7 +2,33 @@ function Invoke-CIPPStandardDisableUserSiteCreate { <# .FUNCTIONALITY Internal + .APINAME + DisableUserSiteCreate + .CAT + SharePoint Standards + .TAG + "highimpact" + .HELPTEXT + Disables users from creating new SharePoint sites + .DOCSDESCRIPTION + Disables standard users from creating SharePoint sites, also disables the ability to fully create teams + .ADDEDCOMPONENT + .LABEL + Disable site creation by standard users + .IMPACT + High Impact + .POWERSHELLEQUIVALENT + Update-MgAdminSharepointSetting + .RECOMMENDEDBY + .DOCSDESCRIPTION + Disables users from creating new SharePoint sites + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $CurrentInfo = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/admin/sharepoint/settings' -tenantid $Tenant -AsApp $true @@ -36,3 +62,7 @@ function Invoke-CIPPStandardDisableUserSiteCreate { Add-CIPPBPAField -FieldName 'DisableUserSiteCreate' -FieldValue $CurrentInfo.isSiteCreationEnabled -StoreAs bool -Tenant $tenant } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableViva.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableViva.ps1 index 2a87da3fef09..efc8ef960fa4 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableViva.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableViva.ps1 @@ -2,7 +2,33 @@ function Invoke-CIPPStandardDisableViva { <# .FUNCTIONALITY Internal + .APINAME + DisableViva + .CAT + Exchange Standards + .TAG + "lowimpact" + .HELPTEXT + Disables the daily viva reports for all users. + .DOCSDESCRIPTION + Disables the daily viva reports for all users. + .ADDEDCOMPONENT + .LABEL + Disable daily Insight/Viva reports + .IMPACT + Low Impact + .POWERSHELLEQUIVALENT + Set-UserBriefingConfig + .RECOMMENDEDBY + .DOCSDESCRIPTION + Disables the daily viva reports for all users. + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) try { @@ -45,3 +71,7 @@ function Invoke-CIPPStandardDisableViva { } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableVoice.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableVoice.ps1 index 0c064013b444..7d8fc7b30d80 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableVoice.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableVoice.ps1 @@ -2,7 +2,30 @@ function Invoke-CIPPStandardDisableVoice { <# .FUNCTIONALITY Internal + .APINAME + DisableVoice + .CAT + Entra (AAD) Standards + .TAG + "highimpact" + .HELPTEXT + This blocks users from using Voice call as an MFA method. If a user only has Voice as a MFA method, they will be unable to log in. + .DOCSDESCRIPTION + Disables Voice call as an MFA method for the tenant. If a user only has Voice call as a MFA method, they will be unable to sign in. + .ADDEDCOMPONENT + .LABEL + Disables Voice call as an MFA method + .IMPACT + High Impact + .DOCSDESCRIPTION + This blocks users from using Voice call as an MFA method. If a user only has Voice as a MFA method, they will be unable to log in. + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $CurrentInfo = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/policies/authenticationmethodspolicy/authenticationMethodConfigurations/Voice' -tenantid $Tenant $State = if ($CurrentInfo.state -eq 'enabled') { $true } else { $false } @@ -27,3 +50,7 @@ function Invoke-CIPPStandardDisableVoice { Add-CIPPBPAField -FieldName 'DisableVoice' -FieldValue $State -StoreAs bool -Tenant $tenant } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisablex509Certificate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisablex509Certificate.ps1 index d59042f1f6c8..b79fa98643c2 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisablex509Certificate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisablex509Certificate.ps1 @@ -2,7 +2,33 @@ function Invoke-CIPPStandardDisablex509Certificate { <# .FUNCTIONALITY Internal + .APINAME + Disablex509Certificate + .CAT + Entra (AAD) Standards + .TAG + "highimpact" + .HELPTEXT + This blocks users from using Certificates as an MFA method. + .DOCSDESCRIPTION + This blocks users from using Certificates as an MFA method. + .ADDEDCOMPONENT + .LABEL + Disables Certificates as an MFA method + .IMPACT + High Impact + .POWERSHELLEQUIVALENT + Update-MgBetaPolicyAuthenticationMethodPolicyAuthenticationMethodConfiguration + .RECOMMENDEDBY + .DOCSDESCRIPTION + This blocks users from using Certificates as an MFA method. + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $CurrentInfo = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/policies/authenticationmethodspolicy/authenticationMethodConfigurations/x509Certificate' -tenantid $Tenant $State = if ($CurrentInfo.state -eq 'enabled') { $true } else { $false } @@ -28,3 +54,7 @@ function Invoke-CIPPStandardDisablex509Certificate { } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableAppConsentRequests.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableAppConsentRequests.ps1 index 835d6a9dfe94..4c427bac19d1 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableAppConsentRequests.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableAppConsentRequests.ps1 @@ -2,7 +2,36 @@ function Invoke-CIPPStandardEnableAppConsentRequests { <# .FUNCTIONALITY Internal + .APINAME + EnableAppConsentRequests + .CAT + Entra (AAD) Standards + .TAG + "lowimpact" + "CIS" + .HELPTEXT + Enables App consent admin requests for the tenant via the GA role. Does not overwrite existing reviewer settings + .DOCSDESCRIPTION + Enables the ability for users to request admin consent for applications. Should be used in conjunction with the "Require admin consent for applications" standards + .ADDEDCOMPONENT + {"type":"AdminRolesMultiSelect","label":"App Consent Reviewer Roles","name":"standards.EnableAppConsentRequests.ReviewerRoles"} + .LABEL + Enable App consent admin requests + .IMPACT + Low Impact + .POWERSHELLEQUIVALENT + Update-MgPolicyAdminConsentRequestPolicy + .RECOMMENDEDBY + "CIS" + .DOCSDESCRIPTION + Enables App consent admin requests for the tenant via the GA role. Does not overwrite existing reviewer settings + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $CurrentInfo = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies/adminConsentRequestPolicy' -tenantid $Tenant @@ -77,3 +106,7 @@ function Invoke-CIPPStandardEnableAppConsentRequests { Add-CIPPBPAField -FieldName 'EnableAppConsentAdminRequests' -FieldValue $CurrentInfo.isEnabled -StoreAs bool -Tenant $tenant } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableCustomerLockbox.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableCustomerLockbox.ps1 index b5741d27ac4d..daabff2b8ccc 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableCustomerLockbox.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableCustomerLockbox.ps1 @@ -2,7 +2,36 @@ function Invoke-CIPPStandardEnableCustomerLockbox { <# .FUNCTIONALITY Internal + .APINAME + EnableCustomerLockbox + .CAT + Global Standards + .TAG + "lowimpact" + "CIS" + "CustomerLockBoxEnabled" + .HELPTEXT + Enables Customer Lockbox that offers an approval process for Microsoft support to access organization data + .DOCSDESCRIPTION + Customer Lockbox ensures that Microsoft can't access your content to do service operations without your explicit approval. Customer Lockbox ensures only authorized requests allow access to your organizations data. + .ADDEDCOMPONENT + .LABEL + Enable Customer Lockbox + .IMPACT + Low Impact + .POWERSHELLEQUIVALENT + Set-OrganizationConfig -CustomerLockBoxEnabled $true + .RECOMMENDEDBY + "CIS" + .DOCSDESCRIPTION + Enables Customer Lockbox that offers an approval process for Microsoft support to access organization data + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $CustomerLockboxStatus = (New-ExoRequest -tenantid $Tenant -cmdlet 'Get-OrganizationConfig').CustomerLockboxEnabled @@ -38,3 +67,7 @@ function Invoke-CIPPStandardEnableCustomerLockbox { Add-CIPPBPAField -FieldName 'CustomerLockboxEnabled' -FieldValue $CustomerLockboxStatus -StoreAs bool -Tenant $tenant } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableFIDO2.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableFIDO2.ps1 index d5d84aa3d8e6..f7111785c042 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableFIDO2.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableFIDO2.ps1 @@ -2,7 +2,33 @@ function Invoke-CIPPStandardEnableFIDO2 { <# .FUNCTIONALITY Internal + .APINAME + EnableFIDO2 + .CAT + Entra (AAD) Standards + .TAG + "lowimpact" + .HELPTEXT + Enables the FIDO2 authenticationMethod for the tenant + .DOCSDESCRIPTION + Enables FIDO2 capabilities for the tenant. This allows users to use FIDO2 keys like a Yubikey for authentication. + .ADDEDCOMPONENT + .LABEL + Enable FIDO2 capabilities + .IMPACT + Low Impact + .POWERSHELLEQUIVALENT + Update-MgBetaPolicyAuthenticationMethodPolicyAuthenticationMethodConfiguration + .RECOMMENDEDBY + .DOCSDESCRIPTION + Enables the FIDO2 authenticationMethod for the tenant + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $CurrentInfo = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/policies/authenticationmethodspolicy/authenticationMethodConfigurations/Fido2' -tenantid $Tenant $State = if ($CurrentInfo.state -eq 'enabled') { $true } else { $false } @@ -31,3 +57,7 @@ function Invoke-CIPPStandardEnableFIDO2 { } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableHardwareOAuth.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableHardwareOAuth.ps1 index 67b0cf7e7bc4..eaf297caf78e 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableHardwareOAuth.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableHardwareOAuth.ps1 @@ -2,7 +2,33 @@ function Invoke-CIPPStandardEnableHardwareOAuth { <# .FUNCTIONALITY Internal + .APINAME + EnableHardwareOAuth + .CAT + Entra (AAD) Standards + .TAG + "lowimpact" + .HELPTEXT + Enables the HardwareOath authenticationMethod for the tenant. This allows you to use hardware tokens for generating 6 digit MFA codes. + .DOCSDESCRIPTION + Enables Hardware OAuth tokens for the tenant. This allows users to use hardware tokens like a Yubikey for authentication. + .ADDEDCOMPONENT + .LABEL + Enable Hardware OAuth tokens + .IMPACT + Low Impact + .POWERSHELLEQUIVALENT + Update-MgBetaPolicyAuthenticationMethodPolicyAuthenticationMethodConfiguration + .RECOMMENDEDBY + .DOCSDESCRIPTION + Enables the HardwareOath authenticationMethod for the tenant. This allows you to use hardware tokens for generating 6 digit MFA codes. + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $CurrentInfo = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/policies/authenticationmethodspolicy/authenticationMethodConfigurations/HardwareOath' -tenantid $Tenant $State = if ($CurrentInfo.state -eq 'enabled') { $true } else { $false } @@ -30,3 +56,7 @@ function Invoke-CIPPStandardEnableHardwareOAuth { } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableLitigationHold.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableLitigationHold.ps1 new file mode 100644 index 000000000000..cecc5e565a4f --- /dev/null +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableLitigationHold.ps1 @@ -0,0 +1,82 @@ +function Invoke-CIPPStandardEnableLitigationHold { + <# + .FUNCTIONALITY + Internal + .APINAME + EnableLitigationHold + .CAT + Exchange Standards + .TAG + "lowimpact" + .HELPTEXT + Enables litigation hold for all UserMailboxes with a valid license. + .ADDEDCOMPONENT + .LABEL + Enable Litigation Hold for all users + .IMPACT + Low Impact + .POWERSHELLEQUIVALENT + Set-Mailbox -LitigationHoldEnabled $true + .RECOMMENDEDBY + .DOCSDESCRIPTION + Enables litigation hold for all UserMailboxes with a valid license. + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + #> + + + + + param($Tenant, $Settings) + + $MailboxesNoLitHold = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-Mailbox' -cmdparams @{ Filter = 'LitigationHoldEnabled -eq "False"'} | Where-Object {$_.PersistedCapabilities -contains "BPOS_S_DlpAddOn" -or $_.PersistedCapabilities -contains "BPOS_S_Enterprise"} + + If ($Settings.remediate -eq $true) { + + if ($null -eq $MailboxesNoLitHold) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Litigation Hold already enabled for all accounts' -sev Info + } else { + try { + $Request = $MailboxesNoLitHold | ForEach-Object { + @{ + CmdletInput = @{ + CmdletName = 'Set-Mailbox' + Parameters = @{ Identity = $_.UserPrincipalName; LitigationHoldEnabled = $true } + } + } + } + + $BatchResults = New-ExoBulkRequest -tenantid $tenant -cmdletArray @($Request) + $BatchResults | ForEach-Object { + if ($_.error) { + $ErrorMessage = Get-NormalizedError -Message $_.error + Write-Host "Failed to Enable Litigation Hold for $($_.Target). Error: $ErrorMessage" + Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to Enable Litigation Hold for $($_.Target). Error: $ErrorMessage" -sev Error + } + } + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to Enable Litigation Hold for all accounts. Error: $ErrorMessage" -sev Error + } + } + + } + + if ($Settings.alert -eq $true) { + + if ($MailboxesNoLitHold) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Mailboxes without Litigation Hold: $($MailboxesNoLitHold.Count)" -sev Alert + } else { + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'All mailboxes have Litigation Hold enabled' -sev Info + } + } + + if ($Settings.report -eq $true) { + $filtered = $MailboxesNoLitHold | Select-Object -Property UserPrincipalName + Add-CIPPBPAField -FieldName 'EnableLitHold' -FieldValue $filtered -StoreAs json -Tenant $Tenant + } +} + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableMailTips.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableMailTips.ps1 index 52d3e3294c18..1abee9433d86 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableMailTips.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableMailTips.ps1 @@ -2,8 +2,36 @@ function Invoke-CIPPStandardEnableMailTips { <# .FUNCTIONALITY Internal + .APINAME + EnableMailTips + .CAT + Exchange Standards + .TAG + "lowimpact" + "CIS" + "exo_mailtipsenabled" + .HELPTEXT + Enables all MailTips in Outlook. MailTips are the notifications Outlook and Outlook on the web shows when an email you create, meets some requirements + .ADDEDCOMPONENT + {"type":"number","name":"standards.EnableMailTips.MailTipsLargeAudienceThreshold","label":"Number of recipients to trigger the large audience MailTip (Default is 25)","placeholder":"Enter a profile name","default":25} + .LABEL + Enable all MailTips + .IMPACT + Low Impact + .POWERSHELLEQUIVALENT + Set-OrganizationConfig + .RECOMMENDEDBY + "CIS" + .DOCSDESCRIPTION + Enables all MailTips in Outlook. MailTips are the notifications Outlook and Outlook on the web shows when an email you create, meets some requirements + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $MailTipsState = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-OrganizationConfig' | Select-Object MailTipsAllTipsEnabled, MailTipsExternalRecipientsTipsEnabled, MailTipsGroupMetricsEnabled, MailTipsLargeAudienceThreshold $StateIsCorrect = if ($MailTipsState.MailTipsAllTipsEnabled -and $MailTipsState.MailTipsExternalRecipientsTipsEnabled -and $MailTipsState.MailTipsGroupMetricsEnabled -and $MailTipsState.MailTipsLargeAudienceThreshold -eq $Settings.MailTipsLargeAudienceThreshold) { $true } else { $false } @@ -38,3 +66,7 @@ function Invoke-CIPPStandardEnableMailTips { } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableMailboxAuditing.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableMailboxAuditing.ps1 index 2374206fc6d0..e085bb128d8a 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableMailboxAuditing.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableMailboxAuditing.ps1 @@ -2,7 +2,36 @@ function Invoke-CIPPStandardEnableMailboxAuditing { <# .FUNCTIONALITY Internal + .APINAME + EnableMailboxAuditing + .CAT + Exchange Standards + .TAG + "lowimpact" + "CIS" + "exo_mailboxaudit" + .HELPTEXT + Enables Mailbox auditing for all mailboxes and on tenant level. Disables audit bypass on all mailboxes. Unified Audit Log needs to be enabled for this standard to function. + .DOCSDESCRIPTION + Enables mailbox auditing on tenant level and for all mailboxes. Disables audit bypass on all mailboxes. By default Microsoft does not enable mailbox auditing for Resource Mailboxes, Public Folder Mailboxes and DiscoverySearch Mailboxes. Unified Audit Log needs to be enabled for this standard to function. + .ADDEDCOMPONENT + .LABEL + Enable Mailbox auditing + .IMPACT + Low Impact + .POWERSHELLEQUIVALENT + Set-OrganizationConfig -AuditDisabled $false + .RECOMMENDEDBY + "CIS" + .DOCSDESCRIPTION + Enables Mailbox auditing for all mailboxes and on tenant level. Disables audit bypass on all mailboxes. Unified Audit Log needs to be enabled for this standard to function. + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $AuditState = (New-ExoRequest -tenantid $Tenant -cmdlet 'Get-OrganizationConfig').AuditDisabled @@ -91,3 +120,7 @@ function Invoke-CIPPStandardEnableMailboxAuditing { } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableOnlineArchiving.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableOnlineArchiving.ps1 index 3c3dbd004c45..90a20d59b356 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableOnlineArchiving.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableOnlineArchiving.ps1 @@ -2,7 +2,31 @@ function Invoke-CIPPStandardEnableOnlineArchiving { <# .FUNCTIONALITY Internal + .APINAME + EnableOnlineArchiving + .CAT + Exchange Standards + .TAG + "lowimpact" + .HELPTEXT + Enables the In-Place Online Archive for all UserMailboxes with a valid license. + .ADDEDCOMPONENT + .LABEL + Enable Online Archive for all users + .IMPACT + Low Impact + .POWERSHELLEQUIVALENT + Enable-Mailbox -Archive $true + .RECOMMENDEDBY + .DOCSDESCRIPTION + Enables the In-Place Online Archive for all UserMailboxes with a valid license. + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $MailboxPlans = @( 'ExchangeOnline', 'ExchangeOnlineEnterprise' ) @@ -56,3 +80,7 @@ function Invoke-CIPPStandardEnableOnlineArchiving { Add-CIPPBPAField -FieldName 'EnableOnlineArchiving' -FieldValue $filtered -StoreAs json -Tenant $Tenant } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnablePronouns.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnablePronouns.ps1 index bd4d6c85e70e..7d20bcee6666 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnablePronouns.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnablePronouns.ps1 @@ -2,7 +2,31 @@ function Invoke-CIPPStandardEnablePronouns { <# .FUNCTIONALITY Internal + .APINAME + EnablePronouns + .CAT + Global Standards + .TAG + "lowimpact" + .HELPTEXT + Enables the Pronouns feature for the tenant. This allows users to set their pronouns in their profile. + .ADDEDCOMPONENT + .LABEL + Enable Pronouns + .IMPACT + Low Impact + .POWERSHELLEQUIVALENT + Update-MgBetaAdminPeoplePronoun -IsEnabledInOrganization:$true + .RECOMMENDEDBY + .DOCSDESCRIPTION + Enables the Pronouns feature for the tenant. This allows users to set their pronouns in their profile. + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param ($Tenant, $Settings) $Uri = 'https://graph.microsoft.com/v1.0/admin/people/pronouns' @@ -47,3 +71,7 @@ function Invoke-CIPPStandardEnablePronouns { Add-CIPPBPAField -FieldName 'PronounsEnabled' -FieldValue $CurrentState.isEnabledInOrganization -StoreAs bool -Tenant $tenant } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardExcludedfileExt.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardExcludedfileExt.ps1 index 321c13c46ad8..28ab4c8ca495 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardExcludedfileExt.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardExcludedfileExt.ps1 @@ -2,7 +2,32 @@ function Invoke-CIPPStandardExcludedfileExt { <# .FUNCTIONALITY Internal + .APINAME + ExcludedfileExt + .CAT + SharePoint Standards + .TAG + "highimpact" + .HELPTEXT + Sets the file extensions that are excluded from syncing with OneDrive. These files will be blocked from upload. '*.' is automatically added to the extension and can be omitted. + .ADDEDCOMPONENT + {"type":"input","name":"standards.ExcludedfileExt.ext","label":"Extensions, Comma separated"} + .LABEL + Exclude File Extensions from Syncing + .IMPACT + High Impact + .POWERSHELLEQUIVALENT + Update-MgAdminSharepointSetting + .RECOMMENDEDBY + .DOCSDESCRIPTION + Sets the file extensions that are excluded from syncing with OneDrive. These files will be blocked from upload. '*.' is automatically added to the extension and can be omitted. + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $CurrentInfo = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/admin/sharepoint/settings' -tenantid $Tenant -AsApp $true $Exts = ($Settings.ext -replace ' ', '') -split ',' @@ -52,3 +77,7 @@ function Invoke-CIPPStandardExcludedfileExt { Add-CIPPBPAField -FieldName 'ExcludedfileExt' -FieldValue $CurrentInfo.excludedFileExtensionsForSyncApp -StoreAs json -Tenant $tenant } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardExternalMFATrusted.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardExternalMFATrusted.ps1 index 6fefb63e5b59..619455fdb0e7 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardExternalMFATrusted.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardExternalMFATrusted.ps1 @@ -2,7 +2,32 @@ function Invoke-CIPPStandardExternalMFATrusted { <# .FUNCTIONALITY Internal + .APINAME + ExternalMFATrusted + .CAT + Entra (AAD) Standards + .TAG + "lowimpact" + .HELPTEXT + Sets the state of the Cross-tenant access setting to trust external MFA. This allows guest users to use their home tenant MFA to access your tenant. + .ADDEDCOMPONENT + {"type":"Select","label":"Select value","name":"standards.ExternalMFATrusted.state","values":[{"label":"Enabled","value":"true"},{"label":"Disabled","value":"false"}]} + .LABEL + Sets the Cross-tenant access setting to trust external MFA + .IMPACT + Low Impact + .POWERSHELLEQUIVALENT + Update-MgBetaPolicyCrossTenantAccessPolicyDefault + .RECOMMENDEDBY + .DOCSDESCRIPTION + Sets the state of the Cross-tenant access setting to trust external MFA. This allows guest users to use their home tenant MFA to access your tenant. + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $ExternalMFATrusted = (New-GraphGetRequest -uri 'https://graph.microsoft.com/v1.0/policies/crossTenantAccessPolicy/default?$select=inboundTrust' -tenantid $Tenant) @@ -48,3 +73,7 @@ function Invoke-CIPPStandardExternalMFATrusted { } } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardFocusedInbox.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardFocusedInbox.ps1 index 6dccab45117f..9a9655ac22a4 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardFocusedInbox.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardFocusedInbox.ps1 @@ -2,7 +2,34 @@ function Invoke-CIPPStandardFocusedInbox { <# .FUNCTIONALITY Internal + .APINAME + FocusedInbox + .CAT + Exchange Standards + .TAG + "lowimpact" + .HELPTEXT + Sets the default Focused Inbox state for the tenant. This can be overridden by the user. + .DOCSDESCRIPTION + Sets the default Focused Inbox state for the tenant. This can be overridden by the user in their Outlook settings. For more information, see [Microsoft's documentation.](https://support.microsoft.com/en-us/office/focused-inbox-for-outlook-f445ad7f-02f4-4294-a82e-71d8964e3978) + .ADDEDCOMPONENT + {"type":"Select","label":"Select value","name":"standards.FocusedInbox.state","values":[{"label":"Enabled","value":"enabled"},{"label":"Disabled","value":"disabled"}]} + .LABEL + Set Focused Inbox state + .IMPACT + Low Impact + .POWERSHELLEQUIVALENT + Set-OrganizationConfig -FocusedInboxOn $true or $false + .RECOMMENDEDBY + .DOCSDESCRIPTION + Sets the default Focused Inbox state for the tenant. This can be overridden by the user. + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) # Input validation @@ -45,3 +72,7 @@ function Invoke-CIPPStandardFocusedInbox { Add-CIPPBPAField -FieldName 'FocusedInboxCorrectState' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $tenant } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGlobalQuarantineNotifications.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGlobalQuarantineNotifications.ps1 index 583a39ecb8f8..a99543882349 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGlobalQuarantineNotifications.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGlobalQuarantineNotifications.ps1 @@ -2,7 +2,34 @@ function Invoke-CIPPStandardGlobalQuarantineNotifications { <# .FUNCTIONALITY Internal + .APINAME + GlobalQuarantineNotifications + .CAT + Exchange Standards + .TAG + "lowimpact" + .HELPTEXT + Sets the Global Quarantine Notification Interval to the selected value. Determines how often the quarantine notification is sent to users. + .DOCSDESCRIPTION + Sets the global quarantine notification interval for the tenant. This is the time between the quarantine notification emails are sent out to users. Default is 24 hours. + .ADDEDCOMPONENT + {"type":"Select","label":"Select value","name":"standards.GlobalQuarantineNotifications.NotificationInterval","values":[{"label":"4 hours","value":"04:00:00"},{"label":"1 day/Daily","value":"1.00:00:00"},{"label":"7 days/Weekly","value":"7.00:00:00"}]} + .LABEL + Set Global Quarantine Notification Interval + .IMPACT + Low Impact + .POWERSHELLEQUIVALENT + Set-QuarantinePolicy -EndUserSpamNotificationFrequency + .RECOMMENDEDBY + .DOCSDESCRIPTION + Sets the Global Quarantine Notification Interval to the selected value. Determines how often the quarantine notification is sent to users. + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param ($Tenant, $Settings) $CurrentState = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-QuarantinePolicy' -cmdParams @{ QuarantinePolicyType = 'GlobalQuarantinePolicy' } @@ -55,3 +82,7 @@ function Invoke-CIPPStandardGlobalQuarantineNotifications { } } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 index 4c1aeeebc2de..62d7a7678aca 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 @@ -12,24 +12,23 @@ function Invoke-CIPPStandardGroupTemplate { $Filter = "PartitionKey eq 'GroupTemplate' and RowKey eq '$($Template.value)'" $groupobj = (Get-AzDataTableEntity @Table -Filter $Filter).JSON | ConvertFrom-Json $email = if ($groupobj.domain) { "$($groupobj.username)@$($groupobj.domain)" } else { "$($groupobj.username)@$($Tenant)" } - $CheckExististing = New-GraphGETRequest -uri 'https://graph.microsoft.com/beta/groups' -tenantid $tenant | Where-Object -Property displayName -EQ $groupobj.displayname + $CheckExististing = New-GraphGETRequest -uri 'https://graph.microsoft.com/beta/groups?$top=999' -tenantid $tenant | Where-Object -Property displayName -EQ $groupobj.displayname + $BodyToship = [pscustomobject] @{ + 'displayName' = $groupobj.Displayname + 'description' = $groupobj.Description + 'mailNickname' = $groupobj.username + mailEnabled = [bool]$false + securityEnabled = [bool]$true + isAssignableToRole = [bool]($groupobj | Where-Object -Property groupType -EQ 'AzureRole') + + } + if ($groupobj.membershipRules) { + $BodyToship | Add-Member -NotePropertyName 'membershipRule' -NotePropertyValue ($groupobj.membershipRules) + $BodyToship | Add-Member -NotePropertyName 'groupTypes' -NotePropertyValue @('DynamicMembership') + $BodyToship | Add-Member -NotePropertyName 'membershipRuleProcessingState' -NotePropertyValue 'On' + } if (!$CheckExististing) { if ($groupobj.groupType -in 'Generic', 'azurerole', 'dynamic') { - - $BodyToship = [pscustomobject] @{ - 'displayName' = $groupobj.Displayname - 'description' = $groupobj.Description - 'mailNickname' = $groupobj.username - mailEnabled = [bool]$false - securityEnabled = [bool]$true - isAssignableToRole = [bool]($groupobj | Where-Object -Property groupType -EQ 'AzureRole') - - } - if ($groupobj.membershipRules) { - $BodyToship | Add-Member -NotePropertyName 'membershipRule' -NotePropertyValue ($groupobj.membershipRules) - $BodyToship | Add-Member -NotePropertyName 'groupTypes' -NotePropertyValue @('DynamicMembership') - $BodyToship | Add-Member -NotePropertyName 'membershipRuleProcessingState' -NotePropertyValue 'On' - } $GraphRequest = New-GraphPostRequest -uri 'https://graph.microsoft.com/beta/groups' -tenantid $tenant -type POST -body (ConvertTo-Json -InputObject $BodyToship -Depth 10) -verbose } else { if ($groupobj.groupType -eq 'dynamicdistribution') { @@ -52,9 +51,30 @@ function Invoke-CIPPStandardGroupTemplate { } } Write-LogMessage -user $request.headers.'x-ms-client-principal' -API 'Standards' -tenant $tenant -message "Created group $($groupobj.displayname) with id $($GraphRequest.id) " -Sev 'Info' - } else { - Write-LogMessage -user $request.headers.'x-ms-client-principal' -API 'Standards' -tenant $tenant -message "Group exists $($groupobj.displayname). Did not create" -Sev 'Info' + if ($groupobj.groupType -in 'Generic', 'azurerole', 'dynamic') { + $GraphRequest = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/groups/$($CheckExististing.id)" -tenantid $tenant -type PATCH -body (ConvertTo-Json -InputObject $BodyToship -Depth 10) -verbose + } else { + if ($groupobj.groupType -eq 'dynamicdistribution') { + $Params = @{ + Name = $groupobj.Displayname + RecipientFilter = $groupobj.membershipRules + PrimarySmtpAddress = $email + } + $GraphRequest = New-ExoRequest -tenantid $tenant -cmdlet 'Set-DynamicDistributionGroup' -cmdParams $params + } else { + $Params = @{ + Identity = $groupobj.Displayname + Alias = $groupobj.username + Description = $groupobj.Description + PrimarySmtpAddress = $email + Type = $groupobj.groupType + RequireSenderAuthenticationEnabled = [bool]!$groupobj.AllowExternal + } + $GraphRequest = New-ExoRequest -tenantid $tenant -cmdlet 'Set-DistributionGroup' -cmdParams $params + } + } + Write-LogMessage -user $request.headers.'x-ms-client-principal' -API 'Standards' -tenant $tenant -message "Group exists $($groupobj.displayname). Updated to latest settings." -Sev 'Info' } } catch { diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardLegacyMFACleanup.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardLegacyMFACleanup.ps1 index 08f43267469f..282d7ebc4778 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardLegacyMFACleanup.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardLegacyMFACleanup.ps1 @@ -2,8 +2,36 @@ function Invoke-CIPPStandardLegacyMFACleanup { <# .FUNCTIONALITY Internal + .APINAME + LegacyMFACleanup + .CAT + Entra (AAD) Standards + .TAG + "mediumimpact" + .HELPTEXT + This standard currently does not function and can be safely disabled + .ADDEDCOMPONENT + .LABEL + Remove Legacy MFA if SD or CA is active + .IMPACT + Medium Impact + .POWERSHELLEQUIVALENT + Set-MsolUser -StrongAuthenticationRequirements $null + .RECOMMENDEDBY + .DOCSDESCRIPTION + This standard currently does not function and can be safely disabled + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) Write-LogMessage -API 'Standards' -tenant $tenant -message 'Per User MFA APIs have been disabled.' -sev Info } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailContacts.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailContacts.ps1 index 8cc14082f3a3..724357d3ef55 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailContacts.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailContacts.ps1 @@ -2,7 +2,39 @@ function Invoke-CIPPStandardMailContacts { <# .FUNCTIONALITY Internal + .APINAME + MailContacts + .CAT + Global Standards + .TAG + "lowimpact" + .HELPTEXT + Defines the email address to receive general updates and information related to M365 subscriptions. Leave a contact field blank if you do not want to update the contact information. + .DOCSDESCRIPTION + Defines the email address to receive general updates and information related to M365 subscriptions. Leave a contact field blank if you do not want to update the contact information. + .DISABLEDFEATURES + + .ADDEDCOMPONENT + {"type":"input","name":"standards.MailContacts.GeneralContact","label":"General Contact"} + {"type":"input","name":"standards.MailContacts.SecurityContact","label":"Security Contact"} + {"type":"input","name":"standards.MailContacts.MarketingContact","label":"Marketing Contact"} + {"type":"input","name":"standards.MailContacts.TechContact","label":"Technical Contact"} + .LABEL + Set contact e-mails + .IMPACT + Low Impact + .POWERSHELLEQUIVALENT + Set-MsolCompanyContactInformation + .RECOMMENDEDBY + .DOCSDESCRIPTION + Defines the email address to receive general updates and information related to M365 subscriptions. Leave a contact field blank if you do not want to update the contact information. + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $TenantID = (New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/organization' -tenantid $tenant) $CurrentInfo = New-GraphGetRequest -Uri "https://graph.microsoft.com/beta/organization/$($TenantID.id)" -tenantid $Tenant @@ -63,3 +95,7 @@ function Invoke-CIPPStandardMailContacts { Add-CIPPBPAField -FieldName 'MailContacts' -FieldValue $CurrentInfo -StoreAs json -Tenant $tenant } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMalwareFilterPolicy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMalwareFilterPolicy.ps1 index 863110cdac69..79fa04d4c3fc 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMalwareFilterPolicy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMalwareFilterPolicy.ps1 @@ -1,8 +1,43 @@ function Invoke-CIPPStandardMalwareFilterPolicy { - <# - .FUNCTIONALITY - Internal - #> + <# + .FUNCTIONALITY + Internal + .APINAME + MalwareFilterPolicy + .CAT + Defender Standards + .TAG + "lowimpact" + "CIS" + "mdo_zapspam" + "mdo_zapphish" + "mdo_zapmalware" + .HELPTEXT + This creates a Malware filter policy that enables the default File filter and Zero-hour auto purge for malware. + .ADDEDCOMPONENT + {"type":"Select","label":"FileTypeAction","name":"standards.MalwareFilterPolicy.FileTypeAction","values":[{"label":"Reject","value":"Reject"},{"label":"Quarantine the message","value":"Quarantine"}]} + {"type":"Select","label":"QuarantineTag","name":"standards.MalwareFilterPolicy.QuarantineTag","values":[{"label":"AdminOnlyAccessPolicy","value":"AdminOnlyAccessPolicy"},{"label":"DefaultFullAccessPolicy","value":"DefaultFullAccessPolicy"},{"label":"DefaultFullAccessWithNotificationPolicy","value":"DefaultFullAccessWithNotificationPolicy"}]} + {"type":"boolean","label":"Enable Internal Sender Admin Notifications","name":"standards.MalwareFilterPolicy.EnableInternalSenderAdminNotifications"} + {"type":"input","name":"standards.MalwareFilterPolicy.InternalSenderAdminAddress","label":"Internal Sender Admin Address"} + {"type":"boolean","label":"Enable External Sender Admin Notifications","name":"standards.MalwareFilterPolicy.EnableExternalSenderAdminNotifications"} + {"type":"input","name":"standards.MalwareFilterPolicy.ExternalSenderAdminAddress","label":"External Sender Admin Address"} + .LABEL + Default Malware Filter Policy + .IMPACT + Low Impact + .POWERSHELLEQUIVALENT + Set-MalwareFilterPolicy or New-MalwareFilterPolicy + .RECOMMENDEDBY + "CIS" + .DOCSDESCRIPTION + This creates a Malware filter policy that enables the default File filter and Zero-hour auto purge for malware. + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + #> + + + + param($Tenant, $Settings) $PolicyName = 'Default Malware Policy' @@ -119,3 +154,7 @@ function Invoke-CIPPStandardMalwareFilterPolicy { } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMessageExpiration.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMessageExpiration.ps1 index 820486c52ef3..b5012d7aefea 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMessageExpiration.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMessageExpiration.ps1 @@ -2,7 +2,33 @@ function Invoke-CIPPStandardMessageExpiration { <# .FUNCTIONALITY Internal + .APINAME + MessageExpiration + .CAT + Exchange Standards + .TAG + "lowimpact" + .HELPTEXT + Sets the transport message configuration to timeout a message at 12 hours. + .DOCSDESCRIPTION + Expires messages in the transport queue after 12 hours. Makes the NDR for failed messages show up faster for users. Default is 24 hours. + .ADDEDCOMPONENT + .LABEL + Lower Transport Message Expiration to 12 hours + .IMPACT + Low Impact + .POWERSHELLEQUIVALENT + Set-TransportConfig -MessageExpirationTimeout 12.00:00:00 + .RECOMMENDEDBY + .DOCSDESCRIPTION + Sets the transport message configuration to timeout a message at 12 hours. + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $MessageExpiration = (New-ExoRequest -tenantid $Tenant -cmdlet 'Get-TransportConfig').messageExpiration @@ -34,3 +60,7 @@ function Invoke-CIPPStandardMessageExpiration { Add-CIPPBPAField -FieldName 'messageExpiration' -FieldValue $MessageExpiration -StoreAs bool -Tenant $tenant } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardNudgeMFA.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardNudgeMFA.ps1 index 6d387b212767..116b3d4ed40f 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardNudgeMFA.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardNudgeMFA.ps1 @@ -2,7 +2,35 @@ function Invoke-CIPPStandardNudgeMFA { <# .FUNCTIONALITY Internal + .APINAME + NudgeMFA + .CAT + Entra (AAD) Standards + .TAG + "lowimpact" + .HELPTEXT + Sets the state of the registration campaign for the tenant + .DOCSDESCRIPTION + Sets the state of the registration campaign for the tenant. If enabled nudges users to set up the Microsoft Authenticator during sign-in. + .ADDEDCOMPONENT + {"type":"Select","label":"Select value","name":"standards.NudgeMFA.state","values":[{"label":"Enabled","value":"enabled"},{"label":"Disabled","value":"disabled"}]} + {"type":"number","name":"standards.NudgeMFA.snoozeDurationInDays","label":"Number of days to allow users to skip registering Authenticator (0-14, default is 1)","default":1} + .LABEL + Sets the state for the request to setup Authenticator + .IMPACT + Low Impact + .POWERSHELLEQUIVALENT + Update-MgPolicyAuthenticationMethodPolicy + .RECOMMENDEDBY + .DOCSDESCRIPTION + Sets the state of the registration campaign for the tenant + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $CurrentInfo = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/policies/authenticationMethodsPolicy' -tenantid $Tenant @@ -57,3 +85,7 @@ function Invoke-CIPPStandardNudgeMFA { } } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOauthConsent.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOauthConsent.ps1 index 38d2b41dfce3..036bd6f011d0 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOauthConsent.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOauthConsent.ps1 @@ -2,7 +2,36 @@ function Invoke-CIPPStandardOauthConsent { <# .FUNCTIONALITY Internal + .APINAME + OauthConsent + .CAT + Entra (AAD) Standards + .TAG + "mediumimpact" + "CIS" + .HELPTEXT + Disables users from being able to consent to applications, except for those specified in the field below + .DOCSDESCRIPTION + Requires users to get administrator consent before sharing data with applications. You can preapprove specific applications. + .ADDEDCOMPONENT + {"type":"input","name":"standards.OauthConsent.AllowedApps","label":"Allowed application IDs, comma separated"} + .LABEL + Require admin consent for applications (Prevent OAuth phishing) + .IMPACT + Medium Impact + .POWERSHELLEQUIVALENT + Update-MgPolicyAuthorizationPolicy + .RECOMMENDEDBY + "CIS" + .DOCSDESCRIPTION + Disables users from being able to consent to applications, except for those specified in the field below + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($tenant, $settings) $State = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/policies/authorizationPolicy/authorizationPolicy' -tenantid $tenant $StateIsCorrect = if ($State.permissionGrantPolicyIdsAssignedToDefaultUserRole -eq 'managePermissionGrantsForSelf.cipp-consent-policy') { $true } else { $false } @@ -46,3 +75,7 @@ function Invoke-CIPPStandardOauthConsent { Add-CIPPBPAField -FieldName 'OauthConsent' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $tenant } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOauthConsentLowSec.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOauthConsentLowSec.ps1 index 42814d48cfb6..ba94c4f77843 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOauthConsentLowSec.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOauthConsentLowSec.ps1 @@ -2,7 +2,32 @@ function Invoke-CIPPStandardOauthConsentLowSec { <# .FUNCTIONALITY Internal + .APINAME + OauthConsentLowSec + .CAT + Entra (AAD) Standards + .TAG + "mediumimpact" + .HELPTEXT + Sets the default oauth consent level so users can consent to applications that have low risks. + .DOCSDESCRIPTION + Allows users to consent to applications with low assigned risk. + .LABEL + Allow users to consent to applications with low security risk (Prevent OAuth phishing. Lower impact, less secure) + .IMPACT + Medium Impact + .POWERSHELLEQUIVALENT + Update-MgPolicyAuthorizationPolicy + .RECOMMENDEDBY + .DOCSDESCRIPTION + Sets the default oauth consent level so users can consent to applications that have low risks. + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $State = (New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/policies/authorizationPolicy/authorizationPolicy' -tenantid $tenant) If ($Settings.remediate -eq $true) { @@ -34,3 +59,7 @@ function Invoke-CIPPStandardOauthConsentLowSec { Add-CIPPBPAField -FieldName 'OauthConsentLowSec' -FieldValue $State.permissionGrantPolicyIdsAssignedToDefaultUserRole -StoreAs bool -Tenant $tenant } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOutBoundSpamAlert.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOutBoundSpamAlert.ps1 index a786a7d044b8..8ab6cab3d30e 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOutBoundSpamAlert.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOutBoundSpamAlert.ps1 @@ -2,7 +2,36 @@ function Invoke-CIPPStandardOutBoundSpamAlert { <# .FUNCTIONALITY Internal + .APINAME + OutBoundSpamAlert + .CAT + Exchange Standards + .TAG + "lowimpact" + "CIS" + .HELPTEXT + Set the Outbound Spam Alert e-mail address + .DOCSDESCRIPTION + Sets the e-mail address to which outbound spam alerts are sent. + .ADDEDCOMPONENT + {"type":"input","name":"standards.OutBoundSpamAlert.OutboundSpamContact","label":"Outbound spam contact"} + .LABEL + Set Outbound Spam Alert e-mail + .IMPACT + Low Impact + .POWERSHELLEQUIVALENT + Set-HostedOutboundSpamFilterPolicy + .RECOMMENDEDBY + "CIS" + .DOCSDESCRIPTION + Set the Outbound Spam Alert e-mail address + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $CurrentInfo = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-HostedOutboundSpamFilterPolicy' -useSystemMailbox $true @@ -35,3 +64,7 @@ function Invoke-CIPPStandardOutBoundSpamAlert { Add-CIPPBPAField -FieldName 'OutboundSpamAlert' -FieldValue $CurrentInfo.NotifyOutboundSpam -StoreAs bool -Tenant $tenant } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPWcompanionAppAllowedState.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPWcompanionAppAllowedState.ps1 index c2e3c7a687b7..8148322ca651 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPWcompanionAppAllowedState.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPWcompanionAppAllowedState.ps1 @@ -2,7 +2,34 @@ function Invoke-CIPPStandardPWcompanionAppAllowedState { <# .FUNCTIONALITY Internal + .APINAME + PWcompanionAppAllowedState + .CAT + Entra (AAD) Standards + .TAG + "lowimpact" + .HELPTEXT + Sets the state of Authenticator Lite, Authenticator lite is a companion app for passwordless authentication. + .DOCSDESCRIPTION + Sets the Authenticator Lite state to enabled. This allows users to use the Authenticator Lite built into the Outlook app instead of the full Authenticator app. + .ADDEDCOMPONENT + {"type":"Select","label":"Select value","name":"standards.PWcompanionAppAllowedState.state","values":[{"label":"Enabled","value":"enabled"},{"label":"Disabled","value":"disabled"}]} + .LABEL + Set Authenticator Lite state + .IMPACT + Low Impact + .POWERSHELLEQUIVALENT + Update-MgBetaPolicyAuthenticationMethodPolicyAuthenticationMethodConfiguration + .RECOMMENDEDBY + .DOCSDESCRIPTION + Sets the state of Authenticator Lite, Authenticator lite is a companion app for passwordless authentication. + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $authenticatorFeaturesState = (New-GraphGetRequest -tenantid $tenant -Uri 'https://graph.microsoft.com/beta/policies/authenticationMethodsPolicy/authenticationMethodConfigurations/microsoftAuthenticator' -Type GET) @@ -58,3 +85,7 @@ function Invoke-CIPPStandardPWcompanionAppAllowedState { } } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPWdisplayAppInformationRequiredState.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPWdisplayAppInformationRequiredState.ps1 index 2f85c01bf859..83b2b276195e 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPWdisplayAppInformationRequiredState.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPWdisplayAppInformationRequiredState.ps1 @@ -2,7 +2,35 @@ function Invoke-CIPPStandardPWdisplayAppInformationRequiredState { <# .FUNCTIONALITY Internal + .APINAME + PWdisplayAppInformationRequiredState + .CAT + Entra (AAD) Standards + .TAG + "lowimpact" + "CIS" + .HELPTEXT + Enables the MS authenticator app to display information about the app that is requesting authentication. This displays the application name. + .DOCSDESCRIPTION + Allows users to use Passwordless with Number Matching and adds location information from the last request + .ADDEDCOMPONENT + .LABEL + Enable Passwordless with Location information and Number Matching + .IMPACT + Low Impact + .POWERSHELLEQUIVALENT + Update-MgBetaPolicyAuthenticationMethodPolicyAuthenticationMethodConfiguration + .RECOMMENDEDBY + "CIS" + .DOCSDESCRIPTION + Enables the MS authenticator app to display information about the app that is requesting authentication. This displays the application name. + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $CurrentInfo = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/policies/authenticationMethodsPolicy/authenticationMethodConfigurations/microsoftAuthenticator' -tenantid $Tenant $State = if ($CurrentInfo.state -eq 'enabled') { $true } else { $false } @@ -27,3 +55,7 @@ function Invoke-CIPPStandardPWdisplayAppInformationRequiredState { Add-CIPPBPAField -FieldName 'PWdisplayAppInformationRequiredState' -FieldValue $State -StoreAs bool -Tenant $tenant } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPasswordExpireDisabled.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPasswordExpireDisabled.ps1 index 5cf8dac138a1..c4b2bff28cd7 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPasswordExpireDisabled.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPasswordExpireDisabled.ps1 @@ -2,7 +2,36 @@ function Invoke-CIPPStandardPasswordExpireDisabled { <# .FUNCTIONALITY Internal + .APINAME + PasswordExpireDisabled + .CAT + Entra (AAD) Standards + .TAG + "lowimpact" + "CIS" + "PWAgePolicyNew" + .HELPTEXT + Disables the expiration of passwords for the tenant by setting the password expiration policy to never expire for any user. + .DOCSDESCRIPTION + Sets passwords to never expire for tenant, recommended to use in conjunction with secure password requirements. + .ADDEDCOMPONENT + .LABEL + Do not expire passwords + .IMPACT + Low Impact + .POWERSHELLEQUIVALENT + Update-MgDomain + .RECOMMENDEDBY + "CIS" + .DOCSDESCRIPTION + Disables the expiration of passwords for the tenant by setting the password expiration policy to never expire for any user. + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $GraphRequest = New-GraphGetRequest -uri 'https://graph.microsoft.com/v1.0/domains' -tenantid $Tenant $DomainswithoutPassExpire = $GraphRequest | Where-Object -Property passwordValidityPeriodInDays -NE '2147483647' @@ -43,3 +72,7 @@ function Invoke-CIPPStandardPasswordExpireDisabled { Add-CIPPBPAField -FieldName 'PasswordExpireDisabled' -FieldValue $DomainswithoutPassExpire -StoreAs json -Tenant $tenant } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPerUserMFA.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPerUserMFA.ps1 index c83204529423..5f08753147f6 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPerUserMFA.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPerUserMFA.ps1 @@ -2,7 +2,31 @@ function Invoke-CIPPStandardPerUserMFA { <# .FUNCTIONALITY Internal + .APINAME + PerUserMFA + .CAT + Entra (AAD) Standards + .TAG + "highimpact" + .HELPTEXT + Enables per user MFA for all users. + .ADDEDCOMPONENT + .LABEL + Enables per user MFA for all users. + .IMPACT + High Impact + .POWERSHELLEQUIVALENT + Graph API + .RECOMMENDEDBY + .DOCSDESCRIPTION + Enables per user MFA for all users. + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $GraphRequest = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users?`$top=999&`$select=UserPrincipalName,accountEnabled" -scope 'https://graph.microsoft.com/.default' -tenantid $Tenant | Where-Object { $_.AccountEnabled -EQ $true } @@ -39,3 +63,7 @@ function Invoke-CIPPStandardPerUserMFA { Add-CIPPBPAField -FieldName 'LegacyMFAUsers' -FieldValue $UsersWithoutMFA -StoreAs json -Tenant $tenant } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishProtection.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishProtection.ps1 index 8d0e37ffe6b0..1ed18b3104f5 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishProtection.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishProtection.ps1 @@ -2,7 +2,34 @@ function Invoke-CIPPStandardPhishProtection { <# .FUNCTIONALITY Internal + .APINAME + PhishProtection + .CAT + Global Standards + .TAG + "lowimpact" + .HELPTEXT + Adds branding to the logon page that only appears if the url is not login.microsoftonline.com. This potentially prevents AITM attacks via EvilNginx. This will also automatically generate alerts if a clone of your login page has been found when set to Remediate. + .ADDEDCOMPONENT + .LABEL + Enable Phishing Protection system via branding CSS + .IMPACT + Low Impact + .DISABLEDFEATURES + + .POWERSHELLEQUIVALENT + Portal only + .RECOMMENDEDBY + "CIPP" + .DOCSDESCRIPTION + Adds branding to the logon page that only appears if the url is not login.microsoftonline.com. This potentially prevents AITM attacks via EvilNginx. This will also automatically generate alerts if a clone of your login page has been found when set to Remediate. + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $TenantId = Get-Tenants | Where-Object -Property defaultDomainName -EQ $tenant @@ -56,3 +83,7 @@ function Invoke-CIPPStandardPhishProtection { Add-CIPPBPAField -FieldName 'PhishProtection' -FieldValue $authstate -StoreAs bool -Tenant $tenant } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRotateDKIM.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRotateDKIM.ps1 index e149d3aec70d..135d55c06641 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRotateDKIM.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRotateDKIM.ps1 @@ -2,7 +2,33 @@ function Invoke-CIPPStandardRotateDKIM { <# .FUNCTIONALITY Internal + .APINAME + RotateDKIM + .CAT + Exchange Standards + .TAG + "lowimpact" + "CIS" + .HELPTEXT + Rotate DKIM keys that are 1024 bit to 2048 bit + .ADDEDCOMPONENT + .LABEL + Rotate DKIM keys that are 1024 bit to 2048 bit + .IMPACT + Low Impact + .POWERSHELLEQUIVALENT + Rotate-DkimSigningConfig + .RECOMMENDEDBY + "CIS" + .DOCSDESCRIPTION + Rotate DKIM keys that are 1024 bit to 2048 bit + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $DKIM = (New-ExoRequest -tenantid $tenant -cmdlet 'Get-DkimSigningConfig') | Where-Object { $_.Selector1KeySize -Eq 1024 -and $_.Enabled -eq $true } @@ -36,3 +62,7 @@ function Invoke-CIPPStandardRotateDKIM { Add-CIPPBPAField -FieldName 'DKIM' -FieldValue $DKIM -StoreAs json -Tenant $tenant } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPAzureB2B.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPAzureB2B.ps1 new file mode 100644 index 000000000000..940bad58b135 --- /dev/null +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPAzureB2B.ps1 @@ -0,0 +1,66 @@ +function Invoke-CIPPStandardSPAzureB2B { + <# + .FUNCTIONALITY + Internal + .COMPONENT + (APIName) SPAzureB2B + .SYNOPSIS + Enable SharePoint and OneDrive integration with Azure AD B2B + .DESCRIPTION + (Helptext) Ensure SharePoint and OneDrive integration with Azure AD B2B is enabled + (DocsDescription) Ensure SharePoint and OneDrive integration with Azure AD B2B is enabled + .NOTES + CAT + SharePoint Standards + TAG + "lowimpact" + "CIS" + ADDEDCOMPONENT + LABEL + Enable SharePoint and OneDrive integration with Azure AD B2B + IMPACT + Low Impact + POWERSHELLEQUIVALENT + Set-SPOTenant -EnableAzureADB2BIntegration $true + RECOMMENDEDBY + "CIS 3.0" + UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + #> + + param($Tenant, $Settings) + $CurrentState = Get-CIPPSPOTenant -TenantFilter $Tenant | + Select-Object -Property EnableAzureADB2BIntegration + + $StateIsCorrect = ($CurrentState.EnableAzureADB2BIntegration -eq $true) + + if ($Settings.remediate -eq $true) { + if ($StateIsCorrect -eq $true) { + Write-LogMessage -API 'Standards' -Message 'SharePoint Azure B2B is already enabled' -Sev Info + } else { + $Properties = @{ + EnableAzureADB2BIntegration = $true + } + + try { + Get-CIPPSPOTenant -TenantFilter $Tenant | Set-CIPPSPOTenant -Properties $Properties + Write-LogMessage -API 'Standards' -Message 'Successfully set the SharePoint Azure B2B to enabled' -Sev Info + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API 'Standards' -Message "Failed to set the SharePoint Azure B2B to enabled. Error: $ErrorMessage" -Sev Error + } + } + } + + if ($Settings.alert -eq $true) { + if ($StateIsCorrect -eq $true) { + Write-LogMessage -API 'Standards' -Message 'SharePoint Azure B2B is enabled' -Sev Info + } else { + Write-LogMessage -API 'Standards' -Message 'SharePoint Azure B2B is not enabled' -Sev Alert + } + } + + if ($Settings.report -eq $true) { + Add-CIPPBPAField -FieldName 'AzureB2B' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $tenant + } +} diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDirectSharing.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDirectSharing.ps1 new file mode 100644 index 000000000000..9cfaf3c10317 --- /dev/null +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDirectSharing.ps1 @@ -0,0 +1,66 @@ +function Invoke-CIPPStandardSPDirectSharing { + <# + .FUNCTIONALITY + Internal + .COMPONENT + (APIName) SPDirectSharing + .SYNOPSIS + Default sharing to Direct users + .DESCRIPTION + (Helptext) Ensure default link sharing is set to Direct in SharePoint and OneDrive + (DocsDescription) Ensure default link sharing is set to Direct in SharePoint and OneDrive + .NOTES + CAT + SharePoint Standards + TAG + "mediumimpact" + "CIS" + ADDEDCOMPONENT + LABEL + Default sharing to Direct users + IMPACT + Medium Impact + POWERSHELLEQUIVALENT + Set-SPOTenant -DefaultSharingLinkType Direct + RECOMMENDEDBY + "CIS 3.0" + UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + #> + + param($Tenant, $Settings) + $CurrentState = Get-CIPPSPOTenant -TenantFilter $Tenant | + Select-Object -Property DefaultSharingLinkType + + $StateIsCorrect = ($CurrentState.DefaultSharingLinkType -eq 'Direct') + + if ($Settings.remediate -eq $true) { + if ($StateIsCorrect -eq $true) { + Write-LogMessage -API 'Standards' -Message 'SharePoint Sharing Restriction is already enabled' -Sev Info + } else { + $Properties = @{ + DefaultSharingLinkType = 1 + } + + try { + Get-CIPPSPOTenant -TenantFilter $Tenant | Set-CIPPSPOTenant -Properties $Properties + Write-LogMessage -API 'Standards' -Message 'Successfully set the SharePoint Sharing Restriction to Direct' -Sev Info + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API 'Standards' -Message "Failed to set the SharePoint Sharing Restriction to Direct. Error: $ErrorMessage" -Sev Error + } + } + } + + if ($Settings.alert -eq $true) { + if ($StateIsCorrect -eq $true) { + Write-LogMessage -API 'Standards' -Message 'SharePoint Sharing Restriction is enabled' -Sev Info + } else { + Write-LogMessage -API 'Standards' -Message 'SharePoint Sharing Restriction is not enabled' -Sev Alert + } + } + + if ($Settings.report -eq $true) { + Add-CIPPBPAField -FieldName 'DirectSharing' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $tenant + } +} diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDisallowInfectedFiles.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDisallowInfectedFiles.ps1 new file mode 100644 index 000000000000..42f3498e4a3e --- /dev/null +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDisallowInfectedFiles.ps1 @@ -0,0 +1,66 @@ +function Invoke-CIPPStandardSPDisallowInfectedFiles { + <# + .FUNCTIONALITY + Internal + .COMPONENT + (APIName) SPDisallowInfectedFiles + .SYNOPSIS + Disallow downloading infected files from SharePoint + .DESCRIPTION + (Helptext) Ensure Office 365 SharePoint infected files are disallowed for download + (DocsDescription) Ensure Office 365 SharePoint infected files are disallowed for download + .NOTES + CAT + SharePoint Standards + TAG + "lowimpact" + "CIS" + ADDEDCOMPONENT + LABEL + Disallow downloading infected files from SharePoint + IMPACT + Low Impact + POWERSHELLEQUIVALENT + Set-SPOTenant -DisallowInfectedFileDownload $true + RECOMMENDEDBY + "CIS 3.0" + UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + #> + + param($Tenant, $Settings) + $CurrentState = Get-CIPPSPOTenant -TenantFilter $Tenant | + Select-Object -Property DisallowInfectedFileDownload + + $StateIsCorrect = ($CurrentState.DisallowInfectedFileDownload -eq $true) + + if ($Settings.remediate -eq $true) { + if ($StateIsCorrect -eq $true) { + Write-LogMessage -API 'Standards' -Message 'Downloading Sharepoint infected files are already disallowed.' -Sev Info + } else { + $Properties = @{ + DisallowInfectedFileDownload = $true + } + + try { + Get-CIPPSPOTenant -TenantFilter $Tenant | Set-CIPPSPOTenant -Properties $Properties + Write-LogMessage -API 'Standards' -Message 'Successfully disallowed downloading SharePoint infected files.' -Sev Info + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API 'Standards' -Message "Failed to disallow downloading Sharepoint infected files. Error: $ErrorMessage" -Sev Error + } + } + } + + if ($Settings.alert -eq $true) { + if ($StateIsCorrect -eq $true) { + Write-LogMessage -API 'Standards' -Message 'Downloading Sharepoint infected files are disallowed.' -Sev Info + } else { + Write-LogMessage -API 'Standards' -Message 'Downloading Sharepoint infected files are allowed.' -Sev Alert + } + } + + if ($Settings.report -eq $true) { + Add-CIPPBPAField -FieldName 'SPDisallowInfectedFiles' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant + } +} diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPEmailAttestation.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPEmailAttestation.ps1 new file mode 100644 index 000000000000..140c65607780 --- /dev/null +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPEmailAttestation.ps1 @@ -0,0 +1,69 @@ +function Invoke-CIPPStandardSPEmailAttestation { + <# + .FUNCTIONALITY + Internal + .COMPONENT + (APIName) SPEmailAttestation + .SYNOPSIS + Require reauthentication with verification code + .DESCRIPTION + (Helptext) Ensure reauthentication with verification code is restricted + (DocsDescription) Ensure reauthentication with verification code is restricted + .NOTES + CAT + SharePoint Standards + TAG + "mediumimpact" + "CIS" + ADDEDCOMPONENT + {"type":"number","name":"standards.SPEmailAttestation.Days","label":"Require reauth every X Days (Default 15)"} + LABEL + Require reauthentication with verification code + IMPACT + Medium Impact + POWERSHELLEQUIVALENT + Set-SPOTenant -EmailAttestationRequired $true -EmailAttestationReAuthDays 15 + RECOMMENDEDBY + "CIS 3.0" + UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + #> + + param($Tenant, $Settings) + $CurrentState = Get-CIPPSPOTenant -TenantFilter $Tenant | + Select-Object -Property EmailAttestationReAuthDays, EmailAttestationRequired + + $StateIsCorrect = ($CurrentState.EmailAttestationReAuthDays -eq $Settings.Days) -and + ($CurrentState.EmailAttestationRequired -eq $true) + + if ($Settings.remediate -eq $true) { + if ($StateIsCorrect -eq $true) { + Write-LogMessage -API 'Standards' -Message 'Sharepoint reauthentication with verification code is already restriction.' -Sev Info + } else { + $Properties = @{ + EmailAttestationReAuthDays = $Settings.Days + EmailAttestationRequired = $true + } + + try { + Get-CIPPSPOTenant -TenantFilter $Tenant | Set-CIPPSPOTenant -Properties $Properties + Write-LogMessage -API 'Standards' -Message 'Successfully set reauthentication with verification code restriction.' -Sev Info + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API 'Standards' -Message "Failed to set reauthentication with verification code restriction. Error: $ErrorMessage" -Sev Error + } + } + } + + if ($Settings.alert -eq $true) { + if ($StateIsCorrect -eq $true) { + Write-LogMessage -API 'Standards' -Message 'Reauthentication with verification code is restriction' -Sev Info + } else { + Write-LogMessage -API 'Standards' -Message 'Reauthentication with verification code is not restricted' -Sev Alert + } + } + + if ($Settings.report -eq $true) { + Add-CIPPBPAField -FieldName 'SPEmailAttestation' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant + } +} diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPExternalUserExpiration.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPExternalUserExpiration.ps1 new file mode 100644 index 000000000000..f05818c0d120 --- /dev/null +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPExternalUserExpiration.ps1 @@ -0,0 +1,69 @@ +function Invoke-CIPPStandardSPExternalUserExpiration { + <# + .FUNCTIONALITY + Internal + .COMPONENT + (APIName) SPExternalUserExpiration + .SYNOPSIS + Set guest access to expire automatically + .DESCRIPTION + (Helptext) Ensure guest access to a site or OneDrive will expire automatically + (DocsDescription) Ensure guest access to a site or OneDrive will expire automatically + .NOTES + CAT + SharePoint Standards + TAG + "mediumimpact" + "CIS" + ADDEDCOMPONENT + {"type":"number","name":"standards.SPExternalUserExpiration.Days","label":"Days until expiration (Default 60)"} + LABEL + Set guest access to expire automatically + IMPACT + Medium Impact + POWERSHELLEQUIVALENT + Set-SPOTenant -ExternalUserExpireInDays 30 -ExternalUserExpirationRequired $True + RECOMMENDEDBY + "CIS 3.0" + UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + #> + + param($Tenant, $Settings) + $CurrentState = Get-CIPPSPOTenant -TenantFilter $Tenant | + Select-Object -Property ExternalUserExpireInDays, ExternalUserExpirationRequired + + $StateIsCorrect = ($CurrentState.ExternalUserExpireInDays -eq $Settings.Days) -and + ($CurrentState.ExternalUserExpirationRequired -eq $true) + + if ($Settings.remediate -eq $true) { + if ($StateIsCorrect -eq $true) { + Write-LogMessage -API 'Standards' -Message 'Sharepoint External User Expiration is already enabled.' -Sev Info + } else { + $Properties = @{ + ExternalUserExpireInDays = $Settings.Days + ExternalUserExpirationRequired = $true + } + + try { + Get-CIPPSPOTenant -TenantFilter $Tenant | Set-CIPPSPOTenant -Properties $Properties + Write-LogMessage -API 'Standards' -Message 'Successfully set External User Expiration' -Sev Info + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API 'Standards' -Message "Failed to set External User Expiration. Error: $ErrorMessage" -Sev Error + } + } + } + + if ($Settings.alert -eq $true) { + if ($StateIsCorrect -eq $true) { + Write-LogMessage -API 'Standards' -Message 'External User Expiration is enabled' -Sev Info + } else { + Write-LogMessage -API 'Standards' -Message 'External User Expiration is not enabled' -Sev Alert + } + } + + if ($Settings.report -eq $true) { + Add-CIPPBPAField -FieldName 'ExternalUserExpiration' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant + } +} diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeAttachmentPolicy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeAttachmentPolicy.ps1 index bad693702eb5..dc080914f7b8 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeAttachmentPolicy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeAttachmentPolicy.ps1 @@ -1,8 +1,41 @@ function Invoke-CIPPStandardSafeAttachmentPolicy { - <# - .FUNCTIONALITY - Internal - #> + <# + .FUNCTIONALITY + Internal + .APINAME + SafeAttachmentPolicy + .CAT + Defender Standards + .TAG + "lowimpact" + "CIS" + "mdo_safedocuments" + "mdo_commonattachmentsfilter" + "mdo_safeattachmentpolicy" + .HELPTEXT + This creates a Safe Attachment policy + .ADDEDCOMPONENT + {"type":"Select","label":"Action","name":"standards.SafeAttachmentPolicy.Action","values":[{"label":"Allow","value":"Allow"},{"label":"Block","value":"Block"},{"label":"DynamicDelivery","value":"DynamicDelivery"}]} + {"type":"Select","label":"QuarantineTag","name":"standards.SafeAttachmentPolicy.QuarantineTag","values":[{"label":"AdminOnlyAccessPolicy","value":"AdminOnlyAccessPolicy"},{"label":"DefaultFullAccessPolicy","value":"DefaultFullAccessPolicy"},{"label":"DefaultFullAccessWithNotificationPolicy","value":"DefaultFullAccessWithNotificationPolicy"}]} + {"type":"boolean","label":"Redirect","name":"standards.SafeAttachmentPolicy.Redirect"} + {"type":"input","name":"standards.SafeAttachmentPolicy.RedirectAddress","label":"Redirect Address"} + .LABEL + Default Safe Attachment Policy + .IMPACT + Low Impact + .POWERSHELLEQUIVALENT + Set-SafeAttachmentPolicy or New-SafeAttachmentPolicy + .RECOMMENDEDBY + "CIS" + .DOCSDESCRIPTION + This creates a Safe Attachment policy + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + #> + + + + param($Tenant, $Settings) $PolicyName = 'Default Safe Attachment Policy' @@ -104,3 +137,7 @@ function Invoke-CIPPStandardSafeAttachmentPolicy { } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksPolicy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksPolicy.ps1 index 1e68ef5a2475..92220f54e19c 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksPolicy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksPolicy.ps1 @@ -1,8 +1,39 @@ function Invoke-CIPPStandardSafeLinksPolicy { - <# - .FUNCTIONALITY - Internal - #> + <# + .FUNCTIONALITY + Internal + .APINAME + SafeLinksPolicy + .CAT + Defender Standards + .TAG + "lowimpact" + "CIS" + "mdo_safelinksforemail" + "mdo_safelinksforOfficeApps" + .HELPTEXT + This creates a safelink policy that automatically scans, tracks, and and enables safe links for Email, Office, and Teams for both external and internal senders + .ADDEDCOMPONENT + {"type":"boolean","label":"AllowClickThrough","name":"standards.SafeLinksPolicy.AllowClickThrough"} + {"type":"boolean","label":"DisableUrlRewrite","name":"standards.SafeLinksPolicy.DisableUrlRewrite"} + {"type":"boolean","label":"EnableOrganizationBranding","name":"standards.SafeLinksPolicy.EnableOrganizationBranding"} + .LABEL + Default SafeLinks Policy + .IMPACT + Low Impact + .POWERSHELLEQUIVALENT + Set-SafeLinksPolicy or New-SafeLinksPolicy + .RECOMMENDEDBY + "CIS" + .DOCSDESCRIPTION + This creates a safelink policy that automatically scans, tracks, and and enables safe links for Email, Office, and Teams for both external and internal senders + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + #> + + + + param($Tenant, $Settings) $PolicyName = 'Default SafeLinks Policy' @@ -116,3 +147,7 @@ function Invoke-CIPPStandardSafeLinksPolicy { } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeSendersDisable.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeSendersDisable.ps1 index d70fe30cef99..de39be25829d 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeSendersDisable.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeSendersDisable.ps1 @@ -2,7 +2,33 @@ function Invoke-CIPPStandardSafeSendersDisable { <# .FUNCTIONALITY Internal + .APINAME + SafeSendersDisable + .CAT + Exchange Standards + .TAG + "mediumimpact" + .HELPTEXT + Loops through all users and removes the Safe Senders list. This is to prevent SPF bypass attacks, as the Safe Senders list is not checked by SPF. + .ADDEDCOMPONENT + .DISABLEDFEATURES + + .LABEL + Remove Safe Senders to prevent SPF bypass + .IMPACT + Medium Impact + .POWERSHELLEQUIVALENT + Set-MailboxJunkEmailConfiguration + .RECOMMENDEDBY + .DOCSDESCRIPTION + Loops through all users and removes the Safe Senders list. This is to prevent SPF bypass attacks, as the Safe Senders list is not checked by SPF. + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) If ($Settings.remediate -eq $true) { @@ -36,3 +62,7 @@ function Invoke-CIPPStandardSafeSendersDisable { } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSecurityDefaults.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSecurityDefaults.ps1 index 07bc25df5021..d563d8c1fd4e 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSecurityDefaults.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSecurityDefaults.ps1 @@ -2,7 +2,33 @@ function Invoke-CIPPStandardSecurityDefaults { <# .FUNCTIONALITY Internal + .APINAME + SecurityDefaults + .CAT + Entra (AAD) Standards + .TAG + "highimpact" + .HELPTEXT + Enables security defaults for the tenant, for newer tenants this is enabled by default. Do not enable this feature if you use Conditional Access. + .DOCSDESCRIPTION + Enables SD for the tenant, which disables all forms of basic authentication and enforces users to configure MFA. Users are only prompted for MFA when a logon is considered 'suspect' by Microsoft. + .ADDEDCOMPONENT + .LABEL + Enable Security Defaults + .IMPACT + High Impact + .POWERSHELLEQUIVALENT + [Read more here](https://www.cyberdrain.com/automating-with-powershell-enabling-secure-defaults-and-sd-explained/) + .RECOMMENDEDBY + .DOCSDESCRIPTION + Enables security defaults for the tenant, for newer tenants this is enabled by default. Do not enable this feature if you use Conditional Access. + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $SecureDefaultsState = (New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/policies/identitySecurityDefaultsEnforcementPolicy' -tenantid $tenant) @@ -36,3 +62,7 @@ function Invoke-CIPPStandardSecurityDefaults { Add-CIPPBPAField -FieldName 'SecurityDefaults' -FieldValue $SecureDefaultsState.IsEnabled -StoreAs bool -Tenant $tenant } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSendFromAlias.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSendFromAlias.ps1 index ce7d56f76454..14b551316e11 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSendFromAlias.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSendFromAlias.ps1 @@ -2,7 +2,33 @@ function Invoke-CIPPStandardSendFromAlias { <# .FUNCTIONALITY Internal + .APINAME + SendFromAlias + .CAT + Exchange Standards + .TAG + "mediumimpact" + .HELPTEXT + Enables the ability for users to send from their alias addresses. + .DOCSDESCRIPTION + Allows users to change the 'from' address to any set in their Azure AD Profile. + .ADDEDCOMPONENT + .LABEL + Allow users to send from their alias addresses + .IMPACT + Medium Impact + .POWERSHELLEQUIVALENT + Set-Mailbox + .RECOMMENDEDBY + .DOCSDESCRIPTION + Enables the ability for users to send from their alias addresses. + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $CurrentInfo = (New-ExoRequest -tenantid $Tenant -cmdlet 'Get-OrganizationConfig').SendFromAliasEnabled @@ -33,3 +59,7 @@ function Invoke-CIPPStandardSendFromAlias { Add-CIPPBPAField -FieldName 'SendFromAlias' -FieldValue $CurrentInfo -StoreAs bool -Tenant $tenant } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSendReceiveLimitTenant.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSendReceiveLimitTenant.ps1 index 68c7519f5e25..a9ba445828ed 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSendReceiveLimitTenant.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSendReceiveLimitTenant.ps1 @@ -2,7 +2,33 @@ function Invoke-CIPPStandardSendReceiveLimitTenant { <# .FUNCTIONALITY Internal + .APINAME + SendReceiveLimitTenant + .CAT + Exchange Standards + .TAG + "lowimpact" + .HELPTEXT + Sets the Send and Receive limits for new users. Valid values are 1MB to 150MB + .ADDEDCOMPONENT + {"type":"number","name":"standards.SendReceiveLimitTenant.SendLimit","label":"Send limit in MB (Default is 35)","default":35} + {"type":"number","name":"standards.SendReceiveLimitTenant.ReceiveLimit","label":"Receive Limit in MB (Default is 36)","default":36} + .LABEL + Set send/receive size limits + .IMPACT + Low Impact + .POWERSHELLEQUIVALENT + Set-MailboxPlan + .RECOMMENDEDBY + .DOCSDESCRIPTION + Sets the Send and Receive limits for new users. Valid values are 1MB to 150MB + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) # Input validation @@ -62,3 +88,7 @@ function Invoke-CIPPStandardSendReceiveLimitTenant { Add-CIPPBPAField -FieldName 'SendReceiveLimit' -FieldValue $NotSetCorrectly -StoreAs json -Tenant $tenant } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardShortenMeetings.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardShortenMeetings.ps1 index f6605904777f..3fe19fbad3c4 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardShortenMeetings.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardShortenMeetings.ps1 @@ -2,7 +2,34 @@ function Invoke-CIPPStandardShortenMeetings { <# .FUNCTIONALITY Internal + .APINAME + ShortenMeetings + .CAT + Exchange Standards + .TAG + "mediumimpact" + .HELPTEXT + Sets the shorten meetings settings on a tenant level. This will shorten meetings by the selected amount of minutes. Valid values are 0 to 29. Short meetings are under 60 minutes, long meetings are over 60 minutes. + .ADDEDCOMPONENT + {"type":"Select","label":"Select value","name":"standards.ShortenMeetings.ShortenEventScopeDefault","values":[{"label":"Disabled/None","value":"None"},{"label":"End early","value":"EndEarly"},{"label":"Start late","value":"StartLate"}]} + {"type":"number","name":"standards.ShortenMeetings.DefaultMinutesToReduceShortEventsBy","label":"Minutes to reduce short calendar events by (Default is 5)","default":5} + {"type":"number","name":"standards.ShortenMeetings.DefaultMinutesToReduceLongEventsBy","label":"Minutes to reduce long calendar events by (Default is 10)","default":10} + .LABEL + Set shorten meetings state + .IMPACT + Medium Impact + .POWERSHELLEQUIVALENT + Set-OrganizationConfig -ShortenEventScopeDefault -DefaultMinutesToReduceShortEventsBy -DefaultMinutesToReduceLongEventsBy + .RECOMMENDEDBY + .DOCSDESCRIPTION + Sets the shorten meetings settings on a tenant level. This will shorten meetings by the selected amount of minutes. Valid values are 0 to 29. Short meetings are under 60 minutes, long meetings are over 60 minutes. + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) # Input validation @@ -55,3 +82,7 @@ function Invoke-CIPPStandardShortenMeetings { } } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpoofWarn.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpoofWarn.ps1 index af9e03c6122c..17e4762e44f8 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpoofWarn.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpoofWarn.ps1 @@ -2,7 +2,36 @@ function Invoke-CIPPStandardSpoofWarn { <# .FUNCTIONALITY Internal + .APINAME + SpoofWarn + .CAT + Exchange Standards + .TAG + "lowimpact" + "CIS" + .HELPTEXT + Adds or removes indicators to e-mail messages received from external senders in Outlook. Works on all Outlook clients/OWA + .DOCSDESCRIPTION + Adds or removes indicators to e-mail messages received from external senders in Outlook. You can read more about this feature on [Microsoft's Exchange Team Blog.](https://techcommunity.microsoft.com/t5/exchange-team-blog/native-external-sender-callouts-on-email-in-outlook/ba-p/2250098) + .ADDEDCOMPONENT + {"type":"Select","label":"Select value","name":"standards.SpoofWarn.state","values":[{"label":"Enabled","value":"enabled"},{"label":"Disabled","value":"disabled"}]} + .LABEL + Enable or disable 'external' warning in Outlook + .IMPACT + Low Impact + .POWERSHELLEQUIVALENT + et-ExternalInOutlook –Enabled $true or $false + .RECOMMENDEDBY + "CIS" + .DOCSDESCRIPTION + Adds or removes indicators to e-mail messages received from external senders in Outlook. Works on all Outlook clients/OWA + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $CurrentInfo = (New-ExoRequest -tenantid $Tenant -cmdlet 'Get-ExternalInOutlook') @@ -45,3 +74,7 @@ function Invoke-CIPPStandardSpoofWarn { } } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTAP.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTAP.ps1 index ce9a3c95ef62..fc53152bb426 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTAP.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTAP.ps1 @@ -2,7 +2,34 @@ function Invoke-CIPPStandardTAP { <# .FUNCTIONALITY Internal + .APINAME + TAP + .CAT + Entra (AAD) Standards + .TAG + "lowimpact" + .HELPTEXT + Enables TAP and sets the default TAP lifetime to 1 hour. This configuration also allows you to select is a TAP is single use or multi-logon. + .DOCSDESCRIPTION + Enables Temporary Password generation for the tenant. + .ADDEDCOMPONENT + {"type":"Select","label":"Select TAP Lifetime","name":"standards.TAP.config","values":[{"label":"Only Once","value":"true"},{"label":"Multiple Logons","value":"false"}]} + .LABEL + Enable Temporary Access Passwords + .IMPACT + Low Impact + .POWERSHELLEQUIVALENT + Update-MgBetaPolicyAuthenticationMethodPolicyAuthenticationMethodConfiguration + .RECOMMENDEDBY + .DOCSDESCRIPTION + Enables TAP and sets the default TAP lifetime to 1 hour. This configuration also allows you to select is a TAP is single use or multi-logon. + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $CurrentInfo = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/policies/authenticationmethodspolicy/authenticationMethodConfigurations/TemporaryAccessPass' -tenantid $Tenant @@ -13,7 +40,7 @@ function Invoke-CIPPStandardTAP { } # Input validation - if (([string]::IsNullOrWhiteSpace($Settings.state) -or $Settings.state -eq 'Select a value') -and ($Settings.remediate -eq $true -or $Settings.alert -eq $true)) { + if (([string]::IsNullOrWhiteSpace($Settings.config) -or $Settings.config -eq 'Select a value') -and ($Settings.remediate -eq $true -or $Settings.alert -eq $true)) { Write-LogMessage -API 'Standards' -tenant $tenant -message 'TAP: Invalid state parameter set' -sev Error Return } @@ -34,3 +61,7 @@ function Invoke-CIPPStandardTAP { } } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsMeetingsByDefault.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsMeetingsByDefault.ps1 index 615dd6e94237..7945d9e4a2f3 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsMeetingsByDefault.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsMeetingsByDefault.ps1 @@ -2,7 +2,32 @@ function Invoke-CIPPStandardTeamsMeetingsByDefault { <# .FUNCTIONALITY Internal + .APINAME + TeamsMeetingsByDefault + .CAT + Exchange Standards + .TAG + "lowimpact" + .HELPTEXT + Sets the default state for automatically turning meetings into Teams meetings for the tenant. This can be overridden by the user in Outlook. + .ADDEDCOMPONENT + {"type":"Select","label":"Select value","name":"standards.TeamsMeetingsByDefault.state","values":[{"label":"Enabled","value":"true"},{"label":"Disabled","value":"false"}]} + .LABEL + Set Teams Meetings by default state + .IMPACT + Low Impact + .POWERSHELLEQUIVALENT + Set-OrganizationConfig -OnlineMeetingsByDefaultEnabled + .RECOMMENDEDBY + .DOCSDESCRIPTION + Sets the default state for automatically turning meetings into Teams meetings for the tenant. This can be overridden by the user in Outlook. + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $CurrentState = (New-ExoRequest -tenantid $Tenant -cmdlet 'Get-OrganizationConfig').OnlineMeetingsByDefaultEnabled @@ -45,3 +70,7 @@ function Invoke-CIPPStandardTeamsMeetingsByDefault { } } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTenantDefaultTimezone.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTenantDefaultTimezone.ps1 index c2651ae346a9..180b8323ca23 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTenantDefaultTimezone.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTenantDefaultTimezone.ps1 @@ -2,8 +2,33 @@ function Invoke-CIPPStandardTenantDefaultTimezone { <# .FUNCTIONALITY Internal + .APINAME + TenantDefaultTimezone + .CAT + SharePoint Standards + .TAG + "lowimpact" + .HELPTEXT + Sets the default timezone for the tenant. This will be used for all new users and sites. + .ADDEDCOMPONENT + {"type":"TimezoneSelect","name":"standards.TenantDefaultTimezone.Timezone","label":"Timezone"} + .LABEL + Set Default Timezone for Tenant + .IMPACT + Low Impact + .POWERSHELLEQUIVALENT + Update-MgBetaAdminSharepointSetting + .RECOMMENDEDBY + .DOCSDESCRIPTION + Sets the default timezone for the tenant. This will be used for all new users and sites. + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $CurrentState = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/admin/sharepoint/settings' -tenantid $Tenant -AsApp $true @@ -43,3 +68,7 @@ function Invoke-CIPPStandardTenantDefaultTimezone { } } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUndoOauth.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUndoOauth.ps1 index 51cef2225307..f662a8f200f3 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUndoOauth.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUndoOauth.ps1 @@ -2,7 +2,31 @@ function Invoke-CIPPStandardUndoOauth { <# .FUNCTIONALITY Internal + .APINAME + UndoOauth + .CAT + Entra (AAD) Standards + .TAG + "highimpact" + .HELPTEXT + Disables App consent and set to Allow user consent for apps + .ADDEDCOMPONENT + .LABEL + Undo App Consent Standard + .IMPACT + High Impact + .POWERSHELLEQUIVALENT + Update-MgPolicyAuthorizationPolicy + .RECOMMENDEDBY + .DOCSDESCRIPTION + Disables App consent and set to Allow user consent for apps + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $CurrentState = New-GraphGetRequest -tenantid $Tenant -Uri 'https://graph.microsoft.com/beta/policies/authorizationPolicy/authorizationPolicy?$select=permissionGrantPolicyIdsAssignedToDefaultUserRole' $State = if ($CurrentState.permissionGrantPolicyIdsAssignedToDefaultUserRole -eq 'ManagePermissionGrantsForSelf.microsoft-user-default-legacy') { $true } else { $false } @@ -36,3 +60,7 @@ function Invoke-CIPPStandardUndoOauth { Add-CIPPBPAField -FieldName 'UndoOauth' -FieldValue $State -StoreAs bool -Tenant $tenant } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserReportDestinationEmail.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserReportDestinationEmail.ps1 new file mode 100644 index 000000000000..98466f02cec4 --- /dev/null +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserReportDestinationEmail.ps1 @@ -0,0 +1,79 @@ +function Invoke-CIPPStandardUserReportDestinationEmail { + <# + .FUNCTIONALITY + Internal + .APINAME + UserReportDestinationEmail + .CAT + Exchange Standards + .TAG + "mediumimpact" + .HELPTEXT + Sets the destination for email when users report them as spam or phishing. Works well together with the 'Set the state of the built-in Report button in Outlook standard'. + .ADDEDCOMPONENT + {"type":"input","name":"standards.UserReportDestinationEmail.Email","label":"Destination email address"} + .LABEL + Set the destination email for user reported emails + .IMPACT + Medium Impact + .POWERSHELLEQUIVALENT + New-ReportSubmissionRule or Set-ReportSubmissionRule + .RECOMMENDEDBY + .DOCSDESCRIPTION + Sets the destination for email when users report them as spam or phishing. Works well together with the 'Set the state of the built-in Report button in Outlook standard'. + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + #> + + + param($Tenant, $Settings) + + # Input validation + if (([string]::IsNullOrWhiteSpace($Settings.Email) -or $Settings.Email -eq 'Select a value' -or $Settings.Email -notmatch '@') -and + ($Settings.remediate -eq $true -or $Settings.alert -eq $true)) { + Write-LogMessage -API 'Standards' -tenant $tenant -message 'UserReportDestinationEmail: Invalid Email parameter set' -sev Error + Return + } + + $CurrentState = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-ReportSubmissionRule' + $StateIsCorrect = if ($CurrentState.SentTo -eq $Settings.Email) { $true } else { $false } + + # Write-Host 'Current State:' + # Write-Host (ConvertTo-Json -InputObject $CurrentState -Depth 5) + # Write-Host 'State is correct: ' $StateIsCorrect + + If ($Settings.remediate -eq $true) { + Write-Host 'Time to remediate!' + + if ($StateIsCorrect -eq $false) { + try { + if ($null -eq $CurrentState) { + New-ExoRequest -tenantid $Tenant -cmdlet 'New-ReportSubmissionRule' -cmdParams @{ Name = 'DefaultReportSubmissionRule'; ReportSubmissionPolicy = 'DefaultReportSubmissionPolicy'; SentTo = ($Settings.Email.Trim()); } -UseSystemMailbox $true + Write-LogMessage -API 'Standards' -tenant $tenant -message "User Report Destination Email set to $($Settings.Email)." -sev Info + } else { + New-ExoRequest -tenantid $Tenant -cmdlet 'Set-ReportSubmissionRule' -cmdParams @{ Identity = $CurrentState.Identity; SentTo = ($Settings.Email.Trim()) } -UseSystemMailbox $true + Write-LogMessage -API 'Standards' -tenant $tenant -message "User Report Destination Email set to $($Settings.Email)." -sev Info + } + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API 'Standards' -tenant $tenant -message "Could not set User Report Destination Email to $($Settings.Email). Error: $ErrorMessage" -sev Error + } + } else { + Write-LogMessage -API 'Standards' -tenant $tenant -message "User Report Destination Email is already set to $($Settings.Email)." -sev Info + } + } + + if ($Settings.alert -eq $true) { + if ($StateIsCorrect) { + Write-LogMessage -API 'Standards' -tenant $tenant -message "User Report Destination Email is set to $($Settings.Email)." -sev Info + } else { + Write-LogMessage -API 'Standards' -tenant $tenant -message "User Report Destination Email is not set to $($Settings.Email)." -sev Alert + } + } + + if ($Settings.report -eq $true) { + Add-CIPPBPAField -FieldName 'UserReportDestinationEmail' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $tenant + } +} + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserSubmissions.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserSubmissions.ps1 index 0d20abaeb57a..4e1c15e55651 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserSubmissions.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserSubmissions.ps1 @@ -2,7 +2,34 @@ function Invoke-CIPPStandardUserSubmissions { <# .FUNCTIONALITY Internal + .APINAME + UserSubmissions + .CAT + Exchange Standards + .TAG + "mediumimpact" + .HELPTEXT + Set the state of the spam submission button in Outlook + .DOCSDESCRIPTION + Set the state of the built-in Report button in Outlook. This gives the users the ability to report emails as spam or phish. + .ADDEDCOMPONENT + {"type":"Select","label":"Select value","name":"standards.UserSubmissions.state","values":[{"label":"Enabled","value":"enable"},{"label":"Disabled","value":"disable"}]} + .LABEL + Set the state of the built-in Report button in Outlook + .IMPACT + Medium Impact + .POWERSHELLEQUIVALENT + New-ReportSubmissionPolicy or Set-ReportSubmissionPolicy + .RECOMMENDEDBY + .DOCSDESCRIPTION + Set the state of the spam submission button in Outlook + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $Policy = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-ReportSubmissionPolicy' @@ -73,3 +100,7 @@ function Invoke-CIPPStandardUserSubmissions { } } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardallowOAuthTokens.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardallowOAuthTokens.ps1 index a5f43f175998..1c26284c9315 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardallowOAuthTokens.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardallowOAuthTokens.ps1 @@ -2,7 +2,33 @@ function Invoke-CIPPStandardallowOAuthTokens { <# .FUNCTIONALITY Internal + .APINAME + allowOAuthTokens + .CAT + Entra (AAD) Standards + .TAG + "lowimpact" + .HELPTEXT + Allows you to use any software OAuth token generator + .DOCSDESCRIPTION + Enables OTP Software OAuth tokens for the tenant. This allows users to use OTP codes generated via software, like a password manager to be used as an authentication method. + .ADDEDCOMPONENT + .LABEL + Enable OTP Software OAuth tokens + .IMPACT + Low Impact + .POWERSHELLEQUIVALENT + Update-MgBetaPolicyAuthenticationMethodPolicyAuthenticationMethodConfiguration + .RECOMMENDEDBY + .DOCSDESCRIPTION + Allows you to use any software OAuth token generator + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $CurrentInfo = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies/authenticationMethodsPolicy/authenticationMethodConfigurations/softwareOath' -tenantid $Tenant @@ -39,3 +65,7 @@ function Invoke-CIPPStandardallowOAuthTokens { } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardallowOTPTokens.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardallowOTPTokens.ps1 index 8459fce1aadc..57dd7c7adb49 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardallowOTPTokens.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardallowOTPTokens.ps1 @@ -2,7 +2,33 @@ function Invoke-CIPPStandardallowOTPTokens { <# .FUNCTIONALITY Internal + .APINAME + allowOTPTokens + .CAT + Entra (AAD) Standards + .TAG + "lowimpact" + .HELPTEXT + Allows you to use MS authenticator OTP token generator + .DOCSDESCRIPTION + Allows you to use Microsoft Authenticator OTP token generator. Useful for using the NPS extension as MFA on VPN clients. + .ADDEDCOMPONENT + .LABEL + Enable OTP via Authenticator + .IMPACT + Low Impact + .POWERSHELLEQUIVALENT + Update-MgBetaPolicyAuthenticationMethodPolicyAuthenticationMethodConfiguration + .RECOMMENDEDBY + .DOCSDESCRIPTION + Allows you to use MS authenticator OTP token generator + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $CurrentInfo = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies/authenticationMethodsPolicy/authenticationMethodConfigurations/microsoftAuthenticator' -tenantid $Tenant @@ -27,3 +53,7 @@ function Invoke-CIPPStandardallowOTPTokens { } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardcalDefault.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardcalDefault.ps1 index 491d35ab7eb5..e716d72e8651 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardcalDefault.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardcalDefault.ps1 @@ -2,7 +2,36 @@ function Invoke-CIPPStandardcalDefault { <# .FUNCTIONALITY Internal + .APINAME + calDefault + .CAT + Exchange Standards + .TAG + "lowimpact" + .HELPTEXT + Sets the default sharing level for the default calendar, for all users + .DOCSDESCRIPTION + Sets the default sharing level for the default calendar for all users in the tenant. You can read about the different sharing levels [here.](https://learn.microsoft.com/en-us/powershell/module/exchange/set-mailboxfolderpermission?view=exchange-ps#-accessrights) + .DISABLEDFEATURES + + .ADDEDCOMPONENT + {"type":"Select","label":"Select Sharing Level","name":"standards.calDefault.permissionlevel","values":[{"label":"Owner - The user can create, read, edit, and delete all items in the folder, and create subfolders. The user is both folder owner and folder contact.","value":"Owner"},{"label":"Publishing Editor - The user can create, read, edit, and delete all items in the folder, and create subfolders.","value":"PublishingEditor"},{"label":"Editor - The user can create items in the folder. The contents of the folder do not appear.","value":"Editor"},{"label":"Publishing Author. The user can read, create all items/subfolders. Can modify and delete only items they create.","value":"PublishingAuthor"},{"label":"Author - The user can create and read items, and modify and delete items that they create.","value":"Author"},{"label":"Non Editing Author - The user has full read access and create items. Can can delete only own items.","value":"NonEditingAuthor"},{"label":"Reviewer - The user can read all items in the folder.","value":"Reviewer"},{"label":"Contributor - The user can create items and folders.","value":"Contributor"},{"label":"Availability Only - Indicates that the user can view only free/busy time within the calendar.","value":"AvailabilityOnly"},{"label":"Limited Details - The user can view free/busy time within the calendar and the subject and location of appointments.","value":"LimitedDetails"},{"label":"None - The user has no permissions on the folder.","value":"none"}]} + .LABEL + Set Sharing Level for Default calendar + .IMPACT + Low Impact + .POWERSHELLEQUIVALENT + Set-MailboxFolderPermission + .RECOMMENDEDBY + .DOCSDESCRIPTION + Sets the default sharing level for the default calendar, for all users + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings, $QueueItem) # Input validation @@ -77,3 +106,7 @@ function Invoke-CIPPStandardcalDefault { Write-LogMessage -API 'Standards' -tenant $Tenant -message "Successfully set default calendar permissions for $SuccessCounter out of $TotalMailboxes mailboxes." -sev Info } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandarddisableMacSync.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandarddisableMacSync.ps1 index b096ade25384..42f8977f066e 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandarddisableMacSync.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandarddisableMacSync.ps1 @@ -2,7 +2,31 @@ function Invoke-CIPPStandarddisableMacSync { <# .FUNCTIONALITY Internal + .APINAME + disableMacSync + .CAT + SharePoint Standards + .TAG + "highimpact" + .HELPTEXT + Disables the ability for Mac devices to sync with OneDrive. + .ADDEDCOMPONENT + .LABEL + Do not allow Mac devices to sync using OneDrive + .IMPACT + High Impact + .POWERSHELLEQUIVALENT + Update-MgAdminSharepointSetting + .RECOMMENDEDBY + .DOCSDESCRIPTION + Disables the ability for Mac devices to sync with OneDrive. + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $CurrentInfo = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/admin/sharepoint/settings' -tenantid $Tenant -AsApp $true @@ -36,3 +60,7 @@ function Invoke-CIPPStandarddisableMacSync { Add-CIPPBPAField -FieldName 'MacSync' -FieldValue $CurrentInfo.isMacSyncAppEnabled -StoreAs bool -Tenant $tenant } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneBrandingProfile.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneBrandingProfile.ps1 index c049ebb95749..1a15822c6e38 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneBrandingProfile.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneBrandingProfile.ps1 @@ -1,8 +1,42 @@ function Invoke-CIPPStandardintuneBrandingProfile { - <# - .FUNCTIONALITY - Internal - #> + <# + .FUNCTIONALITY + Internal + .APINAME + intuneBrandingProfile + .CAT + Intune Standards + .TAG + "lowimpact" + .HELPTEXT + Sets the branding profile for the Intune Company Portal app. This is a tenant wide setting and overrules any settings set on the app level. + .ADDEDCOMPONENT + {"type":"input","name":"standards.intuneBrandingProfile.displayName","label":"Organization name"} + {"type":"boolean","name":"standards.intuneBrandingProfile.showLogo","label":"Show logo"} + {"type":"boolean","name":"standards.intuneBrandingProfile.showDisplayNameNextToLogo","label":"Show organization name next to logo"} + {"type":"input","name":"standards.intuneBrandingProfile.contactITName","label":"Contact IT name"} + {"type":"input","name":"standards.intuneBrandingProfile.contactITPhoneNumber","label":"Contact IT phone number"} + {"type":"input","name":"standards.intuneBrandingProfile.contactITEmailAddress","label":"Contact IT email address"} + {"type":"input","name":"standards.intuneBrandingProfile.contactITNotes","label":"Contact IT notes"} + {"type":"input","name":"standards.intuneBrandingProfile.onlineSupportSiteName","label":"Online support site name"} + {"type":"input","name":"standards.intuneBrandingProfile.onlineSupportSiteUrl","label":"Online support site URL"} + {"type":"input","name":"standards.intuneBrandingProfile.privacyUrl","label":"Privacy statement URL"} + .LABEL + Set Intune Company Portal branding profile + .IMPACT + Low Impact + .POWERSHELLEQUIVALENT + Graph API + .RECOMMENDEDBY + .DOCSDESCRIPTION + Sets the branding profile for the Intune Company Portal app. This is a tenant wide setting and overrules any settings set on the app level. + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + #> + + + + param($Tenant, $Settings) $CurrentState = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/deviceManagement/intuneBrandingProfiles/c3a59481-1bf2-46ce-94b3-66eec07a8d60/' -tenantid $Tenant -AsApp $true @@ -65,3 +99,7 @@ function Invoke-CIPPStandardintuneBrandingProfile { Add-CIPPBPAField -FieldName 'intuneBrandingProfile' -FieldValue [bool]$StateIsCorrect -StoreAs bool -Tenant $tenant } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneDeviceReg.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneDeviceReg.ps1 index e261dbfb859c..9dbcdbba602c 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneDeviceReg.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneDeviceReg.ps1 @@ -2,7 +2,32 @@ function Invoke-CIPPStandardintuneDeviceReg { <# .FUNCTIONALITY Internal + .APINAME + intuneDeviceReg + .CAT + Intune Standards + .TAG + "mediumimpact" + .HELPTEXT + sets the maximum number of devices that can be registered by a user. A value of 0 disables device registration by users + .ADDEDCOMPONENT + {"type":"number","name":"standards.intuneDeviceReg.max","label":"Maximum devices (Enter 2147483647 for unlimited.)"} + .LABEL + Set Maximum Number of Devices per user + .IMPACT + Medium Impact + .POWERSHELLEQUIVALENT + Update-MgBetaPolicyDeviceRegistrationPolicy + .RECOMMENDEDBY + .DOCSDESCRIPTION + sets the maximum number of devices that can be registered by a user. A value of 0 disables device registration by users + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $PreviousSetting = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies/deviceRegistrationPolicy' -tenantid $Tenant $StateIsCorrect = if ($PreviousSetting.userDeviceQuota -eq $Settings.max) { $true } else { $false } @@ -38,3 +63,7 @@ function Invoke-CIPPStandardintuneDeviceReg { Add-CIPPBPAField -FieldName 'intuneDeviceReg' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $tenant } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneDeviceRetirementDays.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneDeviceRetirementDays.ps1 index b150c84e2f0a..1ea419e2639d 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneDeviceRetirementDays.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneDeviceRetirementDays.ps1 @@ -2,7 +2,32 @@ function Invoke-CIPPStandardintuneDeviceRetirementDays { <# .FUNCTIONALITY Internal + .APINAME + intuneDeviceRetirementDays + .CAT + Intune Standards + .TAG + "lowimpact" + .HELPTEXT + A value between 0 and 270 is supported. A value of 0 disables retirement, retired devices are removed from Intune after the specified number of days. + .ADDEDCOMPONENT + {"type":"number","name":"standards.intuneDeviceRetirementDays.days","label":"Maximum days (0 equals disabled)"} + .LABEL + Set inactive device retirement days + .IMPACT + Low Impact + .POWERSHELLEQUIVALENT + Graph API + .RECOMMENDEDBY + .DOCSDESCRIPTION + A value between 0 and 270 is supported. A value of 0 disables retirement, retired devices are removed from Intune after the specified number of days. + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $CurrentInfo = (New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/deviceManagement/managedDeviceCleanupSettings' -tenantid $Tenant) @@ -38,3 +63,7 @@ function Invoke-CIPPStandardintuneDeviceRetirementDays { Add-CIPPBPAField -FieldName 'intuneDeviceRetirementDays' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $tenant } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneRequireMFA.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneRequireMFA.ps1 index 9698085e6cb7..79c0d352d1c9 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneRequireMFA.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneRequireMFA.ps1 @@ -2,7 +2,30 @@ function Invoke-CIPPStandardintuneRequireMFA { <# .FUNCTIONALITY Internal + .APINAME + intuneRequireMFA + .CAT + Intune Standards + .TAG + "mediumimpact" + .HELPTEXT + Requires MFA for all users to register devices with Intune. This is useful when not using Conditional Access. + .LABEL + Require Multifactor Authentication to register or join devices with Microsoft Entra + .IMPACT + Medium Impact + .POWERSHELLEQUIVALENT + Update-MgBetaPolicyDeviceRegistrationPolicy + .RECOMMENDEDBY + .DOCSDESCRIPTION + Requires MFA for all users to register devices with Intune. This is useful when not using Conditional Access. + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $PreviousSetting = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies/deviceRegistrationPolicy' -tenantid $Tenant @@ -37,3 +60,7 @@ function Invoke-CIPPStandardintuneRequireMFA { Add-CIPPBPAField -FieldName 'intuneRequireMFA' -FieldValue $RequireMFA -StoreAs bool -Tenant $tenant } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardlaps.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardlaps.ps1 index 84f24cffa73d..b159a5d5796f 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardlaps.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardlaps.ps1 @@ -2,7 +2,33 @@ function Invoke-CIPPStandardlaps { <# .FUNCTIONALITY Internal + .APINAME + laps + .CAT + Entra (AAD) Standards + .TAG + "lowimpact" + .HELPTEXT + Enables the tenant to use LAPS. You must still create a policy for LAPS to be active on all devices. Use the template standards to deploy this by default. + .DOCSDESCRIPTION + Enables the LAPS functionality on the tenant. Prerequisite for using Windows LAPS via Azure AD. + .ADDEDCOMPONENT + .LABEL + Enable LAPS on the tenant + .IMPACT + Low Impact + .POWERSHELLEQUIVALENT + Portal or Graph API + .RECOMMENDEDBY + .DOCSDESCRIPTION + Enables the tenant to use LAPS. You must still create a policy for LAPS to be active on all devices. Use the template standards to deploy this by default. + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $PreviousSetting = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies/deviceRegistrationPolicy' -tenantid $Tenant @@ -36,3 +62,7 @@ function Invoke-CIPPStandardlaps { Add-CIPPBPAField -FieldName 'laps' -FieldValue $PreviousSetting.localAdminPassword.isEnabled -StoreAs bool -Tenant $tenant } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardsharingCapability.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardsharingCapability.ps1 index 95dfcec5a26d..1f0f3f7e9f5d 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardsharingCapability.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardsharingCapability.ps1 @@ -2,7 +2,34 @@ function Invoke-CIPPStandardsharingCapability { <# .FUNCTIONALITY Internal + .APINAME + sharingCapability + .CAT + SharePoint Standards + .TAG + "highimpact" + "CIS" + .HELPTEXT + Sets the default sharing level for OneDrive and Sharepoint. This is a tenant wide setting and overrules any settings set on the site level + .ADDEDCOMPONENT + {"type":"Select","label":"Select Sharing Level","name":"standards.sharingCapability.Level","values":[{"label":"Users can share only with people in the organization. No external sharing is allowed.","value":"disabled"},{"label":"Users can share with new and existing guests. Guests must sign in or provide a verification code.","value":"externalUserSharingOnly"},{"label":"Users can share with anyone by using links that do not require sign-in.","value":"externalUserAndGuestSharing"},{"label":"Users can share with existing guests (those already in the directory of the organization).","value":"existingExternalUserSharingOnly"}]} + .LABEL + Set Sharing Level for OneDrive and Sharepoint + .IMPACT + High Impact + .POWERSHELLEQUIVALENT + Update-MgBetaAdminSharepointSetting + .RECOMMENDEDBY + "CIS" + .DOCSDESCRIPTION + Sets the default sharing level for OneDrive and Sharepoint. This is a tenant wide setting and overrules any settings set on the site level + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $CurrentInfo = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/admin/sharepoint/settings' -tenantid $Tenant -AsApp $true @@ -43,3 +70,7 @@ function Invoke-CIPPStandardsharingCapability { } } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardsharingDomainRestriction.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardsharingDomainRestriction.ps1 index 9c7e7d11f555..c148a249f8e0 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardsharingDomainRestriction.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardsharingDomainRestriction.ps1 @@ -1,8 +1,35 @@ function Invoke-CIPPStandardsharingDomainRestriction { - <# - .FUNCTIONALITY - Internal - #> + <# + .FUNCTIONALITY + Internal + .APINAME + sharingDomainRestriction + .CAT + SharePoint Standards + .TAG + "highimpact" + "CIS" + .HELPTEXT + Restricts sharing to only users with the specified domain. This is useful for organizations that only want to share with their own domain. + .ADDEDCOMPONENT + {"type":"Select","name":"standards.sharingDomainRestriction.Mode","label":"Limit external sharing by domains","values":[{"label":"Off","value":"none"},{"label":"Restirct sharing to specific domains","value":"allowList"},{"label":"Block sharing to specific domains","value":"blockList"}]} + {"type":"input","name":"standards.sharingDomainRestriction.Domains","label":"Domains to allow/block, comma separated"} + .LABEL + Restrict sharing to a specific domain + .IMPACT + High Impact + .POWERSHELLEQUIVALENT + Update-MgAdminSharepointSetting + .RECOMMENDEDBY + .DOCSDESCRIPTION + Restricts sharing to only users with the specified domain. This is useful for organizations that only want to share with their own domain. + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + #> + + + + param($Tenant, $Settings) $CurrentState = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/admin/sharepoint/settings' -tenantid $Tenant -AsApp $true @@ -61,3 +88,7 @@ function Invoke-CIPPStandardsharingDomainRestriction { Add-CIPPBPAField -FieldName 'sharingDomainRestriction' -FieldValue [bool]$StateIsCorrect -StoreAs bool -Tenant $tenant } } + + + + diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardunmanagedSync.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardunmanagedSync.ps1 index 8a234d8eff2a..cc7f13e36fa8 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardunmanagedSync.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardunmanagedSync.ps1 @@ -2,7 +2,31 @@ function Invoke-CIPPStandardunmanagedSync { <# .FUNCTIONALITY Internal + .APINAME + unmanagedSync + .CAT + SharePoint Standards + .TAG + "highimpact" + .HELPTEXT + The unmanaged Sync standard has been temporarily disabled and does nothing. + .ADDEDCOMPONENT + .LABEL + Only allow users to sync OneDrive from AAD joined devices + .IMPACT + High Impact + .POWERSHELLEQUIVALENT + Update-MgAdminSharepointSetting + .RECOMMENDEDBY + .DOCSDESCRIPTION + The unmanaged Sync standard has been temporarily disabled and does nothing. + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + + + + param($Tenant, $Settings) $CurrentInfo = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/admin/sharepoint/settings' -tenantid $Tenant -AsApp $true @@ -35,3 +59,7 @@ function Invoke-CIPPStandardunmanagedSync { Add-CIPPBPAField -FieldName 'unmanagedSync' -FieldValue $CurrentInfo.isUnmanagedSyncAppForTenantRestricted -StoreAs bool -Tenant $tenant } } + + + + diff --git a/Modules/CIPPCore/Public/Webhooks/Invoke-CIPPWebhookProcessing.ps1 b/Modules/CIPPCore/Public/Webhooks/Invoke-CIPPWebhookProcessing.ps1 index d5db2cf47076..a600f7e41ede 100644 --- a/Modules/CIPPCore/Public/Webhooks/Invoke-CIPPWebhookProcessing.ps1 +++ b/Modules/CIPPCore/Public/Webhooks/Invoke-CIPPWebhookProcessing.ps1 @@ -10,13 +10,7 @@ function Invoke-CippWebhookProcessing { $ExecutingUser ) - <# $ExtendedPropertiesIgnoreList = @( - 'OAuth2:Authorize' - 'OAuth2:Token' - 'SAS:EndAuth' - 'SAS:ProcessAuth' - 'Login:reprocess' - ) #> + Write-Host "Received data. Our Action List is $($data.CIPPAction)" $ActionList = ($data.CIPPAction | ConvertFrom-Json -ErrorAction SilentlyContinue).value diff --git a/Modules/CIPPCore/Public/Webhooks/Test-CIPPAuditLogRules.ps1 b/Modules/CIPPCore/Public/Webhooks/Test-CIPPAuditLogRules.ps1 index dd3158876945..39b8957832c6 100644 --- a/Modules/CIPPCore/Public/Webhooks/Test-CIPPAuditLogRules.ps1 +++ b/Modules/CIPPCore/Public/Webhooks/Test-CIPPAuditLogRules.ps1 @@ -51,11 +51,13 @@ function Test-CIPPAuditLogRules { try { if ($Data.ExtendedProperties) { $Data.CIPPExtendedProperties = ($Data.ExtendedProperties | ConvertTo-Json) - if ($Data.CIPPExtendedProperties.RequestType -in $ExtendedPropertiesIgnoreList) { - Write-Information 'No need to process this operation as its in our ignore list' - continue + $Data.ExtendedProperties | ForEach-Object { + if ($_.Value -in $ExtendedPropertiesIgnoreList) { + Write-Information 'No need to process this operation as its in our ignore list' + continue + } + $Data | Add-Member -NotePropertyName $_.Name -NotePropertyValue $_.Value -Force -ErrorAction SilentlyContinue } - $Data.ExtendedProperties | ForEach-Object { $Data | Add-Member -NotePropertyName $_.Name -NotePropertyValue $_.Value -Force -ErrorAction SilentlyContinue } } if ($Data.DeviceProperties) { $Data.CIPPDeviceProperties = ($Data.DeviceProperties | ConvertTo-Json) @@ -187,4 +189,4 @@ function Test-CIPPAuditLogRules { $Results.DataToProcess = $DataToProcess } $Results -} \ No newline at end of file +} diff --git a/Modules/CippExtensions/CippExtensions.psm1 b/Modules/CippExtensions/CippExtensions.psm1 index ad69dcdfdb5f..d2bab13c84b9 100644 --- a/Modules/CippExtensions/CippExtensions.psm1 +++ b/Modules/CippExtensions/CippExtensions.psm1 @@ -1,7 +1,6 @@ -$Public = @(Get-ChildItem -Path $PSScriptRoot\Public\*.ps1 -ErrorAction SilentlyContinue) -$Private = @(Get-ChildItem -Path $PSScriptRoot\private\*.ps1 -ErrorAction SilentlyContinue) -$NinjaOne = @(Get-ChildItem -Path $PSScriptRoot\NinjaOne\*.ps1 -ErrorAction SilentlyContinue) -$Functions = $Public + $Private + $NinjaOne +$Public = @(Get-ChildItem -Path $PSScriptRoot\Public\*.ps1 -Recurse -ErrorAction SilentlyContinue) +$Private = @(Get-ChildItem -Path $PSScriptRoot\Private\*.ps1 -Recurse -ErrorAction SilentlyContinue) +$Functions = $Public + $Private foreach ($import in @($Functions)) { try { . $import.FullName diff --git a/Modules/CippExtensions/NinjaOne/Get-NinjaOneFieldMapping.ps1 b/Modules/CippExtensions/NinjaOne/Get-NinjaOneFieldMapping.ps1 deleted file mode 100644 index 8be773c2d030..000000000000 --- a/Modules/CippExtensions/NinjaOne/Get-NinjaOneFieldMapping.ps1 +++ /dev/null @@ -1,101 +0,0 @@ -function Get-NinjaOneFieldMapping { - [CmdletBinding()] - param ( - $CIPPMapping - ) - try { - #Get available mappings - $Mappings = [pscustomobject]@{} - - [System.Collections.Generic.List[PSCustomObject]]$CIPPFields = @( - [PSCustomObject]@{ - InternalName = 'TenantLinks' - Description = 'Microsoft 365 Tenant Links - Field Used to Display Links to Microsoft 365 Portals and CIPP' - Scope = 'Organization' - Type = 'WYSIWYG' - }, - [PSCustomObject]@{ - InternalName = 'TenantSummary' - Description = 'Microsoft 365 Tenant Summary - Field Used to Display Tenant Summary Information' - Scope = 'Organization' - Type = 'WYSIWYG' - }, - [PSCustomObject]@{ - InternalName = 'UsersSummary' - Description = 'Microsoft 365 Users Summary - Field Used to Display User Summary Information' - Scope = 'Organization' - Type = 'WYSIWYG' - }, - [PSCustomObject]@{ - InternalName = 'DeviceLinks' - Description = 'Microsoft 365 Device Links - Field Used to Display Links to Microsoft 365 Portals and CIPP' - Scope = 'Device' - Type = 'WYSIWYG' - }, - [PSCustomObject]@{ - InternalName = 'DeviceSummary' - Description = 'Microsoft 365 Device Summary - Field Used to Display Device Summary Information' - Scope = 'Device' - Type = 'WYSIWYG' - }, - [PSCustomObject]@{ - InternalName = 'DeviceCompliance' - Description = 'Intune Device Compliance Status - Field Used to Monitor Device Compliance' - Scope = 'Device' - Type = 'TEXT' - } - ) - - $Filter = "PartitionKey eq 'NinjaFieldMapping'" - Get-AzDataTableEntity @CIPPMapping -Filter $Filter | ForEach-Object { - $Mappings | Add-Member -NotePropertyName $_.RowKey -NotePropertyValue @{ label = "$($_.NinjaOneName)"; value = "$($_.NinjaOne)" } - } - - - $Table = Get-CIPPTable -TableName Extensionsconfig - $Configuration = ((Get-AzDataTableEntity @Table).config | ConvertFrom-Json -ea stop).NinjaOne - - - - $Token = Get-NinjaOneToken -configuration $Configuration - - $NinjaCustomFieldsNodeRaw = (Invoke-WebRequest -uri "https://$($Configuration.Instance)/api/v2/device-custom-fields?scopes=node" -Method GET -Headers @{Authorization = "Bearer $($token.access_token)" } -ContentType 'application/json').content | ConvertFrom-Json -depth 100 - [System.Collections.Generic.List[PSCustomObject]]$NinjaCustomFieldsNode = $NinjaCustomFieldsNodeRaw | Where-Object { $_.apiPermission -eq 'READ_WRITE' -and $_.type -in $CIPPFields.Type } | Select-Object @{n = 'name'; e = { $_.label } }, @{n = 'value'; e = { $_.name } }, type - - $NinjaCustomFieldsOrgRaw = (Invoke-WebRequest -uri "https://$($Configuration.Instance)/api/v2/device-custom-fields?scopes=organization" -Method GET -Headers @{Authorization = "Bearer $($token.access_token)" } -ContentType 'application/json').content | ConvertFrom-Json -depth 100 - [System.Collections.Generic.List[PSCustomObject]]$NinjaCustomFieldsOrg = $NinjaCustomFieldsOrgRaw | Where-Object { $_.apiPermission -eq 'READ_WRITE' -and $_.type -in $CIPPFields.Type } | Select-Object @{n = 'name'; e = { $_.label } }, @{n = 'value'; e = { $_.name } }, type - - if ($Null -eq $NinjaCustomFieldsNode){ - [System.Collections.Generic.List[PSCustomObject]]$NinjaCustomFieldsNode = @() - } - - if ($Null -eq $NinjaCustomFieldsOrg){ - [System.Collections.Generic.List[PSCustomObject]]$NinjaCustomFieldsOrg = @() - } - - } catch { - [System.Collections.Generic.List[PSCustomObject]]$NinjaCustomFieldsNode = @() - [System.Collections.Generic.List[PSCustomObject]]$NinjaCustomFieldsOrg = @() - } - - $DoNotSync = [PSCustomObject]@{ - name = '--- Do not synchronize ---' - value = $null - type = 'unset' - } - - $NinjaCustomFieldsOrg.Insert(0, $DoNotSync) - $NinjaCustomFieldsNode.Insert(0, $DoNotSync) - - - $MappingObj = [PSCustomObject]@{ - CIPPOrgFields = $CIPPFields | Where-Object { $_.Scope -eq 'Organization' } - CIPPNodeFields = @($CIPPFields | Where-Object { $_.Scope -eq 'Device' }) - NinjaOrgFields = @($NinjaCustomFieldsOrg) - NinjaNodeFields = @($NinjaCustomFieldsNode) - Mappings = $Mappings - } - - return $MappingObj - -} \ No newline at end of file diff --git a/Modules/CippExtensions/Private/Get-StringHash.ps1 b/Modules/CippExtensions/Private/Get-StringHash.ps1 new file mode 100644 index 000000000000..a5c94f62b511 --- /dev/null +++ b/Modules/CippExtensions/Private/Get-StringHash.ps1 @@ -0,0 +1,8 @@ +function Get-StringHash { + Param($String) + $StringBuilder = New-Object System.Text.StringBuilder + [System.Security.Cryptography.HashAlgorithm]::Create('SHA1').ComputeHash([System.Text.Encoding]::UTF8.GetBytes($String)) | ForEach-Object { + [Void]$StringBuilder.Append($_.ToString('x2')) + } + $StringBuilder.ToString() +} \ No newline at end of file diff --git a/Modules/CippExtensions/Private/Hudu/Get-HuduFormattedBlock.ps1 b/Modules/CippExtensions/Private/Hudu/Get-HuduFormattedBlock.ps1 new file mode 100644 index 000000000000..c27d9656c819 --- /dev/null +++ b/Modules/CippExtensions/Private/Hudu/Get-HuduFormattedBlock.ps1 @@ -0,0 +1,12 @@ +function Get-HuduFormattedBlock ($Heading, $Body) { + return @" +
+
+

$Heading

+
+
+ $Body +
+
+"@ +} diff --git a/Modules/CippExtensions/Private/Hudu/Get-HuduFormattedField.ps1 b/Modules/CippExtensions/Private/Hudu/Get-HuduFormattedField.ps1 new file mode 100644 index 000000000000..7aa73e9a3521 --- /dev/null +++ b/Modules/CippExtensions/Private/Hudu/Get-HuduFormattedField.ps1 @@ -0,0 +1,13 @@ +function Get-HuduFormattedField ($Title, $Value) { + return @" +
+
+ $Title +
+
+ $Value +
+
+"@ +} + diff --git a/Modules/CippExtensions/Private/Hudu/Get-HuduLinkBlock.ps1 b/Modules/CippExtensions/Private/Hudu/Get-HuduLinkBlock.ps1 new file mode 100644 index 000000000000..5afbaf90de19 --- /dev/null +++ b/Modules/CippExtensions/Private/Hudu/Get-HuduLinkBlock.ps1 @@ -0,0 +1,3 @@ +function Get-HuduLinkBlock($URL, $Icon, $Title) { + return "" +} diff --git a/Modules/CippExtensions/Public/Extension Functions/Add-HuduAssetLayoutM365Field.ps1 b/Modules/CippExtensions/Public/Extension Functions/Add-HuduAssetLayoutM365Field.ps1 new file mode 100644 index 000000000000..5ab07cbc3887 --- /dev/null +++ b/Modules/CippExtensions/Public/Extension Functions/Add-HuduAssetLayoutM365Field.ps1 @@ -0,0 +1,28 @@ +function Add-HuduAssetLayoutM365Field { + Param( + $AssetLayoutId + ) + + $M365Field = @{ + position = 0 + label = 'Microsoft 365' + field_type = 'RichText' + show_in_list = $false + required = $false + expiration = $false + } + + $AssetLayout = Get-HuduAssetLayouts -LayoutId $AssetLayoutId + + if ($AssetLayout.fields.label -contains 'Microsoft 365') { + return $AssetLayout + } + + $AssetLayoutFields = [System.Collections.Generic.List[object]]::new() + $AssetLayoutFields.Add($M365Field) + foreach ($Field in $AssetLayout.fields) { + $Field.position++ + $AssetLayoutFields.Add($Field) + } + Set-HuduAssetLayout -Id $AssetLayoutId -Fields $AssetLayoutFields +} diff --git a/Modules/CippExtensions/Public/Extension Functions/Get-ExtensionCacheData.ps1 b/Modules/CippExtensions/Public/Extension Functions/Get-ExtensionCacheData.ps1 new file mode 100644 index 000000000000..f360a3a873bb --- /dev/null +++ b/Modules/CippExtensions/Public/Extension Functions/Get-ExtensionCacheData.ps1 @@ -0,0 +1,14 @@ +function Get-ExtensionCacheData { + param( + $TenantFilter + ) + + $Table = Get-CIPPTable -TableName CacheExtensionSync + $CacheData = Get-CIPPAzDataTableEntity @Table -Filter "PartitionKey eq '$TenantFilter'" + + $Return = @{} + foreach ($Data in $CacheData) { + $Return[$Data.RowKey] = $Data.Data | ConvertFrom-Json -ErrorAction SilentlyContinue + } + return [PSCustomObject]$Return +} diff --git a/Modules/CippExtensions/Public/Extension Functions/Get-ExtensionMapping.ps1 b/Modules/CippExtensions/Public/Extension Functions/Get-ExtensionMapping.ps1 new file mode 100644 index 000000000000..6a0ac35728c6 --- /dev/null +++ b/Modules/CippExtensions/Public/Extension Functions/Get-ExtensionMapping.ps1 @@ -0,0 +1,15 @@ +function Get-ExtensionMapping { + param( + $Extension + ) + + $Table = Get-CIPPTable -TableName CippMapping + $Mapping = @{} + Get-CIPPAzDataTableEntity @Table -Filter "PartitionKey eq '$($Extension)Mapping'" | ForEach-Object { + $Mapping[$_.RowKey] = @{ + label = "$($_.IntegrationName)" + value = "$($_.IntegrationId)" + } + } + return [PSCustomObject]$Mapping +} \ No newline at end of file diff --git a/Modules/CippExtensions/Public/Extension Functions/Push-CippExtensionData.ps1 b/Modules/CippExtensions/Public/Extension Functions/Push-CippExtensionData.ps1 new file mode 100644 index 000000000000..95e74b54838b --- /dev/null +++ b/Modules/CippExtensions/Public/Extension Functions/Push-CippExtensionData.ps1 @@ -0,0 +1,18 @@ +function Push-CippExtensionData { + param( + $TenantFilter, + $Extension + ) + + $Table = Get-CIPPTable -TableName Extensionsconfig + $Config = (Get-CIPPAzDataTableEntity @Table).config | ConvertFrom-Json -ea stop + + switch ($Extension) { + 'Hudu' { + if ($Config.Hudu.Enabled) { + Write-Host 'Perfoming Hudu Extension Sync...' + Invoke-HuduExtensionSync -Configuration $Config.Hudu -TenantFilter $TenantFilter + } + } + } +} diff --git a/Modules/CippExtensions/Public/Extension Functions/Register-CippExtensionScheduledTasks.ps1 b/Modules/CippExtensions/Public/Extension Functions/Register-CippExtensionScheduledTasks.ps1 new file mode 100644 index 000000000000..ba3ba64a9f42 --- /dev/null +++ b/Modules/CippExtensions/Public/Extension Functions/Register-CippExtensionScheduledTasks.ps1 @@ -0,0 +1,125 @@ +function Register-CIPPExtensionScheduledTasks { + Param( + [switch]$Reschedule + ) + + # get extension configuration and mappings table + $Table = Get-CIPPTable -TableName Extensionsconfig + $Config = ((Get-CIPPAzDataTableEntity @Table).config | ConvertFrom-Json -ea stop) + $MappingsTable = Get-CIPPTable -TableName CippMapping + + # Get existing scheduled usertasks + $ScheduledTasksTable = Get-CIPPTable -TableName ScheduledTasks + $ScheduledTasks = Get-CIPPAzDataTableEntity @ScheduledTasksTable -Filter 'Hidden eq true' | Where-Object { $_.Command -match 'Sync-CippExtensionData' } + $PushTasks = Get-CIPPAzDataTableEntity @ScheduledTasksTable -Filter 'Hidden eq true' | Where-Object { $_.Command -match 'Push-CippExtensionData' } + $Tenants = Get-Tenants -IncludeErrors + + $Extensions = @('Hudu') + $MappedTenants = [System.Collections.Generic.List[string]]::new() + foreach ($Extension in $Extensions) { + $ExtensionConfig = $Config.$Extension + if ($ExtensionConfig.Enabled -eq $true) { + $Mappings = Get-CIPPAzDataTableEntity @MappingsTable -Filter "PartitionKey eq '$($Extension)Mapping'" + $FieldMapping = Get-CIPPAzDataTableEntity @MappingsTable -Filter "PartitionKey eq '$($Extension)FieldMapping'" + $FieldSync = @{} + $SyncTypes = [System.Collections.Generic.List[string]]::new() + + foreach ($Mapping in $FieldMapping) { + $FieldSync[$Mapping.RowKey] = !([string]::IsNullOrEmpty($Mapping.IntegrationId)) + } + + $SyncTypes.Add('Overview') + $SyncTypes.Add('Groups') + + if ($FieldSync.Users) { + $SyncTypes.Add('Users') + $SyncTypes.Add('Mailboxes') + } + if ($FieldSync.Devices) { + $SyncTypes.Add('Devices') + } + + foreach ($Mapping in $Mappings) { + $Tenant = $Tenants | Where-Object { $_.customerId -eq $Mapping.RowKey } + if (!$Tenant) { + Write-Warning "Tenant $($Mapping.RowKey) not found" + continue + } + $MappedTenants.Add($Tenant.defaultDomainName) + foreach ($SyncType in $SyncTypes) { + $ExistingTask = $ScheduledTasks | Where-Object { $_.Tenant -eq $Tenant.defaultDomainName -and $_.SyncType -eq $SyncType } + if (!$ExistingTask -or $Reschedule.IsPresent) { + $unixtime = [int64](([datetime]::UtcNow) - (Get-Date '1/1/1970')).TotalSeconds + $Task = @{ + Name = "Extension Sync - $SyncType" + Command = @{ + value = 'Sync-CippExtensionData' + label = 'Sync-CippExtensionData' + } + Parameters = @{ + TenantFilter = $Tenant.defaultDomainName + SyncType = $SyncType + } + Recurrence = '1d' + ScheduledTime = $unixtime + TenantFilter = $Tenant.defaultDomainName + } + if ($ExistingTask) { + $Task.RowKey = $ExistingTask.RowKey + } + $null = Add-CIPPScheduledTask -Task $Task -hidden $true -SyncType $SyncType + Write-Information "Creating $SyncType task for tenant $($Tenant.defaultDomainName)" + } + } + + $ExistingPushTask = $PushTasks | Where-Object { $_.Tenant -eq $Tenant.defaultDomainName -and $_.SyncType -eq $Extension } + if (!$ExistingPushTask -or $Reschedule.IsPresent) { + # push cached data to extension + $in30mins = [int64](([datetime]::UtcNow.AddMinutes(30)) - (Get-Date '1/1/1970')).TotalSeconds + $Task = @{ + Name = "$Extension Extension Sync" + Command = @{ + value = 'Push-CippExtensionData' + label = 'Push-CippExtensionData' + } + Parameters = @{ + TenantFilter = $Tenant.defaultDomainName + Extension = $Extension + } + Recurrence = '1d' + ScheduledTime = $in30mins + TenantFilter = $Tenant.defaultDomainName + } + if ($ExistingPushTask) { + $Task.RowKey = $ExistingTask.RowKey + } + $null = Add-CIPPScheduledTask -Task $Task -hidden $true -SyncType $Extension + Write-Information "Creating $Extension task for tenant $($Tenant.defaultDomainName)" + } + } + } else { + # remove existing scheduled tasks + $PushTasks | Where-Object { $_.SyncType -eq $Extension } | ForEach-Object { + Write-Information "Extension Disabled: Cleaning up scheduled task $($_.Name) for tenant $($_.Tenant)" + $Entity = $_ | Select-Object -Property PartitionKey, RowKey + Remove-AzDataTableEntity @ScheduledTasksTable -Entity $Entity + } + } + } + $MappedTenants = $MappedTenants | Sort-Object -Unique + + foreach ($Task in $ScheduledTasks) { + if ($Task.Tenant -notin $MappedTenants) { + Write-Information "Tenant Removed: Cleaning up scheduled task $($Task.Name) for tenant $($Task.TenantFilter)" + $Entity = $Task | Select-Object -Property PartitionKey, RowKey + Remove-AzDataTableEntity @ScheduledTasksTable -Entity $Entity + } + } + foreach ($Task in $PushTasks) { + if ($Task.Tenant -notin $MappedTenants) { + Write-Information "Tenant Removed: Cleaning up scheduled task $($Task.Name) for tenant $($Task.TenantFilter)" + $Entity = $Task | Select-Object -Property PartitionKey, RowKey + Remove-AzDataTableEntity @ScheduledTasksTable -Entity $Entity + } + } +} diff --git a/Modules/CippExtensions/Public/Extension Functions/Set-ExtensionFieldMapping.ps1 b/Modules/CippExtensions/Public/Extension Functions/Set-ExtensionFieldMapping.ps1 new file mode 100644 index 000000000000..52d59ab12d77 --- /dev/null +++ b/Modules/CippExtensions/Public/Extension Functions/Set-ExtensionFieldMapping.ps1 @@ -0,0 +1,24 @@ +function Set-ExtensionFieldMapping { + [CmdletBinding()] + param ( + $CIPPMapping, + $Extension, + $APIName, + $Request, + $TriggerMetadata + ) + + foreach ($Mapping in ([pscustomobject]$Request.body.mappings).psobject.properties) { + $AddObject = @{ + PartitionKey = "$($Extension)FieldMapping" + RowKey = "$($mapping.name)" + IntegrationId = "$($mapping.value.value)" + IntegrationName = "$($mapping.value.label)" + } + Add-AzDataTableEntity @CIPPMapping -Entity $AddObject -Force + Write-LogMessage -API $APINAME -user $request.headers.'x-ms-client-principal' -message "Added mapping for $($mapping.name)." -Sev 'Info' + } + $Result = [pscustomobject]@{'Results' = 'Successfully edited mapping table.' } + + Return $Result +} \ No newline at end of file diff --git a/Modules/CippExtensions/Public/Extension Functions/Sync-CippExtensionData.ps1 b/Modules/CippExtensions/Public/Extension Functions/Sync-CippExtensionData.ps1 new file mode 100644 index 000000000000..6a02fdd2c6cb --- /dev/null +++ b/Modules/CippExtensions/Public/Extension Functions/Sync-CippExtensionData.ps1 @@ -0,0 +1,305 @@ +function Sync-CippExtensionData { + <# + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + $TenantFilter, + $SyncType + ) + + $Table = Get-CIPPTable -TableName ExtensionSync + $Extensions = Get-CIPPAzDataTableEntity @Table -Filter "PartitionKey eq '$($SyncType)'" + $LastSync = $Extensions | Where-Object { $_.RowKey -eq $TenantFilter } + $CacheTable = Get-CIPPTable -tablename 'CacheExtensionSync' + + if (!$LastSync) { + $LastSync = @{ + PartitionKey = $SyncType + RowKey = $TenantFilter + Status = 'Not Synced' + Error = '' + LastSync = 'Never' + } + $null = Add-CIPPAzDataTableEntity @Table -Entity $LastSync + } + + try { + switch ($SyncType) { + 'Overview' { + # Build bulk requests array. + [System.Collections.Generic.List[PSCustomObject]]$TenantRequests = @( + @{ + id = 'TenantDetails' + method = 'GET' + url = '/organization' + }, + @{ + id = 'AllRoles' + method = 'GET' + url = '/directoryRoles' + }, + @{ + id = 'Domains' + method = 'GET' + url = '/domains?$top=99' + }, + @{ + id = 'Licenses' + method = 'GET' + url = '/subscribedSkus' + }, + @{ + id = 'ConditionalAccess' + method = 'GET' + url = '/identity/conditionalAccess/policies' + }, + @{ + id = 'SecureScoreControlProfiles' + method = 'GET' + url = '/security/secureScoreControlProfiles?$top=999' + }, + @{ + id = 'Subscriptions' + method = 'GET' + url = '/directory/subscriptions?$top=999' + }, + @{ + id = 'OneDriveUsage' + method = 'GET' + url = "reports/getOneDriveUsageAccountDetail(period='D7')?`$format=application%2fjson" + }, + @{ + id = 'MailboxUsage' + method = 'GET' + url = "reports/getMailboxUsageDetail(period='D7')?`$format=application%2fjson" + } + ) + + $SingleGraphQueries = @(@{ + id = 'SecureScore' + graphRequest = @{ + uri = 'https://graph.microsoft.com/beta/security/secureScores?$top=1' + noPagination = $true + } + }) + $AdditionalRequests = @( + @{ + ParentId = 'AllRoles' + graphRequest = @{ + url = '/directoryRoles/{0}/members?$select=id,displayName,userPrincipalName' + method = 'GET' + } + } + ) + } + 'Users' { + [System.Collections.Generic.List[PSCustomObject]]$TenantRequests = @( + @{ + id = 'Users' + method = 'GET' + url = '/users?$top=999&$select=id,accountEnabled,businessPhones,city,createdDateTime,companyName,country,department,displayName,faxNumber,givenName,isResourceAccount,jobTitle,mail,mailNickname,mobilePhone,onPremisesDistinguishedName,officeLocation,onPremisesLastSyncDateTime,otherMails,postalCode,preferredDataLocation,preferredLanguage,proxyAddresses,showInAddressList,state,streetAddress,surname,usageLocation,userPrincipalName,userType,assignedLicenses,onPremisesSyncEnabled' + } + ) + } + 'Groups' { + [System.Collections.Generic.List[PSCustomObject]]$TenantRequests = @( + @{ + id = 'Groups' + method = 'GET' + url = '/groups?$top=999&$select=id,createdDateTime,displayName,description,mail,mailEnabled,mailNickname,resourceProvisioningOptions,securityEnabled,visibility,organizationId,onPremisesSamAccountName,membershipRule,grouptypes,onPremisesSyncEnabled,resourceProvisioningOptions,userPrincipalName' + } + ) + $AdditionalRequests = @( + @{ + ParentId = 'Groups' + graphRequest = @{ + url = '/groups/{0}/members?$select=id,displayName,userPrincipalName' + method = 'GET' + } + } + ) + } + 'Devices' { + [System.Collections.Generic.List[PSCustomObject]]$TenantRequests = @( + @{ + id = 'Devices' + method = 'GET' + url = '/deviceManagement/managedDevices?$top=999' + }, + @{ + id = 'DeviceCompliancePolicies' + method = 'GET' + url = '/deviceManagement/deviceCompliancePolicies' + }, + @{ + id = 'DeviceApps' + method = 'GET' + url = '/deviceAppManagement/mobileApps' + } + ) + + $AdditionalRequests = @( + @{ + ParentId = 'DeviceCompliancePolicies' + graphRequest = @{ + url = '/deviceManagement/deviceCompliancePolicies/{0}/deviceStatuses?$top=999' + method = 'GET' + } + } + ) + } + 'Mailboxes' { + $Select = 'id,ExchangeGuid,ArchiveGuid,UserPrincipalName,DisplayName,PrimarySMTPAddress,RecipientType,RecipientTypeDetails,EmailAddresses,WhenSoftDeleted,IsInactiveMailbox' + $ExoRequest = @{ + tenantid = $TenantFilter + cmdlet = 'Get-Mailbox' + cmdParams = @{} + Select = $Select + } + $Mailboxes = (New-ExoRequest @ExoRequest) | Select-Object id, ExchangeGuid, ArchiveGuid, WhenSoftDeleted, @{ Name = 'UPN'; Expression = { $_.'UserPrincipalName' } }, + + @{ Name = 'displayName'; Expression = { $_.'DisplayName' } }, + @{ Name = 'primarySmtpAddress'; Expression = { $_.'PrimarySMTPAddress' } }, + @{ Name = 'recipientType'; Expression = { $_.'RecipientType' } }, + @{ Name = 'recipientTypeDetails'; Expression = { $_.'RecipientTypeDetails' } }, + @{ Name = 'AdditionalEmailAddresses'; Expression = { ($_.'EmailAddresses' | Where-Object { $_ -clike 'smtp:*' }).Replace('smtp:', '') -join ', ' } } + + $Entity = @{ + PartitionKey = $TenantFilter + SyncType = 'Mailboxes' + RowKey = 'Mailboxes' + Data = [string]($Mailboxes | ConvertTo-Json -Depth 10 -Compress) + } + $null = Add-CIPPAzDataTableEntity @CacheTable -Entity $Entity -Force + + $SingleGraphQueries = @( + @{ + id = 'CASMailbox' + graphRequest = @{ + uri = "https://outlook.office365.com/adminapi/beta/$($tenantfilter)/CasMailbox" + Tenantid = $tenantfilter + scope = 'ExchangeOnline' + noPagination = $true + } + } + ) + + # Bulk request mailbox permissions using New-ExoBulkRequest for each mailbox - mailboxPermissions is not a valid graph query + $ExoBulkRequests = foreach ($Mailbox in $Mailboxes) { + @{ + CmdletInput = @{ + CmdletName = 'Get-MailboxPermission' + Parameters = @{ Identity = $Mailbox.UPN } + } + } + } + $MailboxPermissions = New-ExoBulkRequest -cmdletArray @($ExoBulkRequests) -tenantid $TenantFilter + $Entity = @{ + PartitionKey = $TenantFilter + SyncType = 'Mailboxes' + RowKey = 'MailboxPermissions' + Data = [string]($MailboxPermissions | ConvertTo-Json -Depth 10 -Compress) + } + $null = Add-CIPPAzDataTableEntity @CacheTable -Entity $Entity -Force + } + } + + if ($TenantRequests) { + Write-Information "Requesting tenant information for $TenantFilter $SyncType" + try { + $TenantResults = New-GraphBulkRequest -Requests @($TenantRequests) -tenantid $TenantFilter + } catch { + Throw "Failed to fetch bulk company data: $_" + } + + $TenantResults | Select-Object id, body | ForEach-Object { + $Data = $_.body.value ?? $_.body + if ($Data -match '^eyJ') { + # base64 decode + $Data = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Data)) | ConvertFrom-Json + $Data = $Data.Value + } + + $Entity = @{ + PartitionKey = $TenantFilter + RowKey = $_.id + SyncType = $SyncType + Data = [string]($Data | ConvertTo-Json -Depth 10 -Compress) + } + $null = Add-CIPPAzDataTableEntity @CacheTable -Entity $Entity -Force + } + + if ($AdditionalRequests) { + foreach ($AdditionalRequest in $AdditionalRequests) { + $ParentId = $AdditionalRequest.ParentId + $GraphRequest = $AdditionalRequest.graphRequest.PSObject.Copy() + $AdditionalRequestQueries = ($TenantResults | Where-Object { $_.id -eq $ParentId }).body.value | ForEach-Object { + if ($_.id) { + [PSCustomObject]@{ + id = $_.id + method = $GraphRequest.method + url = $GraphRequest.url -f $_.id + } + } + } + if (($AdditionalRequestQueries | Measure-Object).Count -gt 0) { + try { + $AdditionalResults = New-GraphBulkRequest -Requests @($AdditionalRequestQueries) -tenantid $TenantFilter + } catch { + throw $_ + } + if ($AdditionalResults) { + $AdditionalResults | ForEach-Object { + $Data = $_.body.value ?? $_.body + if ($Data -match '^eyJ') { + # base64 decode + $Data = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Data)) | ConvertFrom-Json + $Data = $Data.Value + } + $Entity = @{ + PartitionKey = $TenantFilter + SyncType = $SyncType + RowKey = '{0}_{1}' -f $ParentId, $_.id + Data = [string]($Data | ConvertTo-Json -Depth 10 -Compress) + } + try { + $null = Add-CIPPAzDataTableEntity @CacheTable -Entity $Entity -Force + } catch { + throw $_ + } + } + } + + } + } + } + } + + if ($SingleGraphQueries) { + foreach ($SingleGraphQuery in $SingleGraphQueries) { + $Request = $SingleGraphQuery.graphRequest + $Data = New-GraphGetRequest @Request -tenantid $TenantFilter + $Entity = @{ + PartitionKey = $TenantFilter + SyncType = $SyncType + RowKey = $SingleGraphQuery.id + Data = [string]($Data | ConvertTo-Json -Depth 10 -Compress) + } + $null = Add-CIPPAzDataTableEntity @CacheTable -Entity $Entity -Force + } + } + + + $LastSync.LastSync = [datetime]::UtcNow.ToString('yyyy-MM-ddTHH:mm:ssZ') + $LastSync.Status = 'Completed' + $LastSync.Error = '' + } catch { + $LastSync.Status = 'Failed' + $LastSync.Error = [string](Get-CippException -Exception $_ | ConvertTo-Json -Compress) + throw "Failed to sync data: $($_.Exception.Message)" + } finally { + Add-CIPPAzDataTableEntity @Table -Entity $LastSync -Force + } +} diff --git a/Modules/CippExtensions/Private/Get-GradientToken.ps1 b/Modules/CippExtensions/Public/Gradient/Get-GradientToken.ps1 similarity index 100% rename from Modules/CippExtensions/Private/Get-GradientToken.ps1 rename to Modules/CippExtensions/Public/Gradient/Get-GradientToken.ps1 diff --git a/Modules/CippExtensions/Private/New-GradientAlert.ps1 b/Modules/CippExtensions/Public/Gradient/New-GradientAlert.ps1 similarity index 100% rename from Modules/CippExtensions/Private/New-GradientAlert.ps1 rename to Modules/CippExtensions/Public/Gradient/New-GradientAlert.ps1 diff --git a/Modules/CippExtensions/Private/New-GradientServiceSyncRun.ps1 b/Modules/CippExtensions/Public/Gradient/New-GradientServiceSyncRun.ps1 similarity index 100% rename from Modules/CippExtensions/Private/New-GradientServiceSyncRun.ps1 rename to Modules/CippExtensions/Public/Gradient/New-GradientServiceSyncRun.ps1 diff --git a/Modules/CippExtensions/Private/Get-HaloMapping.ps1 b/Modules/CippExtensions/Public/Halo/Get-HaloMapping.ps1 similarity index 68% rename from Modules/CippExtensions/Private/Get-HaloMapping.ps1 rename to Modules/CippExtensions/Public/Halo/Get-HaloMapping.ps1 index fcae99cfd5d1..2a8aae7646ef 100644 --- a/Modules/CippExtensions/Private/Get-HaloMapping.ps1 +++ b/Modules/CippExtensions/Public/Halo/Get-HaloMapping.ps1 @@ -6,16 +6,28 @@ function Get-HaloMapping { #Get available mappings $Mappings = [pscustomobject]@{} + # Migrate legacy mappings $Filter = "PartitionKey eq 'Mapping'" - Get-CIPPAzDataTableEntity @CIPPMapping -Filter $Filter | ForEach-Object { - $Mappings | Add-Member -NotePropertyName $_.RowKey -NotePropertyValue @{ label = "$($_.HaloPSAName)"; value = "$($_.HaloPSA)" } + $MigrateRows = Get-CIPPAzDataTableEntity @CIPPMapping -Filter $Filter | ForEach-Object { + [PSCustomObject]@{ + PartitionKey = 'HaloMapping' + RowKey = $_.RowKey + IntegrationId = $_.HaloPSA + IntegrationName = $_.HaloPSAName + } + Remove-AzDataTableEntity @CIPPMapping -Entity $_ | Out-Null + } + if (($MigrateRows | Measure-Object).Count -gt 0) { + Add-CIPPAzDataTableEntity @CIPPMapping -Entity $MigrateRows -Force } + + $Mappings = Get-ExtensionMapping -Extension 'Halo' + $Tenants = Get-Tenants -IncludeErrors $Table = Get-CIPPTable -TableName Extensionsconfig try { $Configuration = ((Get-CIPPAzDataTableEntity @Table).config | ConvertFrom-Json -ea stop).HaloPSA - $Token = Get-HaloToken -configuration $Configuration $i = 1 $RawHaloClients = do { @@ -32,7 +44,7 @@ function Get-HaloMapping { } Write-LogMessage -Message "Could not get HaloPSA Clients, error: $Message " -Level Error -tenant 'CIPP' -API 'HaloMapping' - $RawHaloClients = @(@{name = "Could not get HaloPSA Clients, error: $Message"; value = '-1' }) + $RawHaloClients = @(@{name = "Could not get HaloPSA Clients, error: $Message"; id = '-1' }) } $HaloClients = $RawHaloClients | ForEach-Object { [PSCustomObject]@{ @@ -41,9 +53,9 @@ function Get-HaloMapping { } } $MappingObj = [PSCustomObject]@{ - Tenants = @($Tenants) - HaloClients = @($HaloClients) - Mappings = $Mappings + Tenants = @($Tenants) + Companies = @($HaloClients) + Mappings = $Mappings } return $MappingObj diff --git a/Modules/CippExtensions/Private/Get-HaloToken.ps1 b/Modules/CippExtensions/Public/Halo/Get-HaloToken.ps1 similarity index 100% rename from Modules/CippExtensions/Private/Get-HaloToken.ps1 rename to Modules/CippExtensions/Public/Halo/Get-HaloToken.ps1 diff --git a/Modules/CippExtensions/Private/New-HaloPSATicket.ps1 b/Modules/CippExtensions/Public/Halo/New-HaloPSATicket.ps1 similarity index 100% rename from Modules/CippExtensions/Private/New-HaloPSATicket.ps1 rename to Modules/CippExtensions/Public/Halo/New-HaloPSATicket.ps1 diff --git a/Modules/CippExtensions/Private/Set-HaloMapping.ps1 b/Modules/CippExtensions/Public/Halo/Set-HaloMapping.ps1 similarity index 72% rename from Modules/CippExtensions/Private/Set-HaloMapping.ps1 rename to Modules/CippExtensions/Public/Halo/Set-HaloMapping.ps1 index 527bbc94fd22..129b1578ad59 100644 --- a/Modules/CippExtensions/Private/Set-HaloMapping.ps1 +++ b/Modules/CippExtensions/Public/Halo/Set-HaloMapping.ps1 @@ -5,20 +5,20 @@ function Set-HaloMapping { $APIName, $Request ) - Get-CIPPAzDataTableEntity @CIPPMapping -Filter "PartitionKey eq 'Mapping'" | ForEach-Object { + Get-CIPPAzDataTableEntity @CIPPMapping -Filter "PartitionKey eq 'HaloMapping'" | ForEach-Object { Remove-AzDataTableEntity @CIPPMapping -Entity $_ } foreach ($Mapping in ([pscustomobject]$Request.body.mappings).psobject.properties) { $AddObject = @{ - PartitionKey = 'Mapping' - RowKey = "$($mapping.name)" - 'HaloPSA' = "$($mapping.value.value)" - 'HaloPSAName' = "$($mapping.value.label)" + PartitionKey = 'HaloMapping' + RowKey = "$($mapping.name)" + IntegrationId = "$($mapping.value.value)" + IntegrationName = "$($mapping.value.label)" } Add-CIPPAzDataTableEntity @CIPPMapping -Entity $AddObject -Force - Write-LogMessage -API $APINAME -user $request.headers.'x-ms-client-principal' -message "Added mapping for $($mapping.name)." -Sev 'Info' + Write-LogMessage -API $APINAME -user $request.headers.'x-ms-client-principal' -message "Added mapping for $($mapping.name)." -Sev 'Info' } $Result = [pscustomobject]@{'Results' = 'Successfully edited mapping table.' } diff --git a/Modules/CippExtensions/Public/Hudu/Connect-HuduAPI.ps1 b/Modules/CippExtensions/Public/Hudu/Connect-HuduAPI.ps1 new file mode 100644 index 000000000000..3117d343ec55 --- /dev/null +++ b/Modules/CippExtensions/Public/Hudu/Connect-HuduAPI.ps1 @@ -0,0 +1,16 @@ +function Connect-HuduAPI { + [CmdletBinding()] + param ( + $Configuration + ) + + if ($env:AzureWebJobsStorage -eq 'UseDevelopmentStorage=true') { + $DevSecretsTable = Get-CIPPTable -tablename 'DevSecrets' + $APIKey = (Get-CIPPAzDataTableEntity @DevSecretsTable -Filter "PartitionKey eq 'Hudu' and RowKey eq 'Hudu'").APIKey + } else { + $null = Connect-AzAccount -Identity + $APIKey = (Get-AzKeyVaultSecret -VaultName $ENV:WEBSITE_DEPLOYMENT_ID -Name 'Hudu' -AsPlainText) + } + New-HuduBaseURL -BaseURL $Configuration.BaseURL + New-HuduAPIKey -ApiKey $APIKey +} \ No newline at end of file diff --git a/Modules/CippExtensions/Public/Hudu/Get-HuduFieldMapping.ps1 b/Modules/CippExtensions/Public/Hudu/Get-HuduFieldMapping.ps1 new file mode 100644 index 000000000000..7004401fd33d --- /dev/null +++ b/Modules/CippExtensions/Public/Hudu/Get-HuduFieldMapping.ps1 @@ -0,0 +1,61 @@ +function Get-HuduFieldMapping { + [CmdletBinding()] + param ( + $CIPPMapping + ) + + $Mappings = Get-ExtensionMapping -Extension 'HuduField' + + $CIPPFieldHeaders = @( + [PSCustomObject]@{ + Title = 'Hudu Asset Layouts' + FieldType = 'Layouts' + Description = 'Use the table below to map your Hudu Asset Layouts to the correct CIPP Data Type. A new Rich Text asset layout field will be created if it does not exist.' + } + ) + $CIPPFields = @( + [PSCustomObject]@{ + FieldName = 'Users' + FieldLabel = 'Asset Layout for M365 Users' + FieldType = 'Layouts' + } + [PSCustomObject]@{ + FieldName = 'Devices' + FieldLabel = 'Asset Layout for M365 Devices' + FieldType = 'Layouts' + } + ) + + $Table = Get-CIPPTable -TableName Extensionsconfig + try { + $Configuration = ((Get-CIPPAzDataTableEntity @Table).config | ConvertFrom-Json -ea stop).Hudu + Connect-HuduAPI -configuration $Configuration + + $AssetLayouts = Get-HuduAssetLayouts | Select-Object @{Name = 'FieldType' ; Expression = { 'Layouts' } }, @{Name = 'value'; Expression = { $_.id } }, name, fields + } catch { + $Message = if ($_.ErrorDetails.Message) { + Get-NormalizedError -Message $_.ErrorDetails.Message + } else { + $_.Exception.message + } + + Write-LogMessage -Message "Could not get Hudu Asset Layouts, error: $Message " -Level Error -tenant 'CIPP' -API 'HuduMapping' + $AssetLayouts = @(@{name = "Could not get Hudu Asset Layouts, error: $Message"; value = '-1' }) + } + + $Unset = [PSCustomObject]@{ + name = '--- Do not synchronize ---' + value = $null + type = 'unset' + } + + $MappingObj = [PSCustomObject]@{ + CIPPFields = $CIPPFields + CIPPFieldHeaders = $CIPPFieldHeaders + IntegrationFields = @($Unset) + @($AssetLayouts) + Mappings = $Mappings + } + + return $MappingObj + +} diff --git a/Modules/CippExtensions/Public/Hudu/Get-HuduMapping.ps1 b/Modules/CippExtensions/Public/Hudu/Get-HuduMapping.ps1 new file mode 100644 index 000000000000..7ffbddfa57a0 --- /dev/null +++ b/Modules/CippExtensions/Public/Hudu/Get-HuduMapping.ps1 @@ -0,0 +1,41 @@ +function Get-HuduMapping { + [CmdletBinding()] + param ( + $CIPPMapping + ) + + $Mappings = Get-ExtensionMapping -Extension 'Hudu' + + $Tenants = Get-Tenants -IncludeErrors + $Table = Get-CIPPTable -TableName Extensionsconfig + try { + $Configuration = ((Get-CIPPAzDataTableEntity @Table).config | ConvertFrom-Json -ea stop).Hudu + + Connect-HuduAPI -configuration $Configuration + $HuduCompanies = Get-HuduCompanies + + } catch { + $Message = if ($_.ErrorDetails.Message) { + Get-NormalizedError -Message $_.ErrorDetails.Message + } else { + $_.Exception.message + } + + Write-LogMessage -Message "Could not get Hudu Companies, error: $Message " -Level Error -tenant 'CIPP' -API 'HuduMapping' + $HuduCompanies = @(@{name = "Could not get Hudu Companies, error: $Message"; value = '-1' }) + } + $HuduCompanies = $HuduCompanies | ForEach-Object { + [PSCustomObject]@{ + name = $_.name + value = "$($_.id)" + } + } + $MappingObj = [PSCustomObject]@{ + Tenants = @($Tenants) + Companies = @($HuduCompanies) + Mappings = $Mappings + } + + return $MappingObj + +} diff --git a/Modules/CippExtensions/Public/Hudu/Invoke-HuduExtensionSync.ps1 b/Modules/CippExtensions/Public/Hudu/Invoke-HuduExtensionSync.ps1 new file mode 100644 index 000000000000..80f92e6ad662 --- /dev/null +++ b/Modules/CippExtensions/Public/Hudu/Invoke-HuduExtensionSync.ps1 @@ -0,0 +1,917 @@ +function Invoke-HuduExtensionSync { + <# + .FUNCTIONALITY + Internal + #> + Param( + $Configuration, + $TenantFilter + ) + Connect-HuduAPI -configuration $Configuration + + # Get mapping configuration + $MappingTable = Get-CIPPTable -TableName 'CippMapping' + $Mappings = Get-CIPPAzDataTableEntity @MappingTable -Filter "PartitionKey eq 'HuduMapping' or PartitionKey eq 'HuduFieldMapping'" + #Write-Host ($Mappings | ConvertTo-Json) + $defaultdomain = $TenantFilter + $Tenant = Get-Tenants -IncludeErrors | Where-Object { $_.defaultDomainName -eq $TenantFilter } + $TenantMap = $Mappings | Where-Object { $_.RowKey -eq $Tenant.customerId } + + $HuduAssetCache = Get-CippTable -tablename 'CacheHuduAssets' + + if (!$TenantMap) { + return 'Tenant not found in mapping table' + } + + $CompanyResult = [PSCustomObject]@{ + Name = $Tenant.displayName + Users = 0 + Devices = 0 + Errors = [System.Collections.Generic.List[string]]@() + } + + $PeopleLayoutId = $Mappings | Where-Object { $_.RowKey -eq 'Users' } | Select-Object -ExpandProperty IntegrationId + $CreateUsers = $Configuration.CreateMissingUsers + $DeviceLayoutId = $Mappings | Where-Object { $_.RowKey -eq 'Devices' } | Select-Object -ExpandProperty IntegrationId + $CreateDevices = $Configuration.CreateMissingDevices + + $null = Add-HuduAssetLayoutM365Field -AssetLayoutId $PeopleLayoutId + $null = Add-HuduAssetLayoutM365Field -AssetLayoutId $DeviceLayoutId + + $importDomains = $false + #$monitorDomains = [System.Convert]::ToBoolean($env:monitorDomains) + $IntuneDesktopDeviceTypes = $env:IntuneDesktopDeviceTypes -split ',' + $ExcludeSerials = $env:ExcludeSerials -split ',' + + Set-Location (Get-Item $PSScriptRoot).Parent.Parent.Parent.Parent.FullName + $LicTable = Import-Csv Conversiontable.csv + + #$AssignedMap = Get-AssignedMap + #$AssignedNameMap = Get-AssignedNameMap + + $EnableCIPP = $true + + $ConfigTable = Get-Cipptable -tablename 'Config' + $Config = Get-CippAzDataTableEntity @ConfigTable -Filter "PartitionKey eq 'InstanceProperties' and RowKey eq 'CIPPURL'" + $CIPPURL = 'https://{0}' -f $Config.Value + + $ExtensionCache = Get-ExtensionCacheData -TenantFilter $Tenant.defaultDomainName + + try { + $company_id = $TenantMap.IntegrationId + + $PeopleLayout = Get-HuduAssetLayouts -Id $PeopleLayoutId + $People = Get-HuduAssets -CompanyId $company_id -AssetLayoutId $PeopleLayout.id + + $DesktopsLayout = Get-HuduAssetLayouts -Id $DeviceLayoutId + $HuduDesktopDevices = Get-HuduAssets -CompanyId $company_id -AssetLayoutId $DesktopsLayout.id + + $HuduRelations = Get-HuduRelations + + $HuduDevices = $HuduDesktopDevices + + $CustomerLinks = "
+
+
+
+
+
+
+
" + + + #$Users = Get-BulkResultByID -Results $TenantResults -ID 'Users' + $Users = $ExtensionCache.Users + $licensedUsers = $Users | Where-Object { $null -ne $_.assignedLicenses.skuId } | Sort-Object userPrincipalName + + $CompanyResult.users = ($licensedUsers | Measure-Object).count + + #$AllRoles = Get-BulkResultByID -Results $TenantResults -ID 'AllRoles' + $AllRoles = $ExtensionCache.AllRoles + + + $Roles = foreach ($Role in $AllRoles) { + # Get members from cache + $Members = ($ExtensionCache."AllRoles_$($Role.id)") + [PSCustomObject]@{ + ID = $Result.id + DisplayName = $Role.displayName + Description = $Role.description + Members = $Members + ParsedMembers = $Members.displayName -join ', ' + } + } + + $pre = "
+

Assigned Roles

+
" + + $post = '
' + $RolesHtml = $Roles | Select-Object DisplayName, Description, ParsedMembers | ConvertTo-Html -PreContent $pre -PostContent $post -Fragment | ForEach-Object { $tmp = $_ -replace '<', '<'; $tmp -replace '>', '>'; } | Out-String + + $AdminUsers = (($Roles | Where-Object { $_.Displayname -match 'Administrator' }).Members | Where-Object { $null -ne $_.displayName } | Select-Object @{N = 'Name'; E = { "$($_.DisplayName) - $($_.UserPrincipalName)" } } -Unique).name -join '
' + + $Domains = $ExtensionCache.Domains + + $customerDomains = ($Domains | Where-Object { $_.isVerified -eq $True }).id -join ', ' | Out-String + + $detailstable = "
+
+

Basic Info

+
+
+
+
+

Tenant Name

+

+ $($Tenant.displayName) +

+
+
+

Tenant ID

+

+ $($Tenant.customerId) +

+
+
+

Default Domain

+

+ $defaultdomain +

+
+
+

Customer Domains

+

+ $customerDomains +

+
+
+

Admin Users

+

+ $AdminUsers +

+
+
+

Last Updated

+

+ $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') +

+
+
+
+
+" + #$Licenses = Get-BulkResultByID -Results $TenantResults -ID 'Licenses' + $Licenses = $ExtensionCache.Licenses + if ($Licenses) { + $pre = "
+

Current Licenses

+
" + + $post = '
' + + $licenseOut = $Licenses | Where-Object { $_.PrepaidUnits.Enabled -gt 0 } | Select-Object @{N = 'License Name'; E = { Convert-SKUname -skuName $_.SkuPartNumber -ConvertTable $LicTable } }, @{N = 'Active'; E = { $_.PrepaidUnits.Enabled } }, @{N = 'Consumed'; E = { $_.ConsumedUnits } }, @{N = 'Unused'; E = { $_.PrepaidUnits.Enabled - $_.ConsumedUnits } } + $licenseHTML = $licenseOut | ConvertTo-Html -PreContent $pre -PostContent $post -Fragment | Out-String + } + + #$devices = Get-BulkResultByID -Results $TenantResults -ID 'Devices' + $devices = $ExtensionCache.Devices + $CompanyResult.Devices = ($Devices | Measure-Object).count + + #$DeviceCompliancePolicies = Get-BulkResultByID -Results $TenantResults -ID 'DeviceCompliancePolicies' + $DeviceCompliancePolicies = $ExtensionCache.DeviceCompliancePolicies + <#[System.Collections.Generic.List[PSCustomObject]]$PolicyRequestArray = @() + foreach ($CompliancePolicy in $DeviceCompliancePolicies) { + $PolicyRequestArray.add(@{ + id = $CompliancePolicy.id + method = 'GET' + url = "/deviceManagement/deviceCompliancePolicies/$($CompliancePolicy.id)/deviceStatuses" + }) + } + + try { + $PolicyReturn = New-GraphBulkRequest -Headers $AuthHeaders -Requests $PolicyRequestArray -tenantid $TenantFilter + } catch { + $CompanyResult.Errors.add("Company: Unable to fetch Policies $_") + $PolicyReturn = $null + }#> + + $DeviceComplianceDetails = foreach ($Policy in $DeviceCompliancePolicies) { + $DeviceStatuses = $ExtensionCache."DeviceCompliancePolicy_$($Policy.id)" + [pscustomobject]@{ + ID = $Policy.id + DisplayName = $Policy.displayName + DeviceStatuses = $DeviceStatuses + } + } + + <#$DeviceApps = Get-BulkResultByID -Results $TenantResults -ID 'DeviceApps' + + [System.Collections.Generic.List[PSCustomObject]]$RequestArray = @() + foreach ($InstalledApp in $DeviceApps | Where-Object { $_.isAssigned -eq $True }) { + $RequestArray.add(@{ + id = $InstalledApp.id + method = 'GET' + url = "/deviceAppManagement/mobileApps/$($InstalledApp.id)/deviceStatuses" + }) + } + try { + $InstalledAppDetailsReturn = New-GraphBulkRequest -Headers $AuthHeaders -Requests $RequestArray -tenantid $TenantFilter + } catch { + $CompanyResult.Errors.add("Company: Unable to fetch Installed Device Details $_") + $InstalledAppDetailsReturn = $null + } + + $DeviceAppInstallDetails = foreach ($Result in $InstalledAppDetailsReturn) { + [pscustomobject]@{ + ID = $Result.id + DisplayName = ($DeviceApps | Where-Object { $_.id -eq $Result.id }).DisplayName + InstalledAppDetails = $result.body.value + } + } +#> + #$AllGroups = Get-BulkResultByID -Results $TenantResults -ID 'Groups' + $AllGroups = $ExtensionCache.Groups + + <#[System.Collections.Generic.List[PSCustomObject]]$GroupRequestArray = @() + foreach ($Group in $AllGroups) { + $GroupRequestArray.add(@{ + id = $Group.id + method = 'GET' + url = "/groups/$($Group.id)/members" + }) + } + try { + $GroupMembersReturn = New-GraphBulkRequest -Headers $AuthHeaders -Requests $GroupRequestArray -tenantid $TenantFilter + } catch { + $CompanyResult.Errors.add("Company: Unable to fetch Group Membership Details $_") + $GroupMembersReturn = $null + }#> + + $Groups = foreach ($Group in $AllGroups) { + $Members = $ExtensionCache."Groups_$($Result.id)" + [pscustomobject]@{ + ID = $Group.id + DisplayName = $Group.displayName + Members = $Members + } + } + + #$AllConditionalAccessPolicies = Get-BulkResultByID -Results $TenantResults -ID 'ConditionalAccess' + $AllConditionalAccessPolicies = $ExtensionCache.ConditionalAccess + + $ConditionalAccessMembers = foreach ($CAPolicy in $AllConditionalAccessPolicies) { + [System.Collections.Generic.List[PSCustomObject]]$CAMembers = @() + + if ($CAPolicy.conditions.users.includeUsers -contains 'All') { + $Users | ForEach-Object { $null = $CAMembers.add($_.id) } + } else { + $CAPolicy.conditions.users.includeUsers | ForEach-Object { $null = $CAMembers.add($_) } + } + + foreach ($CAIGroup in $CAPolicy.conditions.users.includeGroups) { + foreach ($Member in ($Groups | Where-Object { $_.id -eq $CAIGroup }).Members) { + $null = $CAMembers.add($Member.id) + } + } + + foreach ($CAIRole in $CAPolicy.conditions.users.includeRoles) { + foreach ($Member in ($Roles | Where-Object { $_.id -eq $CAIRole }).Members) { + $null = $CAMembers.add($Member.id) + } + } + + $CAMembers = $CAMembers | Select-Object -Unique + + if ($CAMembers) { + $CAPolicy.conditions.users.excludeUsers | ForEach-Object { $null = $CAMembers.remove($_) } + + foreach ($CAEGroup in $CAPolicy.conditions.users.excludeGroups) { + foreach ($Member in ($Groups | Where-Object { $_.id -eq $CAEGroup }).Members) { + $null = $CAMembers.remove($Member.id) + } + } + + foreach ($CAIRole in $CAPolicy.conditions.users.excludeRoles) { + foreach ($Member in ($Roles | Where-Object { $_.id -eq $CAERole }).Members) { + $null = $CAMembers.remove($Member.id) + } + } + } + + [pscustomobject]@{ + ID = $CAPolicy.id + DisplayName = $CAPolicy.DisplayName + Members = $CAMembers + } + } + + if ($ExtensionCache.OneDriveUsage) { + #$OneDriveDetails = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/reports/getOneDriveUsageAccountDetail(period='D7')" -tenantid $TenantFilter | ConvertFrom-Csv + $OneDriveDetails = $ExtensionCache.OneDriveUsage + } else { + $CompanyResult.Errors.add("Company: Unable to fetch One Drive Details $_") + $OneDriveDetails = $null + } + + <#try { + $CASFull = New-GraphGetRequest -uri "https://outlook.office365.com/adminapi/beta/$($tenantfilter)/CasMailbox" -Tenantid $tenantfilter -scope ExchangeOnline -noPagination $true + } catch { + $CASFull = $null + $CompanyResult.Errors.add("Company: Unable to fetch CAS Mailbox Details $_") + }#> + + if ($ExtensionCache.CASMailbox) { + $CASFull = $ExtensionCache.CASMailbox + } else { + $CompanyResult.Errors.add('Company: Unable to fetch CAS Mailbox Details') + $CASFull = $null + + } + + <#try { + $MailboxDetailedFull = New-ExoRequest -TenantID $TenantFilter -cmdlet 'Get-Mailbox' + } catch { + $CompanyResult.Errors.add("Company: Unable to fetch Mailbox Details $_") + $MailboxDetailedFull = $null + }#> + $MailboxDetailedFull = $ExtensionCache.Mailboxes + + + <#try { + $MailboxStatsFull = New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/reports/getMailboxUsageDetail(period='D7')" -tenantid $TenantFilter | ConvertFrom-Csv + } catch { + $CompanyResult.Errors.add("Company: Unable to fetch Mailbox Statistic Details $_") + $MailboxStatsFull = $null + }#> + if ($ExtensionCache.MailboxUsage) { + $MailboxStatsFull = $ExtensionCache.MailboxUsage + } else { + $MailboxStatsFull = $null + $CompanyResult.Errors.add('Company: Unable to fetch Mailbox Statistic Details') + } + + $Permissions = $ExtensionCache.MailboxPermissions + if ($licensedUsers) { + $pre = "
+

Licensed Users

+
" + + $post = '
' + + $OutputUsers = foreach ($user in $licensedUsers) { + try { + + $UserGroups = foreach ($Group in $Groups) { + if ($User.id -in $Group.Members.id) { + $FoundGroup = $AllGroups | Where-Object { $_.id -eq $Group.id } + [PSCustomObject]@{ + 'Display Name' = $FoundGroup.displayName + 'Mail Enabled' = $FoundGroup.mailEnabled + 'Mail' = $FoundGroup.mail + 'Security Group' = $FoundGroup.securityEnabled + 'Group Types' = $FoundGroup.groupTypes -join ',' + } + } + } + + $UserPolicies = foreach ($cap in $ConditionalAccessMembers) { + if ($User.id -in $Cap.Members) { + $temp = [PSCustomObject]@{ + displayName = $cap.displayName + } + $temp + } + } + + $PermsRequest = '' + $StatsRequest = '' + $MailboxDetailedRequest = '' + $CASRequest = '' + + $CASRequest = $CASFull | Where-Object { $_.ExternalDirectoryObjectId -eq $User.iD } + $MailboxDetailedRequest = $MailboxDetailedFull | Where-Object { $_.ExternalDirectoryObjectId -eq $User.iD } + $StatsRequest = $MailboxStatsFull | Where-Object { $_.'userPrincipalName' -eq $User.UserPrincipalName } + + <#try { + $PermsRequest = New-GraphGetRequest -uri "https://outlook.office365.com/adminapi/beta/$($tenantfilter)/Mailbox('$($User.ID)')/MailboxPermission" -Tenantid $tenantfilter -scope ExchangeOnline -noPagination $true + } catch { + $PermsRequest = $null + }#> + $PermsRequest = $Permissions | Where-Object { $_.Identity -eq $User.ID } + + $ParsedPerms = foreach ($Perm in $PermsRequest) { + if ($Perm.User -ne 'NT AUTHORITY\SELF') { + [pscustomobject]@{ + User = $Perm.User + AccessRights = $Perm.PermissionList.AccessRights -join ', ' + } + } + } + + try { + $TotalItemSize = [math]::Round($StatsRequest.storageUsedInBytes / 1Gb, 2) + } catch { + $TotalItemSize = 0 + } + + $UserMailSettings = [pscustomobject]@{ + ForwardAndDeliver = $MailboxDetailedRequest.DeliverToMailboxAndForward + ForwardingAddress = $MailboxDetailedRequest.ForwardingAddress + ' ' + $MailboxDetailedRequest.ForwardingSmtpAddress + LitiationHold = $MailboxDetailedRequest.LitigationHoldEnabled + HiddenFromAddressLists = $MailboxDetailedRequest.HiddenFromAddressListsEnabled + EWSEnabled = $CASRequest.EwsEnabled + MailboxMAPIEnabled = $CASRequest.MAPIEnabled + MailboxOWAEnabled = $CASRequest.OWAEnabled + MailboxImapEnabled = $CASRequest.ImapEnabled + MailboxPopEnabled = $CASRequest.PopEnabled + MailboxActiveSyncEnabled = $CASRequest.ActiveSyncEnabled + Permissions = $ParsedPerms + ProhibitSendQuota = [math]::Round([float]($MailboxDetailedRequest.ProhibitSendQuota -split ' GB')[0], 2) + ProhibitSendReceiveQuota = [math]::Round([float]($MailboxDetailedRequest.ProhibitSendReceiveQuota -split ' GB')[0], 2) + ItemCount = [math]::Round($StatsRequest.'itemCount', 2) + TotalItemSize = $TotalItemSize + } + + $userDevices = ($devices | Where-Object { $_.userPrincipalName -eq $user.UserPrincipalName } | Select-Object @{N = 'Name'; E = { "$($_.deviceName) ($($_.operatingSystem))" } }).name -join '
' + + $UserDevicesDetailsRaw = $devices | Where-Object { $_.userPrincipalName -eq $user.UserPrincipalName } | Select-Object @{N = 'Name'; E = { "
$($_.deviceName)" } }, @{n = 'Owner'; e = { $_.managedDeviceOwnerType } }, ` + @{n = 'Enrolled'; e = { $_.enrolledDateTime } }, ` + @{n = 'Last Sync'; e = { $_.lastSyncDateTime } }, ` + @{n = 'OS'; e = { $_.operatingSystem } }, ` + @{n = 'OS Version'; e = { $_.osVersion } }, ` + @{n = 'State'; e = { $_.complianceState } }, ` + @{n = 'Model'; e = { $_.model } }, ` + @{n = 'Manufacturer'; e = { $_.manufacturer } }, + deviceName, + @{n = 'url'; e = { "https://endpoint.microsoft.com/$($Tenant.defaultDomainName)/#blade/Microsoft_Intune_Devices/DeviceSettingsBlade/overview/mdmDeviceId/$($_.id)" } } + + $aliases = (($user.ProxyAddresses | Where-Object { $_ -cnotmatch 'SMTP' -and $_ -notmatch '.onmicrosoft.com' }) -replace 'SMTP:', ' ') -join ', ' + + $userLicenses = ($user.AssignedLicenses.SkuID | ForEach-Object { + $UserLic = $_ + $SkuPartNumber = ($Licenses | Where-Object { $_.SkuId -eq $UserLic }).SkuPartNumber + $DisplayName = Convert-SKUname -skuName $SkuPartNumber -ConvertTable $LicTable + if (!$DisplayName) { + $DisplayName = $SkuPartNumber + } + $DisplayName + }) -join ', ' + + $UserOneDriveDetails = $OneDriveDetails | Where-Object { $_.ownerPrincipalName -eq $user.UserPrincipalName } + + + + [System.Collections.Generic.List[PSCustomObject]]$OneDriveFormatted = @() + if ($UserOneDriveDetails) { + try { + $OneDriveUsePercent = [math]::Round([float](($UserOneDriveDetails.storageUsedInBytes / $UserOneDriveDetails.storageAllocatedInBytes) * 100), 2) + $StorageUsed = [math]::Round($UserOneDriveDetails.storageUsedInBytes / 1024 / 1024 / 1024, 2) + $StorageAllocated = [math]::Round($UserOneDriveDetails.storageAllocatedInBytes / 1024 / 1024 / 1024, 2) + } catch { + $OneDriveUsePercent = 100 + $StorageUsed = 0 + $StorageAllocated = 0 + } + + $OneDriveFormatted.add($(Get-HuduFormattedField -Title 'Owner Principal Name' -Value "$($UserOneDriveDetails.ownerPrincipalName)")) + $OneDriveFormatted.add($(Get-HuduFormattedField -Title 'One Drive URL' -Value "$($UserOneDriveDetails.siteUrl)")) + $OneDriveFormatted.add($(Get-HuduFormattedField -Title 'Is Deleted' -Value "$($UserOneDriveDetails.isDeleted)")) + $OneDriveFormatted.add($(Get-HuduFormattedField -Title 'Last Activity Date' -Value "$($UserOneDriveDetails.lastActivityDate)")) + $OneDriveFormatted.add($(Get-HuduFormattedField -Title 'File Count' -Value "$($UserOneDriveDetails.fileCount)")) + $OneDriveFormatted.add($(Get-HuduFormattedField -Title 'Active File Count' -Value "$($UserOneDriveDetails.activeFileCount)")) + $OneDriveFormatted.add($(Get-HuduFormattedField -Title 'Storage Used (Byte)' -Value "$($UserOneDriveDetails.storageUsedInBytes)")) + $OneDriveFormatted.add($(Get-HuduFormattedField -Title 'Storage Allocated (Byte)' -Value "$($UserOneDriveDetails.storageAllocatedInBytes)")) + $OneDriveUserUsage = @" +
+
+
+
+
$($StorageUsed) GB used, $OneDriveUsePercent% of $($StorageAllocated) GB
+
+"@ + + $OneDriveFormatted.add($(Get-HuduFormattedField -Title 'One Drive Usage' -Value $OneDriveUserUsage)) + } + + [System.Collections.Generic.List[PSCustomObject]]$UserMailSettingsFormatted = @() + [System.Collections.Generic.List[PSCustomObject]]$UserMailboxDetailsFormatted = @() + if ($UserMailSettings) { + $UserMailSettingsFormatted.add($(Get-HuduFormattedField -Title 'Forward and Deliver' -Value "$($UserMailSettings.ForwardAndDeliver)")) + $UserMailSettingsFormatted.add($(Get-HuduFormattedField -Title 'Forwarding Address' -Value "$($UserMailSettings.ForwardingAddress)")) + $UserMailSettingsFormatted.add($(Get-HuduFormattedField -Title 'Litiation Hold' -Value "$($UserMailSettings.LitiationHold)")) + $UserMailSettingsFormatted.add($(Get-HuduFormattedField -Title 'Hidden From Address Lists' -Value "$($UserMailSettings.HiddenFromAddressLists)")) + $UserMailSettingsFormatted.add($(Get-HuduFormattedField -Title 'EWS Enabled' -Value "$($UserMailSettings.EWSEnabled)")) + $UserMailSettingsFormatted.add($(Get-HuduFormattedField -Title 'MAPI Enabled' -Value "$($UserMailSettings.MailboxMAPIEnabled)")) + $UserMailSettingsFormatted.add($(Get-HuduFormattedField -Title 'OWA Enabled' -Value "$($UserMailSettings.MailboxOWAEnabled)")) + $UserMailSettingsFormatted.add($(Get-HuduFormattedField -Title 'IMAP Enabled' -Value "$($UserMailSettings.MailboxImapEnabled)")) + $UserMailSettingsFormatted.add($(Get-HuduFormattedField -Title 'POP Enabled' -Value "$($UserMailSettings.MailboxPopEnabled)")) + $UserMailSettingsFormatted.add($(Get-HuduFormattedField -Title 'Active Sync Enabled' -Value "$($UserMailSettings.MailboxActiveSyncEnabled)")) + + + $UserMailboxDetailsFormatted.add($(Get-HuduFormattedField -Title 'Permissions' -Value "$($UserMailSettings.Permissions | ConvertTo-Html -Fragment | Out-String)")) + $UserMailboxDetailsFormatted.add($(Get-HuduFormattedField -Title 'Prohibit Send Quota' -Value "$($UserMailSettings.ProhibitSendQuota)")) + $UserMailboxDetailsFormatted.add($(Get-HuduFormattedField -Title 'Prohibit Send Receive Quota' -Value "$($UserMailSettings.ProhibitSendReceiveQuota)")) + $UserMailboxDetailsFormatted.add($(Get-HuduFormattedField -Title 'Item Count' -Value "$($UserMailSettings.ItemCount)")) + $UserMailboxDetailsFormatted.add($(Get-HuduFormattedField -Title 'Total Mailbox Size' -Value "$($UserMailSettings.TotalItemSize)")) + try { + $UserMailboxUsePercent = [math]::Round([float](($UserMailSettings.TotalItemSize / $UserMailSettings.ProhibitSendReceiveQuota) * 100), 2) + } catch { + $UserMailboxUsePercent = 100 + } + $UserMailboxUsage = @" +
+
+
+
+
$([math]::Round($UserMailSettings.TotalItemSize,2)) GB used, $UserMailboxUsePercent% of $([math]::Round($UserMailSettings.ProhibitSendReceiveQuota, 2)) GB
+
+"@ + $UserMailboxDetailsFormatted.add($(Get-HuduFormattedField -Title 'Mailbox Usage' -Value $UserMailboxUsage)) + + } + + $UserPoliciesFormatted = '' + + [System.Collections.Generic.List[PSCustomObject]]$UserOverviewFormatted = @() + $UserOverviewFormatted.add($(Get-HuduFormattedField -Title 'User Name' -Value "$($User.displayName)")) + $UserOverviewFormatted.add($(Get-HuduFormattedField -Title 'User Principal Name' -Value "$($User.userPrincipalName)")) + $UserOverviewFormatted.add($(Get-HuduFormattedField -Title 'User ID' -Value "$($User.ID)")) + $UserOverviewFormatted.add($(Get-HuduFormattedField -Title 'User Enabled' -Value "$($User.accountEnabled)")) + $UserOverviewFormatted.add($(Get-HuduFormattedField -Title 'Job Title' -Value "$($User.jobTitle)")) + $UserOverviewFormatted.add($(Get-HuduFormattedField -Title 'Mobile Phone' -Value "$($User.mobilePhone)")) + $UserOverviewFormatted.add($(Get-HuduFormattedField -Title 'Business Phones' -Value "$($User.businessPhones -join ', ')")) + $UserOverviewFormatted.add($(Get-HuduFormattedField -Title 'Office Location' -Value "$($User.officeLocation)")) + $UserOverviewFormatted.add($(Get-HuduFormattedField -Title 'Aliases' -Value "$aliases")) + $UserOverviewFormatted.add($(Get-HuduFormattedField -Title 'Licenses' -Value "$($userLicenses)")) + + $AssignedPlans = $User.assignedplans | Where-Object { $_.capabilityStatus -eq 'Enabled' } | Select-Object @{n = 'Assigned'; e = { $_.assignedDateTime } }, @{n = 'Service'; e = { $_.service } } -Unique + [System.Collections.Generic.List[PSCustomObject]]$AssignedPlansFormatted = @() + foreach ($AssignedPlan in $AssignedPlans) { + if ($AssignedPlan.service -in ($AssignedMap | Get-Member -MemberType NoteProperty).name) { + $CSSClass = $AssignedMap."$($AssignedPlan.service)" + $PlanDisplayName = $AssignedNameMap."$($AssignedPlan.service)" + $ParsedDate = Get-Date($AssignedPlan.Assigned) -Format 'yyyy-MM-dd HH:mm:ss' + $AssignedPlansFormatted.add("
$PlanDisplayNameAssigned $($ParsedDate)
") + } + } + $AssignedPlansBlock = "
$($AssignedPlansFormatted -join '')
" + + if ($UserMailSettingsFormatted) { + $UserMailSettingsBlock = Get-HuduFormattedBlock -Heading 'Mailbox Settings' -Body ($UserMailSettingsFormatted -join '') + } else { + $UserMailSettingsBlock = $null + } + + if ($UserMailboxDetailsFormatted) { + $UserMailDetailsBlock = Get-HuduFormattedBlock -Heading 'Mailbox Details' -Body ($UserMailboxDetailsFormatted -join '') + } else { + $UserMailDetailsBlock = $null + } + + if ($UserGroups) { + $UserGroupsBlock = Get-HuduFormattedBlock -Heading 'User Groups' -Body $($UserGroups | ConvertTo-Html -Fragment -As Table | Out-String) + } else { + $UserGroupsBlock = $null + } + + if ($UserPoliciesFormatted) { + $UserPoliciesBlock = Get-HuduFormattedBlock -Heading 'Assigned Conditional Access Policies' -Body $UserPoliciesFormatted + } else { + $UserPoliciesBlock = $null + } + + if ($OneDriveFormatted) { + $OneDriveBlock = Get-HuduFormattedBlock -Heading 'One Drive Details' -Body ($OneDriveFormatted -join '') + } else { + $OneDriveBlock = $null + } + + if ($UserOverviewFormatted) { + $UserOverviewBlock = Get-HuduFormattedBlock -Heading 'User Details' -Body $UserOverviewFormatted + } else { + $UserOverviewBlock = $null + } + + if ($UserDevicesDetailsRaw) { + $UserDevicesDetailsBlock = Get-HuduFormattedBlock -Heading 'Intune Devices' -Body $($UserDevicesDetailsRaw | Select-Object -ExcludeProperty deviceName, url | ConvertTo-Html -Fragment | ForEach-Object { $tmp = $_ -replace '<', '<'; $tmp -replace '>', '>'; } | Out-String) + } else { + $UserDevicesDetailsBlock = $null + } + + $HuduUser = $People | Where-Object { $_.email_address -eq $user.UserPrincipalName -or $_.primary_mail -eq $user.UserPrincipalName -or ($_.cards.integrator_name -eq 'cw_manage' -and $_.cards.data.communicationItems.communicationType -eq 'Email' -and $_.cards.data.communicationItems.value -eq $user.UserPrincipalName) } + + [System.Collections.Generic.List[PSCustomObject]]$CIPPLinksFormatted = @() + if ($EnableCIPP) { + $CIPPLinksFormatted.add((Get-HuduLinkBlock -URL "$($CIPPURL)/identity/administration/users/view?customerId=$($Tenant.customerid)&userId=$($User.id)&tenantDomain=$($Tenant.defaultDomainName)&userEmail=$($User.UserPrincipalName)" -Icon 'far fa-eye' -Title 'CIPP - View User')) + $CIPPLinksFormatted.add((Get-HuduLinkBlock -URL "$($CIPPURL)/identity/administration/users/edit?customerId=$($Tenant.customerid)&userId=$($User.id)&tenantDomain=$($Tenant.defaultDomainName)&userEmail=$($User.UserPrincipalName)" -Icon 'fas fa-user-cog' -Title 'CIPP - Edit User')) + $CIPPLinksFormatted.add((Get-HuduLinkBlock -URL "$($CIPPURL)/identity/administration/ViewBec?customerId=$($Tenant.customerid)&userId=$($User.id)&tenantDomain=$($Tenant.defaultDomainName)&userEmail=$($User.UserPrincipalName)" -Icon 'fas fa-user-secret' -Title 'CIPP - BEC Tool')) + } + + [System.Collections.Generic.List[PSCustomObject]]$UserLinksFormatted = @() + $UserLinksFormatted.add((Get-HuduLinkBlock -URL "https://aad.portal.azure.com/$($Tenant.defaultDomainName)/#blade/Microsoft_AAD_IAM/UserDetailsMenuBlade/Profile/userId/$($User.id)" -Icon 'fas fa-users-cog' -Title 'Entra ID')) + $UserLinksFormatted.add((Get-HuduLinkBlock -URL "https://aad.portal.azure.com/$($Tenant.defaultDomainName)/#blade/Microsoft_AAD_IAM/UserDetailsMenuBlade/SignIns/userId/$($User.id)" -Icon 'fas fa-history' -Title 'Sign Ins')) + $UserLinksFormatted.add((Get-HuduLinkBlock -URL "https://admin.teams.microsoft.com/users/$($User.id)/account?delegatedOrg=$($Tenant.defaultDomainName)" -Icon 'fas fa-users' -Title 'Teams Admin')) + $UserLinksFormatted.add((Get-HuduLinkBlock -URL "https://intune.microsoft.com/$($Tenant.defaultDomainName)/#blade/Microsoft_AAD_IAM/UserDetailsMenuBlade/Profile/userId/$($User.ID)" -Icon 'fas fa-laptop' -Title 'Intune (User)')) + $UserLinksFormatted.add((Get-HuduLinkBlock -URL "https://intune.microsoft.com/$($Tenant.defaultDomainName)/#blade/Microsoft_AAD_IAM/UserDetailsMenuBlade/Devices/userId/$($User.ID)" -Icon 'fas fa-laptop' -Title 'Intune (Devices)')) + + if ($HuduUser) { + $HaloCard = $HuduUser.cards | Where-Object { $_.integrator_name -eq 'halo' } + if ($HaloCard) { + $UserLinksFormatted.add((Get-HuduLinkBlock -URL "$($PSAUserUrl)$($HaloCard.sync_id)" -Icon 'far fa-id-card' -Title 'Halo PSA')) + } + } + + $UserLinksBlock = "
Management Links
$($UserLinksFormatted -join '')$($CIPPLinksFormatted -join '')
" + + $UserBody = "
$AssignedPlansBlock
$UserLinksBlock
$($UserOverviewBlock)$($UserMailDetailsBlock)$($OneDriveBlock)$($UserMailSettingsBlock)$($UserPoliciesBlock)
$($UserDevicesDetailsBlock)
$($UserGroupsBlock)
" + + $UserAssetFields = @{ + microsoft_365 = $UserBody + email_address = $user.UserPrincipalName + } + $NewHash = Get-StringHash -String $UserBody + + $HuduUserCount = ($HuduUser | Measure-Object).count + if ($HuduUserCount -eq 1) { + $ExistingAsset = Get-CIPPAzDataTableEntity @HuduAssetCache -Filter "PartitionKey eq 'HuduUser' and CompanyId eq $company_id and RowKey eq '$($HuduUser.id)'" + $ExistingHash = $ExistingAsset.Hash + + if (!$ExistingAsset -or $ExistingHash -ne $NewHash) { + $null = Set-HuduAsset -asset_id $HuduUser.id -Name $HuduUser.name -company_id $company_id -asset_layout_id $PeopleLayout.id -Fields $UserAssetFields + $AssetCache = [PSCustomObject]@{ + PartitionKey = 'HuduUser' + RowKey = [string]$HuduUser.id + CompanyId = [string]$company_id + Hash = [string]$NewHash + } + Add-CIPPAzDataTableEntity @HuduAssetCache -Entity $AssetCache -Force + } + + } elseif ($HuduUserCount -eq 0) { + if ($CreateUsers -eq $True) { + $HuduUser = (New-HuduAsset -Name $User.DisplayName -company_id $company_id -asset_layout_id $PeopleLayout.id -Fields $UserAssetFields -primary_mail $user.UserPrincipalName).asset + $AssetCache = [PSCustomObject]@{ + PartitionKey = 'HuduUser' + RowKey = [string]$HuduUser.id + CompanyId = [string]$company_id + Hash = [string]$NewHash + } + Add-CIPPAzDataTableEntity @HuduAssetCache -Entity $AssetCache -Force + } + } else { + $CompanyResult.Errors.add("User $($User.UserPrincipalName): Multiple Users Matched to email address in Hudu: ($($HuduUser.name -join ', ') - $($($HuduUser.id -join ', '))) $_") + } + + + $UserLink = "$($user.DisplayName)" + + [PSCustomObject]@{ + 'Display Name' = $UserLink + 'Addresses' = "$($user.UserPrincipalName)
$aliases" + 'EPM Devices' = $userDevices + 'Assigned Licenses' = $userLicenses + 'Options' = "Azure AD | M365 Admin" + } + } catch { + $CompanyResult.Errors.add("User $($User.UserPrincipalName): A fatal error occured while processing user $_") + } + } + + $licensedUserHTML = $OutputUsers | ConvertTo-Html -PreContent $pre -PostContent $post -Fragment | ForEach-Object { $tmp = $_ -replace '<', '<'; $tmp -replace '>', '>'; } | Out-String + + } + + foreach ($Device in $Devices) { + try { + [System.Collections.Generic.List[PSCustomObject]]$DeviceOverviewFormatted = @() + $DeviceOverviewFormatted.add($(Get-HuduFormattedField -Title 'Device Name' -Value "$($Device.deviceName)")) + $DeviceOverviewFormatted.add($(Get-HuduFormattedField -Title 'User' -Value "$($Device.userDisplayName)")) + $DeviceOverviewFormatted.add($(Get-HuduFormattedField -Title 'User Email' -Value "$($Device.userPrincipalName)")) + $DeviceOverviewFormatted.add($(Get-HuduFormattedField -Title 'Owner' -Value "$($Device.ownerType)")) + $DeviceOverviewFormatted.add($(Get-HuduFormattedField -Title 'Enrolled' -Value "$($Device.enrolledDateTime)")) + $DeviceOverviewFormatted.add($(Get-HuduFormattedField -Title 'Last Checkin' -Value "$($Device.lastSyncDateTime)")) + if ($Device.complianceState -eq 'compliant') { + $CompliantSymbol = '   ' + } else { + $CompliantSymbol = '   ' + } + $DeviceOverviewFormatted.add($(Get-HuduFormattedField -Title 'Compliant' -Value "$($CompliantSymbol)$($Device.complianceState)")) + $DeviceOverviewFormatted.add($(Get-HuduFormattedField -Title 'Management Type' -Value "$($Device.managementAgent)")) + + [System.Collections.Generic.List[PSCustomObject]]$DeviceHardwareFormatted = @() + $DeviceHardwareFormatted.add($(Get-HuduFormattedField -Title 'Serial Number' -Value "$($Device.serialNumber)")) + $DeviceHardwareFormatted.add($(Get-HuduFormattedField -Title 'OS' -Value "$($Device.operatingSystem)")) + $DeviceHardwareFormatted.add($(Get-HuduFormattedField -Title 'OS Versions' -Value "$($Device.osVersion)")) + $DeviceHardwareFormatted.add($(Get-HuduFormattedField -Title 'Chassis' -Value "$($Device.chassisType)")) + $DeviceHardwareFormatted.add($(Get-HuduFormattedField -Title 'Model' -Value "$($Device.model)")) + $DeviceHardwareFormatted.add($(Get-HuduFormattedField -Title 'Manufacturer' -Value "$($Device.manufacturer)")) + $DeviceHardwareFormatted.add($(Get-HuduFormattedField -Title 'Total Storage' -Value "$([math]::Round($Device.totalStorageSpaceInBytes /1024 /1024 /1024, 2))")) + $DeviceHardwareFormatted.add($(Get-HuduFormattedField -Title 'Free Storage' -Value "$([math]::Round($Device.freeStorageSpaceInBytes /1024 /1024 /1024, 2))")) + + [System.Collections.Generic.List[PSCustomObject]]$DeviceEnrollmentFormatted = @() + $DeviceEnrollmentFormatted.add($(Get-HuduFormattedField -Title 'Enrollment Type' -Value "$($Device.deviceEnrollmentType)")) + $DeviceEnrollmentFormatted.add($(Get-HuduFormattedField -Title 'Join Type' -Value "$($Device.joinType)")) + $DeviceEnrollmentFormatted.add($(Get-HuduFormattedField -Title 'Registration State' -Value "$($Device.deviceRegistrationState)")) + $DeviceEnrollmentFormatted.add($(Get-HuduFormattedField -Title 'Autopilot Enrolled' -Value "$($Device.autopilotEnrolled)")) + $DeviceEnrollmentFormatted.add($(Get-HuduFormattedField -Title 'Device Guard Requirements' -Value "$($Device.hardwareinformation.deviceGuardVirtualizationBasedSecurityHardwareRequirementState)")) + $DeviceEnrollmentFormatted.add($(Get-HuduFormattedField -Title 'Virtualistation Based Security' -Value "$($Device.hardwareinformation.deviceGuardVirtualizationBasedSecurityState)")) + $DeviceEnrollmentFormatted.add($(Get-HuduFormattedField -Title 'Credential Guard' -Value "$($Device.hardwareinformation.deviceGuardLocalSystemAuthorityCredentialGuardState)")) + + $DevicePoliciesTable = foreach ($Policy in $DeviceComplianceDetails) { + if ($device.deviceName -in $Policy.DeviceStatuses.deviceDisplayName) { + $Status = $Policy.DeviceStatuses | Where-Object { $_.deviceDisplayName -eq $device.deviceName } + if ($Status.status -ne 'unknown') { + [PSCustomObject]@{ + Name = $Policy.DisplayName + Status = ($Status.status | Select-Object -Unique) -join ', ' + 'Last Report' = "$(Get-Date($Status.lastReportedDateTime[0]) -Format 'yyyy-MM-dd HH:mm:ss')" + 'Grace Expiry' = "$(Get-Date($Status.complianceGracePeriodExpirationDateTime[0]) -Format 'yyyy-MM-dd HH:mm:ss')" + } + } + } + } + $DevicePoliciesFormatted = $DevicePoliciesTable | ConvertTo-Html -Fragment | Out-String + + $DeviceGroupsTable = foreach ($Group in $Groups) { + if ($device.azureADDeviceId -in $Group.members.deviceId) { + [PSCustomObject]@{ + Name = $Group.displayName + } + } + } + $DeviceGroupsFormatted = $DeviceGroupsTable | ConvertTo-Html -Fragment | Out-String + + $DeviceAppsTable = foreach ($App in $DeviceAppInstallDetails) { + if ($device.id -in $App.InstalledAppDetails.deviceId) { + $Status = $App.InstalledAppDetails | Where-Object { $_.deviceId -eq $device.id } + [PSCustomObject]@{ + Name = $App.DisplayName + 'Install Status' = ($Status.installState | Select-Object -Unique ) -join ',' + } + } + } + $DeviceAppsFormatted = $DeviceAppsTable | ConvertTo-Html -Fragment | Out-String + + $DeviceOverviewBlock = Get-HuduFormattedBlock -Heading 'Device Details' -Body ($DeviceOverviewFormatted -join '') + $DeviceHardwareBlock = Get-HuduFormattedBlock -Heading 'Hardware Details' -Body ($DeviceHardwareFormatted -join '') + $DeviceEnrollmentBlock = Get-HuduFormattedBlock -Heading 'Device Enrollment Details' -Body ($DeviceEnrollmentFormatted -join '') + $DevicePolicyBlock = Get-HuduFormattedBlock -Heading 'Compliance Policies' -Body ($DevicePoliciesFormatted -join '') + $DeviceAppsBlock = Get-HuduFormattedBlock -Heading 'App Details' -Body ($DeviceAppsFormatted -join '') + $DeviceGroupsBlock = Get-HuduFormattedBlock -Heading 'Device Groups' -Body ($DeviceGroupsFormatted -join '') + + if ("$($device.serialNumber)" -in $ExcludeSerials) { + $HuduDevice = $HuduDevices | Where-Object { $_.name -eq $device.deviceName -or ($_.cards.integrator_name -eq 'cw_manage' -and $_.cards.data.name -contains $device.deviceName) } + } else { + $HuduDevice = $HuduDevices | Where-Object { $_.primary_serial -eq $device.serialNumber -or ($_.cards.integrator_name -eq 'cw_manage' -and $_.cards.data.serialNumber -eq $device.serialNumber) } + } + + [System.Collections.Generic.List[PSCustomObject]]$DeviceLinksFormatted = @() + $DeviceLinksFormatted.add((Get-HuduLinkBlock -URL "https://endpoint.microsoft.com/$($Tenant.defaultDomainName)/#blade/Microsoft_Intune_Devices/DeviceSettingsBlade/overview/mdmDeviceId/$($Device.id)" -Icon 'fas fa-laptop' -Title 'Endpoint Manager')) + + if ($HuduDevice) { + $DRMMCard = $HuduDevice.cards | Where-Object { $_.integrator_name -eq 'dattormm' } + if ($DRMMCard) { + $DeviceLinksFormatted.add((Get-HuduLinkBlock -URL "$($RMMDeviceURL)$($DRMMCard.data.id)" -Icon 'fas fa-laptop-code' -Title 'Datto RMM')) + $DeviceLinksFormatted.add((Get-HuduLinkBlock -URL "$($RMMRemoteURL)$($DRMMCard.data.id)" -Icon 'fas fa-desktop' -Title 'Datto RMM Remote')) + } + $ManageCard = $HuduDevice.cards | Where-Object { $_.integrator_name -eq 'cw_manage' } + if ($ManageCard) { + $DeviceLinksFormatted.add((Get-HuduLinkBlock -URL $ManageCard.data.managementLink -Icon 'fas fa-laptop-code' -Title 'CW Automate')) + $DeviceLinksFormatted.add((Get-HuduLinkBlock -URL $ManageCard.data.remoteLink -Icon 'fas fa-desktop' -Title 'CW Control')) + } + } + + $DeviceLinksBlock = "
Management Links
$($DeviceLinksFormatted -join '')
" + + $DeviceIntuneDetailshtml = "
$DeviceLinksBlock
$($DeviceOverviewBlock)$($DeviceHardwareBlock)$($DeviceEnrollmentBlock)$($DevicePolicyBlock)$($DeviceAppsBlock)$($DeviceGroupsBlock)
" + + $DeviceAssetFields = @{ + microsoft_365 = $DeviceIntuneDetailshtml + } + $NewHash = Get-StringHash -String $DeviceIntuneDetailshtml + + if ($HuduDevice) { + if (($HuduDevice | Measure-Object).count -eq 1) { + $ExistingAsset = Get-CIPPAzDataTableEntity @HuduAssetCache -Filter "PartitionKey eq 'HuduDevice' and CompanyId eq $company_id and RowKey eq '$($HuduDevice.id)'" + $ExistingHash = $ExistingAsset.Hash + + if (!$ExistingAsset -or $ExistingAsset.Hash -ne $NewHash) { + $null = Set-HuduAsset -asset_id $HuduDevice.id -Name $HuduDevice.name -company_id $company_id -asset_layout_id $HuduDevice.asset_layout_id -Fields $DeviceAssetFields -PrimarySerial $Device.serialNumber + $AssetCache = [PSCustomObject]@{ + PartitionKey = 'HuduDevice' + RowKey = [string]$HuduDevice.id + CompanyId = [string]$company_id + Hash = [string]$NewHash + } + Add-CIPPAzDataTableEntity @HuduAssetCache -Entity $AssetCache -Force + + $HuduUser = $People | Where-Object { $_.primary_mail -eq $Device.userPrincipalName -or ($_.cards.integrator_name -eq 'cw_manage' -and $_.cards.data.communicationItems.communicationType -eq 'Email' -and $_.cards.data.communicationItems.value -eq $Device.userPrincipalName) } + + if ($HuduUser) { + $Relation = $HuduRelations | Where-Object { $_.fromable_type -eq 'Asset' -and $_.fromable_id -eq $HuduUser.id -and $_.toable_type -eq 'Asset' -and $_toable_id -eq $HuduDevice.id } + if (-not $Relation) { + try { + $null = New-HuduRelation -FromableType 'Asset' -FromableID $HuduUser.id -ToableType 'Asset' -ToableID $HuduDevice.id -ea stop + } catch {} + } + } + } + } else { + $CompanyResult.Errors.add("Device $($HuduDevice.name): Multiple devices matched on name or serial ($($device.serialNumber -join ', '))") + } + } else { + if ($device.deviceType -in $IntuneDesktopDeviceTypes) { + $DeviceLayoutID = $DesktopsLayout.id + $DeviceCreation = $CreateDevices + } else { + $DeviceLayoutID = $MobilesLayout.id + $DeviceCreation = $CreateMobileDevices + } + if ($DeviceCreation -eq $true) { + $HuduDevice = (New-HuduAsset -Name $device.deviceName -company_id $company_id -asset_layout_id $DeviceLayoutID -Fields $DeviceAssetFields -PrimarySerial $Device.serialNumber).asset + + $AssetCache = [PSCustomObject]@{ + PartitionKey = 'HuduDevice' + RowKey = [string]$HuduDevice.id + CompanyId = [string]$company_id + Hash = [string]$NewHash + } + Add-CIPPAzDataTableEntity @HuduAssetCache -Entity $AssetCache -Force + + $HuduUser = $People | Where-Object { $_.primary_mail -eq $Device.userPrincipalName -or ($_.cards.integrator_name -eq 'cw_manage' -and $_.cards.data.communicationItems.communicationType -eq 'Email' -and $_.cards.data.communicationItems.value -eq $Device.userPrincipalName) } + if ($HuduUser) { + try { + $null = New-HuduRelation -FromableType 'Asset' -FromableID $HuduUser.id -ToableType 'Asset' -ToableID $HuduDevice.id -ea stop + } catch { + # No need to do anything here as its will be when relations already exist. + } + } + } + } + } catch { + $CompanyResult.Errors.add("Device $($device.deviceName): A Fatal Error occured while processing the device $_") + } + + } + + + $body = "
+
+

Administrative Portals

+
+
$CustomerLinks
+
+
+
+
+ $detailstable + $licenseHTML +
+
+
+ $RolesHtml +
+
+
+ $licensedUserHTML +
" + + try { + $null = Set-HuduMagicDash -Title "Microsoft 365 - $($Tenant.displayName)" -company_name $TenantMap.IntegrationName -Message "$($licensedUsers.count) Licensed Users" -Icon 'fab fa-microsoft' -Content $body -Shade 'success' + } catch { + $CompanyResult.Errors.add("Company: Failed to add Magic Dash to Company: $_") + } + + try { + if ($importDomains) { + $domainstoimport = $RawDomains + foreach ($imp in $domainstoimport) { + $impdomain = $imp.id + $huduimpdomain = Get-HuduWebsites -Name "https://$impdomain" + if ($($huduimpdomain.id.count) -eq 0) { + if ($monitorDomains) { + $null = New-HuduWebsite -Name "https://$impdomain" -Notes $HuduNotes -Paused 'false' -CompanyId $company_id -DisableDNS 'false' -DisableSSL 'false' -DisableWhois 'false' + } else { + $null = New-HuduWebsite -Name "https://$impdomain" -Notes $HuduNotes -Paused 'true' -CompanyId $company_id -DisableDNS 'true' -DisableSSL 'true' -DisableWhois 'true' + } + + } + } + + } + } catch { + $CompanyResult.Errors.add("Company: Failed to import domain: $_") + Write-LogMessage -tenant $Tenant.defaultDomainName -tenantid $Tenant.customerId -API 'Hudu Sync' -message "Company: Failed to import domain: $_" -level 'Error' + } + Write-LogMessage -tenant $Tenant.defaultDomainName -tenantid $Tenant.customerId -API 'Hudu Sync' -message 'Company: Completed Sync' -level 'Information' + } catch { + $CompanyResult.Errors.add("Company: A fatal error occured: $_") + Write-LogMessage -tenant $Tenant.defaultDomainName -tenantid $Tenant.customerId -API 'Hudu Sync' -message "Company: A fatal error occured: $_" -level 'Error' + } + return $CompanyResult +} diff --git a/Modules/CippExtensions/Public/Hudu/Set-HuduMapping.ps1 b/Modules/CippExtensions/Public/Hudu/Set-HuduMapping.ps1 new file mode 100644 index 000000000000..03c6dddb8fb3 --- /dev/null +++ b/Modules/CippExtensions/Public/Hudu/Set-HuduMapping.ps1 @@ -0,0 +1,26 @@ +function Set-HuduMapping { + [CmdletBinding()] + param ( + $CIPPMapping, + $APIName, + $Request + ) + Get-CIPPAzDataTableEntity @CIPPMapping -Filter "PartitionKey eq 'HuduMapping'" | ForEach-Object { + Remove-AzDataTableEntity @CIPPMapping -Entity $_ + } + foreach ($Mapping in ([pscustomobject]$Request.body.mappings).psobject.properties) { + $AddObject = @{ + PartitionKey = 'HuduMapping' + RowKey = "$($mapping.name)" + IntegrationId = "$($mapping.value.value)" + IntegrationName = "$($mapping.value.label)" + } + + Add-CIPPAzDataTableEntity @CIPPMapping -Entity $AddObject -Force + + Write-LogMessage -API $APINAME -user $request.headers.'x-ms-client-principal' -message "Added mapping for $($mapping.name)." -Sev 'Info' + } + $Result = [pscustomobject]@{'Results' = 'Successfully edited mapping table.' } + + Return $Result +} \ No newline at end of file diff --git a/Modules/CippExtensions/Public/New-CippExtAlert.ps1 b/Modules/CippExtensions/Public/New-CippExtAlert.ps1 index 827347a613d2..21f5acf1923e 100644 --- a/Modules/CippExtensions/Public/New-CippExtAlert.ps1 +++ b/Modules/CippExtensions/Public/New-CippExtAlert.ps1 @@ -11,18 +11,18 @@ function New-CippExtAlert { $MappingFile = (Get-CIPPAzDataTableEntity @MappingTable) foreach ($ConfigItem in $Configuration.psobject.properties.name) { switch ($ConfigItem) { - "HaloPSA" { + 'HaloPSA' { If ($Configuration.HaloPSA.enabled) { $TenantId = (Get-Tenants | Where-Object defaultDomainName -EQ $Alert.TenantId).customerId Write-Host "TenantId: $TenantId" - $MappedId = ($MappingFile | Where-Object RowKey -EQ $TenantId).HaloPSA + $MappedId = ($MappingFile | Where-Object { $_.PartitionKey -eq 'HaloMapping' -and $_.RowKey -eq $TenantId }).IntegrationId Write-Host "MappedId: $MappedId" if (!$mappedId) { $MappedId = 1 } Write-Host "MappedId: $MappedId" - New-HaloPSATicket -Title $Alert.AlertTitle -Description $Alert.AlertText -Client $mappedId + New-HaloPSATicket -Title $Alert.AlertTitle -Description $Alert.AlertText -Client $mappedId } } - "Gradient" { + 'Gradient' { If ($Configuration.Gradient.enabled) { New-GradientAlert -Title $Alert.AlertTitle -Description $Alert.AlertText -Client $Alert.TenantId } diff --git a/Modules/CippExtensions/Public/NinjaOne/Get-NinjaOneFieldMapping.ps1 b/Modules/CippExtensions/Public/NinjaOne/Get-NinjaOneFieldMapping.ps1 new file mode 100644 index 000000000000..e88f53ceba9c --- /dev/null +++ b/Modules/CippExtensions/Public/NinjaOne/Get-NinjaOneFieldMapping.ps1 @@ -0,0 +1,117 @@ +function Get-NinjaOneFieldMapping { + [CmdletBinding()] + param ( + $CIPPMapping + ) + try { + #Get available mappings + $Mappings = [pscustomobject]@{} + + [System.Collections.Generic.List[object]]$CIPPFieldHeaders = @( + [PSCustomObject]@{ + Title = 'NinjaOne Organization Global Custom Field Mapping' + FieldType = 'Organization' + Description = 'Use the table below to map your Organization Field to the correct NinjaOne Field' + } + [PSCustomObject]@{ + Title = 'NinjaOne Device Custom Field Mapping' + FieldType = 'Device' + Description = 'Use the table below to map your Device Field to the correct NinjaOne Field' + } + ) + + [System.Collections.Generic.List[object]]$CIPPFields = @( + [PSCustomObject]@{ + FieldName = 'TenantLinks' + FieldLabel = 'Microsoft 365 Tenant Links - Field Used to Display Links to Microsoft 365 Portals and CIPP' + FieldType = 'Organization' + Type = 'WYSIWYG' + }, + [PSCustomObject]@{ + FieldName = 'TenantSummary' + FieldLabel = 'Microsoft 365 Tenant Summary - Field Used to Display Tenant Summary Information' + FieldType = 'Organization' + Type = 'WYSIWYG' + }, + [PSCustomObject]@{ + FieldName = 'UsersSummary' + FieldLabel = 'Microsoft 365 Users Summary - Field Used to Display User Summary Information' + FieldType = 'Organization' + Type = 'WYSIWYG' + }, + [PSCustomObject]@{ + FieldName = 'DeviceLinks' + FieldLabel = 'Microsoft 365 Device Links - Field Used to Display Links to Microsoft 365 Portals and CIPP' + FieldType = 'Device' + Type = 'WYSIWYG' + }, + [PSCustomObject]@{ + FieldName = 'DeviceSummary' + FieldLabel = 'Microsoft 365 Device Summary - Field Used to Display Device Summary Information' + FieldType = 'Device' + Type = 'WYSIWYG' + }, + [PSCustomObject]@{ + FieldName = 'DeviceCompliance' + FieldLabel = 'Intune Device Compliance Status - Field Used to Monitor Device Compliance' + FieldType = 'Device' + Type = 'TEXT' + } + ) + + $MappingFieldMigrate = Get-CIPPAzDataTableEntity @CIPPMapping -Filter "PartitionKey eq 'NinjaFieldMapping'" | ForEach-Object { + [PSCustomObject]@{ + PartitionKey = 'NinjaOneFieldMapping' + RowKey = $_.RowKey + IntegrationId = $_.NinjaOne + IntegrationName = $_.NinjaOneName + } + Remove-AzDataTableEntity @CIPPMapping -Entity $_ + } + if (($MappingFieldMigrate | Measure-Object).count -gt 0) { + Add-CIPPAzDataTableEntity @CIPPMapping -Entity $MappingFieldMigrate -Force + } + + $Mappings = Get-ExtensionMapping -Extension 'NinjaOneField' + + $Table = Get-CIPPTable -TableName Extensionsconfig + $Configuration = ((Get-AzDataTableEntity @Table).config | ConvertFrom-Json -ea stop).NinjaOne + + $Token = Get-NinjaOneToken -configuration $Configuration + + $NinjaCustomFieldsNodeRaw = (Invoke-WebRequest -Uri "https://$($Configuration.Instance)/api/v2/device-custom-fields?scopes=node" -Method GET -Headers @{Authorization = "Bearer $($token.access_token)" } -ContentType 'application/json').content | ConvertFrom-Json -Depth 100 + + [System.Collections.Generic.List[object]]$NinjaCustomFieldsNode = $NinjaCustomFieldsNodeRaw | Where-Object { $_.apiPermission -eq 'READ_WRITE' -and $_.type -in $CIPPFields.Type } | Select-Object @{n = 'name'; e = { $_.label } }, @{n = 'value'; e = { $_.name } }, type, @{n = 'FieldType'; e = { 'Device' } } + + $NinjaCustomFieldsOrgRaw = (Invoke-WebRequest -Uri "https://$($Configuration.Instance)/api/v2/device-custom-fields?scopes=organization" -Method GET -Headers @{Authorization = "Bearer $($token.access_token)" } -ContentType 'application/json').content | ConvertFrom-Json -Depth 100 + + [System.Collections.Generic.List[object]]$NinjaCustomFieldsOrg = $NinjaCustomFieldsOrgRaw | Where-Object { $_.apiPermission -eq 'READ_WRITE' -and $_.type -in $CIPPFields.Type } | Select-Object @{n = 'name'; e = { $_.label } }, @{n = 'value'; e = { $_.name } }, type, @{n = 'FieldType'; e = { 'Organization' } } + + if ($Null -eq $NinjaCustomFieldsNode) { + [System.Collections.Generic.List[object]]$NinjaCustomFieldsNode = @() + } + + if ($Null -eq $NinjaCustomFieldsOrg) { + [System.Collections.Generic.List[object]]$NinjaCustomFieldsOrg = @() + } + $Unset = [PSCustomObject]@{ + name = '--- Do not synchronize ---' + value = $null + type = 'unset' + } + + } catch { + [System.Collections.Generic.List[object]]$NinjaCustomFieldsNode = @() + [System.Collections.Generic.List[objecgt]]$NinjaCustomFieldsOrg = @() + } + + $MappingObj = [PSCustomObject]@{ + CIPPFields = $CIPPFields + CIPPFieldHeaders = $CIPPFieldHeaders + IntegrationFields = @($Unset) + @($NinjaCustomFieldsOrg) + @($NinjaCustomFieldsNode) + Mappings = $Mappings + } + + return $MappingObj + +} \ No newline at end of file diff --git a/Modules/CippExtensions/NinjaOne/Get-NinjaOneOrgMapping.ps1 b/Modules/CippExtensions/Public/NinjaOne/Get-NinjaOneOrgMapping.ps1 similarity index 64% rename from Modules/CippExtensions/NinjaOne/Get-NinjaOneOrgMapping.ps1 rename to Modules/CippExtensions/Public/NinjaOne/Get-NinjaOneOrgMapping.ps1 index a0274cb0d16f..24c7e6405560 100644 --- a/Modules/CippExtensions/NinjaOne/Get-NinjaOneOrgMapping.ps1 +++ b/Modules/CippExtensions/Public/NinjaOne/Get-NinjaOneOrgMapping.ps1 @@ -4,14 +4,25 @@ function Get-NinjaOneOrgMapping { $CIPPMapping ) try { - #Get available mappings - $Mappings = [pscustomobject]@{} - $Tenants = Get-Tenants + $Tenants = Get-Tenants -IncludeErrors $Filter = "PartitionKey eq 'NinjaOrgsMapping'" - Get-AzDataTableEntity @CIPPMapping -Filter $Filter | ForEach-Object { - $Mappings | Add-Member -NotePropertyName $_.RowKey -NotePropertyValue @{ label = "$($_.NinjaOneName)"; value = "$($_.NinjaOne)" } + $MigrateRows = Get-AzDataTableEntity @CIPPMapping -Filter $Filter | ForEach-Object { + #$Mappings | Add-Member -NotePropertyName $_.RowKey -NotePropertyValue @{ label = "$($_.NinjaOneName)"; value = "$($_.NinjaOne)" } + [PSCustomObject]@{ + RowKey = $_.RowKey + IntegrationName = $_.NinjaOneName + IntegrationId = $_.NinjaOne + PartitionKey = 'NinjaOneMapping' + } + Remove-AzDataTableEntity @CIPPMapping -Entity $_ } + + if (($MigrateRows | Measure-Object).Count -gt 0) { + Add-AzDataTableEntity @CIPPMapping -Entity $MigrateRows -Force + } + + $Mappings = Get-ExtensionMapping -Extension 'NinjaOne' #Get Available Tenants #Get available Ninja clients @@ -43,7 +54,7 @@ function Get-NinjaOneOrgMapping { $MappingObj = [PSCustomObject]@{ Tenants = @($Tenants) - NinjaOrgs = @($NinjaOrgs | Sort-Object name) + Companies = @($NinjaOrgs | Sort-Object name) Mappings = $Mappings } diff --git a/Modules/CippExtensions/NinjaOne/Get-NinjaOneToken.ps1 b/Modules/CippExtensions/Public/NinjaOne/Get-NinjaOneToken.ps1 similarity index 99% rename from Modules/CippExtensions/NinjaOne/Get-NinjaOneToken.ps1 rename to Modules/CippExtensions/Public/NinjaOne/Get-NinjaOneToken.ps1 index d4cb86838ed5..ba293404b5bd 100644 --- a/Modules/CippExtensions/NinjaOne/Get-NinjaOneToken.ps1 +++ b/Modules/CippExtensions/Public/NinjaOne/Get-NinjaOneToken.ps1 @@ -17,7 +17,6 @@ function Get-NinjaOneToken { $ClientSecret = $ENV:NinjaClientSecret } - $body = @{ grant_type = 'client_credentials' client_id = $Configuration.ClientId diff --git a/Modules/CippExtensions/NinjaOne/Invoke-NinjaOneDeviceWebhook.ps1 b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneDeviceWebhook.ps1 similarity index 90% rename from Modules/CippExtensions/NinjaOne/Invoke-NinjaOneDeviceWebhook.ps1 rename to Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneDeviceWebhook.ps1 index a8363917efcc..9213a7015b1a 100644 --- a/Modules/CippExtensions/NinjaOne/Invoke-NinjaOneDeviceWebhook.ps1 +++ b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneDeviceWebhook.ps1 @@ -8,9 +8,9 @@ function Invoke-NinjaOneDeviceWebhook { Write-LogMessage -user $ExecutingUser -API $APIName -message "Webhook Recieved - Updating NinjaOne Device compliance for $($Data.resourceData.id) in $($Data.tenantId)" -Sev 'Info' -tenant $TenantFilter $MappedFields = [pscustomobject]@{} $CIPPMapping = Get-CIPPTable -TableName CippMapping - $Filter = "PartitionKey eq 'NinjaFieldMapping'" - Get-AzDataTableEntity @CIPPMapping -Filter $Filter | Where-Object { $Null -ne $_.NinjaOne -and $_.NinjaOne -ne '' } | ForEach-Object { - $MappedFields | Add-Member -NotePropertyName $_.RowKey -NotePropertyValue $($_.NinjaOne) + $Filter = "PartitionKey eq 'NinjaOneFieldMapping'" + Get-AzDataTableEntity @CIPPMapping -Filter $Filter | Where-Object { $Null -ne $_.IntegrationId -and $_.IntegrationId -ne '' } | ForEach-Object { + $MappedFields | Add-Member -NotePropertyName $_.RowKey -NotePropertyValue $($_.IntegrationId) } if ($MappedFields.DeviceCompliance) { @@ -18,14 +18,14 @@ function Invoke-NinjaOneDeviceWebhook { $M365DeviceID = $Data.resourceData.id $DeviceM365 = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/devices/$($M365DeviceID)" -Tenantid $tenantfilter - + $DeviceFilter = "PartitionKey eq '$($tenantfilter)' and RowKey eq '$($DeviceM365.deviceID)'" $DeviceMapTable = Get-CippTable -tablename 'NinjaOneDeviceMap' $Device = Get-CIPPAzDataTableEntity @DeviceMapTable -Filter $DeviceFilter - + if (($Device | Measure-Object).count -eq 1) { - $Token = Get-NinjaOneToken -configuration $Configuration - + $Token = Get-NinjaOneToken -configuration $Configuration + if ($DeviceM365.isCompliant -eq $True) { $Compliant = 'Compliant' } else { @@ -37,16 +37,16 @@ function Invoke-NinjaOneDeviceWebhook { } | ConvertTo-Json $Null = Invoke-WebRequest -Uri "https://$($Configuration.Instance)/api/v2/device/$($Device.NinjaOneID)/custom-fields" -Method PATCH -Body $ComplianceBody -Headers @{Authorization = "Bearer $($token.access_token)" } -ContentType 'application/json' - + Write-Host 'Updated NinjaOne Device Compliance' - + } else { Write-LogMessage -API 'NinjaOneSync' -user 'CIPP' -message "$($DeviceM365.displayName) ($($M365DeviceID)) was not matched in Ninja for $($tenantfilter)" -Sev 'Info' } } - + } catch { $Message = if ($_.ErrorDetails.Message) { Get-NormalizedError -Message $_.ErrorDetails.Message @@ -56,7 +56,7 @@ function Invoke-NinjaOneDeviceWebhook { Write-Error "Failed NinjaOne Device Webhook for: $($Data | ConvertTo-Json -Depth 100) Linenumber: $($_.InvocationInfo.ScriptLineNumber) Error: $Message" Write-LogMessage -API 'NinjaOneSync' -user 'CIPP' -message "Failed NinjaOne Device Webhook Linenumber: $($_.InvocationInfo.ScriptLineNumber) Error: $Message" -Sev 'Error' } - - + + } \ No newline at end of file diff --git a/Modules/CippExtensions/NinjaOne/Invoke-NinjaOneDocumentTemplate.ps1 b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneDocumentTemplate.ps1 similarity index 100% rename from Modules/CippExtensions/NinjaOne/Invoke-NinjaOneDocumentTemplate.ps1 rename to Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneDocumentTemplate.ps1 diff --git a/Modules/CippExtensions/NinjaOne/Invoke-NinjaOneExtensionScheduler.ps1 b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneExtensionScheduler.ps1 similarity index 97% rename from Modules/CippExtensions/NinjaOne/Invoke-NinjaOneExtensionScheduler.ps1 rename to Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneExtensionScheduler.ps1 index 1faf7d92c833..ca69e5b10935 100644 --- a/Modules/CippExtensions/NinjaOne/Invoke-NinjaOneExtensionScheduler.ps1 +++ b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneExtensionScheduler.ps1 @@ -26,8 +26,8 @@ function Invoke-NinjaOneExtensionScheduler { Write-Host "Current Interval: $CurrentInterval" $CIPPMapping = Get-CIPPTable -TableName CippMapping - $Filter = "PartitionKey eq 'NinjaOrgsMapping'" - $TenantsToProcess = Get-AzDataTableEntity @CIPPMapping -Filter $Filter | Where-Object { $Null -ne $_.NinjaOne -and $_.NinjaOne -ne '' } + $Filter = "PartitionKey eq 'NinjaOneMapping'" + $TenantsToProcess = Get-AzDataTableEntity @CIPPMapping -Filter $Filter | Where-Object { $Null -ne $_.IntegrationId -and $_.IntegrationId -ne '' } if ($Null -eq $LastRunTime -or $LastRunTime -le (Get-Date).addhours(-25) -or $TimeSetting -eq $CurrentInterval) { Write-Host 'Executing' diff --git a/Modules/CippExtensions/NinjaOne/Invoke-NinjaOneOrgMapping.ps1 b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneOrgMapping.ps1 similarity index 93% rename from Modules/CippExtensions/NinjaOne/Invoke-NinjaOneOrgMapping.ps1 rename to Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneOrgMapping.ps1 index 6ea239b73e36..6b5687d6059f 100644 --- a/Modules/CippExtensions/NinjaOne/Invoke-NinjaOneOrgMapping.ps1 +++ b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneOrgMapping.ps1 @@ -9,9 +9,9 @@ function Invoke-NinjaOneOrgMapping { #Get available mappings $Mappings = [pscustomobject]@{} - $Filter = "PartitionKey eq 'NinjaOrgsMapping'" + $Filter = "PartitionKey eq 'NinjaOneMapping'" Get-AzDataTableEntity @CIPPMapping -Filter $Filter | ForEach-Object { - $Mappings | Add-Member -NotePropertyName $_.RowKey -NotePropertyValue @{ label = "$($_.NinjaOneName)"; value = "$($_.NinjaOne)" } + $Mappings | Add-Member -NotePropertyName $_.RowKey -NotePropertyValue @{ label = "$($_.IntegrationName)"; value = "$($_.IntegrationId)" } } #Get Available Tenants @@ -81,10 +81,10 @@ function Invoke-NinjaOneOrgMapping { $MatchedM365Tenants.add($Tenant) $MatchedNinjaOrgs.add($MatchedOrg) $AddObject = @{ - PartitionKey = 'NinjaOrgsMapping' - RowKey = "$($Tenant.customerId)" - 'NinjaOne' = "$($MatchedOrg.id)" - 'NinjaOneName' = "$($MatchedOrg.name)" + PartitionKey = 'NinjaOneMapping' + RowKey = "$($Tenant.customerId)" + IntegrationId = "$($MatchedOrg.id)" + IntegrationName = "$($MatchedOrg.name)" } Add-AzDataTableEntity @CIPPMapping -Entity $AddObject -Force Write-LogMessage -API 'NinjaOneAutoMap_Queue' -user 'CIPP' -message "Added mapping from Organization name match for $($Tenant.customerId). to $($($MatchedOrg.name))" -Sev 'Info' diff --git a/Modules/CippExtensions/NinjaOne/Invoke-NinjaOneOrgMappingTenant.ps1 b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneOrgMappingTenant.ps1 similarity index 69% rename from Modules/CippExtensions/NinjaOne/Invoke-NinjaOneOrgMappingTenant.ps1 rename to Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneOrgMappingTenant.ps1 index 317770c3bb78..c3f05acf1cc3 100644 --- a/Modules/CippExtensions/NinjaOne/Invoke-NinjaOneOrgMappingTenant.ps1 +++ b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneOrgMappingTenant.ps1 @@ -14,7 +14,7 @@ function Invoke-NinjaOneOrgMappingTenant { $TenantFilter = $Tenant.customerId - $M365DevicesRaw = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/managedDevices" -Tenantid $tenantfilter + $M365DevicesRaw = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/managedDevices' -Tenantid $tenantfilter $M365Devices = foreach ($Device in $M365DevicesRaw) { [pscustomobject]@{ @@ -28,10 +28,10 @@ function Invoke-NinjaOneOrgMappingTenant { [System.Collections.Generic.List[PSCustomObject]]$MatchedDevices = @() # Match devices on serial - $DevicesToMatchSerial = $M365Devices | where-object { $null -ne $_.DeviceSerial } + $DevicesToMatchSerial = $M365Devices | Where-Object { $null -ne $_.DeviceSerial } foreach ($SerialMatchDevice in $DevicesToMatchSerial) { - $MatchedDevice = $NinjaDevices | where-object { $_.Serial -eq $SerialMatchDevice.DeviceSerial -or $_.BiosSerialNumber -eq $SerialMatchDevice.DeviceSerial } - if (($MatchedDevice | measure-object).count -eq 1) { + $MatchedDevice = $NinjaDevices | Where-Object { $_.Serial -eq $SerialMatchDevice.DeviceSerial -or $_.BiosSerialNumber -eq $SerialMatchDevice.DeviceSerial } + if (($MatchedDevice | Measure-Object).count -eq 1) { $Match = [pscustomobject]@{ M365 = $SerialMatchDevice Ninja = $MatchedDevice @@ -41,10 +41,10 @@ function Invoke-NinjaOneOrgMappingTenant { } # Try to match on Name - $DevicesToMatchName = $M365Devices | where-object { $_ -notin $MatchedDevices.M365 } + $DevicesToMatchName = $M365Devices | Where-Object { $_ -notin $MatchedDevices.M365 } foreach ($NameMatchDevice in $DevicesToMatchName) { - $MatchedDevice = $NinjaDevices | where-object { $_.SystemName -eq $NameMatchDevice.DeviceName -or $_.DNSName -eq $NameMatchDevice.DeviceName } - if (($MatchedDevice | measure-object).count -eq 1) { + $MatchedDevice = $NinjaDevices | Where-Object { $_.SystemName -eq $NameMatchDevice.DeviceName -or $_.DNSName -eq $NameMatchDevice.DeviceName } + if (($MatchedDevice | Measure-Object).count -eq 1) { $Match = [pscustomobject]@{ M365 = $NameMatchDevice Ninja = $MatchedDevice @@ -56,17 +56,17 @@ function Invoke-NinjaOneOrgMappingTenant { # Match on the Org with the most devices that match if (($MatchedDevices.Ninja.ID | Measure-Object).Count -eq 1) { - $MatchedOrgID = ($MatchedDevices.Ninja | group-object OrgID | sort-object Count -desc)[0].name + $MatchedOrgID = ($MatchedDevices.Ninja | Group-Object OrgID | Sort-Object Count -desc)[0].name $MatchedOrg = $NinjaOrgs | Where-Object { $_.id -eq $MatchedOrgID } $AddObject = @{ - PartitionKey = 'NinjaOrgsMapping' - RowKey = "$($Tenant.customerId)" - 'NinjaOne' = "$($MatchedOrg.id)" - 'NinjaOneName' = "$($MatchedOrg.name)" + PartitionKey = 'NinjaOneMapping' + RowKey = "$($Tenant.customerId)" + IntegrationId = "$($MatchedOrg.id)" + IntegrationName = "$($MatchedOrg.name)" } Add-AzDataTableEntity @CIPPMapping -Entity $AddObject -Force - Write-LogMessage -API 'NinjaOneAutoMap_Queue' -user 'CIPP' -message "Added mapping from Device match for $($Tenant.displayName) to $($($MatchedOrg.name))" -Sev 'Info' + Write-LogMessage -API 'NinjaOneAutoMap_Queue' -user 'CIPP' -message "Added mapping from Device match for $($Tenant.displayName) to $($($MatchedOrg.name))" -Sev 'Info' } diff --git a/Modules/CippExtensions/NinjaOne/Invoke-NinjaOneSync.ps1 b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneSync.ps1 similarity index 91% rename from Modules/CippExtensions/NinjaOne/Invoke-NinjaOneSync.ps1 rename to Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneSync.ps1 index 5567ddb7c1b8..c6fb732eb30a 100644 --- a/Modules/CippExtensions/NinjaOne/Invoke-NinjaOneSync.ps1 +++ b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneSync.ps1 @@ -3,8 +3,8 @@ function Invoke-NinjaOneSync { $Table = Get-CIPPTable -TableName NinjaOneSettings $CIPPMapping = Get-CIPPTable -TableName CippMapping - $Filter = "PartitionKey eq 'NinjaOrgsMapping'" - $TenantsToProcess = Get-AzDataTableEntity @CIPPMapping -Filter $Filter | Where-Object { $Null -ne $_.NinjaOne -and $_.NinjaOne -ne '' } + $Filter = "PartitionKey eq 'NinjaOneMapping'" + $TenantsToProcess = Get-AzDataTableEntity @CIPPMapping -Filter $Filter | Where-Object { $Null -ne $_.IntegrationId -and $_.IntegrationId -ne '' } $Batch = foreach ($Tenant in $TenantsToProcess) { diff --git a/Modules/CippExtensions/NinjaOne/Invoke-NinjaOneTenantSync.ps1 b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 similarity index 96% rename from Modules/CippExtensions/NinjaOne/Invoke-NinjaOneTenantSync.ps1 rename to Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 index 9828682c6348..9a3b6949b056 100644 --- a/Modules/CippExtensions/NinjaOne/Invoke-NinjaOneTenantSync.ps1 +++ b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 @@ -6,13 +6,13 @@ function Invoke-NinjaOneTenantSync { try { $StartQueueTime = Get-Date Write-Host "$(Get-Date) - Starting NinjaOne Sync" - - # Stagger start + + # Stagger start # Check Global Rate Limiting - $CurrentMap = Get-ExtensionRateLimit -ExtensionName 'NinjaOne' -ExtensionPartitionKey 'NinjaOrgsMapping' -RateLimit 5 -WaitTime 10 + $CurrentMap = Get-ExtensionRateLimit -ExtensionName 'NinjaOne' -ExtensionPartitionKey 'NinjaOneMapping' -RateLimit 5 -WaitTime 10 $StartTime = Get-Date - + # Parse out the Tenant we are processing $MappedTenant = $QueueItem.MappedTenant @@ -21,7 +21,7 @@ function Invoke-NinjaOneTenantSync { $StartDate = try { Get-Date($CurrentItem.lastStartTime) } catch { $Null } $EndDate = try { Get-Date($CurrentItem.lastEndTime) } catch { $Null } - + if (($null -ne $CurrentItem.lastStartTime) -and ($StartDate -gt (Get-Date).AddMinutes(-10)) -and ( $Null -eq $CurrentItem.lastEndTime -or ($StartDate -gt $EndDate))) { Throw "NinjaOne Sync for Tenant $($MappedTenant.RowKey) is still running, please wait 10 minutes and try again." } @@ -40,19 +40,19 @@ function Invoke-NinjaOneTenantSync { $Table = Get-CIPPTable -TableName NinjaOneSettings $NinjaSettings = (Get-CIPPAzDataTableEntity @Table) $CIPPUrl = ($NinjaSettings | Where-Object { $_.RowKey -eq 'CIPPURL' }).SettingValue - - - $Customer = Get-Tenants | Where-Object { $_.customerId -eq $MappedTenant.RowKey } + + + $Customer = Get-Tenants -IncludeErrors | Where-Object { $_.customerId -eq $MappedTenant.RowKey } Write-Host "Processing: $($Customer.displayName) - Queued for $((New-TimeSpan -Start $StartQueueTime -End $StartTime).TotalSeconds)" - Write-LogMessage -API 'NinjaOneSync' -user 'NinjaOneSync' -message "Processing NinjaOne Synchronization for $($Customer.displayName) - Queued for $((New-TimeSpan -Start $StartQueueTime -End $StartTime).TotalSeconds)" -Sev 'Info' + Write-LogMessage -API 'NinjaOneSync' -user 'NinjaOneSync' -message "Processing NinjaOne Synchronization for $($Customer.displayName) - Queued for $((New-TimeSpan -Start $StartQueueTime -End $StartTime).TotalSeconds)" -Sev 'Info' if (($Customer | Measure-Object).count -ne 1) { Throw "Unable to match the recieved ID to a tenant QueueItem: $($QueueItem | ConvertTo-Json -Depth 100 | Out-String) Matched Customer: $($Customer| ConvertTo-Json -Depth 100 | Out-String)" } $TenantFilter = $Customer.defaultDomainName - $NinjaOneOrg = $MappedTenant.NinjaOne + $NinjaOneOrg = $MappedTenant.IntegrationId # Get the NinjaOne general extension settings. @@ -62,9 +62,9 @@ function Invoke-NinjaOneTenantSync { # Pull the list of field Mappings so we know which fields to render. $MappedFields = [pscustomobject]@{} $CIPPMapping = Get-CIPPTable -TableName CippMapping - $Filter = "PartitionKey eq 'NinjaFieldMapping'" - Get-CIPPAzDataTableEntity @CIPPMapping -Filter $Filter | Where-Object { $Null -ne $_.NinjaOne -and $_.NinjaOne -ne '' } | ForEach-Object { - $MappedFields | Add-Member -NotePropertyName $_.RowKey -NotePropertyValue $($_.NinjaOne) + $Filter = "PartitionKey eq 'NinjaOneFieldMapping'" + Get-CIPPAzDataTableEntity @CIPPMapping -Filter $Filter | Where-Object { $Null -ne $_.IntegrationId -and $_.IntegrationId -ne '' } | ForEach-Object { + $MappedFields | Add-Member -NotePropertyName $_.RowKey -NotePropertyValue $($_.IntegrationId) } # Get NinjaOne Devices @@ -76,14 +76,14 @@ function Invoke-NinjaOneTenantSync { $Result $ResultCount = ($Result.id | Measure-Object -Maximum) $After = $ResultCount.maximum - + } while ($ResultCount.count -eq $PageSize) Write-Host 'Fetched NinjaOne Devices' - + [System.Collections.Generic.List[PSCustomObject]]$NinjaOneUserDocs = @() - if ($Configuration.UserDocumentsEnabled -eq $True) { + if ($Configuration.UserDocumentsEnabled -eq $True) { # Get NinjaOne User Documents $UserDocTemplate = [PSCustomObject]@{ name = 'CIPP - Microsoft 365 Users' @@ -169,7 +169,7 @@ function Invoke-NinjaOneTenantSync { # Get NinjaOne Users [System.Collections.Generic.List[PSCustomObject]]$NinjaOneUserDocs = ((Invoke-WebRequest -Uri "https://$($Configuration.Instance)/api/v2/organization/documents?organizationIds=$($NinjaOneOrg)&templateIds=$($NinjaOneUsersTemplate.id)" -Method GET -Headers @{Authorization = "Bearer $($token.access_token)" } -ContentType 'application/json').content | ConvertFrom-Json -Depth 100) - + foreach ($NinjaDoc in $NinjaOneUserDocs) { $ParsedFields = [pscustomobject]@{} foreach ($Field in $NinjaDoc.Fields) { @@ -185,7 +185,7 @@ function Invoke-NinjaOneTenantSync { Write-Host 'Fetched NinjaOne User Docs' } - + [System.Collections.Generic.List[PSCustomObject]]$NinjaOneLicenseDocs = @() if ($Configuration.LicenseDocumentsEnabled) { # NinjaOne License Documents @@ -236,10 +236,10 @@ function Invoke-NinjaOneTenantSync { } $NinjaOneLicenseTemplate = Invoke-NinjaOneDocumentTemplate -Template $LicenseDocTemplate -Token $Token - + # Get NinjaOne Licenses [System.Collections.Generic.List[PSCustomObject]]$NinjaOneLicenseDocs = ((Invoke-WebRequest -Uri "https://$($Configuration.Instance)/api/v2/organization/documents?organizationIds=$($NinjaOneOrg)&templateIds=$($NinjaOneLicenseTemplate.id)" -Method GET -Headers @{Authorization = "Bearer $($token.access_token)" } -ContentType 'application/json').content | ConvertFrom-Json -Depth 100) - + foreach ($NinjaLic in $NinjaOneLicenseDocs) { $ParsedFields = [pscustomobject]@{} foreach ($Field in $NinjaLic.Fields) { @@ -328,8 +328,8 @@ function Invoke-NinjaOneTenantSync { id = 'Subscriptions' method = 'GET' url = '/directory/subscriptions' - } - + } + ) Write-Verbose "$(Get-Date) - Fetching Bulk Data" @@ -346,14 +346,14 @@ function Invoke-NinjaOneTenantSync { $SecureScore = Get-GraphBulkResultByID -value -Results $TenantResults -ID 'SecureScore' $Subscriptions = Get-GraphBulkResultByID -value -Results $TenantResults -ID 'Subscriptions' - + [System.Collections.Generic.List[PSCustomObject]]$SecureScoreProfiles = Get-GraphBulkResultByID -value -Results $TenantResults -ID 'SecureScoreControlProfiles' $CurrentSecureScore = ($SecureScore | Sort-Object createDateTime -Descending | Select-Object -First 1) $MaxSecureScoreRank = ($SecureScoreProfiles.rank | Measure-Object -Maximum).maximum $MaxSecureScore = $CurrentSecureScore.maxScore - + [System.Collections.Generic.List[PSCustomObject]]$SecureScoreParsed = Foreach ($Score in $CurrentSecureScore.controlScores) { $MatchedProfile = $SecureScoreProfiles | Where-Object { $_.id -eq $Score.controlName } [PSCustomObject]@{ @@ -368,20 +368,20 @@ function Invoke-NinjaOneTenantSync { maxScore = $MatchedProfile.maxScore rank = $MatchedProfile.rank adjustedRank = $MaxSecureScoreRank - $MatchedProfile.rank - + } } $TenantDetails = Get-GraphBulkResultByID -value -Results $TenantResults -ID 'TenantDetails' Write-Verbose "$(Get-Date) - Parsing Users" - # Grab licensed users - $licensedUsers = $Users | Where-Object { $null -ne $_.AssignedLicenses.SkuId } | Sort-Object UserPrincipalName - - Write-Verbose "$(Get-Date) - Parsing Roles" + # Grab licensed users + $licensedUsers = $Users | Where-Object { $null -ne $_.AssignedLicenses.SkuId } | Sort-Object UserPrincipalName + + Write-Verbose "$(Get-Date) - Parsing Roles" # Get All Roles $AllRoles = Get-GraphBulkResultByID -value -Results $TenantResults -ID 'AllRoles' - + $SelectList = 'id', 'displayName', 'userPrincipalName' [System.Collections.Generic.List[PSCustomObject]]$RolesRequestArray = @() @@ -410,11 +410,11 @@ function Invoke-NinjaOneTenantSync { ParsedMembers = $Result.body.value.Displayname -join ', ' } } - + $AdminUsers = (($Roles | Where-Object { $_.Displayname -match 'Administrator' }).Members | Where-Object { $null -ne $_.displayName }) - + Write-Verbose "$(Get-Date) - Fetching Domains" try { $RawDomains = Get-GraphBulkResultByID -value -Results $TenantResults -ID 'RawDomains' @@ -422,8 +422,8 @@ function Invoke-NinjaOneTenantSync { $RawDomains = $null } $customerDomains = ($RawDomains | Where-Object { $_.IsVerified -eq $True }).id -join ', ' | Out-String - - + + Write-Verbose "$(Get-Date) - Parsing Licenses" # Get Licenses $Licenses = Get-GraphBulkResultByID -value -Results $TenantResults -ID 'Licenses' @@ -432,7 +432,7 @@ function Invoke-NinjaOneTenantSync { if ($Licenses) { $LicensesParsed = $Licenses | Where-Object { $_.PrepaidUnits.Enabled -gt 0 } | Select-Object @{N = 'License Name'; E = { (Get-Culture).TextInfo.ToTitleCase((convert-skuname -skuname $_.SkuPartNumber).Tolower()) } }, @{N = 'Active'; E = { $_.PrepaidUnits.Enabled } }, @{N = 'Consumed'; E = { $_.ConsumedUnits } }, @{N = 'Unused'; E = { $_.PrepaidUnits.Enabled - $_.ConsumedUnits } } } - + Write-Verbose "$(Get-Date) - Parsing Devices" # Get all devices from Intune $devices = Get-GraphBulkResultByID -value -Results $TenantResults -ID 'Devices' @@ -440,7 +440,7 @@ function Invoke-NinjaOneTenantSync { Write-Verbose "$(Get-Date) - Parsing Device Compliance Polcies" # Fetch Compliance Policy Status $DeviceCompliancePolicies = Get-GraphBulkResultByID -value -Results $TenantResults -ID 'DeviceCompliancePolicies' - + # Get the status of each device for each policy [System.Collections.Generic.List[PSCustomObject]]$PolicyRequestArray = @() foreach ($CompliancePolicy in $DeviceCompliancePolicies) { @@ -466,9 +466,9 @@ function Invoke-NinjaOneTenantSync { DeviceStatuses = $Result.body.value } } - + Write-Verbose "$(Get-Date) - Parsing Groups" - # Fetch Groups + # Fetch Groups $AllGroups = Get-GraphBulkResultByID -value -Results $TenantResults -ID 'Groups' # Fetch the App status for each device @@ -492,7 +492,7 @@ function Invoke-NinjaOneTenantSync { $Groups = foreach ($Result in $GroupMembersReturn) { [pscustomobject]@{ ID = $Result.id - DisplayName = ($AllGroups | Where-Object { $_.id -eq $Result.id }).DisplayName + DisplayName = ($AllGroups | Where-Object { $_.id -eq $Result.id }).DisplayName Members = $result.body.value } } @@ -555,10 +555,10 @@ function Invoke-NinjaOneTenantSync { Members = $CAMembers } } - + Write-Verbose "$(Get-Date) - Fetching One Drive Details" try { - $OneDriveDetails = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/reports/getOneDriveUsageAccountDetail(period='D7')" -tenantid $TenantFilter | ConvertFrom-Csv + $OneDriveDetails = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/reports/getOneDriveUsageAccountDetail(period='D7')" -tenantid $TenantFilter | ConvertFrom-Csv } catch { Write-Error "Failed to fetch Onedrive Details: $_" $OneDriveDetails = $null @@ -571,7 +571,7 @@ function Invoke-NinjaOneTenantSync { Write-Error "Failed to fetch CAS Details: $_" $CASFull = $null } - + Write-Verbose "$(Get-Date) - Fetching Mailbox Details" try { $MailboxDetailedFull = New-ExoRequest -TenantID $Customer.defaultDomainName -cmdlet 'Get-Mailbox' @@ -590,12 +590,12 @@ function Invoke-NinjaOneTenantSync { Write-Verbose "$(Get-Date) - Fetching Mailbox Stats" try { - $MailboxStatsFull = New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/reports/getMailboxUsageDetail(period='D7')" -tenantid $TenantFilter | ConvertFrom-Csv + $MailboxStatsFull = New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/reports/getMailboxUsageDetail(period='D7')" -tenantid $TenantFilter | ConvertFrom-Csv } catch { Write-Error "Failed to fetch Mailbox Stats: $_" $MailboxStatsFull = $null } - + Write-Host 'Fetched M365 Additional Data' @@ -604,7 +604,7 @@ function Invoke-NinjaOneTenantSync { ############################ Format and Synchronize to NinjaOne ############################ $DeviceTable = Get-CippTable -tablename 'CacheNinjaOneParsedDevices' $DeviceMapTable = Get-CippTable -tablename 'NinjaOneDeviceMap' - + $DeviceFilter = "PartitionKey eq '$($Customer.CustomerId)'" [System.Collections.Generic.List[PSCustomObject]]$RawParsedDevices = Get-CIPPAzDataTableEntity @DeviceTable -Filter $DeviceFilter @@ -621,13 +621,13 @@ function Invoke-NinjaOneTenantSync { # Parse Devices Foreach ($Device in $Devices | Where-Object { $_.id -notin $ParsedDevices.id }) { - + # First lets match on serial $MatchedNinjaDevice = $NinjaDevices | Where-Object { $_.system.biosSerialNumber -eq $Device.SerialNumber -or $_.system.serialNumber -eq $Device.SerialNumber } # See if we found just one device, if not match on name if (($MatchedNinjaDevice | Measure-Object).count -ne 1) { - $MatchedNinjaDevice = $NinjaDevices | Where-Object { $_.systemName -eq $Device.Name -or $_.dnsName -eq $Device.Name } + $MatchedNinjaDevice = $NinjaDevices | Where-Object { $_.systemName -eq $Device.Name -or $_.dnsName -eq $Device.Name } } # Check on a match again and set name @@ -658,8 +658,8 @@ function Invoke-NinjaOneTenantSync { Add-CIPPAzDataTableEntity @DeviceMapTable -Entity $MappedDevice -Force } - - + + Foreach ($DeviceUser in $Device.usersloggedon) { $FoundUser = ($Users | Where-Object { $_.id -eq $DeviceUser.userid }) @@ -690,7 +690,7 @@ function Invoke-NinjaOneTenantSync { }) } } - + } } @@ -743,7 +743,7 @@ function Invoke-NinjaOneTenantSync { } -Force $ParsedDevices.add($ParsedDevice) - + ### Update NinjaOne Device Fields if ($MatchedNinjaDevice) { $NinjaDeviceUpdate = [PSCustomObject]@{} @@ -767,7 +767,7 @@ function Invoke-NinjaOneTenantSync { ) - + $DeviceLinksHTML = Get-NinjaOneLinks -Data $DeviceLinksData -SmallCols 2 -MedCols 3 -LargeCols 3 -XLCols 3 $DeviceLinksHtml = '
' + $DeviceLinksHTML + '
' @@ -778,7 +778,7 @@ function Invoke-NinjaOneTenantSync { } if ($MappedFields.DeviceSummary) { - + # Set Compliance Status if ($Device.complianceState -eq 'compliant') { $Compliance = '   Compliant' @@ -795,9 +795,9 @@ function Invoke-NinjaOneTenantSync { 'Enrolled' = $Device.enrolledDateTime 'Last Checkin' = $Device.lastSyncDateTime 'Compliant' = $Compliance - 'Management Type' = $Device.managementAgent + 'Management Type' = $Device.managementAgent } - + $DeviceDetailsCard = Get-NinjaOneInfoCard -Title 'Device Details' -Data $DeviceDetailsData -Icon 'fas fa-laptop' # Device Hardware @@ -808,8 +808,8 @@ function Invoke-NinjaOneTenantSync { 'Chassis' = $Device.chassisType 'Model' = $Device.model 'Manufacturer' = $Device.manufacturer - } - + } + $DeviceHardwareCard = Get-NinjaOneInfoCard -Title 'Device Details' -Data $DeviceHardwareData -Icon 'fas fa-microchip' # Device Enrollment @@ -821,8 +821,8 @@ function Invoke-NinjaOneTenantSync { 'Device Guard Requirements' = $Device.hardwareinformation.deviceGuardVirtualizationBasedSecurityHardwareRequirementState 'Virtualistation Based Security' = $Device.hardwareinformation.deviceGuardVirtualizationBasedSecurityState 'Credential Guard' = $Device.hardwareinformation.deviceGuardLocalSystemAuthorityCredentialGuardState - } - + } + $DeviceEnrollmentCard = Get-NinjaOneInfoCard -Title 'Device Enrollment' -Data $DeviceEnrollmentData -Icon 'fas fa-table-list' @@ -831,7 +831,7 @@ function Invoke-NinjaOneTenantSync { $DevicePoliciesHTML = ([System.Web.HttpUtility]::HtmlDecode($DevicePoliciesFormatted) -replace '', '') -replace '', '' $TitleLink = "https://intune.microsoft.com/$($Customer.defaultDomainName)/#view/Microsoft_Intune_Devices/DeviceSettingsMenuBlade/~/compliance/mdmDeviceId/$($Device.id)/primaryUserId/" $DeviceCompliancePoliciesCard = Get-NinjaOneCard -Title 'Device Compliance Policies' -Body $DevicePoliciesHTML -Icon 'fas fa-list-check' -TitleLink $TitleLink - + # Device Groups $DeviceGroupsTable = foreach ($Group in $Groups) { if ($device.azureADDeviceId -in $Group.members.deviceId) { @@ -844,16 +844,16 @@ function Invoke-NinjaOneTenantSync { $DeviceGroupsHTML = ([System.Web.HttpUtility]::HtmlDecode($DeviceGroupsFormatted) -replace '', '') -replace '', '' $DeviceGroupsCard = Get-NinjaOneCard -Title 'Device Groups' -Body $DeviceGroupsHTML -Icon 'fas fa-layer-group' - $DeviceSummaryHTML = '
' + - '
' + $DeviceDetailsCard + + $DeviceSummaryHTML = '
' + + '
' + $DeviceDetailsCard + '
' + $DeviceHardwareCard + - '
' + $DeviceEnrollmentCard + + '
' + $DeviceEnrollmentCard + '
' + $DeviceCompliancePoliciesCard + '
' + $DeviceGroupsCard + '
' - - $NinjaDeviceUpdate | Add-Member -NotePropertyName $MappedFields.DeviceSummary -NotePropertyValue @{'html' = $DeviceSummaryHTML } - } + + $NinjaDeviceUpdate | Add-Member -NotePropertyName $MappedFields.DeviceSummary -NotePropertyValue @{'html' = $DeviceSummaryHTML } + } } if ($MappedFields.DeviceCompliance) { @@ -863,7 +863,7 @@ function Invoke-NinjaOneTenantSync { $Compliant = 'Non-Compliant' } $NinjaDeviceUpdate | Add-Member -NotePropertyName $MappedFields.DeviceCompliance -NotePropertyValue $Compliant - + } # Update Device @@ -888,11 +888,11 @@ function Invoke-NinjaOneTenantSync { $SyncUsers = $Users } - + $UsersTable = Get-CippTable -tablename 'CacheNinjaOneParsedUsers' $UsersUpdateTable = Get-CippTable -tablename 'CacheNinjaOneUsersUpdate' $UsersMapTable = Get-CippTable -tablename 'NinjaOneUserMap' - + $UsersFilter = "PartitionKey eq '$($Customer.CustomerId)'" [System.Collections.Generic.List[PSCustomObject]]$ParsedUsers = Get-CIPPAzDataTableEntity @UsersTable -Filter $UsersFilter @@ -923,7 +923,7 @@ function Invoke-NinjaOneTenantSync { foreach ($user in $SyncUsers | Where-Object { $_.id -notin $ParsedUsers.RowKey }) { try { - + $NinjaOneUser = $NinjaOneUserDocs | Where-Object { $_.ParsedFields.cippUserID -eq $User.ID } if (($NinjaOneUser | Measure-Object).count -gt 1) { Throw 'Multiple Users with the same ID found' @@ -1010,7 +1010,7 @@ function Invoke-NinjaOneTenantSync { $MatchedNinjaDevice = $UserDevice.NinjaDevice $ParsedDeviceName = $UserDevice.DeviceLink - + # Set Last Login Time $LastLoginTime = ($UserDevice.UserDetails | Where-Object { $_.id -eq $User.id }).lastLogin if (!$LastLoginTime) { @@ -1033,7 +1033,7 @@ function Invoke-NinjaOneTenantSync { } '
  • ' + "$ComplianceIcon $OSIcon $($ParsedDeviceName) ($LastLoginTime)
  • " - + } @@ -1048,7 +1048,7 @@ function Invoke-NinjaOneTenantSync { } catch {} }) -join '' - + $UserOneDriveStats = $OneDriveDetails | Where-Object { $_.'Owner Principal Name' -eq $User.userPrincipalName } | Select-Object -First 1 $UserOneDriveUse = $UserOneDriveStats.'Storage Used (Byte)' / 1GB @@ -1083,7 +1083,7 @@ function Invoke-NinjaOneTenantSync { $OneDriveParsed = 'Not Enabled' } - + if ($UserOneDriveStats) { $OneDriveCardData = [PSCustomObject]@{ 'One Drive URL' = '' + ($UserOneDriveStats.'Site URL') + '' @@ -1100,9 +1100,9 @@ function Invoke-NinjaOneTenantSync { $OneDriveCardData = [PSCustomObject]@{ 'One Drive' = 'Disabled' } - } + } + - $UserMailboxStats = $MailboxStatsFull | Where-Object { $_.'User Principal Name' -eq $User.userPrincipalName } | Select-Object -First 1 $UserMailUse = $UserMailboxStats.'Storage Used (Byte)' / 1GB $UserMailTotal = $UserMailboxStats.'Prohibit Send/Receive Quota (Byte)' / 1GB @@ -1243,7 +1243,7 @@ function Invoke-NinjaOneTenantSync {     "@ - + # Return Data for Users Summary Table $ParsedUser = [PSCustomObject]@{ @@ -1264,8 +1264,8 @@ function Invoke-NinjaOneTenantSync { Add-CIPPAzDataTableEntity @UsersTable -Entity $ParsedUser -Force $ParsedUsers.add($ParsedUser) - - + + if ($Configuration.UserDocumentsEnabled -eq $True) { # Format into Ninja HTML @@ -1283,13 +1283,13 @@ function Invoke-NinjaOneTenantSync { $UserPolciesCard = Get-NinjaOneCard -Title 'Assigned Conditional Access Policies' -Body $UserPoliciesFormatted - $UserSummaryHTML = '
    ' + - '
    ' + $UserOverviewCardHTML + + $UserSummaryHTML = '
    ' + + '
    ' + $UserOverviewCardHTML + '
    ' + $MailboxDetailsCardHTML + '
    ' + $MailboxSettingsCardHTML + - '
    ' + $OneDriveCardHTML + - '
    ' + $UserPolciesCard + - '
    ' + $DeviceSummaryCardHTML + + '
    ' + $OneDriveCardHTML + + '
    ' + $UserPolciesCard + + '
    ' + $DeviceSummaryCardHTML + '
    ' @@ -1301,10 +1301,10 @@ function Invoke-NinjaOneTenantSync { @{n = 'State'; e = { $_.Compliance } }, @{n = 'Model'; e = { $_.Model } }, @{n = 'Manufacturer'; e = { $_.Make } } - + $UserDeviceDetailHTML = $UserDeviceDetailsTable | ConvertTo-Html -As Table -Fragment $UserDeviceDetailHTML = ([System.Web.HttpUtility]::HtmlDecode($UserDeviceDetailHTML) -replace '', '') -replace '', '' - + $UserFields = @{ cippUserLinks = @{'html' = $UserLinksHTML } @@ -1361,7 +1361,7 @@ function Invoke-NinjaOneTenantSync { } Catch { Write-Host "Bulk Creation Error, but may have been successful as only 1 record with an issue could have been the cause: $_" } - + try { # Update Users if (($NinjaUserUpdates | Measure-Object).count -ge 100) { @@ -1385,7 +1385,7 @@ function Invoke-NinjaOneTenantSync { } else { $Field = $UserDoc.fields | Where-Object { $_.name -eq 'cippUserID' } } - + if ($Null -ne $Field.value -and $Field.value -ne '') { $MappedUser = ($UsersMap | Where-Object { $_.M365ID -eq $Field.value }) @@ -1411,15 +1411,15 @@ function Invoke-NinjaOneTenantSync { } - + } } catch { Write-Error "User $($User.UserPrincipalName): A fatal error occured while processing user $_" } - + } - + $CreatedUsers = $Null $UpdatedUsers = $Null @@ -1431,12 +1431,12 @@ function Invoke-NinjaOneTenantSync { Write-Host 'Creating NinjaOne Users' [System.Collections.Generic.List[PSCustomObject]]$CreatedUsers = (Invoke-WebRequest -Uri "https://$($Configuration.Instance)/api/v2/organization/documents" -Method POST -Headers @{Authorization = "Bearer $($token.access_token)" } -ContentType 'application/json; charset=utf-8' -Body ("[$($NinjaUserCreation.body -join ',')]") -EA Stop).content | ConvertFrom-Json -Depth 100 Remove-AzDataTableEntity @UsersUpdateTable -Entity $NinjaUserCreation - + } } Catch { Write-Host "Bulk Creation Error, but may have been successful as only 1 record with an issue could have been the cause: $_" } - + try { # Update Users if (($NinjaUserUpdates | Measure-Object).count -ge 1) { @@ -1450,8 +1450,8 @@ function Invoke-NinjaOneTenantSync { ### Relationship Mapping # Parse out the NinjaOne ID to MS ID - - + + [System.Collections.Generic.List[PSCustomObject]]$UserDocResults = $UpdatedUsers + $CreatedUsers if (($UserDocResults | Where-Object { $Null -ne $_ -and $_ -ne '' } | Measure-Object).count -ge 1) { @@ -1462,7 +1462,7 @@ function Invoke-NinjaOneTenantSync { } else { $Field = $UserDoc.fields | Where-Object { $_.name -eq 'cippUserID' } } - + if ($Null -ne $Field.value -and $Field.value -ne '') { $MappedUser = ($UsersMap | Where-Object { $_.M365ID -eq $Field.value }) @@ -1486,8 +1486,8 @@ function Invoke-NinjaOneTenantSync { } } - - + + # Relate Users to Devices Foreach ($LinkDevice in $ParsedDevices | Where-Object { $null -ne $_.NinjaDevice }) { $RelatedItems = (Invoke-WebRequest -Uri "https://$($Configuration.Instance)/api/v2/related-items/with-entity/NODE/$($LinkDevice.NinjaDevice.id)" -Method GET -Headers @{Authorization = "Bearer $($token.access_token)" } -ContentType 'application/json').content | ConvertFrom-Json -Depth 100 @@ -1507,7 +1507,7 @@ function Invoke-NinjaOneTenantSync { } } - + try { # Update Relations @@ -1534,7 +1534,7 @@ function Invoke-NinjaOneTenantSync { $FriendlyLicenseName = $License.SkuPartNumber } - + $LicenseUsers = foreach ($SubUser in $Users) { $MatchedLicense = $SubUser.assignedLicenses | Where-Object { $License.skuId -in $_.skuId } $MatchedPlans = $SubUser.AssignedPlans | Where-Object { $_.servicePlanId -in $License.servicePlans.servicePlanID } @@ -1551,7 +1551,7 @@ function Invoke-NinjaOneTenantSync { 'License Assigned' = $(try { $(Get-Date(($MatchedPlans | Group-Object assignedDateTime | Sort-Object Count -Desc | Select-Object -First 1).name) -Format u) } catch { 'Unknown' }) NinjaUserDocID = $SubRelUserID } - } + } } $LicenseUsersHTML = $LicenseUsers | Select-Object -ExcludeProperty NinjaUserDocID | ConvertTo-Html -As Table -Fragment @@ -1578,12 +1578,12 @@ function Invoke-NinjaOneTenantSync { $LicenseItemsTable = $License.servicePlans | Select-Object @{n = 'Plan Name'; e = { convert-skuname -skuname $_.servicePlanName } }, @{n = 'Applies To'; e = { $_.appliesTo } }, @{n = 'Provisioning Status'; e = { $_.provisioningStatus } } $LicenseItemsHTML = $LicenseItemsTable | ConvertTo-Html -As Table -Fragment $LicenseItemsHTML = ([System.Web.HttpUtility]::HtmlDecode($LicenseItemsHTML) -replace '', '') -replace '', '' - + $LicenseItemsCardHTML = Get-NinjaOneCard -Title 'License Items' -Body $LicenseItemsHTML -Icon 'fas fa-chart-bar' - $LicenseSummaryHTML = '
    ' + - '
    ' + $LicenseOverviewCardHTML + + $LicenseSummaryHTML = '
    ' + + '
    ' + $LicenseOverviewCardHTML + '
    ' + $SubscriptionCardHTML + '
    ' + $LicenseItemsCardHTML + '
    ' @@ -1630,7 +1630,7 @@ function Invoke-NinjaOneTenantSync { } Catch { Write-Host "Bulk Creation Error, but may have been successful as only 1 record with an issue could have been the cause: $_" } - + try { # Update Subscriptions if (($NinjaLicenseUpdates | Measure-Object).count -ge 1) { @@ -1663,7 +1663,7 @@ function Invoke-NinjaOneTenantSync { ) } } - + try { # Update Relations @@ -1750,7 +1750,7 @@ function Invoke-NinjaOneTenantSync { $M365LinksHTML = Get-NinjaOneLinks -Data $ManagementLinksData -Title 'Portals' -SmallCols 2 -MedCols 3 -LargeCols 3 -XLCols 3 $CIPPLinksData = @( - + @{ Name = 'CIPP Tenant Dashboard' Link = "https://$CIPPUrl/home?customerId=$($Customer.CustomerId)" @@ -1802,8 +1802,8 @@ function Invoke-NinjaOneTenantSync { ### Tenant Overview Card $ParsedAdmins = [PSCustomObject]@{} - - $AdminUsers | Select-Object displayname, userPrincipalName -Unique | ForEach-Object { + + $AdminUsers | Select-Object displayname, userPrincipalName -Unique | ForEach-Object { $ParsedAdmins | Add-Member -NotePropertyName $_.displayname -NotePropertyValue $_.userPrincipalName } @@ -1814,7 +1814,7 @@ function Invoke-NinjaOneTenantSync { 'Creation Date' = $TenantDetails.createdDateTime 'Domains' = $customerDomains 'Admin Users' = ($AdminUsers | ForEach-Object { "$($_.DisplayName)" }) -join ', ' - + } $TenantSummaryCard = Get-NinjaOneInfoCard -Title 'Tenant Details' -Data $TenantDetailsItems -Icon 'fas fa-building' @@ -1826,8 +1826,8 @@ function Invoke-NinjaOneTenantSync { $LicensedUsersCount = ($licensedUsers | Measure-Object).count $UnlicensedUsersCount = $TotalUsersCount - $GuestUsersCount - $LicensedUsersCount $UsersEnabledCount = ($Users | Where-Object { $_.accountEnabled -eq $True } | Measure-Object).count - - # Enabled Users + + # Enabled Users $Data = @( @{ @@ -1841,10 +1841,10 @@ function Invoke-NinjaOneTenantSync { Colour = '#D53948' } ) - - + + $UsersEnabledChartHTML = Get-NinjaInLineBarGraph -Title 'User Status' -Data $Data -KeyInLine - + # User Types $Data = @( @@ -1863,8 +1863,8 @@ function Invoke-NinjaOneTenantSync { Amount = $GuestUsersCount Colour = '#8063BF' } - ) - + ) + $UsersTypesChartHTML = Get-NinjaInLineBarGraph -Title 'User Types' -Data $Data -KeyInLine # Create the Users Card @@ -1901,8 +1901,8 @@ function Invoke-NinjaOneTenantSync { Colour = '#D53948' } ) - - + + $DeviceComplianceChartHTML = Get-NinjaInLineBarGraph -Title 'Device Compliance' -Data $Data -KeyInLine # Device OS Types @@ -1928,8 +1928,8 @@ function Invoke-NinjaOneTenantSync { Amount = $IOSCount Colour = '#007AFF' } - ) - + ) + $DeviceOsChartHTML = Get-NinjaInLineBarGraph -Title 'Device Operating Systems' -Data $Data -KeyInLine # Last online time @@ -1946,7 +1946,7 @@ function Invoke-NinjaOneTenantSync { Colour = '#CCCCCC' } ) - + $DeviceOnlineChartHTML = Get-NinjaInLineBarGraph -Title 'Devices Online in the last 30 days' -Data $Data -KeyInLine # Create the Devices Card @@ -1974,7 +1974,7 @@ function Invoke-NinjaOneTenantSync { Colour = '#CCCCCC' } ) - + $SecureScoreHTML = Get-NinjaInLineBarGraph -Title "Secure Score - $([System.Math]::Round((($CurrentSecureScore.currentScore / $MaxSecureScore) * 100),2))%" -Data $Data -KeyInLine -NoCount -NoSort # Recommended Actions HTML @@ -1995,7 +1995,7 @@ function Invoke-NinjaOneTenantSync { $Table = Get-CippTable -tablename 'standards' - $Filter = "PartitionKey eq 'standards'" + $Filter = "PartitionKey eq 'standards'" $AllStandards = (Get-CIPPAzDataTableEntity @Table -Filter $Filter).JSON | ConvertFrom-Json -Depth 100 @@ -2025,11 +2025,11 @@ function Invoke-NinjaOneTenantSync { Write-Host 'License Details' $LicenseTableHTML = $LicensesParsed | Sort-Object 'License Name' | ConvertTo-Html -As Table -Fragment $LicenseTableHTML = '
    ' + (([System.Web.HttpUtility]::HtmlDecode($LicenseTableHTML) -replace '', '') -replace '', '') + '
    ' - + $TitleLink = "https://$CIPPUrl/tenant/administration/list-licenses?customerId=$($Customer.customerId)" $LicensesSummaryCardHTML = Get-NinjaOneCard -Title 'Licenses' -Body $LicenseTableHTML -Icon 'fas fa-chart-bar' -TitleLink $TitleLink - + ### Summary Stats Write-Host 'Widget Details' @@ -2056,7 +2056,7 @@ function Invoke-NinjaOneTenantSync { # Colour = $ResultColour # Link = "https://$CIPPUrl/tenant/standards/bpa-report?SearchNow=true&Report=CIPP+Best+Practices+v1.0+-+Tenant+view&tenantFilter=$($Customer.customerId)" # }) - + # Unused Licenses $WidgetData.add([PSCustomObject]@{ Value = $( @@ -2076,15 +2076,15 @@ function Invoke-NinjaOneTenantSync { Colour = $ResultColour Link = "https://$CIPPUrl/tenant/standards/bpa-report?SearchNow=true&Report=CIPP+Best+Practices+v1.5+-+Tenant+view&tenantFilter=$($Customer.customerId)" }) - - + + # Unified Audit Log $WidgetData.add([PSCustomObject]@{ Value = $(if ($BPAData.UnifiedAuditLog -eq $True) { - $ResultColour = '#26A644' + $ResultColour = '#26A644' '' } else { - $ResultColour = '#D53948' + $ResultColour = '#D53948' '' } ) @@ -2092,14 +2092,14 @@ function Invoke-NinjaOneTenantSync { Colour = $ResultColour Link = "https://security.microsoft.com/auditlogsearch?viewid=Async%20Search&tid=$($Customer.customerId)" }) - + # Passwords Never Expire $WidgetData.add([PSCustomObject]@{ Value = $(if ($BPAData.PasswordNeverExpires -eq $True) { - $ResultColour = '#26A644' + $ResultColour = '#26A644' '' } else { - $ResultColour = '#D53948' + $ResultColour = '#D53948' '' } ) @@ -2111,10 +2111,10 @@ function Invoke-NinjaOneTenantSync { # oAuth App Consent $WidgetData.add([PSCustomObject]@{ Value = $(if ($BPAData.OAuthAppConsent -eq $True) { - $ResultColour = '#26A644' + $ResultColour = '#26A644' '' } else { - $ResultColour = '#D53948' + $ResultColour = '#D53948' '' } ) @@ -2122,7 +2122,7 @@ function Invoke-NinjaOneTenantSync { Colour = $ResultColour Link = "https://entra.microsoft.com/$($Customer.customerId)/#view/Microsoft_AAD_IAM/ConsentPoliciesMenuBlade/~/UserSettings" }) - + } # Blocked Senders @@ -2146,7 +2146,7 @@ function Invoke-NinjaOneTenantSync { Colour = '#CCCCCC' Link = "https://$CIPPUrl/identity/administration/users?customerId=$($Customer.customerId)" }) - + # Devices $WidgetData.add([PSCustomObject]@{ Value = ($Devices | Measure-Object).count @@ -2211,11 +2211,11 @@ function Invoke-NinjaOneTenantSync { Link = "https://entra.microsoft.com/$($Customer.customerId)/#view/Microsoft_AAD_IAM/DirectoriesADConnectBlade" }) - - + + Write-Host 'Summary Details' $SummaryDetailsCardHTML = Get-NinjaOneWidgetCard -Data $WidgetData -Icon 'fas fa-building' -SmallCols 2 -MedCols 3 -LargeCols 4 -XLCols 6 -NoCard @@ -2223,15 +2223,15 @@ function Invoke-NinjaOneTenantSync { # Create the Tenant Summary Field Write-Host 'Complete Tenant Summary' $TenantSummaryHTML = '
    ' + $SummaryDetailsCardHTML + '
    ' + - '
    ' + - '
    ' + $TenantSummaryCard + + '
    ' + + '
    ' + $TenantSummaryCard + '
    ' + $LicensesSummaryCardHTML + - '
    ' + $DeviceSummaryCardHTML + + '
    ' + $DeviceSummaryCardHTML + '
    ' + $CIPPStandardsSummaryCardHTML + - '
    ' + $SecureScoreSummaryCardHTML + - '
    ' + $UserSummaryCardHTML + + '
    ' + $SecureScoreSummaryCardHTML + + '
    ' + $UserSummaryCardHTML + '
    ' - + $NinjaOrgUpdate | Add-Member -NotePropertyName $MappedFields.TenantSummary -NotePropertyValue @{'html' = $TenantSummaryHTML } @@ -2241,7 +2241,7 @@ function Invoke-NinjaOneTenantSync { if ($MappedFields.UsersSummary) { Write-Host 'User Details Section' - $UsersTableFornatted = $ParsedUsers | Sort-Object name | Select-Object -First 100 Name, + $UsersTableFornatted = $ParsedUsers | Sort-Object name | Select-Object -First 100 Name, @{n = 'User Principal Name'; e = { $_.UPN } }, #Aliases, Licenses, @@ -2250,7 +2250,7 @@ function Invoke-NinjaOneTenantSync { @{n = 'Devices (Last Login)'; e = { $_.Devices } }, Actions - + $UsersTableHTML = $UsersTableFornatted | ConvertTo-Html -As Table -Fragment $UsersTableHTML = ([System.Web.HttpUtility]::HtmlDecode($UsersTableHTML) -replace '', '') -replace '', '' @@ -2270,47 +2270,47 @@ function Invoke-NinjaOneTenantSync { } else { $Overflow = '' } - + $NinjaOrgUpdate | Add-Member -NotePropertyName $MappedFields.UsersSummary -NotePropertyValue @{'html' = $Overflow + $UsersTableHTML } } - + Write-Host 'Posting Details' - + $Token = Get-NinjaOneToken -configuration $Configuration Write-Host "Ninja Body: $($NinjaOrgUpdate | ConvertTo-Json -Depth 100)" - $Result = Invoke-WebRequest -Uri "https://$($Configuration.Instance)/api/v2/organization/$($MappedTenant.NinjaOne)/custom-fields" -Method PATCH -Headers @{Authorization = "Bearer $($token.access_token)" } -ContentType 'application/json; charset=utf-8' -Body ($NinjaOrgUpdate | ConvertTo-Json -Depth 100) + $Result = Invoke-WebRequest -Uri "https://$($Configuration.Instance)/api/v2/organization/$($MappedTenant.IntegrationId)/custom-fields" -Method PATCH -Headers @{Authorization = "Bearer $($token.access_token)" } -ContentType 'application/json; charset=utf-8' -Body ($NinjaOrgUpdate | ConvertTo-Json -Depth 100) Write-Host 'Cleaning Users Cache' if (($ParsedUsers | Measure-Object).count -gt 0) { Remove-AzDataTableEntity @UsersTable -Entity ($ParsedUsers | Select-Object PartitionKey, RowKey) } - + Write-Host 'Cleaning Device Cache' if (($ParsedDevices | Measure-Object).count -gt 0) { Remove-AzDataTableEntity @DeviceTable -Entity ($ParsedDevices | Select-Object PartitionKey, RowKey) } - + Write-Host "Total Fetch Time: $((New-TimeSpan -Start $StartTime -End $FetchEnd).TotalSeconds)" - Write-Host "Completed Total Time: $((New-TimeSpan -Start $StartTime -End (Get-Date)).TotalSeconds)" + Write-Host "Completed Total Time: $((New-TimeSpan -Start $StartTime -End (Get-Date)).TotalSeconds)" # Set Last End Time $CurrentItem | Add-Member -NotePropertyName lastEndTime -NotePropertyValue ([string]$((Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ'))) -Force $CurrentItem | Add-Member -NotePropertyName lastStatus -NotePropertyValue 'Completed' -Force Add-CIPPAzDataTableEntity @MappingTable -Entity $CurrentItem -Force - Write-LogMessage -API 'NinjaOneSync' -user 'NinjaOneSync' -message "Completed NinjaOne Sync for $($Customer.displayName). Queued for $((New-TimeSpan -Start $StartQueueTime -End $StartTime).TotalSeconds) seconds. Data fetched in $((New-TimeSpan -Start $StartTime -End $FetchEnd).TotalSeconds) seconds. Total processing time $((New-TimeSpan -Start $StartTime -End (Get-Date)).TotalSeconds) seconds" -Sev 'info' + Write-LogMessage -API 'NinjaOneSync' -user 'NinjaOneSync' -message "Completed NinjaOne Sync for $($Customer.displayName). Queued for $((New-TimeSpan -Start $StartQueueTime -End $StartTime).TotalSeconds) seconds. Data fetched in $((New-TimeSpan -Start $StartTime -End $FetchEnd).TotalSeconds) seconds. Total processing time $((New-TimeSpan -Start $StartTime -End (Get-Date)).TotalSeconds) seconds" -Sev 'info' } catch { $Message = if ($_.ErrorDetails.Message) { Get-NormalizedError -Message $_.ErrorDetails.Message } else { $_.Exception.message - } + } Write-Error "Failed NinjaOne Processing for $($Customer.displayName) Linenumber: $($_.InvocationInfo.ScriptLineNumber) Error: $Message" Write-LogMessage -API 'NinjaOneSync' -user 'NinjaOneSync' -message "Failed NinjaOne Processing for $($Customer.displayName) Linenumber: $($_.InvocationInfo.ScriptLineNumber) Error: $Message" -Sev 'Error' $CurrentItem | Add-Member -NotePropertyName lastEndTime -NotePropertyValue ([string]$((Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ'))) -Force diff --git a/Modules/CippExtensions/NinjaOne/NinjaOneHelper.ps1 b/Modules/CippExtensions/Public/NinjaOne/NinjaOneHelper.ps1 similarity index 100% rename from Modules/CippExtensions/NinjaOne/NinjaOneHelper.ps1 rename to Modules/CippExtensions/Public/NinjaOne/NinjaOneHelper.ps1 diff --git a/Modules/CippExtensions/NinjaOne/Push-NinjaOneQueue.ps1 b/Modules/CippExtensions/Public/NinjaOne/Push-NinjaOneQueue.ps1 similarity index 100% rename from Modules/CippExtensions/NinjaOne/Push-NinjaOneQueue.ps1 rename to Modules/CippExtensions/Public/NinjaOne/Push-NinjaOneQueue.ps1 diff --git a/Modules/CippExtensions/NinjaOne/Set-NinjaOneFieldMapping.ps1 b/Modules/CippExtensions/Public/NinjaOne/Set-NinjaOneFieldMapping.ps1 similarity index 72% rename from Modules/CippExtensions/NinjaOne/Set-NinjaOneFieldMapping.ps1 rename to Modules/CippExtensions/Public/NinjaOne/Set-NinjaOneFieldMapping.ps1 index 4653d51ed13a..87d243b8cda1 100644 --- a/Modules/CippExtensions/NinjaOne/Set-NinjaOneFieldMapping.ps1 +++ b/Modules/CippExtensions/Public/NinjaOne/Set-NinjaOneFieldMapping.ps1 @@ -6,7 +6,7 @@ function Set-NinjaOneFieldMapping { $Request, $TriggerMetadata ) - + $SettingsTable = Get-CIPPTable -TableName NinjaOneSettings $AddObject = @{ PartitionKey = 'NinjaConfig' @@ -17,15 +17,15 @@ function Set-NinjaOneFieldMapping { foreach ($Mapping in ([pscustomobject]$Request.body.mappings).psobject.properties) { $AddObject = @{ - PartitionKey = 'NinjaFieldMapping' - RowKey = "$($mapping.name)" - 'NinjaOne' = "$($mapping.value.value)" - 'NinjaOneName' = "$($mapping.value.label)" + PartitionKey = 'NinjaOneFieldMapping' + RowKey = "$($mapping.name)" + IntegrationId = "$($mapping.value.value)" + IntegrationName = "$($mapping.value.label)" } Add-AzDataTableEntity @CIPPMapping -Entity $AddObject -Force - Write-LogMessage -API $APINAME -user $request.headers.'x-ms-client-principal' -message "Added mapping for $($mapping.name)." -Sev 'Info' + Write-LogMessage -API $APINAME -user $request.headers.'x-ms-client-principal' -message "Added mapping for $($mapping.name)." -Sev 'Info' } - $Result = [pscustomobject]@{'Results' = "Successfully edited mapping table." } + $Result = [pscustomobject]@{'Results' = 'Successfully edited mapping table.' } Return $Result } \ No newline at end of file diff --git a/Modules/CippExtensions/NinjaOne/Set-NinjaOneOrgMapping.ps1 b/Modules/CippExtensions/Public/NinjaOne/Set-NinjaOneOrgMapping.ps1 similarity index 71% rename from Modules/CippExtensions/NinjaOne/Set-NinjaOneOrgMapping.ps1 rename to Modules/CippExtensions/Public/NinjaOne/Set-NinjaOneOrgMapping.ps1 index ee09580b94bf..43b1c597e3b0 100644 --- a/Modules/CippExtensions/NinjaOne/Set-NinjaOneOrgMapping.ps1 +++ b/Modules/CippExtensions/Public/NinjaOne/Set-NinjaOneOrgMapping.ps1 @@ -6,18 +6,18 @@ function Set-NinjaOneOrgMapping { $Request ) - Get-CIPPAzDataTableEntity @CIPPMapping -Filter "PartitionKey eq 'NinjaOrgsMapping'" | ForEach-Object { + Get-CIPPAzDataTableEntity @CIPPMapping -Filter "PartitionKey eq 'NinjaOneMapping'" | ForEach-Object { Remove-AzDataTableEntity @CIPPMapping -Entity $_ } foreach ($Mapping in ([pscustomobject]$Request.body.mappings).psobject.properties) { $AddObject = @{ - PartitionKey = 'NinjaOrgsMapping' - RowKey = "$($mapping.name)" - 'NinjaOne' = "$($mapping.value.value)" - 'NinjaOneName' = "$($mapping.value.label)" + PartitionKey = 'NinjaOneMapping' + RowKey = "$($mapping.name)" + IntegrationId = "$($mapping.value.value)" + IntegrationName = "$($mapping.value.label)" } Add-AzDataTableEntity @CIPPMapping -Entity $AddObject -Force - Write-LogMessage -API $APINAME -user $request.headers.'x-ms-client-principal' -message "Added mapping for $($mapping.name)." -Sev 'Info' + Write-LogMessage -API $APINAME -user $request.headers.'x-ms-client-principal' -message "Added mapping for $($mapping.name)." -Sev 'Info' } $Result = [pscustomobject]@{'Results' = 'Successfully edited mapping table.' } diff --git a/Modules/CippExtensions/Public/New-PwPushLink.ps1 b/Modules/CippExtensions/Public/PwPush/New-PwPushLink.ps1 similarity index 100% rename from Modules/CippExtensions/Public/New-PwPushLink.ps1 rename to Modules/CippExtensions/Public/PwPush/New-PwPushLink.ps1 diff --git a/Modules/CippExtensions/Private/Set-PwPushConfig.ps1 b/Modules/CippExtensions/Public/PwPush/Set-PwPushConfig.ps1 similarity index 100% rename from Modules/CippExtensions/Private/Set-PwPushConfig.ps1 rename to Modules/CippExtensions/Public/PwPush/Set-PwPushConfig.ps1 diff --git a/Modules/HuduAPI/2.4.9/HuduAPI.psd1 b/Modules/HuduAPI/2.4.9/HuduAPI.psd1 new file mode 100644 index 000000000000..c5654110bc01 --- /dev/null +++ b/Modules/HuduAPI/2.4.9/HuduAPI.psd1 @@ -0,0 +1,154 @@ +# +# Module manifest for module 'HuduAPI' +# +# Generated by: Luke Whitelock +# +# Generated on: 06/30/2024 +# + +@{ + +# Script module or binary module file associated with this manifest. +RootModule = '.\HuduAPI.psm1' + +# Version number of this module. +ModuleVersion = '2.4.9' + +# Supported PSEditions +# CompatiblePSEditions = @() + +# ID used to uniquely identify this module +GUID = '4e0a4feb-1658-416b-b854-ab9e913a56de' + +# Author of this module +Author = 'Luke Whitelock' + +# Company or vendor of this module +CompanyName = 'MSPP' + +# Copyright statement for this module +Copyright = '(c) 2021 Luke Whitelock. All rights reserved.' + +# Description of the functionality provided by this module +Description = 'This module provides an interface to the Hudu Rest API further information can be found at https://github.com/lwhitelock/HuduAPI' + +# Minimum version of the PowerShell engine required by this module +PowerShellVersion = '7.0' + +# Name of the PowerShell host required by this module +# PowerShellHostName = '' + +# Minimum version of the PowerShell host required by this module +# PowerShellHostVersion = '' + +# Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. +# DotNetFrameworkVersion = '' + +# Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. +# ClrVersion = '' + +# Processor architecture (None, X86, Amd64) required by this module +# ProcessorArchitecture = '' + +# Modules that must be imported into the global environment prior to importing this module +# RequiredModules = @() + +# Assemblies that must be loaded prior to importing this module +# RequiredAssemblies = @() + +# Script files (.ps1) that are run in the caller's environment prior to importing this module. +# ScriptsToProcess = @() + +# Type files (.ps1xml) to be loaded when importing this module +# TypesToProcess = @() + +# Format files (.ps1xml) to be loaded when importing this module +# FormatsToProcess = @() + +# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess +# NestedModules = @() + +# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. +FunctionsToExport = 'Get-HuduActivityLogs', 'Get-HuduApiKey', 'Get-HuduAppInfo', + 'Get-HuduArticles', 'Get-HuduAssetLayoutFieldID', + 'Get-HuduAssetLayouts', 'Get-HuduAssets', 'Get-HuduBaseURL', + 'Get-HuduCard', 'Get-HuduCompanies', 'Get-HuduExpirations', + 'Get-HuduFolderMap', 'Get-HuduFolders', 'Get-HuduIntegrationMatchers', + 'Get-HuduMagicDashes', 'Get-HuduObjectByUrl', + 'Get-HuduPasswordFolders', 'Get-HuduPasswords', 'Get-HuduProcesses', + 'Get-HuduPublicPhotos', 'Get-HuduRelations', 'Get-HuduUploads', + 'Get-HuduWebsites', 'Initialize-HuduFolder', + 'Move-HuduAssetsToNewLayout', 'New-HuduAPIKey', 'New-HuduArticle', + 'New-HuduAsset', 'New-HuduAssetLayout', 'New-HuduBaseURL', + 'New-HuduCompany', 'New-HuduCustomHeaders', 'New-HuduFolder', + 'New-HuduPassword', 'New-HuduPublicPhoto', 'New-HuduRelation', + 'New-HuduUpload', 'New-HuduWebsite', 'Remove-HuduAPIKey', + 'Remove-HuduArticle', 'Remove-HuduAsset', 'Remove-HuduBaseURL', + 'Remove-HuduCompany', 'Remove-HuduCustomHeaders', + 'Remove-HuduMagicDash', 'Remove-HuduPassword', 'Remove-HuduRelation', + 'Remove-HuduUpload', 'Remove-HuduWebsite', 'Set-HuduArticle', + 'Set-HuduArticleArchive', 'Set-HuduAsset', 'Set-HuduAssetArchive', + 'Set-HuduAssetLayout', 'Set-HuduCompany', 'Set-HuduCompanyArchive', + 'Set-HuduFolder', 'Set-HuduIntegrationMatcher', 'Set-HuduMagicDash', + 'Set-HuduPassword', 'Set-HuduPasswordArchive', 'Set-HuduWebsite' + +# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. +CmdletsToExport = @() + +# Variables to export from this module +VariablesToExport = '*' + +# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. +AliasesToExport = @() + +# DSC resources to export from this module +# DscResourcesToExport = @() + +# List of all modules packaged with this module +# ModuleList = @() + +# List of all files packaged with this module +# FileList = @() + +# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. +PrivateData = @{ + + PSData = @{ + + # Tags applied to this module. These help with module discovery in online galleries. + # Tags = @() + + # A URL to the license for this module. + # LicenseUri = '' + + # A URL to the main website for this project. + # ProjectUri = '' + + # A URL to an icon representing this module. + # IconUri = '' + + # ReleaseNotes of this module + # ReleaseNotes = '' + + # Prerelease string of this module + # Prerelease = '' + + # Flag to indicate whether the module requires explicit user acceptance for install/update/save + # RequireLicenseAcceptance = $false + + # External dependent modules of this module + # ExternalModuleDependencies = @() + + } # End of PSData hashtable + + +} # End of PrivateData hashtable + +# HelpInfo URI of this module +# HelpInfoURI = '' + +# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. +# DefaultCommandPrefix = '' + +} + diff --git a/Modules/HuduAPI/2.4.9/HuduAPI.psm1 b/Modules/HuduAPI/2.4.9/HuduAPI.psm1 new file mode 100644 index 000000000000..24c908667318 --- /dev/null +++ b/Modules/HuduAPI/2.4.9/HuduAPI.psm1 @@ -0,0 +1,4201 @@ +#Region './Private/ArgumentCompleters/AssetLayoutCompleter.ps1' -1 + +$AssetLayoutCompleter = { + param ( + $CommandName, + $ParamName, + $AssetLayout, + $CommandAst, + $fakeBoundParameters + ) + if (!$script:AssetLayouts) { + Get-HuduAssetLayouts | Out-Null + } + + $AssetLayout = $AssetLayout -replace "'", '' + ($script:AssetLayouts).name | Where-Object { $_ -match "$AssetLayout" } | ForEach-Object { "'$_'" } +} + +Register-ArgumentCompleter -CommandName Get-HuduAssets -ParameterName AssetLayout -ScriptBlock $AssetLayoutCompleter +#EndRegion './Private/ArgumentCompleters/AssetLayoutCompleter.ps1' 18 +#Region './Private/Get-HuduCompanyFolders.ps1' -1 + +function Get-HuduCompanyFolders { + [CmdletBinding()] + Param ( + [PSCustomObject]$FoldersRaw + ) + + $RootFolders = $FoldersRaw | Where-Object { $null -eq $_.parent_folder_id } + $ReturnObject = [PSCustomObject]@{} + foreach ($folder in $RootFolders) { + $SubFolders = Get-HuduSubFolders -id $folder.id -FoldersRaw $FoldersRaw + foreach ($SubFolder in $SubFolders) { + $Folder | Add-Member -MemberType NoteProperty -Name $(Get-HuduFolderCleanName $($SubFolder.PSObject.Properties.name)) -Value $SubFolder.PSObject.Properties.value + } + $ReturnObject | Add-Member -MemberType NoteProperty -Name $(Get-HuduFolderCleanName $($folder.name)) -Value $folder + } + return $ReturnObject +} +#EndRegion './Private/Get-HuduCompanyFolders.ps1' 18 +#Region './Private/Get-HuduFolderCleanName.ps1' -1 + +function Get-HuduFolderCleanName { + [CmdletBinding()] + param( + [string]$Name + ) + + $FieldNames = @('id', 'company_id', 'icon', 'description', 'name', 'parent_folder_id', 'created_at', 'updated_at') + + if ($Name -in $FieldNames) { + Return "fld_$Name" + } else { + Return $Name + } + +} +#EndRegion './Private/Get-HuduFolderCleanName.ps1' 16 +#Region './Private/Get-HuduSubFolders.ps1' -1 + +function Get-HuduSubFolders { + [CmdletBinding()] + Param( + [int]$id, + [PSCustomObject]$FoldersRaw + ) + + $SubFolders = $FoldersRaw | Where-Object { $_.parent_folder_id -eq $id } + $ReturnFolders = [System.Collections.ArrayList]@() + foreach ($Folder in $SubFolders) { + $SubSubFolders = Get-HuduSubFolders -id $Folder.id -FoldersRaw $FoldersRaw + foreach ($AddFolder in $SubSubFolders) { + $null = $folder | Add-Member -MemberType NoteProperty -Name $(Get-HuduFolderCleanName $($AddFolder.PSObject.Properties.name)) -Value $AddFolder.PSObject.Properties.value + } + $ReturnObject = [PSCustomObject]@{ + $(Get-HuduFolderCleanName $($Folder.name)) = $Folder + } + $null = $ReturnFolders.add($ReturnObject) + } + + return $ReturnFolders + +} +#EndRegion './Private/Get-HuduSubFolders.ps1' 24 +#Region './Private/Invoke-HuduRequest.ps1' -1 + +function Invoke-HuduRequest { + <# + .SYNOPSIS + Main Hudu API function + + .DESCRIPTION + Calls Hudu API with token + + .PARAMETER Method + GET,POST,DELETE,PUT,etc + + .PARAMETER Path + Path to API endpoint + + .PARAMETER Params + Hashtable of parameters + + .PARAMETER Body + JSON encoded body string + + .PARAMETER Form + Multipart form data + + .EXAMPLE + Invoke-HuduRequest -Resource '/api/v1/articles' -Method GET + #> + [CmdletBinding()] + Param( + [Parameter()] + [string]$Method = 'GET', + + [Parameter()] + [ValidateNotNullOrEmpty()] + [string]$Resource, + + [Parameter()] + [hashtable]$Params = @{}, + + [Parameter()] + [string]$Body, + + [Parameter()] + [hashtable]$Form + ) + + $HuduAPIKey = Get-HuduApiKey + $HuduBaseURL = Get-HuduBaseURL + + # Assemble parameters + $ParamCollection = [System.Web.HttpUtility]::ParseQueryString([String]::Empty) + + # Sort parameters + foreach ($Item in ($Params.GetEnumerator() | Sort-Object -CaseSensitive -Property Key)) { + $ParamCollection.Add($Item.Key, $Item.Value) + } + + # Query string + $Request = $ParamCollection.ToString() + + $Headers = @{ + 'x-api-key' = (New-Object PSCredential 'user', $HuduAPIKey).GetNetworkCredential().Password; + } + + if (($Script:Int_HuduCustomHeaders | Measure-Object).count -gt 0){ + + foreach($Entry in $Int_HuduCustomHeaders.GetEnumerator()) { + $Headers[$Entry.Name] = $Entry.Value + } + } + + $ContentType = 'application/json; charset=utf-8' + + $Uri = '{0}{1}' -f $HuduBaseURL, $Resource + # Make API call URI + if ($Request) { + $UriBuilder = [System.UriBuilder]$Uri + $UriBuilder.Query = $Request + $Uri = $UriBuilder.Uri + } + Write-Verbose ( '{0} [{1}]' -f $Method, $Uri ) + + $RestMethod = @{ + Method = $Method + Uri = $Uri + Headers = $Headers + ContentType = $ContentType + } + + if ($Body) { + $RestMethod.Body = $Body + Write-Verbose $Body + } + + if ($Form) { + $RestMethod.Form = $Form + Write-Verbose ( $Form | Out-String ) + } + + try { + $Results = Invoke-RestMethod @RestMethod + } catch { + if ("$_".trim() -eq 'Retry later' -or "$_".trim() -eq 'The remote server returned an error: (429) Too Many Requests.') { + Write-Information 'Hudu API Rate limited. Waiting 30 Seconds then trying again' + Start-Sleep 30 + $Results = Invoke-HuduRequest @RestMethod + } else { + Write-Error "'$_'" + } + } + + $Results +} +#EndRegion './Private/Invoke-HuduRequest.ps1' 113 +#Region './Private/Invoke-HuduRequestPaginated.ps1' -1 + +function Invoke-HuduRequestPaginated { + <# + .SYNOPSIS + Paginated requests to Hudu API + + .DESCRIPTION + Wraps Invoke-HuduRequest with page sizes + + .PARAMETER HuduRequest + Request to paginate + + .PARAMETER Property + Property name to return (don't specify to return entire response object) + + .PARAMETER PageSize + Number of results to return per page (default 1000) + + #> + [CmdletBinding()] + Param( + [hashtable]$HuduRequest, + [string]$Property, + [int]$PageSize = 1000 + ) + + $i = 1 + do { + $HuduRequest.Params.page = $i + $HuduRequest.Params.page_size = $PageSize + $Response = Invoke-HuduRequest @HuduRequest + $i++ + if ($Property) { + $Response.$Property + } + + else { + $Response + } + } while (($Property -and $Response.$Property.count % $PageSize -eq 0 -and $Response.$Property.count -ne 0) -or (!$Property -and $Response.count % $PageSize -eq 0 -and $Response.count -ne 0)) +} +#EndRegion './Private/Invoke-HuduRequestPaginated.ps1' 41 +#Region './Public/Get-HuduActivityLogs.ps1' -1 + +function Get-HuduActivityLogs { + <# + .SYNOPSIS + Get activity logs for account + + .DESCRIPTION + Calls Hudu API to retrieve activity logs with filters + + .PARAMETER UserId + Filter logs by user_id + + .PARAMETER UserEmail + Filter logs by email address + + .PARAMETER ResourceId + Filter logs by resource id. Must be coupled with resource_type + + .PARAMETER ResourceType + Filter logs by resource type (Asset, AssetPassword, Company, Article, etc.). Must be coupled with resource_id + + .PARAMETER ActionMessage + Filter logs by action + + .PARAMETER StartDate + Filter logs by start date. Converts string to ISO 8601 format + + .PARAMETER EndDate + Filter logs by end date, should be coupled with start date to limit results + + .EXAMPLE + Get-HuduActivityLogs -StartDate 2023-02-01 + + #> + [CmdletBinding()] + Param ( + [Alias('user_id')] + [Int]$UserId = '', + [Alias('user_email')] + [String]$UserEmail = '', + [Alias('resource_id')] + [Int]$ResourceId = '', + [Alias('resource_type')] + [String]$ResourceType = '', + [Alias('action_message')] + [String]$ActionMessage = '', + [Alias('start_date')] + [DateTime]$StartDate, + [Alias('end_date')] + [DateTime]$EndDate + ) + + $Params = @{} + + if ($UserId) { $Params.user_id = $UserId } + if ($UserEmail) { $Params.user_email = $UserEmail } + if ($ResourceId) { $Params.resource_id = $ResourceId } + if ($ResourceType) { $Params.resource_type = $ResourceType } + if ($ActionMessage) { $Params.action_message = $ActionMessage } + if ($StartDate) { + $ISO8601Date = $StartDate.ToString('o'); + $Params.start_date = $ISO8601Date + } + + $HuduRequest = @{ + Method = 'GET' + Resource = '/api/v1/activity_logs' + Params = $Params + } + + $AllActivity = Invoke-HuduRequestPaginated -HuduRequest $HuduRequest + + if ($EndDate) { + $AllActivity = $AllActivity | Where-Object { $([DateTime]::Parse($_.created_at)) -le $EndDate } + } + + return $AllActivity +} +#EndRegion './Public/Get-HuduActivityLogs.ps1' 78 +#Region './Public/Get-HuduApiKey.ps1' -1 + +function Get-HuduApiKey { + <# + .SYNOPSIS + Get Hudu API key + + .DESCRIPTION + Returns Hudu API key in securestring format + + .EXAMPLE + Get-HuduApiKey + + #> + [CmdletBinding()] + Param() + if ($null -eq $Int_HuduAPIKey) { + Write-Error 'No API key has been set. Please use New-HuduAPIKey to set it.' + } else { + $Int_HuduAPIKey + } +} +#EndRegion './Public/Get-HuduApiKey.ps1' 21 +#Region './Public/Get-HuduAppInfo.ps1' -1 + +function Get-HuduAppInfo { + <# + .SYNOPSIS + Retrieve information regarding API + + .DESCRIPTION + Calls Hudu API to retrieve version number and date + + .EXAMPLE + Get-HuduAppInfo + + #> + [CmdletBinding()] + Param() + + [version]$script:HuduRequiredVersion = '2.21' + + try { + Invoke-HuduRequest -Resource '/api/v1/api_info' + } catch { + [PSCustomObject]@{ + version = '0.0.0.0' + date = '2000-01-01' + } + } +} +#EndRegion './Public/Get-HuduAppInfo.ps1' 27 +#Region './Public/Get-HuduArticles.ps1' -1 + +function Get-HuduArticles { + <# + .SYNOPSIS + Get Knowledge Base Articles + + .DESCRIPTION + Calls Hudu API to retrieve KB articles by Id or a list + + .PARAMETER Id + Id of the Article + + .PARAMETER CompanyId + Filter by company id + + .PARAMETER Name + Filter by name of article + + .PARAMETER Slug + Filter by slug of article + + .EXAMPLE + Get-HuduArticles -Name 'Article name' + + #> + [CmdletBinding()] + Param ( + [Int]$Id = '', + [Alias('company_id')] + [Int]$CompanyId = '', + [String]$Name = '', + [String]$Slug + ) + + if ($Id) { + Invoke-HuduRequest -Method get -Resource "/api/v1/articles/$Id" + } else { + $Params = @{} + + if ($CompanyId) { $Params.company_id = $CompanyId } + if ($Name) { $Params.name = $Name } + if ($Slug) { $Params.slug = $Slug } + + $HuduRequest = @{ + Method = 'GET' + Resource = '/api/v1/articles' + Params = $Params + } + + Invoke-HuduRequestPaginated -HuduRequest $HuduRequest -Property articles + } +} +#EndRegion './Public/Get-HuduArticles.ps1' 52 +#Region './Public/Get-HuduAssetLayoutFieldID.ps1' -1 + +function Get-HuduAssetLayoutFieldID { + <# + .SYNOPSIS + Get Hudu Asset Layout Field ID + + .DESCRIPTION + Retrieves ID for Hudu Asset Layout Fields + + .PARAMETER Name + Name of Field + + .PARAMETER LayoutId + Asset Layout Id + + .EXAMPLE + Get-HuduAssetLayoutFieldID -Name 'Extra Info' -LayoutId 1 + + #> + [CmdletBinding()] + Param ( + [Parameter(Mandatory = $true)] + [String]$Name, + [Alias('asset_layout_id')] + [Parameter(Mandatory = $true)] + [Int]$LayoutId + ) + + $Layout = Get-HuduAssetLayouts -LayoutId $LayoutId + + $Fields = [Collections.Generic.List[Object]]($Layout.fields) + $Index = $Fields.FindIndex( { $args[0].label -eq $Name } ) + $Fields[$Index].id +} +#EndRegion './Public/Get-HuduAssetLayoutFieldID.ps1' 34 +#Region './Public/Get-HuduAssetLayouts.ps1' -1 + +function Get-HuduAssetLayouts { + <# + .SYNOPSIS + Get a list of Asset Layouts + + .DESCRIPTION + Call Hudu API to retrieve asset layouts for server + + .PARAMETER Name + Filter by name of Asset Layout + + .PARAMETER LayoutId + Id of Asset Layout + + .PARAMETER Slug + Filter by url slug + + .EXAMPLE + Get-HuduAssetLayouts -Name 'Contacts' + + #> + [CmdletBinding()] + Param ( + [String]$Name, + [Alias('id', 'layout_id')] + [int]$LayoutId, + [String]$Slug + ) + + $HuduRequest = @{ + Resource = '/api/v1/asset_layouts' + Method = 'GET' + } + + if ($LayoutId) { + $HuduRequest.Resource = '{0}/{1}' -f $HuduRequest.Resource, $LayoutId + $AssetLayout = Invoke-HuduRequest @HuduRequest + return $AssetLayout.asset_layout + } else { + $Params = @{} + if ($Name) { $Params.name = $Name } + if ($Slug) { $Params.slug = $Slug } + $HuduRequest.Params = $Params + + $AssetLayouts = Invoke-HuduRequestPaginated -HuduRequest $HuduRequest -Property 'asset_layouts' -PageSize 25 + + if (!$Name -and !$Slug) { + $script:AssetLayouts = $AssetLayouts | Sort-Object -Property name + } + $AssetLayouts + } +} +#EndRegion './Public/Get-HuduAssetLayouts.ps1' 53 +#Region './Public/Get-HuduAssets.ps1' -1 + +function Get-HuduAssets { + <# + .SYNOPSIS + Get a list of Assets + + .DESCRIPTION + Call Hudu API to retrieve Assets + + .PARAMETER Id + Id of requested asset + + .PARAMETER AssetLayoutId + Id of the requested asset layout + + .PARAMETER AssetLayout + Name of the requested asset layout + + .PARAMETER CompanyId + Id of the requested company + + .PARAMETER Name + Filter by name + + .PARAMETER Archived + Show archived results + + .PARAMETER PrimarySerial + Filter by primary serial + + .PARAMETER Slug + Filter by slug + + .EXAMPLE + Get-HuduAssets -AssetLayout 'Contacts' + + #> + [CmdletBinding()] + Param ( + [ValidateRange(1, [int]::MaxValue)] + [Int]$Id = '', + [Alias('asset_layout_id')] + [ValidateRange(1, [int]::MaxValue)] + [Int]$AssetLayoutId = '', + [string]$AssetLayout, + [Alias('company_id')] + [ValidateRange(1, [int]::MaxValue)] + [Int]$CompanyId = '', + [String]$Name = '', + [switch]$Archived, + [Alias('primary_serial')] + [String]$PrimarySerial = '', + [String]$Slug + ) + + if ($AssetLayout) { + if (!$script:AssetLayouts) { Get-HuduAssetLayouts | Out-Null } + $AssetLayoutId = $script:AssetLayouts | Where-Object { $_.name -eq $AssetLayout } | Select-Object -ExpandProperty id + } + + if ($id -and $CompanyId) { + $HuduRequest = @{ + Resource = "/api/v1/companies/$CompanyId/assets/$Id" + Method = 'GET' + } + Invoke-HuduRequest @HuduRequest + } else { + $Params = @{} + if ($CompanyId) { $Params.company_id = $CompanyId } + if ($AssetLayoutId) { $Params.asset_layout_id = $AssetLayoutId } + if ($Name) { $Params.name = $Name } + if ($Archived.IsPresent) { $params.archived = $Archived.IsPresent } + if ($PrimarySerial) { $Params.primary_serial = $PrimarySerial } + if ($Id) { $Params.id = $Id } + if ($Slug) { $Params.slug = $Slug } + + $HuduRequest = @{ + Resource = '/api/v1/assets' + Method = 'GET' + Params = $Params + } + Invoke-HuduRequestPaginated -HuduRequest $HuduRequest -Property assets + } +} +#EndRegion './Public/Get-HuduAssets.ps1' 84 +#Region './Public/Get-HuduBaseURL.ps1' -1 + +function Get-HuduBaseURL { + <# + .SYNOPSIS + Get Hudu Base URL + + .DESCRIPTION + Returns Hudu Base URL + + .EXAMPLE + Get-HuduBaseURL + + #> + [CmdletBinding()] + Param() + if ($null -eq $Int_HuduBaseURL) { + Write-Error 'No Base URL has been set. Please use New-HuduBaseURL to set it.' + } else { + $Int_HuduBaseURL + } +} +#EndRegion './Public/Get-HuduBaseURL.ps1' 21 +#Region './Public/Get-HuduCard.ps1' -1 + +function Get-HuduCard { + <# + .SYNOPSIS + Get Integration Cards + + .DESCRIPTION + Lookup cards with outside integration details + + .PARAMETER IntegrationSlug + Identifier of outside integration + + .PARAMETER IntegrationId + ID in the integration. Must be present, unless integration_identifier is set + + .PARAMETER IntegrationIdentifier + Identifier in the integration (if integration_id is not set) + + .EXAMPLE + Get-HuduCard -IntegrationSlug cw_manage -IntegrationId 1 + + #> + [CmdletBinding()] + Param ( + [Parameter(Mandatory = $true)] + [Alias('integration_slug')] + [String]$IntegrationSlug, + + [Alias('integration_id')] + [String]$IntegrationId, + + [Alias('integration_identifier')] + [String]$IntegrationIdentifier + ) + + $Params = @{ + integration_slug = $IntegrationSlug + } + + if ($IntegrationId) { $Params.integration_id = $IntegrationId } + if ($IntegrationIdentifier) { $Params.integration_identifier = $IntegrationIdentifier } + + if (!$IntegrationId -and !$IntegrationIdentifier) { + throw 'IntegrationId or IntegrationIdentifier required' + } + + $HuduRequest = @{ + Method = 'GET' + Resource = '/api/v1/cards/lookup' + Params = $Params + } + + Invoke-HuduRequestPaginated -HuduRequest $HuduRequest -Property integrator_cards +} +#EndRegion './Public/Get-HuduCard.ps1' 54 +#Region './Public/Get-HuduCompanies.ps1' -1 + +function Get-HuduCompanies { + <# + .SYNOPSIS + Get a list of companies + + .DESCRIPTION + Call Hudu API to retrieve company list + + .PARAMETER Id + Filter companies by id + + .PARAMETER Name + Filter companies by name + + .PARAMETER PhoneNumber + filter companies by phone number + + .PARAMETER Website + Filter companies by website + + .PARAMETER City + Filter companies by city + + .PARAMETER State + Filter companies by state + + .PARAMETER Search + Filter by search query + + .PARAMETER Slug + Filter by url slug + + .PARAMETER IdInIntegration + Filter companies by id/identifier in PSA/RMM/outside integration + + .EXAMPLE + Get-HuduCompanies -Search 'Vendor' + + #> + [CmdletBinding()] + Param ( + [String]$Name = '', + [Alias('phone_number')] + [String]$PhoneNumber = '', + [String]$Website = '', + [String]$City = '', + [String]$State = '', + [Alias('id_in_integration')] + [Int]$IdInIntegration = '', + [Int]$Id = '', + [string]$Search, + [String]$Slug + ) + + if ($Id) { + $Company = (Invoke-HuduRequest -Method get -Resource "/api/v1/companies/$Id").company + return $Company + } else { + $Params = @{} + if ($Name) { $Params.name = $Name } + if ($PhoneNumber) { $Params.phone_number = $PhoneNumber } + if ($Website) { $Params.website = $Website } + if ($City) { $Params.city = $City } + if ($State) { $Params.state = $State } + if ($IdInIntegration) { $Params.id_in_integration = $IdInIntegration } + if ($Search) { $Params.search = $Search } + if ($Slug) { $Params.slug = $Slug } + + $HuduRequest = @{ + Method = 'GET' + Resource = '/api/v1/companies' + Params = $Params + } + + Invoke-HuduRequestPaginated -HuduRequest $HuduRequest -Property 'companies' + } +} +#EndRegion './Public/Get-HuduCompanies.ps1' 78 +#Region './Public/Get-HuduExpirations.ps1' -1 + +function Get-HuduExpirations { + <# + .SYNOPSIS + Get expirations for account + + .DESCRIPTION + Calls Hudu API to retrieve expirations + + .PARAMETER CompanyId + Filter expirations by company_id + + .PARAMETER ExpirationType + Filter expirations by expiration type (undeclared, domain, ssl_certificate, warranty, asset_field, article_expiration) + + .PARAMETER ResourceId + Filter logs by resource id. Must be coupled with resource_type + + .PARAMETER ResourceType + Filter logs by resource type (Asset, AssetPassword, Company, Article, etc.). Must be coupled with resource_id + + .EXAMPLE + Get-HuduExpirations -ExpirationType domain + + #> + [CmdletBinding()] + Param ( + [Alias('company_id')] + [Int]$CompanyId = '', + + [ValidateSet('undeclared', 'domain', 'ssl_certificate', 'warranty', 'asset_field', 'article_expiration')] + [Alias('expiration_type')] + [String]$ExpirationType = '', + + [Alias('resource_id')] + [Int]$ResourceId = '', + + [Alias('resource_type')] + [String]$ResourceType = '' + ) + + $Params = @{} + + if ($CompanyId) { $Params.company_id = $CompanyId } + if ($ExpirationType) { $Params.expiration_type = $ExpirationType } + if ($ResourceType) { $Params.resource_type = $ResourceType } + if ($ResourceId) { $Params.resource_id = $ResourceId } + + $HuduRequest = @{ + Method = 'GET' + Resource = '/api/v1/expirations' + Params = $Params + } + + Invoke-HuduRequestPaginated -HuduRequest $HuduRequest +} +#EndRegion './Public/Get-HuduExpirations.ps1' 56 +#Region './Public/Get-HuduFolderMap.ps1' -1 + +function Get-HuduFolderMap { + [CmdletBinding()] + Param ( + [Alias('company_id')] + [Int]$CompanyId = '' + ) + + if ($CompanyId) { + $FoldersRaw = Get-HuduFolders -company_id $CompanyId + $SubFolders = Get-HuduCompanyFolders -FoldersRaw $FoldersRaw + } else { + $FoldersRaw = Get-HuduFolders + $FoldersProcessed = $FoldersRaw | Where-Object { $null -eq $_.company_id } + $SubFolders = Get-HuduCompanyFolders -FoldersRaw $FoldersProcessed + } + + return $SubFolders +} +#EndRegion './Public/Get-HuduFolderMap.ps1' 19 +#Region './Public/Get-HuduFolders.ps1' -1 + +function Get-HuduFolders { + <# + .SYNOPSIS + Get a list of Folders + + .DESCRIPTION + Calls Hudu API to retrieve folders + + .PARAMETER Id + Id of the folder + + .PARAMETER Name + Filter by name + + .PARAMETER CompanyId + Filter by company_id + + .EXAMPLE + Get-HuduFolders + + #> + [CmdletBinding()] + Param ( + [Int]$Id = '', + [String]$Name = '', + [Alias('company_id')] + [Int]$CompanyId = '' + ) + + if ($id) { + $Folder = Invoke-HuduRequest -Method get -Resource "/api/v1/folders/$id" + return $Folder.Folder + } else { + $Params = @{} + + if ($CompanyId) { $Params.company_id = $CompanyId } + if ($Name) { $Params.name = $Name } + + $HuduRequest = @{ + Method = 'GET' + Resource = '/api/v1/folders' + Params = $Params + } + Invoke-HuduRequestPaginated -HuduRequest $HuduRequest -Property folders + } +} +#EndRegion './Public/Get-HuduFolders.ps1' 47 +#Region './Public/Get-HuduIntegrationMatchers.ps1' -1 + +function Get-HuduIntegrationMatchers { + <# + .SYNOPSIS + List matchers for an integration + + .DESCRIPTION + Calls Hudu API to get list of integration matching + + .PARAMETER IntegrationId + ID of the integration. Can be found in the URL when editing an integration + + .PARAMETER Matched + Filter on whether the company already been matched + + .PARAMETER SyncId + Filter by ID of the record in the integration. This is used if the id that the integration uses is an integer. + + .PARAMETER Identifier + Filter by Identifier in the integration (if sync_id is not set). This is used if the id that the integration uses is a string. + + .PARAMETER CompanyId + Filter on company id + + .EXAMPLE + Get-HuduIntegrationMatchers -IntegrationId 1 + + #> + [CmdletBinding()] + Param ( + [Parameter(Mandatory = $true)] + [int]$IntegrationId, + + [switch]$Matched, + + [int]$SyncId = '', + + [string]$Identifier = '', + + [int]$CompanyId + ) + + $Params = @{ + integration_id = $IntegrationId + } + + if ($Matched.IsPresent) { $Params.matched = 'true' } + if ($CompanyId) { $Params.company_id = $CompanyId } + if ($Identifier) { $Params.identifier = $Identifier } + if ($SyncId) { $Params.sync_id = $SyncId } + + $HuduRequest = @{ + Method = 'GET' + Resource = '/api/v1/matchers' + Params = $Params + } + Invoke-HuduRequestPaginated -HuduRequest $HuduRequest -Property 'matchers' +} +#EndRegion './Public/Get-HuduIntegrationMatchers.ps1' 58 +#Region './Public/Get-HuduMagicDashes.ps1' -1 + +function Get-HuduMagicDashes { + <# + .SYNOPSIS + Get all Magic Dash Items + + .DESCRIPTION + Call Hudu API to retrieve Magic Dashes + + .PARAMETER CompanyId + Filter by company id + + .PARAMETER Title + Filter by title + + .EXAMPLE + Get-HuduMagicDashes -Title 'Microsoft 365 - ...' + + #> + Param ( + [Alias('company_id')] + [Int]$CompanyId, + [String]$Title + ) + + $Params = @{} + + if ($CompanyId) { $Params.company_id = $CompanyId } + if ($Title) { $Params.title = $Title } + + $HuduRequest = @{ + Method = 'GET' + Resource = '/api/v1/magic_dash' + Params = $Params + } + Invoke-HuduRequestPaginated -HuduRequest $HuduRequest +} +#EndRegion './Public/Get-HuduMagicDashes.ps1' 37 +#Region './Public/Get-HuduObjectByUrl.ps1' -1 + +function Get-HuduObjectByUrl { + <# + .SYNOPSIS + Get Hudu object from URL + + .DESCRIPTION + Calls Hudu API to retrieve object based on URL string + + .PARAMETER Url + Url to retrieve object from + + .EXAMPLE + Get-HuduObject -Url https://your-hudu-server/a/some-asset-1z8z7a + + #> + [CmdletBinding()] + Param ( + [uri]$Url + ) + + if ((Get-HuduBaseURL) -match $Url.Authority) { + $null, $Type, $Slug = $Url.PathAndQuery -split '/' + + $SlugSplat = @{ + Slug = $Slug + } + + switch ($Type) { + 'a' { + # Asset + Get-HuduAssets @SlugSplat + } + 'admin' { + # Admin path + $null, $null, $Type, $Slug = $Url.PathAndQuery -split '/' + $SlugSplat = @{ + Slug = $Slug + } + switch ($Type) { + 'asset_layouts' { + # Asset layouts + Get-HuduAssetLayouts @SlugSplat + } + } + } + 'c' { + # Company + Get-HuduCompanies @SlugSplat + } + 'kba' { + # KB article + Get-HuduArticles @SlugSplat + } + 'passwords' { + # Passwords + Get-HuduPasswords @SlugSplat + } + 'websites' { + # Website + Get-HuduWebsites @SlugSplat + } + default { + Write-Error "Unsupported object type $Type" + } + } + } else { + Write-Error 'Provided URL does not match Hudu Base URL' + } +} +#EndRegion './Public/Get-HuduObjectByUrl.ps1' 70 +#Region './Public/Get-HuduPasswordFolders.ps1' -1 + +function Get-HuduPasswordFolders { + <# + .SYNOPSIS + Get a list of Password Folders + + .DESCRIPTION + Calls Hudu API to retrieve folders + + .PARAMETER Id + Id of the folder + + .PARAMETER Name + Filter by name + + .PARAMETER CompanyId + Filter by company_id + + .EXAMPLE + Get-HuduFolders + + #> + [CmdletBinding()] + Param ( + [Int]$Id = '', + [String]$Name = '', + [String]$Search = '', + [Alias('company_id')] + [Int]$CompanyId = '', + [Int]$page = '', + [Int]$page_size = '' + ) + + if ($id) { + $Folder = Invoke-HuduRequest -Method get -Resource "/api/v1/password_folders/$id" + return $Folder.password_folder + } else { + $Params = @{} + + if ($CompanyId) { $Params.company_id = $CompanyId } + if ($Name) { $Params.name = $Name } + + $HuduRequest = @{ + Method = 'GET' + Resource = '/api/v1/password_folders' + Params = $Params + } + Invoke-HuduRequestPaginated -HuduRequest $HuduRequest -Property password_folders + } +} +#EndRegion './Public/Get-HuduPasswordFolders.ps1' 50 +#Region './Public/Get-HuduPasswords.ps1' -1 + +function Get-HuduPasswords { + <# + .SYNOPSIS + Get a list of Passwords + + .DESCRIPTION + Calls Hudu API to list password assets + + .PARAMETER Id + Id of the password + + .PARAMETER CompanyId + Filter by company id + + .PARAMETER Name + Filter by password name + + .PARAMETER Slug + Filter by url slug + + .PARAMETER Search + Filter by search query + + .EXAMPLE + Get-HuduPasswords -CompanyId 1 + + #> + [CmdletBinding()] + Param ( + [Int]$Id, + + [Alias('company_id')] + [Int]$CompanyId, + + [String]$Name, + + [String]$Slug, + + [string]$Search + ) + + if ($Id) { + $Password = Invoke-HuduRequest -Method get -Resource "/api/v1/asset_passwords/$id" + return $Password + } else { + $Params = @{} + if ($CompanyId) { $Params.company_id = $CompanyId } + if ($Name) { $Params.name = $Name } + if ($Slug) { $Params.slug = $Slug } + if ($Search) { $Params.search = $Search } + } + + $HuduRequest = @{ + Method = 'GET' + Resource = '/api/v1/asset_passwords' + Params = $Params + } + Invoke-HuduRequestPaginated -HuduRequest $HuduRequest -Property 'asset_passwords' +} +#EndRegion './Public/Get-HuduPasswords.ps1' 60 +#Region './Public/Get-HuduProcesses.ps1' -1 + +function Get-HuduProcesses { + <# + .SYNOPSIS + Get a list of Procedures (Processes) + + .DESCRIPTION + Calls Hudu API to retrieve list of procedures + + .PARAMETER Id + Id of the Procedure + + .PARAMETER CompanyId + Filter by company id + + .PARAMETER Name + Fitler by name of article + + .PARAMETER Slug + Filter by url slug + + .EXAMPLE + Get-HuduProcedures -Name 'Procedure 1' + + #> + [CmdletBinding()] + Param ( + [Int]$Id = '', + [Alias('company_id')] + [Int]$CompanyId = '', + [String]$Name = '', + [String]$Slug + ) + + if ($Id) { + Invoke-HuduRequest -Method get -Resource "/api/v1/procedures/$id" + } else { + $Params = @{} + + if ($CompanyId) { $Params.company_id = $CompanyId } + if ($Name) { $Params.name = $Name } + if ($Slug) { $Params.slug = $Slug } + + + $HuduRequest = @{ + Method = 'GET' + Resource = '/api/v1/procedures' + Params = $Params + } + Invoke-HuduRequestPaginated -HuduRequest $HuduRequest -Property 'procedures' + } +} +#EndRegion './Public/Get-HuduProcesses.ps1' 52 +#Region './Public/Get-HuduPublicPhotos.ps1' -1 + +function Get-HuduPublicPhotos { + <# + .SYNOPSIS + Get a list of Public_Photos + + .DESCRIPTION + Calls Hudu API to retrieve public photos + + .EXAMPLE + Get-HuduPublicPhotos + + #> + [CmdletBinding()] + Param() + + $HuduRequest = @{ + Method = 'GET' + Resource = '/api/v1/public_photos' + Params = @{} + } + Invoke-HuduRequestPaginated -HuduRequest $HuduRequest -Property 'public_photos' +} +#EndRegion './Public/Get-HuduPublicPhotos.ps1' 23 +#Region './Public/Get-HuduRelations.ps1' -1 + +function Get-HuduRelations { + <# + .SYNOPSIS + Get a list of all relations + + .DESCRIPTION + Calls Hudu API to retrieve object relationsihps + + .EXAMPLE + Get-HuduRelations -CompanyId 1 + + #> + [CmdletBinding()] + Param() + + $HuduRequest = @{ + Method = 'GET' + Resource = '/api/v1/relations' + Params = @{} + } + + Invoke-HuduRequestPaginated -HuduRequest $HuduRequest -Property 'relations' +} +#EndRegion './Public/Get-HuduRelations.ps1' 24 +#Region './Public/Get-HuduUploads.ps1' -1 + +function Get-HuduUploads { + <# + .SYNOPSIS + Get a list of uploads + + .DESCRIPTION + Calls Hudu API to retrieve uploads + + .EXAMPLE + Get-HuduUploads + + #> + [CmdletBinding()] + Param( + [Int]$Id + ) + + if ($Id) { + $Upload = Invoke-HuduRequest -Method Get -Resource "/api/v1/uploads/$Id" + } else { + $Upload = Invoke-HuduRequest -Method Get -Resource "/api/v1/uploads" + } + return $Upload +} +#EndRegion './Public/Get-HuduUploads.ps1' 25 +#Region './Public/Get-HuduWebsites.ps1' -1 + +function Get-HuduWebsites { + <# + .SYNOPSIS + Get a list of all websites + + .DESCRIPTION + Calls Hudu API to get websites + + .PARAMETER Name + Filter websites by name + + .PARAMETER Id + ID of website + + .PARAMETER Slug + Filter by url slug + + .PARAMETER Search + Fitler by search query + + .EXAMPLE + Get-HuduWebsites -Search 'domain.com' + + #> + [CmdletBinding()] + Param ( + [String]$Name, + [Alias('website_id')] + [Int]$WebsiteId, + [String]$Slug, + [string]$Search + ) + + if ($WebsiteId) { + Invoke-HuduRequest -Method get -Resource "/api/v1/websites/$($WebsiteId)" + } else { + $Params = @{} + if ($Name) { $Params.name = $Name } + if ($Slug) { $Params.slug = $Slug } + if ($Search) { $Params.search = $Search } + + $HuduRequest = @{ + Method = 'GET' + Resource = '/api/v1/websites' + Params = $Params + } + Invoke-HuduRequestPaginated -HuduRequest $HuduRequest + } +} +#EndRegion './Public/Get-HuduWebsites.ps1' 50 +#Region './Public/Initialize-HuduFolder.ps1' -1 + +function Initialize-HuduFolder { + [CmdletBinding()] + param( + [String[]]$FolderPath, + [Alias('company_id')] + [int]$CompanyId + ) + + if ($CompanyId) { + $FolderMap = Get-HuduFolderMap -company_id $CompanyId + } else { + $FolderMap = Get-HuduFolderMap + } + + $CurrentFolder = $Foldermap + foreach ($Folder in $FolderPath) { + if ($CurrentFolder.$(Get-HuduFolderCleanName $Folder)) { + $CurrentFolder = $CurrentFolder.$(Get-HuduFolderCleanName $Folder) + } else { + $CurrentFolder = (New-HuduFolder -Name $Folder -company_id $CompanyID -parent_folder_id $CurrentFolder.id).folder + } + } + + return $CurrentFolder +} +#EndRegion './Public/Initialize-HuduFolder.ps1' 26 +#Region './Public/Move-HuduAssetsToNewLayout.ps1' -1 + +function Move-HuduAssetsToNewLayout { +<# + .SYNOPSIS + Helper function that uses the Set-HuduAsset function to move an asset between asset layouts. This will leave behind orphan data in the database. + Review the article https://portal.risingtidegroup.net/kb?id=29 for more details. + + .DESCRIPTION + Calls the Hudu API to update an asset by switching its asset_layout_id property to a different asset layout. + This function migrates the asset to the specified new layout while maintaining its fields. Note that this + operation may leave behind orphaned data in the Hudu database, so use it with caution. + + .PARAMETER AssetsToMove + An array of assets to be moved to a new asset layout. Each asset must contain both 'id' and 'fields' properties. + + .PARAMETER NewAssetLayoutID + The ID of the new asset layout to which the assets will be moved. + + .EXAMPLE + $AssetLayout = Get-HuduAssetLayouts -Name "Servers" + $AssetsToUpdate = Get-HuduAssets -AssetLayoutId 9 + Move-HuduAssetsToNewLayout -AssetsToMove $AssetsToUpdate -NewAssetLayoutID $AssetLayout.id + + This example retrieves the asset layout with the name "Servers" and the assets with the layout ID 9, then moves those assets to the new layout. + + .NOTES + Ensure that the new asset layout ID is valid and that the assets to be moved contain the required properties. + Using this function may result in orphaned data in your Hudu database. Review the provided article for more details. +#> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] + param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [ValidateScript({ + if ($BadAssets = ($_ | where {(-not $_.id)})) { + $BadAssets + throw "Assets must be an object with an ID" + } + return $true + })] + [array] + $AssetsToMove, + + [Parameter(Mandatory = $true)] + [int] + $NewAssetLayoutID + ) + + Write-Warning "Performing this function will leave behind orphaned data in your Hudu database. Please review https://portal.risingtidegroup.net/kb?id=29" + Read-Host "Press Enter to continue or (CTRL+C) to cancel..." + + $assets = foreach ($AssetToMove in $AssetsToMove) { + if (-not ($AssetToMove.PSObject.Properties.Match('id')) -or -not ($AssetToMove.PSObject.Properties.Match('fields'))) { + Write-Error "Asset does not contain both 'id' and 'fields' properties. Skipping this asset." + continue + } + + if (-not $AssetToMove.fields) { + Write-Warning "Asset ID: $($AssetToMove.id) has no fields. Proceeding with moving the asset." + } + + $assetId = $AssetToMove.id + + if ($PSCmdlet.ShouldProcess("Asset ID: $assetId", "Move to new layout with ID $NewAssetLayoutID")) { + try { + Write-Verbose "Processing Asset ID: $assetId" + + $fields = New-Object -TypeName psobject + foreach ($field in $AssetToMove.fields) { + $fieldName = $field.label.replace(' ', '_').tolower() + $fields | Add-Member -MemberType NoteProperty -Name $fieldName -Value $field.value -Force + } + + (Set-HuduAsset -Id $assetId -AssetLayoutId $NewAssetLayoutID -Fields $fields).asset + + Write-Verbose "Successfully moved Asset ID: $assetId" + } + catch { + Write-Error "Failed to move Asset ID: $assetId. Error: $_" + } + finally { + Remove-Variable -Name fields -ErrorAction SilentlyContinue + } + } + } + return $assets +} +#EndRegion './Public/Move-HuduAssetsToNewLayout.ps1' 87 +#Region './Public/New-HuduAPIKey.ps1' -1 + +function New-HuduAPIKey { + <# + .SYNOPSIS + Set Hudu API Key + + .DESCRIPTION + API keys are required to interact with Hudu + + .PARAMETER ApiKey + The API key + + .EXAMPLE + New-HuduAPIKey -ApiKey abdc1234 + + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '', Scope = 'Function')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Scope = 'Function')] + [CmdletBinding()] + Param ( + [Parameter(Mandatory = $false, ValueFromPipeline = $true)] + [String]$ApiKey + ) + + process { + if ($ApiKey) { + $SecApiKey = ConvertTo-SecureString $ApiKey -AsPlainText -Force + } else { + $SecApiKey = Read-Host -Prompt 'Please enter your Hudu API key, you can obtain it from https://your-hudu-domain/admin/api_keys:' -AsSecureString + } + Set-Variable -Name 'Int_HuduAPIKey' -Value $SecApiKey -Visibility Private -Scope script -Force + + if ($script:Int_HuduBaseURL) { + [version]$version = (Get-HuduAppInfo).version + if ($version -lt $script:HuduRequiredVersion) { + Write-Warning "A connection error occured or Hudu version is below $script:HuduRequiredVersion" + } + } + } +} +#EndRegion './Public/New-HuduAPIKey.ps1' 40 +#Region './Public/New-HuduArticle.ps1' -1 + +function New-HuduArticle { + <# + .SYNOPSIS + Create a Knowledge Base Article + + .DESCRIPTION + Uses Hudu API to create KB articles + + .PARAMETER Name + Name of article + + .PARAMETER Content + Article HTML contents + + .PARAMETER EnableSharing + Create public URL for users to view without being authenticated + + .PARAMETER FolderId + Associate article with folder id + + .PARAMETER CompanyId + Associate article with company id + + .PARAMETER Slug + Manually define slug for Article + + .EXAMPLE + New-HuduArticle -Name "Test" -CompanyId 1 -Content '

    Testing

    ' -EnableSharing -Slug 'this-is-a-test' + + #> + [CmdletBinding(SupportsShouldProcess)] + Param ( + [Parameter(Mandatory = $true)] + [String]$Name, + + [Parameter(Mandatory = $true)] + [String]$Content, + + [switch]$EnableSharing, + + [Alias('folder_id')] + [Int]$FolderId = '', + + [Alias('company_id')] + [Int]$CompanyId = '', + + [string]$Slug + ) + + $Article = [ordered]@{article = [ordered]@{} } + + $Article.article.add('name', $Name) + $Article.article.add('content', $Content) + + if ($FolderId) { + $Article.article.add('folder_id', $FolderId) + } + + if ($CompanyId) { + $Article.article.add('company_id', $CompanyId) + } + + if ($EnableSharing.IsPresent) { + $Article.article.add('enable_sharing', 'true') + } + + if ($Slug) { + $Article.article.add('slug', $Slug) + } + + $JSON = $Article | ConvertTo-Json -Depth 10 + + if ($PSCmdlet.ShouldProcess($Name)) { + Invoke-HuduRequest -Method post -Resource '/api/v1/articles' -Body $JSON + } +} +#EndRegion './Public/New-HuduArticle.ps1' 77 +#Region './Public/New-HuduAsset.ps1' -1 + +function New-HuduAsset { + <# + .SYNOPSIS + Create an Asset + + .DESCRIPTION + Uses Hudu API to create assets using custom layouts + + .PARAMETER Name + Name of the Asset + + .PARAMETER CompanyId + Company id for asset + + .PARAMETER AssetLayoutId + Asset layout id + + .PARAMETER Fields + Array of custom fields and values + + .PARAMETER PrimarySerial + Asset primary serial number + + .PARAMETER PrimaryMail + Asset primary mail + + .PARAMETER PrimaryModel + Asset primary model + + .PARAMETER PrimaryManufacturer + Asset primary manufacturer + + .PARAMETER Slug + Url identifier + + .EXAMPLE + New-HuduAsset -Name 'Some asset' -CompanyId 1 -Fields @(@{'field_name'='Field Value'}) + + #> + [CmdletBinding(SupportsShouldProcess)] + Param ( + [Parameter(Mandatory = $true)] + [String]$Name, + + [Alias('company_id')] + [Parameter(Mandatory = $true)] + [Int]$CompanyId, + + [Alias('asset_layout_id')] + [Parameter(Mandatory = $true)] + [Int]$AssetLayoutId, + + [Array]$Fields, + + [Alias('primary_serial')] + [string]$PrimarySerial, + + [Alias('primary_mail')] + [string]$PrimaryMail, + + [Alias('primary_model')] + [string]$PrimaryModel, + + [Alias('primary_manufacturer')] + [string]$PrimaryManufacturer + ) + + $Asset = [ordered]@{asset = [ordered]@{} } + + $Asset.asset.add('name', $Name) + $Asset.asset.add('asset_layout_id', $AssetLayoutId) + + + if ($PrimarySerial) { + $Asset.asset.add('primary_serial', $PrimarySerial) + } + + if ($PrimaryMail) { + $Asset.asset.add('primary_mail', $PrimaryMail) + } + + if ($PrimaryModel) { + $Asset.asset.add('primary_model', $PrimaryModel) + } + + if ($PrimaryManufacturer) { + $Asset.asset.add('primary_manufacturer', $PrimaryManufacturer) + } + + if ($Fields) { + $Asset.asset.add('custom_fields', $Fields) + } + + if ($Slug) { + $Asset.asset.add('slug', $Slug) + } + + $JSON = $Asset | ConvertTo-Json -Depth 10 + + if ($PSCmdlet.ShouldProcess($Name)) { + Invoke-HuduRequest -Method post -Resource "/api/v1/companies/$CompanyId/assets" -Body $JSON + } +} +#EndRegion './Public/New-HuduAsset.ps1' 104 +#Region './Public/New-HuduAssetLayout.ps1' -1 + +function New-HuduAssetLayout { + <# + .SYNOPSIS + Create an Asset Layout + + .DESCRIPTION + Uses Hudu API to create new custom asset layout + + .PARAMETER Name + Name of the layout + + .PARAMETER Icon + FontAwesome Icon class name, example: "fas fa-home" + + .PARAMETER Color + Background color hex code + + .PARAMETER IconColor + Icon color hex code + + .PARAMETER IncludePasswords + Boolean for including passwords + + .PARAMETER IncludePhotos + Boolean for including photos + + .PARAMETER IncludeComments + Boolean for including comments + + .PARAMETER IncludeFiles + Boolean for including files + + .PARAMETER PasswordTypes + List of password types, separated with new line characters + + .PARAMETER Slug + Url identifier + + .PARAMETER Fields + Array of hashtable or custom objects representing layout fields. Most field types only require a label and type. + Valid field types are: Text, RichText, Heading, CheckBox, Website (aka Link), Password (aka ConfidentialText), Number, Date, DropDown, Embed, Email (aka CopyableText), Phone, AssetLink + Field types are Case Sensitive as of Hudu V2.27 due to a known issue with asset type validation. + + .EXAMPLE + New-HuduAssetLayout -Name 'Test asset layout' -Icon 'fas fa-home' -IncludePassword $true + + .EXAMPLE + New-HuduAssetLayout -Name 'Test asset layout' -Icon 'fas fa-home' -IncludePassword $true -Fields @( + @{label = 'Test field'; 'field_type' = 'Text'} + ) + #> + [CmdletBinding(SupportsShouldProcess)] + # This will silence the warning for variables with Password in their name. + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', '')] + Param ( + [Parameter(Mandatory = $true)] + [String]$Name, + + [Parameter(Mandatory = $true)] + [String]$Icon, + + [Parameter(Mandatory = $true)] + [String]$Color, + + [Alias('icon_color')] + [Parameter(Mandatory = $true)] + [String]$IconColor, + + [Alias('include_passwords')] + [bool]$IncludePasswords = '', + + [Alias('include_photos')] + [bool]$IncludePhotos = '', + + [Alias('include_comments')] + [bool]$IncludeComments = '', + + [Alias('include_files')] + [bool]$IncludeFiles = '', + + [Alias('password_types')] + [String]$PasswordTypes = '', + + [Parameter(Mandatory = $true)] + [system.collections.generic.list[hashtable]]$Fields + ) + + foreach ($field in $fields) { + if ($field.show_in_list) { $field.show_in_list = [System.Convert]::ToBoolean($field.show_in_list) } else { $field.remove('show_in_list') } + if ($field.required) { $field.required = [System.Convert]::ToBoolean($field.required) } else { $field.remove('required') } + if ($field.expiration) { $field.expiration = [System.Convert]::ToBoolean($field.expiration) } else { $field.remove('expiration') } + # A bug in versions of Hudu 2.27 and earlier can cause asset layouts to become corrupted if the field type value is not properly cased. + switch ($field.'field_type') { + 'text' { $field.'field_type' = 'Text' } + 'richtext' { $field.'field_type' = 'RichText' } + 'heading' { $field.'field_type' = 'Heading' } + 'checkbox' { $field.'field_type' = 'CheckBox' } + 'number' { $field.'field_type' = 'Number' } + 'date' { $field.'field_type' = 'Date' } + 'dropdown' { $field.'field_type' = 'Dropdown' } + 'embed' { $field.'field_type' = 'Embed' } + 'phone' { $field.'field_type' = 'Phone' } + 'email' { $field.'field_type' = 'Email' } + 'copyabletext' { $field.'field_type' = 'Email' } + 'assettag' { $field.'field_type' = 'AssetTag' } + 'assetlink' { $field.'field_type' = 'AssetTag' } + 'website' { $field.'field_type' = 'Website' } + 'link' { $field.'field_type' = 'Website' } + 'password' { $field.'field_type' = 'Password' } + 'confidentialtext' { $field.'field_type' = 'Password' } + Default { throw "Invalid field type: $($field.'field_type') found in field $($field.name)" } + } + } + + $AssetLayout = [ordered]@{asset_layout = [ordered]@{} } + + $AssetLayout.asset_layout.add('name', $Name) + $AssetLayout.asset_layout.add('icon', $Icon) + $AssetLayout.asset_layout.add('color', $Color) + $AssetLayout.asset_layout.add('icon_color', $IconColor) + $AssetLayout.asset_layout.add('fields', $Fields) + #$AssetLayout.asset_layout.add('active', $Active) + + if ($IncludePasswords) { + $AssetLayout.asset_layout.add('include_passwords', [System.Convert]::ToBoolean($IncludePasswords)) + } + + if ($IncludePhotos) { + $AssetLayout.asset_layout.add('include_photos', [System.Convert]::ToBoolean($IncludePhotos)) + } + + if ($IncludeComments) { + $AssetLayout.asset_layout.add('include_comments', [System.Convert]::ToBoolean($IncludeComments)) + } + + if ($IncludeFiles) { + $AssetLayout.asset_layout.add('include_files', [System.Convert]::ToBoolean($IncludeFiles)) + } + + if ($PasswordTypes) { + $AssetLayout.asset_layout.add('password_types', $PasswordTypes) + } + + if ($Slug) { + $AssetLayout.asset_layout.add('slug', $Slug) + } + + $JSON = $AssetLayout | ConvertTo-Json -Depth 10 + + Write-Verbose $JSON + + if ($PSCmdlet.ShouldProcess($Name)) { + Invoke-HuduRequest -Method post -Resource '/api/v1/asset_layouts' -Body $JSON + } +} +#EndRegion './Public/New-HuduAssetLayout.ps1' 156 +#Region './Public/New-HuduBaseURL.ps1' -1 + +function New-HuduBaseURL { + <# + .SYNOPSIS + Set Hudu Base URL + + .DESCRIPTION + In order to access the Hudu API the Base URL must be set + + .PARAMETER BaseURL + Url with no trailing slash e.g. https://demo.huducloud.com + + .EXAMPLE + New-HuduBaseURL -BaseURL https://demo.huducloud.com + + .NOTES + General notes + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Scope = 'Function')] + [CmdletBinding()] + Param ( + [Parameter(Mandatory = $false, + ValueFromPipeline = $true)] + [String] + $BaseURL + ) + process { + if (!$BaseURL) { + $BaseURL = Read-Host -Prompt 'Please enter your Hudu Base URL with no trailing /, for example https://demo.huducloud.com :' + } + + $Protocol = $BaseURL[0..7] -join '' + if ($Protocol -ne 'https://') { + if ($Protocol -like 'http://*') { + Write-Warning "Non HTTPS Base URL was set, rewriting URL to be secure transport only. If connection fails please make sure hostname is correct and HTTPS is enabld." + $BaseURL = $BaseURL.Replace('http://','https://') + } + else { + Write-Warning "No protocol was specified, adding https:// to the beginning of the specified hostname" + $BaseURL = "https://$BaseURL" + } + } + + Set-Variable -Name 'Int_HuduBaseURL' -Value $BaseURL -Visibility Private -Scope script -Force + + if ($script:Int_HuduAPIKey) { + [version]$Version = (Get-HuduAppInfo).version + if ($Version -lt $script:HuduRequiredVersion) { + Write-Warning "A connection error occured or Hudu version is below $script:HuduRequiredVersion" + } + } + } +} +#EndRegion './Public/New-HuduBaseURL.ps1' 53 +#Region './Public/New-HuduCompany.ps1' -1 + +function New-HuduCompany { + <# + .SYNOPSIS + Create a company + + .DESCRIPTION + Uses Hudu API to create a new company + + .PARAMETER Name + Company name + + .PARAMETER Nickname + Company nickname + + .PARAMETER CompanyType + Company type + + .PARAMETER AddressLine1 + Address line 1 + + .PARAMETER AddressLine2 + Address line 2 + + .PARAMETER City + City + + .PARAMETER State + State + + .PARAMETER Zip + Zip + + .PARAMETER CountryName + Country + + .PARAMETER PhoneNumber + Phone number + + .PARAMETER FaxNumber + Fax number + + .PARAMETER Website + Website + + .PARAMETER IdNumber + Company id number + + .PARAMETER ParentCompanyId + Parent company id number + + .PARAMETER Notes + Parameter description + + .PARAMETER Slug + Url identifier + + .EXAMPLE + New-HuduCompany -Name 'Company name' + + #> + [CmdletBinding(SupportsShouldProcess)] + Param ( + [Parameter(Mandatory = $true)] + [String]$Name, + + [String]$Nickname = '', + + [Alias('company_type')] + [String]$CompanyType = '', + + [Alias('address_line_1')] + [String]$AddressLine1 = '', + + [Alias('address_line_2')] + [String]$AddressLine2 = '', + + [String]$City = '', + + [String]$State = '', + + [Alias('PostalCode', 'PostCode')] + [String]$Zip = '', + + [Alias('country_name')] + [String]$CountryName = '', + + [Alias('phone_number')] + [String]$PhoneNumber = '', + + [Alias('fax_number')] + [String]$FaxNumber = '', + + [String]$Website = '', + + [Alias('id_number')] + [String]$IdNumber = '', + + [Alias('parent_company_id')] + [int]$ParentCompanyId, + + [String]$Notes = '', + + [string]$Slug + ) + + + $Company = [ordered]@{company = [ordered]@{} } + + $Company.company.add('name', $Name) + if (-not ([string]::IsNullOrEmpty($Nickname))) { $Company.company.add('nickname', $Nickname) } + if (-not ([string]::IsNullOrEmpty($Nickname))) { $Company.company.add('company_type', $CompanyType) } + if (-not ([string]::IsNullOrEmpty($AddressLine1))) { $Company.company.add('address_line_1', $AddressLine1) } + if (-not ([string]::IsNullOrEmpty($AddressLine2))) { $Company.company.add('address_line_2', $AddressLine2) } + if (-not ([string]::IsNullOrEmpty($City))) { $Company.company.add('city', $City) } + if (-not ([string]::IsNullOrEmpty($State))) { $Company.company.add('state', $State) } + if (-not ([string]::IsNullOrEmpty($Zip))) { $Company.company.add('zip', $Zip) } + if (-not ([string]::IsNullOrEmpty($CountryName))) { $Company.company.add('country_name', $CountryName) } + if (-not ([string]::IsNullOrEmpty($PhoneNumber))) { $Company.company.add('phone_number', $PhoneNumber) } + if (-not ([string]::IsNullOrEmpty($FaxNumber))) { $Company.company.add('fax_number', $FaxNumber) } + if (-not ([string]::IsNullOrEmpty($Website))) { $Company.company.add('website', $Website) } + if (-not ([string]::IsNullOrEmpty($IdNumber))) { $Company.company.add('id_number', $IdNumber) } + if (-not ([string]::IsNullOrEmpty($ParentCompanyId))) { $Company.company.add('parent_company_id', $ParentCompanyId) } + if (-not ([string]::IsNullOrEmpty($Notes))) { $Company.company.add('notes', $Notes) } + if (-not ([string]::IsNullOrEmpty($Slug))) { $Company.company.add('slug', $Slug) } + + $JSON = $Company | ConvertTo-Json -Depth 10 + Write-Verbose $JSON + + if ($PSCmdlet.ShouldProcess($Name)) { + Invoke-HuduRequest -Method post -Resource '/api/v1/companies' -Body $JSON + } +} +#EndRegion './Public/New-HuduCompany.ps1' 133 +#Region './Public/New-HuduCustomHeaders.ps1' -1 + +function New-HuduCustomHeaders { + <# + .SYNOPSIS + Set Hudu custom headers to be injected into each request + + .DESCRIPTION + There may be times when one might need to use custom headers e.g. Service Tokens for Cloudflare Zero Trust + + .PARAMETER Headers + Hashtable with the Custom Headers that need to be injected into each request + + .EXAMPLE + New-HuduCustomHeaders -Headers @{"CF-Access-Client-Id" = "x"; "CF-Access-Client-Secret" = "y"} + + .NOTES + General notes + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Scope = 'Function')] + [CmdletBinding()] + Param ( + [Parameter(Mandatory = $true, + ValueFromPipeline = $true)] + [hashtable] + $Headers + ) + process { + if ($Headers.Count -eq 0) { + Write-Host "Empty Custom Header hashtable was provided, no Custom Headers will be set" + return 0 + } + + Set-Variable -Name 'Int_HuduCustomHeaders' -Value $Headers -Visibility Private -Scope script -Force + } +} +#EndRegion './Public/New-HuduCustomHeaders.ps1' 35 +#Region './Public/New-HuduFolder.ps1' -1 + +function New-HuduFolder { + <# + .SYNOPSIS + Create a Folder + + .DESCRIPTION + Uses Hudu API to create a new folder + + .PARAMETER Name + Name of the folder + + .PARAMETER Icon + Folder Icon + + .PARAMETER Description + Folder description + + .PARAMETER ParentFolderId + Parent folder ID + + .PARAMETER CompanyId + Company id + + .EXAMPLE + New-HuduFolder -Name 'Test folder' -CompanyId 1 + + #> + [CmdletBinding(SupportsShouldProcess)] + Param ( + [Parameter(Mandatory = $true)] + [String]$Name, + [String]$Icon = '', + [String]$Description = '', + [Alias('parent_folder_id')] + [Int]$ParentFolderId = '', + [Alias('company_id')] + [Int]$CompanyId = '' + ) + + $Folder = [ordered]@{folder = [ordered]@{} } + + $Folder.folder.add('name', $Name) + + if ($Icon) { + $Folder.folder.add('icon', $Icon) + } + + if ($Description) { + $Folder.folder.add('description', $Description) + } + + if ($ParentFolderId) { + $Folder.folder.add('parent_folder_id', $ParentFolderId) + } + + if ($CompanyId) { + $Folder.folder.add('company_id', $CompanyId) + } + + $JSON = $Folder | ConvertTo-Json + + if ($PSCmdlet.ShouldProcess($Name)) { + Invoke-HuduRequest -Method post -Resource '/api/v1/folders' -Body $JSON + } +} +#EndRegion './Public/New-HuduFolder.ps1' 66 +#Region './Public/New-HuduPassword.ps1' -1 + +function New-HuduPassword { + <# + .SYNOPSIS + Create a Password + + .DESCRIPTION + Uses Hudu API to create a new password + + .PARAMETER Name + Name of the password + + .PARAMETER CompanyId + Company id + + .PARAMETER PasswordableType + Asset type for the password + + .PARAMETER PasswordableId + Asset id for the password + + .PARAMETER InPortal + Boolean for in portal + + .PARAMETER Password + Password + + .PARAMETER OTPSecret + OTP secret + + .PARAMETER URL + Password URL + + .PARAMETER Username + Username + + .PARAMETER Description + Password description + + .PARAMETER PasswordType + Password type + + .PARAMETER PasswordFolderId + Password folder id + + .PARAMETER Slug + Url identifier + + .EXAMPLE + New-HuduPassword -Name 'Some website password' -Username 'user@domain.com' -Password '12345' + + #> + [CmdletBinding(SupportsShouldProcess)] + # This will silence the warning for variables with Password in their name. + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', '')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingUsernameAndPasswordParams', '')] + Param ( + [Parameter(Mandatory = $true)] + [String]$Name, + + [Alias('company_id')] + [Parameter(Mandatory = $true)] + [Int]$CompanyId, + + [Alias('passwordable_type')] + [String]$PasswordableType = '', + + [Alias('passwordable_id')] + [int]$PasswordableId = '', + + [Alias('in_portal')] + [Bool]$InPortal = $false, + + [Parameter(Mandatory = $true)] + [String]$Password = '', + + [Alias('otp_secret')] + [string]$OTPSecret = '', + + [String]$URL = '', + + [String]$Username = '', + + [String]$Description = '', + + [Alias('password_type')] + [String]$PasswordType = '', + + [Alias('password_folder_id')] + [int]$PasswordFolderId, + + [string]$Slug + ) + + $AssetPassword = [ordered]@{asset_password = [ordered]@{} } + + $AssetPassword.asset_password.add('name', $Name) + $AssetPassword.asset_password.add('company_id', $CompanyId) + $AssetPassword.asset_password.add('password', $Password) + $AssetPassword.asset_password.add('in_portal', $InPortal) + + if ($PasswordableType) { + $AssetPassword.asset_password.add('passwordable_type', $PasswordableType) + } + if ($PasswordableId) { + $AssetPassword.asset_password.add('passwordable_id', $PasswordableId) + } + + if ($OTPSecret) { + $AssetPassword.asset_password.add('otp_secret', $OTPSecret) + } + + if ($URL) { + $AssetPassword.asset_password.add('url', $URL) + } + + if ($Username) { + $AssetPassword.asset_password.add('username', $Username) + } + + if ($Description) { + $AssetPassword.asset_password.add('description', $Description) + } + + if ($PasswordType) { + $AssetPassword.asset_password.add('password_type', $PasswordType) + } + + if ($PasswordFolderId) { + $AssetPassword.asset_password.add('password_folder_id', $PasswordFolderId) + } + + if ($Slug) { + $AssetPassword.asset_password.add('slug', $Slug) + } + + $JSON = $AssetPassword | ConvertTo-Json -Depth 10 + + if ($PSCmdlet.ShouldProcess($Name)) { + Invoke-HuduRequest -Method post -Resource '/api/v1/asset_passwords' -Body $JSON + } +} +#EndRegion './Public/New-HuduPassword.ps1' 142 +#Region './Public/New-HuduPublicPhoto.ps1' -1 + +function New-HuduPublicPhoto { + <# + .SYNOPSIS + Create a Public Photo + + .DESCRIPTION + Uses Hudu API to upload an image for use in an asset or article + + .PARAMETER FilePath + Path to the image + + .PARAMETER RecordId + Record id to associate with the photo + + .PARAMETER RecordType + Record type to associate with the photo + + .EXAMPLE + New-HuduPublicPhoto -FilePath 'c:\path\to\image.png' -RecordId 1 -RecordType 'asset' + + #> + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter(Mandatory)] + [string]$FilePath, + + [Alias('record_id')] + [int]$RecordId, + + [Alias('record_type')] + [string]$RecordType + ) + + $File = Get-Item $FilePath + $form = @{ + photo = $File + } + + if ($RecordId) { $form['record_id'] = $RecordId } + if ($RecordType) { $form['record_type'] = $RecordType } + + if ($PSCmdlet.ShouldProcess($File.FullName)) { + Invoke-HuduRequest -Method POST -Resource '/api/v1/public_photos' -Form $form + } +} +#EndRegion './Public/New-HuduPublicPhoto.ps1' 46 +#Region './Public/New-HuduRelation.ps1' -1 + +function New-HuduRelation { + <# + .SYNOPSIS + Create a Relation + + .DESCRIPTION + Uses Hudu API to create relationships between objects + + .PARAMETER Description + Give a description to the relation so you know why two things are related + + .PARAMETER FromableType + The type of the FROM relation (Asset, Website, Procedure, AssetPassword, Company, Article) + + .PARAMETER FromableID + The ID of the FROM relation + + .PARAMETER ToableType + The type of the TO relation (Asset, Website, Procedure, AssetPassword, Company, Article) + + .PARAMETER ToableID + The ID of the TO relation + + .PARAMETER IsInverse + When a relation is created, it will also create another relation that is the inverse. When this is true, this relation is the inverse. + + .EXAMPLE + An example + + .NOTES + General notes + #> + [CmdletBinding(SupportsShouldProcess)] + Param ( + [String]$Description, + + [Parameter(Mandatory = $true)] + [ValidateSet('Asset', 'Website', 'Procedure', 'AssetPassword', 'Company', 'Article')] + [Alias('fromable_type')] + [String]$FromableType, + + [Alias('fromable_id')] + [int]$FromableID, + + [Alias('toable_type')] + [String]$ToableType, + + [Alias('toable_id')] + [int]$ToableID, + + [Alias('is_inverse')] + [string]$IsInverse + ) + + $Relation = [ordered]@{relation = [ordered]@{} } + + $Relation.relation.add('fromable_type', $FromableType) + $Relation.relation.add('fromable_id', $FromableID) + $Relation.relation.add('toable_type', $ToableType) + $Relation.relation.add('toable_id', $ToableID) + + if ($Description) { + $Relation.relation.add('description', $Description) + } + + if ($ISInverse) { + $Relation.relation.add('is_inverse', $ISInverse) + } + + $JSON = $Relation | ConvertTo-Json -Depth 100 + + if ($PSCmdlet.ShouldProcess($FromableType)) { + Invoke-HuduRequest -Method post -Resource '/api/v1/relations' -Body $JSON + } +} +#EndRegion './Public/New-HuduRelation.ps1' 76 +#Region './Public/New-HuduUpload.ps1' -1 + +function New-HuduUpload { + <# + .SYNOPSIS + Create a Upload + + .DESCRIPTION + Uses Hudu API to upload a file for use in an asset. RecordType can be of 'asset','website','procedure','assetpassword','comapny','article'. + + .PARAMETER FilePath + Path to the file + + .PARAMETER RecordId + Record id to associate with the Upload + + .PARAMETER RecordType + Record type to associate with the Upload + + .EXAMPLE + New-HuduUpload -FilePath 'c:\path\to\file.png' -RecordId 1 -RecordType 'asset' + + #> + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter(Mandatory)] + [string]$FilePath, + + [Parameter(Mandatory)] + [Alias('record_id','recordid')] + [int]$uploadable_id, + + [Parameter(Mandatory)] + [Alias('record_type','recordtype')] + [ValidateSet('Asset', 'Website', 'Procedure', 'AssetPassword', 'Company', 'Article')] + [string]$uploadable_type + ) + + $File = Get-Item $FilePath + + $form = @{ + file = $File + "upload[uploadable_id]" = $uploadable_id + "upload[uploadable_type]" = $uploadable_type + } + + if ($PSCmdlet.ShouldProcess($File.FullName)) { + Invoke-HuduRequest -Method POST -Resource '/api/v1/uploads' -Form $form + } +} +#EndRegion './Public/New-HuduUpload.ps1' 49 +#Region './Public/New-HuduWebsite.ps1' -1 + +function New-HuduWebsite { + <# + .SYNOPSIS + Create a Website + + .DESCRIPTION + Uses Hudu API to create a website + + .PARAMETER Name + Website name (e.g. https://domain.com) + + .PARAMETER Notes + Used to add additional notes to a website + + .PARAMETER Paused + When true, website monitoring is paused + + .PARAMETER CompanyId + Used to associate website with company + + .PARAMETER DisableDNS + When true, dns monitoring is paused. + + .PARAMETER DisableSSL + When true, ssl cert monitoring is paused. + + .PARAMETER DisableWhois + When true, whois monitoring is paused. + + .PARAMETER Slug + Url identifier + + .EXAMPLE + New-HuduWebsite -CompanyId 1 -Name https://domain.com + + #> + [CmdletBinding(SupportsShouldProcess)] + Param ( + [Parameter(Mandatory = $true)] + [String]$Name, + + [String]$Notes = '', + + [String]$Paused = '', + + [Alias('company_id')] + [Parameter(Mandatory = $true)] + [Int]$CompanyId, + + [Alias('disable_dns')] + [String]$DisableDNS = '', + + [Alias('disable_ssl')] + [String]$DisableSSL = '', + + [Alias('disable_whois')] + [String]$DisableWhois = '', + + [string]$Slug + ) + + $Website = [ordered]@{website = [ordered]@{} } + + $Website.website.add('name', $Name) + + if ($Notes) { + $Website.website.add('notes', $Notes) + } + + if ($Paused) { + $Website.website.add('paused', $Paused) + } + + $Website.website.add('company_id', $CompanyId) + + if ($DisableDNS) { + $Website.website.add('disable_dns', $DisableDNS) + } + + if ($DisableSSL) { + $Website.website.add('disable_ssl', $DisableSSL) + } + + if ($DisableWhois) { + $Website.website.add('disable_whois', $DisableWhois) + } + + if ($Slug) { + $Website.website.add('slug', $Slug) + } + + $JSON = $Website | ConvertTo-Json + + if ($PSCmdlet.ShouldProcess($Name)) { + Invoke-HuduRequest -Method post -Resource '/api/v1/websites' -Body $JSON + } +} +#EndRegion './Public/New-HuduWebsite.ps1' 98 +#Region './Public/Remove-HuduAPIKey.ps1' -1 + +function Remove-HuduAPIKey { + <# + .SYNOPSIS + Remove API key + + .DESCRIPTION + Unsets the variable for the Hudu API Key + + .EXAMPLE + Remove-HuduAPIKey + + #> + [CmdletBinding(SupportsShouldProcess)] + Param() + + if ($PSCmdlet.ShouldProcess('API Key')) { + Remove-Variable -Name 'Int_HuduAPIKey' -Scope script -Force + } +} +#EndRegion './Public/Remove-HuduAPIKey.ps1' 20 +#Region './Public/Remove-HuduArticle.ps1' -1 + +function Remove-HuduArticle { + <# + .SYNOPSIS + Delete a Knowledge Base Article + + .DESCRIPTION + Uses Hudu API to remove a KB article + + .PARAMETER Id + Id of the requested article + + .EXAMPLE + Remove-HuduArticle -Id 1 + + .NOTES + General notes + #> + [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] + Param ( + [Parameter(ValueFromPipelineByPropertyName = $true, Mandatory = $true)] + [Int]$Id + ) + process { + if ($PSCmdlet.ShouldProcess($Id)) { + Invoke-HuduRequest -Method delete -Resource "/api/v1/articles/$Id" + } + } +} +#EndRegion './Public/Remove-HuduArticle.ps1' 29 +#Region './Public/Remove-HuduAsset.ps1' -1 + +function Remove-HuduAsset { + <# + .SYNOPSIS + Delete an Asset + + .DESCRIPTION + Uses Hudu API to remove an Asset from a company + + .PARAMETER Id + Id of the requested Asset + + .PARAMETER CompanyId + Id of the requested parent Company + + .EXAMPLE + Remove-HuduAsset -CompanyId 1 -Id 1 + + #> + [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] + Param ( + [Parameter(ValueFromPipelineByPropertyName = $true, Mandatory = $true)] + [Int]$Id, + [Alias('company_id')] + [Parameter(ValueFromPipelineByPropertyName = $true, Mandatory = $true)] + [Int]$CompanyId + ) + + process { + if ($PSCmdlet.ShouldProcess($Id)) { + Invoke-HuduRequest -Method delete -Resource "/api/v1/companies/$CompanyId/assets/$Id" + } + } +} +#EndRegion './Public/Remove-HuduAsset.ps1' 34 +#Region './Public/Remove-HuduBaseURL.ps1' -1 + +function Remove-HuduBaseURL { + <# + .SYNOPSIS + Remove base URL + + .DESCRIPTION + Unsets the Hudu Base URL variable + + .EXAMPLE + Remove-HuduBaseURL + + #> + [CmdletBinding(SupportsShouldProcess)] + Param() + if ($PSCmdlet.ShouldProcess('Base URL')) { + Remove-Variable -Name 'Int_HuduBaseURL' -Scope script -Force + } +} +#EndRegion './Public/Remove-HuduBaseURL.ps1' 19 +#Region './Public/Remove-HuduCompany.ps1' -1 + +function Remove-HuduCompany { + <# + .SYNOPSIS + Delete a Website + + .DESCRIPTION + Uses Hudu API to delete a company + + .PARAMETER Id + Id of the Company to delete + + .EXAMPLE + Remove-HuduCompany -Id 1 + + #> + [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] + Param ( + [Parameter(ValueFromPipelineByPropertyName = $true, Mandatory = $true)] + [Int]$Id + ) + + process { + if ($PSCmdlet.ShouldProcess($Id)) { + Invoke-HuduRequest -Method delete -Resource "/api/v1/companies/$Id" + } + } +} +#EndRegion './Public/Remove-HuduCompany.ps1' 28 +#Region './Public/Remove-HuduCustomHeaders.ps1' -1 + +function Remove-HuduCustomHeaders { + <# + .SYNOPSIS + Remove Custom Headers that are injected into each request + + .DESCRIPTION + Unsets the Hudu Custom Header variable + + .EXAMPLE + Remove-HuduCustomHeaders + + #> + [CmdletBinding(SupportsShouldProcess)] + Param() + if ($PSCmdlet.ShouldProcess('Custom Headers')) { + Remove-Variable -Name 'Int_HuduCustomHeaders' -Scope script -Force + } +} +#EndRegion './Public/Remove-HuduCustomHeaders.ps1' 19 +#Region './Public/Remove-HuduMagicDash.ps1' -1 + +function Remove-HuduMagicDash { + <# + .SYNOPSIS + Delete a Magic Dash Item + + .DESCRIPTION + Uses Hudu API to remove Magic Dash by Id or Title and Company Name + + .PARAMETER Title + Title of the Magic Dash + + .PARAMETER CompanyName + Company Name + + .PARAMETER Id + Id of the Magic Dash + + .EXAMPLE + Remove-HuduMagicDash -Id 1 + + .EXAMPLE + Remove-HuduMagicDash -Title 'Microsoft 365' -CompanyName 'AcmeCorp' + + #> + [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High', DefaultParameterSetName = 'Id')] + Param ( + [Parameter(ValueFromPipelineByPropertyName = $true, Mandatory = $true, ParameterSetName = 'TitleCompany')] + [String]$Title, + + [Parameter(ValueFromPipelineByPropertyName = $true, Mandatory = $true, ParameterSetName = 'TitleCompany')] + [Alias('company_name')] + [String]$CompanyName, + + [Parameter(ValueFromPipelineByPropertyName = $true, Mandatory = $true, ParameterSetName = 'Id')] + [int]$Id + ) + + process { + if ($id) { + if ($PSCmdlet.ShouldProcess($Id)) { + $null = Invoke-HuduRequest -Method delete -Resource "/api/v1/magic_dash/$Id" + } + } else { + $MagicDash = @{} + + $MagicDash.add('title', $Title) + $MagicDash.add('company_name', $CompanyName) + + $JSON = $MagicDash | ConvertTo-Json + + if ($PSCmdlet.ShouldProcess("$Company - $Title")) { + $null = Invoke-HuduRequest -Method delete -Resource '/api/v1/magic_dash' -Body $JSON + } + } + } +} +#EndRegion './Public/Remove-HuduMagicDash.ps1' 57 +#Region './Public/Remove-HuduPassword.ps1' -1 + +function Remove-HuduPassword { + <# + .SYNOPSIS + Delete a Password + + .DESCRIPTION + Uses Hudu API to remove asset password + + .PARAMETER Id + Id of the password + + .EXAMPLE + Remove-HuduPassword -Id 1 + + #> + [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] + Param ( + [Parameter(ValueFromPipelineByPropertyName = $true, Mandatory = $true)] + [Int]$Id + ) + process { + if ($PSCmdlet.ShouldProcess($Id)) { + Invoke-HuduRequest -Method delete -Resource "/api/v1/asset_passwords/$Id" + } + } +} +#EndRegion './Public/Remove-HuduPassword.ps1' 27 +#Region './Public/Remove-HuduRelation.ps1' -1 + +function Remove-HuduRelation { + <# + .SYNOPSIS + Delete a Relation + + .DESCRIPTION + Uses Hudu API to delete object relationships + + .PARAMETER Id + Id of the requested Relation + + .EXAMPLE + Remove-HuduRelation -Id 1 + + #> + [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] + Param ( + [Parameter(ValueFromPipelineByPropertyName = $true, Mandatory = $true)] + [Int]$Id + ) + + process { + if ($PSCmdlet.ShouldProcess($Id)) { + Invoke-HuduRequest -Method delete -Resource "/api/v1/relations/$Id" + } + } +} +#EndRegion './Public/Remove-HuduRelation.ps1' 28 +#Region './Public/Remove-HuduUpload.ps1' -1 + +function Remove-HuduUpload { + <# + .SYNOPSIS + Delete an Upload by ID + + .DESCRIPTION + Calls Hudu API to delete uploads by specifying the ID value + + .EXAMPLE + Remove-HuduUpload + + #> + [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] + Param( + [Parameter(ValueFromPipelineByPropertyName = $true, Mandatory = $true)] + [Int]$Id + ) + + process { + if ($PSCmdlet.ShouldProcess($Id)) { + Invoke-HuduRequest -Method delete -Resource "/api/v1/uploads/$Id" + } + } + +} +#EndRegion './Public/Remove-HuduUpload.ps1' 26 +#Region './Public/Remove-HuduWebsite.ps1' -1 + +function Remove-HuduWebsite { + <# + .SYNOPSIS + Delete a Website + + .DESCRIPTION + Uses Hudu API to delete a website + + .PARAMETER Id + Id of the requested Website + + .EXAMPLE + Remove-HuduWebsite -Id 1 + + #> + [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] + Param ( + [Parameter(ValueFromPipelineByPropertyName = $true, Mandatory = $true)] + [Int]$Id + ) + + process { + if ($PSCmdlet.ShouldProcess($Id)) { + Invoke-HuduRequest -Method delete -Resource "/api/v1/websites/$Id" + } + } +} +#EndRegion './Public/Remove-HuduWebsite.ps1' 28 +#Region './Public/Set-HuduArticle.ps1' -1 + +function Set-HuduArticle { + <# + .SYNOPSIS + Update a Knowledge Base Article + + .DESCRIPTION + Uses Hudu API to update KB Article + + .PARAMETER Name + Name of the Article + + .PARAMETER Content + Article Content + + .PARAMETER EnableSharing + Set article to public and generate a URL + + .PARAMETER FolderId + Used to associate article with folder + + .PARAMETER CompanyId + Used to associate article with company + + .PARAMETER ArticleId + Id of the requested article + + .PARAMETER Slug + Url identifier + + .EXAMPLE + Set-HuduArticle -ArticleId 1 -Name 'Article Name' -Content '

    New article contents

    ' + + #> + [CmdletBinding(SupportsShouldProcess)] + Param ( + [String]$Name, + + [String]$Content, + [switch]$EnableSharing, + + [Alias('folder_id')] + [Int]$FolderId = '', + + [Alias('company_id')] + [Int]$CompanyId = '', + + [Alias('article_id', 'id')] + [Parameter(Mandatory = $true)] + [Int]$ArticleId, + + [string]$Slug + ) + + $Object = Get-HuduArticles -Id $ArticleId + $Article = [ordered]@{article = $Object.article } + + if ($Name) { + $Article.article.name = $Name + } + + if ($Content) { + $Article.article.content = $Content + } + + if ($FolderId) { + $Article.article.folder_id = $FolderId + } + + if ($CompanyId) { + $Article.article.company_id = $CompanyId + } + + if ($EnableSharing.IsPresent) { + $Article.article.enable_sharing = $true + } + + if ($Slug) { + $Article.article.slug = $Slug + } + + $JSON = $Article | ConvertTo-Json -Depth 10 + + if ($PSCmdlet.ShouldProcess($Name)) { + Invoke-HuduRequest -Method put -Resource "/api/v1/articles/$ArticleId" -Body $JSON + } +} +#EndRegion './Public/Set-HuduArticle.ps1' 87 +#Region './Public/Set-HuduArticleArchive.ps1' -1 + +function Set-HuduArticleArchive { + <# + .SYNOPSIS + Archive/Unarchive a Knowledge Base Article + + .DESCRIPTION + Uses Hudu API to archive or unarchive an article + + .PARAMETER Id + Id of the requested article + + .PARAMETER Archive + Boolean for archive status + + .EXAMPLE + Set-HuduArticleArchive -Id 1 -Archive $true + + #> + [CmdletBinding(SupportsShouldProcess)] + Param ( + [Parameter(Mandatory = $true)] + [Int]$Id, + [Parameter(Mandatory = $true)] + [Bool]$Archive + ) + + if ($Archive) { + $Action = 'archive' + } else { + $Action = 'unarchive' + } + + if ($PSCmdlet.ShouldProcess($Id)) { + Invoke-HuduRequest -Method put -Resource "/api/v1/articles/$Id/$Action" + } +} +#EndRegion './Public/Set-HuduArticleArchive.ps1' 37 +#Region './Public/Set-HuduAsset.ps1' -1 + +function Set-HuduAsset { + <# + .SYNOPSIS + Update an Asset + + .DESCRIPTION + Uses Hudu API to update an Asset + + .PARAMETER Name + Name of the Asset + + .PARAMETER CompanyId + Company id of the Asset + + .PARAMETER AssetLayoutId + Asset layout id + + .PARAMETER Fields + List of fields + + .PARAMETER AssetId + Id of the requested Asset + + .PARAMETER PrimarySerial + Primary serial number + + .PARAMETER PrimaryMail + Primary mail + + .PARAMETER PrimaryModel + Primary model + + .PARAMETER PrimaryManufacturer + Primary manufacturer + + .PARAMETER Slug + Url identifier + + .EXAMPLE + Set-HuduAsset -AssetId 1 -CompanyId 1 -Fields @(@{'field_name'='Field Value'}) + + .NOTES + General notes + #> + [CmdletBinding(SupportsShouldProcess)] + Param ( + [String]$Name, + + [Alias('company_id')] + [Int]$CompanyId, + + [Alias('asset_layout_id')] + [Int]$AssetLayoutId, + + [Array]$Fields, + + [Alias('asset_id','assetid')] + [Parameter(Mandatory = $true)] + [ValidateRange(1, [int]::MaxValue)] + [Int]$Id, + + [Alias('primary_serial')] + [string]$PrimarySerial, + + [Alias('primary_mail')] + [string]$PrimaryMail, + + [Alias('primary_model')] + [string]$PrimaryModel, + + [Alias('primary_manufacturer')] + [string]$PrimaryManufacturer, + + [string]$Slug + ) + + $Object = Get-HuduAssets -id $Id | Select-Object name,asset_layout_id,company_id,slug,primary_serial,primary_model,primary_mail,id,primary_manufacturer,@{n='custom_fields';e={$_.fields | ForEach-Object {[pscustomobject]@{$_.label.replace(' ','_').tolower()= $_.value}}}} + if ($Object) { + $Asset = [ordered]@{asset = $Object } + $CompanyId = $Object.company_id + + if ($Name) { + $Asset.asset.name = $Name + } + + if ($AssetLayoutId) { + $Asset.asset.asset_layout_id = $AssetLayoutId + } + + if ($PrimarySerial) { + $Asset.asset.primary_serial = $PrimarySerial + } + + if ($PrimaryMail) { + $Asset.asset.primary_mail = $PrimaryMail + } + + if ($PrimaryModel) { + $Asset.asset.primary_model = $PrimaryModel + } + + if ($PrimaryManufacturer) { + $Asset.asset.primary_manufacturer = $PrimaryManufacturer + } + + if ($Fields) { + $Asset.asset.custom_fields = $Fields + } + + if ($Slug) { + $Asset.asset.slug = $Slug + } + + $JSON = $Asset | ConvertTo-Json -Depth 10 + + if ($PSCmdlet.ShouldProcess("ID: $($Asset.id) Name: $($Asset.Name)", "Set Hudu Asset")) { + Invoke-HuduRequest -Method put -Resource "/api/v1/companies/$CompanyId/assets/$Id" -Body $JSON + } + } else { + throw "A valid asset could not be found to update, please double check the ID and try again" + } +} +#EndRegion './Public/Set-HuduAsset.ps1' 123 +#Region './Public/Set-HuduAssetArchive.ps1' -1 + +function Set-HuduAssetArchive { + <# + .SYNOPSIS + Archive/Unarchive an Asset + + .DESCRIPTION + Uses Hudu API to archive or unarchive an asset + + .PARAMETER Id + Id of the requested Asset + + .PARAMETER CompanyId + Id of the requested parent company + + .PARAMETER Archive + Boolean for archive status + + .EXAMPLE + Set-HuduAssetArchive -Id 1 -CompanyId 1 -Archive $true + + #> + [CmdletBinding(SupportsShouldProcess)] + Param ( + [Parameter(Mandatory = $true)] + [Int]$Id, + [Alias('company_id')] + [Parameter(Mandatory = $true)] + [Int]$CompanyId, + [Parameter(Mandatory = $true)] + [Bool]$Archive + ) + + if ($Archive) { + $Action = 'archive' + } else { + $Action = 'unarchive' + } + + if ($PSCmdlet.ShouldProcess($Id)) { + Invoke-HuduRequest -Method put -Resource "/api/v1/companies/$CompanyId/assets/$Id/$Action" + } +} +#EndRegion './Public/Set-HuduAssetArchive.ps1' 43 +#Region './Public/Set-HuduAssetLayout.ps1' -1 + +function Set-HuduAssetLayout { + <# + .SYNOPSIS + Update an Asset Layout + + .DESCRIPTION + Uses Hudu API to update an Asset Layout + + .PARAMETER Id + Id of the requested Asset Layout + + .PARAMETER Name + Name of the Asset Layout + + .PARAMETER Icon + Icon class name, example: "fas fa-home" + + .PARAMETER Color + Hex code for background color, example: #000000 + + .PARAMETER IconColor + Hex code for background color, example: #000000 + + .PARAMETER IncludePasswords + Boolean to include passwords + + .PARAMETER IncludePhotos + Boolean to include photos + + .PARAMETER IncludeComments + Boolean to include comments + + .PARAMETER IncludeFiles + Boolean to include files + + .PARAMETER PasswordTypes + List of password types, separated with new line characters + + .PARAMETER Slug + Url identifier + + .PARAMETER Fields + Array of hashtable or custom objects representing layout fields. Most field types only require a label and type. + Valid field types are: Text, RichText, Heading, CheckBox, Website (aka Link), Password (aka ConfidentialText), Number, Date, DropDown, Embed, Email (aka CopyableText), Phone, AssetLink + Field types are Case Sensitive as of Hudu V2.27 due to a known issue with asset type validation. + + .EXAMPLE + Set-HuduAssetLayout -Id 12 -Name 'Test asset layout' -Icon 'fas fa-home' -IncludePassword $true + + .EXAMPLE + Set-HuduAssetLayout -Id 12 -Fields @( + @{label = 'Test field'; 'field_type' = 'Text'} + ) + #> + [CmdletBinding(SupportsShouldProcess)] + # This will silence the warning for variables with Password in their name. + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', '')] + Param ( + [Parameter(Mandatory = $true)] + [Int]$Id, + + [String]$Name, + + [String]$Icon, + + [String]$Color, + + [Alias('icon_color')] + [String]$IconColor, + + [Alias('include_passwords')] + [bool]$IncludePasswords, + + [Alias('include_photos')] + [bool]$IncludePhotos, + + [Alias('include_comments')] + [bool]$IncludeComments, + + [Alias('include_files')] + [bool]$IncludeFiles, + + [Alias('password_types')] + [String]$PasswordTypes = '', + + [bool]$Active, + + [string]$Slug, + + [array]$Fields + ) + + foreach ($Field in $Fields) { + $Field.show_in_list = [System.Convert]::ToBoolean($Field.show_in_list) + $Field.required = [System.Convert]::ToBoolean($Field.required) + $Field.expiration = [System.Convert]::ToBoolean($Field.expiration) + # A bug in versions of Hudu 2.27 and earlier can cause asset layouts to become corrupted if the field type value is not properly cased. + switch ($field.'field_type') { + 'text' { $field.'field_type' = 'Text' } + 'richtext' { $field.'field_type' = 'RichText' } + 'heading' { $field.'field_type' = 'Heading' } + 'checkbox' { $field.'field_type' = 'CheckBox' } + 'number' { $field.'field_type' = 'Number' } + 'date' { $field.'field_type' = 'Date' } + 'dropdown' { $field.'field_type' = 'Dropdown' } + 'embed' { $field.'field_type' = 'Embed' } + 'phone' { $field.'field_type' = 'Phone' } + ('email' -or 'copyabletext') { $field.'field_type' = 'Email' } + ('assettag' -or 'assetlink') { $field.'field_type' = 'AssetTag' } + ('website' -or 'link') { $field.'field_type' = 'Website' } + ('password' -or 'confidentialtext') { $field.'field_type' = 'Password' } + Default { Write-Error "Invalid field type: $($field.'field_type') found in field $($field.name)"; break } + } + } + $Object = Get-HuduAssetLayouts -id $Id + + $AssetLayout = [ordered]@{asset_layout = $Object } + #$AssetLayout.asset_layout = $Object + + if ($Name) { + $AssetLayout.asset_layout.name = $Name + } + + if ($Icon) { + $AssetLayout.asset_layout.icon = $Icon + } + + if ($Color) { + $AssetLayout.asset_layout.color = $Color + } + + if ($IconColor) { + $AssetLayout.asset_layout.icon_color = $IconColor + } + + if ($Fields) { + $AssetLayout.asset_layout.fields = $Fields + } + + if ($IncludePasswords) { + $AssetLayout.asset_layout.include_passwords = [System.Convert]::ToBoolean($IncludePasswords) + } + + if ($IncludePhotos) { + $AssetLayout.asset_layout.include_photos = [System.Convert]::ToBoolean($IncludePhotos) + } + + if ($IncludeComments) { + $AssetLayout.asset_layout.include_comments = [System.Convert]::ToBoolean($IncludeComments) + } + + if ($IncludeFiles) { + $AssetLayout.asset_layout.include_files = [System.Convert]::ToBoolean($IncludeFiles) + } + + if ($PasswordTypes) { + $AssetLayout.asset_layout.password_types = $PasswordTypes + } + + if ($SidebarFolderID) { + $AssetLayout.asset_layout.sidebar_folder_id = $SidebarFolderID + } + + if ($Slug) { + $AssetLayout.asset_layout.slug = $Slug + } + + if ($Active) { + $AssetLayout.asset_layout.active = [System.Convert]::ToBoolean($Active) + } + + $JSON = $AssetLayout | ConvertTo-Json -Depth 10 + + if ($PSCmdlet.ShouldProcess($Id)) { + Invoke-HuduRequest -Method put -Resource "/api/v1/asset_layouts/$Id" -Body $JSON + } +} +#EndRegion './Public/Set-HuduAssetLayout.ps1' 178 +#Region './Public/Set-HuduCompany.ps1' -1 + +function Set-HuduCompany { + <# + .SYNOPSIS + Update a company + + .DESCRIPTION + Uses Hudu API to update a Company + + .PARAMETER Id + Id of the requested company + + .PARAMETER Name + Name of the company + + .PARAMETER Nickname + Nickname of the company + + .PARAMETER CompanyType + Company type + + .PARAMETER AddressLine1 + Address line 1 + + .PARAMETER AddressLine2 + Address line 2 + + .PARAMETER City + City + + .PARAMETER State + State + + .PARAMETER Zip + Zip + + .PARAMETER CountryName + Country name + + .PARAMETER PhoneNumber + Phone number + + .PARAMETER FaxNumber + Fax number + + .PARAMETER Website + Webste + + .PARAMETER IdNumber + Id number + + .PARAMETER ParentCompanyId + Parent company id + + .PARAMETER Notes + Company notes + + .PARAMETER Slug + Url identifier + + .EXAMPLE + Set-HuduCompany -Id 1 -Name 'New company name' + + #> + [CmdletBinding(SupportsShouldProcess)] + Param ( + [Parameter(Mandatory = $true)] + [Int]$Id, + + [String]$Name, + + [String]$Nickname = '', + + [Alias('company_type')] + [String]$CompanyType = '', + + [Alias('address_line_1')] + [String]$AddressLine1 = '', + + [Alias('address_line_2')] + [String]$AddressLine2 = '', + + [String]$City = '', + + [String]$State = '', + + [Alias('PostalCode', 'PostCode')] + [String]$Zip = '', + + [Alias('country_name')] + [String]$CountryName = '', + + [Alias('phone_number')] + [String]$PhoneNumber = '', + + [Alias('fax_number')] + [String]$FaxNumber = '', + + [String]$Website = '', + + [Alias('id_number')] + [String]$IdNumber = '', + + [Alias('parent_company_id')] + [Int]$ParentCompanyId, + + [String]$Notes = '', + + [string]$Slug + ) + + $Object = Get-HuduCompanies -Id $Id + + $Company = [ordered]@{company = $Object } + + if ($Name) { + $Company.company.name = $Name + } + + if ($Nickname) { + $Company.company.nickname = $Nickname + } + + if ($CompanyType) { + $Company.company.company_type = $CompanyType + } + + if ($AddressLine1) { + $Company.company.address_line_1 = $AddressLine1 + } + + if ($AddressLine2) { + $Company.company.address_line_2 = $AddressLine2 + } + + if ($City) { + $Company.company.city = $City + } + + if ($State) { + $Company.company.state = $State + } + + if ($Zip) { + $Company.company.zip = $Zip + } + + if ($CountryName) { + $Company.company.country_name = $CountryName + } + + if ($PhoneNumber) { + $Company.company.phone_number = $PhoneNumber + } + + if ($FaxNumber) { + $Company.company.fax_number = $FaxNumber + } + + if ($Website) { + $Company.company.website = $Website + } + + if ($IdNumber) { + $Company.company.id_number = $IdNumber + } + + if ($ParentCompanyId) { + $Company.company.parent_company_id = $ParentCompanyId + } + + if ($Notes) { + $Company.company.notes = $Notes + } + + if ($Slug) { + $Company.company.slug = $Slug + } + + $JSON = $Company | ConvertTo-Json -Depth 10 + + if ($PSCmdlet.ShouldProcess($Id)) { + Invoke-HuduRequest -Method put -Resource "/api/v1/companies/$Id" -Body $JSON + } +} +#EndRegion './Public/Set-HuduCompany.ps1' 185 +#Region './Public/Set-HuduCompanyArchive.ps1' -1 + +function Set-HuduCompanyArchive { + <# + .SYNOPSIS + Archive/Unarchive a company + + .DESCRIPTION + Uses Hudu API to set archive status on a company + + .PARAMETER Id + Id of the requested company + + .PARAMETER Archive + Boolean for archive status + + .EXAMPLE + Set-HuduCompanyArchive -Id 1 -Archive $true + + #> + [CmdletBinding(SupportsShouldProcess)] + Param ( + [Parameter(Mandatory = $true)] + [Int]$Id, + [Parameter(Mandatory = $true)] + [Bool]$Archive + ) + + if ($Archive -eq $true) { + $Action = 'archive' + } else { + $Action = 'unarchive' + } + if ($PSCmdlet.ShouldProcess($Id)) { + Invoke-HuduRequest -Method put -Resource "/api/v1/companies/$Id/$Action" + } +} +#EndRegion './Public/Set-HuduCompanyArchive.ps1' 36 +#Region './Public/Set-HuduFolder.ps1' -1 + +function Set-HuduFolder { + <# + .SYNOPSIS + Update a Folder + + .DESCRIPTION + Uses Hudu API to update a folder + + .PARAMETER Id + Id of the requested folder + + .PARAMETER Name + Name of the folder + + .PARAMETER Icon + Folder icon + + .PARAMETER Description + Folder description + + .PARAMETER ParentFolderId + Folder parent id + + .PARAMETER CompanyId + Folder company id + + .EXAMPLE + Set-HuduFolder -Id 1 -Name 'New folder name' + + #> + [CmdletBinding(SupportsShouldProcess)] + Param ( + [Parameter(Mandatory = $true)] + [Int]$Id, + + [Parameter(Mandatory = $true)] + [String]$Name, + + [String]$Icon = '', + + [String]$Description = '', + + [Alias('parent_folder_id')] + [Int]$ParentFolderId = '', + + [Alias('company_id')] + [Int]$CompanyId = '' + ) + + $Folder = [ordered]@{folder = [ordered]@{} } + + $Folder.folder.add('name', $Name) + + if ($icon) { + $Folder.folder.add('icon', $Icon) + } + + if ($Description) { + $Folder.folder.add('description', $Description) + } + + if ($ParentFolderId) { + $Folder.folder.add('parent_folder_id', $ParentFolderId) + } + + if ($CompanyId) { + $Folder.folder.add('company_id', $CompanyId) + } + + $JSON = $Folder | ConvertTo-Json + + if ($PSCmdlet.ShouldProcess($Id)) { + Invoke-HuduRequest -Method put -Resource "/api/v1/folders/$Id" -Body $JSON + } +} +#EndRegion './Public/Set-HuduFolder.ps1' 76 +#Region './Public/Set-HuduIntegrationMatcher.ps1' -1 + +function Set-HuduIntegrationMatcher { + <# + .SYNOPSIS + Update a Matcher + + .DESCRIPTION + Uses Hudu API to set integration matchers + + .PARAMETER Id + Id of the requested matcher + + .PARAMETER AcceptSuggestedMatch + Set the Sync Id/Identifier to the suggested one + + .PARAMETER CompanyId + Requested company id to match + + .PARAMETER PotentialCompanyId + Potential company id to match + + .PARAMETER SyncId + Sync id to match + + .PARAMETER Identifier + Identifier to match + + .EXAMPLE + Set-HuduIntegrationMatcher -Id 1 -AcceptSuggestedMatch + + #> + [CmdletBinding(SupportsShouldProcess)] + Param ( + [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] + [String]$Id, + + [Parameter(ParameterSetName = 'AcceptSuggestedMatch')] + [switch]$AcceptSuggestedMatch, + + [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'SetCompanyId')] + [Alias('company_id')] + [String]$CompanyId, + + [Parameter(ValueFromPipelineByPropertyName = $true)] + [Alias('potential_company_id')] + [String]$PotentialCompanyId, + + [Parameter(ValueFromPipelineByPropertyName = $true)] + [Alias('sync_id')] + [String]$SyncId, + + [Parameter(ValueFromPipelineByPropertyName = $true)] + [String]$Identifier + ) + + process { + $Matcher = [ordered]@{matcher = [ordered]@{} } + + if ($AcceptSuggestedMatch) { + $Matcher.matcher.add('company_id', $PotentialCompanyId) | Out-Null + } else { + $Matcher.matcher.add('company_id', $CompanyId) | Out-Null + } + + if ($PotentialCompanyId) { + $Matcher.matcher.add('potential_company_id', $PotentialCompanyId) | Out-Null + } + if ($SyncId) { + $Matcher.matcher.add('sync_id', $SyncId) | Out-Null + } + if ($Identifier) { + $Matcher.matcher.add('identifier', $identifier) | Out-Null + } + + $JSON = $Matcher | ConvertTo-Json -Depth 10 + + if ($PSCmdlet.ShouldProcess($Id)) { + Invoke-HuduRequest -Method put -Resource "/api/v1/matchers/$Id" -Body $JSON + } + } +} +#EndRegion './Public/Set-HuduIntegrationMatcher.ps1' 81 +#Region './Public/Set-HuduMagicDash.ps1' -1 + +function Set-HuduMagicDash { + <# + .SYNOPSIS + Create or Update a Magic Dash Item + + .DESCRIPTION + Magic Dash takes just simple key-pairs. Whether you want to add a new Magic Dash Item, or update one, you can use the same endpoint, so it is really easy! It uses the title, and company_name to match. + + .PARAMETER Title + This is the title. If there is an existing Magic Dash Item with matching title and company_name, then it will match into that item. + + .PARAMETER CompanyName + This is the attribute we use to match to an existing company. If there is an existing Magic Dash Item with matching title and company_name, then it will match into that item. + + .PARAMETER Message + This will be the first content that will be displayed on the Magic Dash Item. + + .PARAMETER Icon + Either fill this in, or image_url. Use a (FontAwesome icon for the header of a Magic Dash Item. Must be in the format of fas fa-circle + + .PARAMETER ImageURL + Either fill this in, or icon. Used in the header of a Magic Dash Item. + + .PARAMETER ContentLink + Either fill this in, or content, or leave both blank. Used to have a link to an external website. + + .PARAMETER Content + Either fill this in, or content_link, or leave both blank. Fill in with HTML (tables, images, videos, etc.) to display more content in your Magic Dash Item. + + .PARAMETER Shade + Use a different color for your Magic Dash Item for different contextual states. Options are to leave it blank, success, or danger + + .EXAMPLE + Set-HuduMagicDash -Title 'Test Dash' -CompanyName 'Test Company' -Message 'This will be displayed first' + + #> + [CmdletBinding(SupportsShouldProcess)] + Param ( + [Parameter(Mandatory = $true)] + [String]$Title, + + [Alias('company_name')] + [Parameter(Mandatory = $true)] + [String]$CompanyName, + + [Parameter(Mandatory = $true)] + [String]$Message, + + [String]$Icon = '', + + [Alias('image_url')] + [String]$ImageURL = '', + + [Alias('content_link')] + [String]$ContentLink = '', + + [String]$Content = '', + + [String]$Shade = '' + ) + + if ($Icon -and $ImageURL) { + Write-Error ('You can only use one of icon or image URL') + exit 1 + } + + if ($content_link -and $content) { + Write-Error ('You can only use one of content or content_link') + exit 1 + } + + $MagicDash = [ordered]@{} + + if ($Title) { + $MagicDash.add('title', $Title) + } + + if ($CompanyName) { + $MagicDash.add('company_name', $CompanyName) + } + + if ($Message) { + $MagicDash.add('message', $Message) + } + + if ($Icon) { + $MagicDash.add('icon', $Icon) + } + + if ($ImageURL) { + $MagicDash.add('image_url', $ImageURL) + } + + if ($ContentLink) { + $MagicDash.add('content_link', $ContentLink) + } + + if ($Content) { + $MagicDash.add('content', $Content) + } + + if ($Shade) { + $MagicDash.add('shade', $Shade) + } + + $JSON = $MagicDash | ConvertTo-Json + + if ($PSCmdlet.ShouldProcess("$Companyname - $Title")) { + Invoke-HuduRequest -Method post -Resource '/api/v1/magic_dash' -Body $JSON + } +} +#EndRegion './Public/Set-HuduMagicDash.ps1' 112 +#Region './Public/Set-HuduPassword.ps1' -1 + +function Set-HuduPassword { + <# + .SYNOPSIS + Update a Password + + .DESCRIPTION + Uses Hudu API to update a password + + .PARAMETER Id + Id of the requested Password + + .PARAMETER Name + Password name + + .PARAMETER CompanyId + Id of requested company + + .PARAMETER PasswordableType + Type of asset to associate with the password + + .PARAMETER PasswordableId + Id of the asset to associate with the password + + .PARAMETER InPortal + Display password in portal + + .PARAMETER Password + Password + + .PARAMETER OTPSecret + OTP secret + + .PARAMETER URL + Url for the password + + .PARAMETER Username + Username + + .PARAMETER Description + Password description + + .PARAMETER PasswordType + Password type + + .PARAMETER PasswordFolderId + Id of requested password folder + + .PARAMETER Slug + Url identifier + + .EXAMPLE + Set-HuduPassword -Id 1 -CompanyId 1 -Password 'this_is_my_new_password' + + #> + [CmdletBinding(SupportsShouldProcess)] + # This will silence the warning for variables with Password in their name. + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', '')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingUsernameAndPasswordParams', '')] + Param ( + [Parameter(Mandatory = $true)] + [Int]$Id, + + [String]$Name, + + [Alias('company_id')] + [Int]$CompanyId, + + [Alias('passwordable_type')] + [String]$PasswordableType = '', + + [Alias('passwordable_id')] + [int]$PasswordableId = '', + + [Alias('in_portal')] + [Bool]$InPortal = $false, + [String]$Password = '', + + [Alias('otp_secret')] + [string]$OTPSecret = '', + + [String]$URL = '', + + [String]$Username = '', + + [String]$Description = '', + + [Alias('password_type')] + [String]$PasswordType = '', + + [Alias('password_folder_id')] + [int]$PasswordFolderId, + + [string]$Slug + ) + + $Object = Get-HuduPasswords -Id $Id + $AssetPassword = [ordered]@{asset_password = $Object } + + if ($Name) { + $AssetPassword.asset_password | Add-Member -MemberType NoteProperty -Name name -Force -Value $Name + + } + + if ($CompanyId) { + $AssetPassword.asset_password | Add-Member -MemberType NoteProperty -Name company_id -Force -Value $CompanyId + } + + if ($Password) { + $AssetPassword.asset_password | Add-Member -MemberType NoteProperty -Name password -Force -Value $Password + } + + if ($InPortal) { + $AssetPassword.asset_password | Add-Member -MemberType NoteProperty -Name in_portal -Force -Value $InPortal + } + + + if ($PasswordableType) { + $AssetPassword.asset_password | Add-Member -MemberType NoteProperty -Name passwordable_type -Force -Value $PasswordableType + } + if ($PasswordableId) { + $AssetPassword.asset_password | Add-Member -MemberType NoteProperty -Name passwordable_id -Force -Value $PasswordableId + } + + if ($OTPSecret) { + $AssetPassword.asset_password | Add-Member -MemberType NoteProperty -Name otp_secret -Force -Value $OTPSecret + } + + if ($URL) { + $AssetPassword.asset_password | Add-Member -MemberType NoteProperty -Name url -Force -Value $URL + } + + if ($Username) { + $AssetPassword.asset_password | Add-Member -MemberType NoteProperty -Name username -Force -Value $Username + } + + if ($Description) { + $AssetPassword.asset_password | Add-Member -MemberType NoteProperty -Name description -Force -Value $Description + } + + if ($PasswordType) { + $AssetPassword.asset_password | Add-Member -MemberType NoteProperty -Name password_type -Force -Value $PasswordType + } + + if ($PasswordFolderId) { + $AssetPassword.asset_password | Add-Member -MemberType NoteProperty -Name password_folder_id -Force -Value $PasswordFolderId + } + + if ($Slug) { + $AssetPassword.asset_password | Add-Member -MemberType NoteProperty -Name slug -Force -Value $Slug + } + + $JSON = $AssetPassword | ConvertTo-Json -Depth 10 + + if ($PSCmdlet.ShouldProcess($Id)) { + Invoke-HuduRequest -Method put -Resource "/api/v1/asset_passwords/$Id" -Body $JSON + } +} +#EndRegion './Public/Set-HuduPassword.ps1' 158 +#Region './Public/Set-HuduPasswordArchive.ps1' -1 + +function Set-HuduPasswordArchive { + <# + .SYNOPSIS + Archive/Unarchive a Password + + .DESCRIPTION + Uses Hudu API to archive or unarchive a password + + .PARAMETER Id + Id of the requested Password + + .PARAMETER Archive + Boolean of archive status + + .EXAMPLE + Set-HuduPasswordArchive -Archive $true -Id 1 + + #> + [CmdletBinding(SupportsShouldProcess)] + Param ( + [Parameter(ValueFromPipelineByPropertyName = $true, Mandatory = $true)] + [Int]$Id, + [Parameter(Mandatory = $true)] + [Bool]$Archive + ) + + process { + if ($Archive) { + $Action = 'archive' + } else { + $Action = 'unarchive' + } + + if ($PSCmdlet.ShouldProcess($Id)) { + Invoke-HuduRequest -Method put -Resource "/api/v1/asset_passwords/$Id/$Action" + } + } +} +#EndRegion './Public/Set-HuduPasswordArchive.ps1' 39 +#Region './Public/Set-HuduWebsite.ps1' -1 + +function Set-HuduWebsite { + <# + .SYNOPSIS + Update a Website + + .DESCRIPTION + Uses Hudu API to update a website + + .PARAMETER Id + Id of requested website + + .PARAMETER Name + Website name (e.g. https://example.com) + + .PARAMETER Notes + Website Notes + + .PARAMETER Paused + When true, website monitoring is paused. + + .PARAMETER CompanyId + Used to associate website with company + + .PARAMETER DisableDNS + When true, dns monitoring is paused. + + .PARAMETER DisableSSL + When true, ssl cert monitoring is paused. + + .PARAMETER DisableWhois + When true, whois monitoring is paused. + + .PARAMETER Slug + Url identifier + + .EXAMPLE + Set-HuduWebsite -Id 1 -Paused $true + + #> + [CmdletBinding(SupportsShouldProcess)] + Param ( + [Parameter(Mandatory = $true)] + [Int]$Id, + + [Parameter(Mandatory = $true)] + [String]$Name, + + [String]$Notes = '', + + [String]$Paused = '', + + [Alias('company_id')] + [Parameter(Mandatory = $true)] + [Int]$CompanyId, + + [Alias('disable_dns')] + [String]$DisableDNS = '', + + [Alias('disable_ssl')] + [String]$DisableSSL = '', + + [Alias('disable_whois')] + [String]$DisableWhois = '', + + [string]$Slug + ) + + $Website = [ordered]@{website = [ordered]@{} } + + $Website.website.add('name', $Name) + + if ($Notes) { + $Website.website.add('notes', $Notes) + } + + if ($Paused) { + $Website.website.add('paused', $Paused) + } + + $Website.website.add('company_id', $companyid) + + if ($DisableDNS) { + $Website.website.add('disable_dns', $DisableDNS) + } + + if ($DisableSSL) { + $Website.website.add('disable_ssl', $DisableSSL) + } + + if ($DisableWhois) { + $Website.website.add('disable_whois', $DisableWhois) + } + + if ($Slug) { + $Website.website.add('slug', $Slug) + } + + $JSON = $Website | ConvertTo-Json + + if ($PSCmdlet.ShouldProcess($Id)) { + Invoke-HuduRequest -Method put -Resource "/api/v1/websites/$Id" -Body $JSON + } +} +#EndRegion './Public/Set-HuduWebsite.ps1' 104 diff --git a/Modules/HuduAPI/2.4.9/PSGetModuleInfo.xml b/Modules/HuduAPI/2.4.9/PSGetModuleInfo.xml new file mode 100644 index 000000000000..bc673458843c --- /dev/null +++ b/Modules/HuduAPI/2.4.9/PSGetModuleInfo.xml @@ -0,0 +1,248 @@ + + + + Microsoft.PowerShell.Commands.PSRepositoryItemInfo + System.Management.Automation.PSCustomObject + System.Object + + + HuduAPI + 2.4.9 + Module + This module provides an interface to the Hudu Rest API further information can be found at https://github.com/lwhitelock/HuduAPI + Luke Whitelock + + + System.Object[] + System.Array + System.Object + + + mspp + homotechsual + johnduprey + + + (c) 2021 Luke Whitelock. All rights reserved. +
    2024-06-30T03:42:36-04:00
    + + + + + + + + + PSModule + + + + + System.Collections.Hashtable + System.Object + + + + RoleCapability + + + + + + + DscResource + + + + Function + + + + Get-HuduActivityLogs + Get-HuduApiKey + Get-HuduAppInfo + Get-HuduArticles + Get-HuduAssetLayoutFieldID + Get-HuduAssetLayouts + Get-HuduAssets + Get-HuduBaseURL + Get-HuduCard + Get-HuduCompanies + Get-HuduExpirations + Get-HuduFolderMap + Get-HuduFolders + Get-HuduIntegrationMatchers + Get-HuduMagicDashes + Get-HuduObjectByUrl + Get-HuduPasswordFolders + Get-HuduPasswords + Get-HuduProcesses + Get-HuduPublicPhotos + Get-HuduRelations + Get-HuduUploads + Get-HuduWebsites + Initialize-HuduFolder + Move-HuduAssetsToNewLayout + New-HuduAPIKey + New-HuduArticle + New-HuduAsset + New-HuduAssetLayout + New-HuduBaseURL + New-HuduCompany + New-HuduCustomHeaders + New-HuduFolder + New-HuduPassword + New-HuduPublicPhoto + New-HuduRelation + New-HuduUpload + New-HuduWebsite + Remove-HuduAPIKey + Remove-HuduArticle + Remove-HuduAsset + Remove-HuduBaseURL + Remove-HuduCompany + Remove-HuduCustomHeaders + Remove-HuduMagicDash + Remove-HuduPassword + Remove-HuduRelation + Remove-HuduUpload + Remove-HuduWebsite + Set-HuduArticle + Set-HuduArticleArchive + Set-HuduAsset + Set-HuduAssetArchive + Set-HuduAssetLayout + Set-HuduCompany + Set-HuduCompanyArchive + Set-HuduFolder + Set-HuduIntegrationMatcher + Set-HuduMagicDash + Set-HuduPassword + Set-HuduPasswordArchive + Set-HuduWebsite + + + + + Cmdlet + + + + Command + + + + Get-HuduActivityLogs + Get-HuduApiKey + Get-HuduAppInfo + Get-HuduArticles + Get-HuduAssetLayoutFieldID + Get-HuduAssetLayouts + Get-HuduAssets + Get-HuduBaseURL + Get-HuduCard + Get-HuduCompanies + Get-HuduExpirations + Get-HuduFolderMap + Get-HuduFolders + Get-HuduIntegrationMatchers + Get-HuduMagicDashes + Get-HuduObjectByUrl + Get-HuduPasswordFolders + Get-HuduPasswords + Get-HuduProcesses + Get-HuduPublicPhotos + Get-HuduRelations + Get-HuduUploads + Get-HuduWebsites + Initialize-HuduFolder + Move-HuduAssetsToNewLayout + New-HuduAPIKey + New-HuduArticle + New-HuduAsset + New-HuduAssetLayout + New-HuduBaseURL + New-HuduCompany + New-HuduCustomHeaders + New-HuduFolder + New-HuduPassword + New-HuduPublicPhoto + New-HuduRelation + New-HuduUpload + New-HuduWebsite + Remove-HuduAPIKey + Remove-HuduArticle + Remove-HuduAsset + Remove-HuduBaseURL + Remove-HuduCompany + Remove-HuduCustomHeaders + Remove-HuduMagicDash + Remove-HuduPassword + Remove-HuduRelation + Remove-HuduUpload + Remove-HuduWebsite + Set-HuduArticle + Set-HuduArticleArchive + Set-HuduAsset + Set-HuduAssetArchive + Set-HuduAssetLayout + Set-HuduCompany + Set-HuduCompanyArchive + Set-HuduFolder + Set-HuduIntegrationMatcher + Set-HuduMagicDash + Set-HuduPassword + Set-HuduPasswordArchive + Set-HuduWebsite + + + + + Workflow + + + + + + + + + + + https://www.powershellgallery.com/api/v2 + PSGallery + NuGet + + + System.Management.Automation.PSCustomObject + System.Object + + + (c) 2021 Luke Whitelock. All rights reserved. + This module provides an interface to the Hudu Rest API further information can be found at https://github.com/lwhitelock/HuduAPI + False + True + True + 2653 + 1260331 + 24052 + 6/30/2024 3:42:36 AM -04:00 + 6/30/2024 3:42:36 AM -04:00 + 7/1/2024 8:11:32 PM -04:00 + PSModule PSFunction_Get-HuduActivityLogs PSCommand_Get-HuduActivityLogs PSFunction_Get-HuduApiKey PSCommand_Get-HuduApiKey PSFunction_Get-HuduAppInfo PSCommand_Get-HuduAppInfo PSFunction_Get-HuduArticles PSCommand_Get-HuduArticles PSFunction_Get-HuduAssetLayoutFieldID PSCommand_Get-HuduAssetLayoutFieldID PSFunction_Get-HuduAssetLayouts PSCommand_Get-HuduAssetLayouts PSFunction_Get-HuduAssets PSCommand_Get-HuduAssets PSFunction_Get-HuduBaseURL PSCommand_Get-HuduBaseURL PSFunction_Get-HuduCard PSCommand_Get-HuduCard PSFunction_Get-HuduCompanies PSCommand_Get-HuduCompanies PSFunction_Get-HuduExpirations PSCommand_Get-HuduExpirations PSFunction_Get-HuduFolderMap PSCommand_Get-HuduFolderMap PSFunction_Get-HuduFolders PSCommand_Get-HuduFolders PSFunction_Get-HuduIntegrationMatchers PSCommand_Get-HuduIntegrationMatchers PSFunction_Get-HuduMagicDashes PSCommand_Get-HuduMagicDashes PSFunction_Get-HuduObjectByUrl PSCommand_Get-HuduObjectByUrl PSFunction_Get-HuduPasswordFolders PSCommand_Get-HuduPasswordFolders PSFunction_Get-HuduPasswords PSCommand_Get-HuduPasswords PSFunction_Get-HuduProcesses PSCommand_Get-HuduProcesses PSFunction_Get-HuduPublicPhotos PSCommand_Get-HuduPublicPhotos PSFunction_Get-HuduRelations PSCommand_Get-HuduRelations PSFunction_Get-HuduUploads PSCommand_Get-HuduUploads PSFunction_Get-HuduWebsites PSCommand_Get-HuduWebsites PSFunction_Initialize-HuduFolder PSCommand_Initialize-HuduFolder PSFunction_Move-HuduAssetsToNewLayout PSCommand_Move-HuduAssetsToNewLayout PSFunction_New-HuduAPIKey PSCommand_New-HuduAPIKey PSFunction_New-HuduArticle PSCommand_New-HuduArticle PSFunction_New-HuduAsset PSCommand_New-HuduAsset PSFunction_New-HuduAssetLayout PSCommand_New-HuduAssetLayout PSFunction_New-HuduBaseURL PSCommand_New-HuduBaseURL PSFunction_New-HuduCompany PSCommand_New-HuduCompany PSFunction_New-HuduCustomHeaders PSCommand_New-HuduCustomHeaders PSFunction_New-HuduFolder PSCommand_New-HuduFolder PSFunction_New-HuduPassword PSCommand_New-HuduPassword PSFunction_New-HuduPublicPhoto PSCommand_New-HuduPublicPhoto PSFunction_New-HuduRelation PSCommand_New-HuduRelation PSFunction_New-HuduUpload PSCommand_New-HuduUpload PSFunction_New-HuduWebsite PSCommand_New-HuduWebsite PSFunction_Remove-HuduAPIKey PSCommand_Remove-HuduAPIKey PSFunction_Remove-HuduArticle PSCommand_Remove-HuduArticle PSFunction_Remove-HuduAsset PSCommand_Remove-HuduAsset PSFunction_Remove-HuduBaseURL PSCommand_Remove-HuduBaseURL PSFunction_Remove-HuduCompany PSCommand_Remove-HuduCompany PSFunction_Remove-HuduCustomHeaders PSCommand_Remove-HuduCustomHeaders PSFunction_Remove-HuduMagicDash PSCommand_Remove-HuduMagicDash PSFunction_Remove-HuduPassword PSCommand_Remove-HuduPassword PSFunction_Remove-HuduRelation PSCommand_Remove-HuduRelation PSFunction_Remove-HuduUpload PSCommand_Remove-HuduUpload PSFunction_Remove-HuduWebsite PSCommand_Remove-HuduWebsite PSFunction_Set-HuduArticle PSCommand_Set-HuduArticle PSFunction_Set-HuduArticleArchive PSCommand_Set-HuduArticleArchive PSFunction_Set-HuduAsset PSCommand_Set-HuduAsset PSFunction_Set-HuduAssetArchive PSCommand_Set-HuduAssetArchive PSFunction_Set-HuduAssetLayout PSCommand_Set-HuduAssetLayout PSFunction_Set-HuduCompany PSCommand_Set-HuduCompany PSFunction_Set-HuduCompanyArchive PSCommand_Set-HuduCompanyArchive PSFunction_Set-HuduFolder PSCommand_Set-HuduFolder PSFunction_Set-HuduIntegrationMatcher PSCommand_Set-HuduIntegrationMatcher PSFunction_Set-HuduMagicDash PSCommand_Set-HuduMagicDash PSFunction_Set-HuduPassword PSCommand_Set-HuduPassword PSFunction_Set-HuduPasswordArchive PSCommand_Set-HuduPasswordArchive PSFunction_Set-HuduWebsite PSCommand_Set-HuduWebsite PSIncludes_Function + False + 2024-07-01T20:11:32Z + 2.4.9 + Luke Whitelock + false + Module + HuduAPI.nuspec|HuduAPI.psm1|HuduAPI.psd1 + 4e0a4feb-1658-416b-b854-ab9e913a56de + 7.0 + MSPP + + + C:\GitHub\CIPP Workspace\CIPP-API\Modules\HuduAPI\2.4.9 +
    +
    +
    diff --git a/Scheduler_UserTasks/function.json b/Scheduler_UserTasks/function.json index f7af84092121..017acb166958 100644 --- a/Scheduler_UserTasks/function.json +++ b/Scheduler_UserTasks/function.json @@ -2,7 +2,7 @@ "bindings": [ { "name": "Timer", - "schedule": "0 */15 * * * *", + "schedule": "0 */5 * * * *", "direction": "in", "type": "timerTrigger" }, diff --git a/Tools/Initialize-DevEnvironment.ps1 b/Tools/Initialize-DevEnvironment.ps1 index d712e396ad04..8612e74156cf 100644 --- a/Tools/Initialize-DevEnvironment.ps1 +++ b/Tools/Initialize-DevEnvironment.ps1 @@ -12,4 +12,6 @@ ForEach ($Key in $CIPPSettings.PSObject.Properties.Name) { Import-Module "$CippRoot\Modules\AzBobbyTables" Import-Module "$CippRoot\Modules\DNSHealth" Import-Module "$CippRoot\Modules\CippCore" -Get-CIPPAuthentication \ No newline at end of file +Import-Module "$CippRoot\Modules\CippExtensions" + +Get-CIPPAuthentication diff --git a/Tools/Update-StandardsComments.ps1 b/Tools/Update-StandardsComments.ps1 new file mode 100644 index 000000000000..a4f6c6a0f082 --- /dev/null +++ b/Tools/Update-StandardsComments.ps1 @@ -0,0 +1,110 @@ +<# +.SYNOPSIS + This script updates the comment block in the CIPP standard files. + +.DESCRIPTION + The script reads the standards.json file and updates the comment block in the corresponding CIPP standard files. + It adds or modifies the comment block based on the properties defined in the standards.json file. + This is made to be able to generate the help documentation for the CIPP standards automatically. + +.INPUTS + None. You cannot pipe objects to this script. + +.OUTPUTS + None. The script modifies the CIPP standard files directly. + +.EXAMPLE + Update-StandardsComments.ps1 + + This example runs the script to update the comment block in the CIPP standard files. + + +#> +param ( + [switch]$WhatIf +) + +# Find the paths to the standards.json file based on the current script path +$StandardsJSONPath = Split-Path (Split-Path $PSScriptRoot) +$StandardsJSONPath = Resolve-Path "$StandardsJSONPath\*\src\data\standards.json" +$StandardsInfo = Get-Content -Path $StandardsJSONPath | ConvertFrom-Json -Depth 10 + +foreach ($Standard in $StandardsInfo) { + + # Calculate the standards file name and path + $StandardFileName = $Standard.name -replace 'standards.', 'Invoke-CIPPStandard' + $StandardsFilePath = Resolve-Path "$(Split-Path $PSScriptRoot)\Modules\CIPPCore\Public\Standards\$StandardFileName.ps1" + if (-not (Test-Path $StandardsFilePath)) { + Write-Host "No file found for standard $($Standard.name)" -ForegroundColor Yellow + continue + } + $Content = (Get-Content -Path $StandardsFilePath -Raw).TrimEnd() + + # Remove random newlines before the param block + $regexPattern = '#>\s*\r?\n\s*\r?\n\s*param' + $Content = $Content -replace $regexPattern, "#>`n`n param" + + # Regex to match the existing comment block + $Regex = '<#(.|\n)*?\.FUNCTIONALITY\s*Internal(.|\n)*?#>' + + if ($Content -match $Regex) { + $NewComment = [System.Collections.Generic.List[string]]::new() + # Add the initial scatic comments + $NewComment.Add("<#`r`n") + $NewComment.Add(" .FUNCTIONALITY`r`n") + $NewComment.Add(" Internal`r`n") + $NewComment.Add(" .COMPONENT`r`n") + $NewComment.Add(" (APIName) $($Standard.name -replace 'standards.', '')`r`n") + $NewComment.Add(" .SYNOPSIS`r`n") + $NewComment.Add(" (Label) $($Standard.label.ToString())`r`n") + $NewComment.Add(" .DESCRIPTION`r`n") + if ([string]::IsNullOrWhiteSpace($Standard.docsDescription)) { + $NewComment.Add(" (Helptext) $($Standard.helpText.ToString())`r`n") + $NewComment.Add(" (DocsDescription) $($Standard.helpText.ToString())`r`n") + } else { + $NewComment.Add(" (Helptext) $($Standard.helpText.ToString())`r`n") + $NewComment.Add(" (DocsDescription) $($Standard.docsDescription.ToString())`r`n") + } + $NewComment.Add(" .NOTES`r`n") + + # Loop through the rest of the properties of the standard and add them to the NOTES field + foreach ($Property in $Standard.PSObject.Properties) { + switch ($Property.Name) { + 'name' { continue } + 'impactColour' { continue } + 'docsDescription' { continue } + 'helpText' { continue } + 'label' { continue } + Default { + $NewComment.Add(" $($Property.Name.ToUpper())`r`n") + if ($Property.Value -is [System.Object[]]) { + foreach ($Value in $Property.Value) { + $NewComment.Add(" $(ConvertTo-Json -InputObject $Value -Depth 5 -Compress)`r`n") + } + continue + } + $NewComment.Add(" $($Property.Value.ToString())`r`n") + } + } + + } + + # Add header about how to update the comment block with this script + $NewComment.Add(" UPDATECOMMENTBLOCK`r`n") + $NewComment.Add(" Run the Tools\Update-StandardsComments.ps1 script to update this comment block`r`n") + # -Online help link + $NewComment.Add(" .LINK`r`n") + $NewComment.Add(" https://docs.cipp.app/user-documentation/tenant/standards/edit-standards`r`n") + $NewComment.Add(' #>') + + # Write the new comment block to the file + if ($WhatIf.IsPresent) { + Write-Host "Would update $StandardsFilePath with the following comment block:" + $NewComment + } else { + $Content -replace $Regex, $NewComment | Set-Content -Path $StandardsFilePath -Encoding utf8 + } + } else { + Write-Host "No comment block found in $StandardsFilePath" -ForegroundColor Yellow + } +} diff --git a/openapi.json b/openapi.json index 20a5281ec68b..9deb23f30eee 100644 --- a/openapi.json +++ b/openapi.json @@ -4405,46 +4405,6 @@ } } }, - "/ExecDisableEmailForward": { - "post": { - "description": "ExecDisableEmailForward", - "summary": "ExecDisableEmailForward", - "tags": [ - "POST" - ], - "parameters": [ - { - "required": true, - "schema": { - "type": "string" - }, - "name": "tenantfilter", - "in": "body" - }, - { - "required": true, - "schema": { - "type": "string" - }, - "name": "user", - "in": "body" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "properties": {}, - "type": "object" - } - } - }, - "description": "Successful operation" - } - } - } - }, "/ExecEditCalendarPermissions": { "get": { "description": "ExecEditCalendarPermissions", diff --git a/version_latest.txt b/version_latest.txt index d2d714f2a990..09b254e90c61 100644 --- a/version_latest.txt +++ b/version_latest.txt @@ -1 +1 @@ -5.9.4 \ No newline at end of file +6.0.0