diff --git a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Domain Analyser/Push-DomainAnalyserDomain.ps1 b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Domain Analyser/Push-DomainAnalyserDomain.ps1 index 2a4357ec7d98..508da7a7e02e 100644 --- a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Domain Analyser/Push-DomainAnalyserDomain.ps1 +++ b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Domain Analyser/Push-DomainAnalyserDomain.ps1 @@ -62,6 +62,7 @@ function Push-DomainAnalyserDomain { MSCNAMEDKIMSelectors = '' EnterpriseEnrollment = '' EnterpriseRegistration = '' + AutoDiscover = '' Score = '' MaximumScore = 160 ScorePercentage = '' @@ -293,6 +294,26 @@ function Push-DomainAnalyserDomain { } #EndRegion Intune Enrollment CNAME Check + #Region AutoDiscover Check + try { + $AutoDiscoverRecord = Read-AutoDiscoverRecord -Domain $Domain + $AutoDiscoverFailCount = $AutoDiscoverRecord.ValidationFails | Measure-Object | Select-Object -ExpandProperty Count + $AutoDiscoverWarnCount = $AutoDiscoverRecord.ValidationWarns | Measure-Object | Select-Object -ExpandProperty Count + if ($AutoDiscoverFailCount -eq 0 -and $AutoDiscoverWarnCount -eq 0) { + $Result.AutoDiscover = 'Correct' + } elseif ($AutoDiscoverFailCount -eq 0) { + $Result.AutoDiscover = "$($AutoDiscoverRecord.RecordType): $($AutoDiscoverRecord.Record)" + $ScoreExplanation.Add("AutoDiscover $($AutoDiscoverRecord.RecordType) record points to unexpected target") | Out-Null + } else { + $Result.AutoDiscover = 'No Record' + $ScoreExplanation.Add('No AutoDiscover DNS record found') | Out-Null + } + } catch { + $Result.AutoDiscover = 'Error' + Write-LogMessage -API 'DomainAnalyser' -tenant $DomainObject.TenantId -message "AutoDiscover check error for $Domain" -LogData (Get-CippException -Exception $_) -sev Error + } + #EndRegion AutoDiscover Check + #Region MSCNAME DKIM Records # Get Microsoft DKIM CNAME selector Records # Ugly, but i needed to create a scope/loop i could break out of without breaking the rest of the function diff --git a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Graph Requests/Push-ListGraphRequestQueue.ps1 b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Graph Requests/Push-ListGraphRequestQueue.ps1 index 9c838711e42c..fb6c7fe15bcc 100644 --- a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Graph Requests/Push-ListGraphRequestQueue.ps1 +++ b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Graph Requests/Push-ListGraphRequestQueue.ps1 @@ -63,6 +63,15 @@ function Push-ListGraphRequestQueue { Data = [string]$Json } Add-CIPPAzDataTableEntity @Table -Entity $GraphResults -Force | Out-Null + + if ($env:CIPPNG -eq 'true') { + try { + [Craft.Services.CacheBridge]::InvalidateByScope('AllTenants') + } catch { + Write-Information "CacheBridge invalidation skipped: $($_.Exception.Message)" + } + } + return $true } catch { Write-Warning "Queue Error: $($_.Exception.Message)" diff --git a/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertIntunePolicyConflicts.ps1 b/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertIntunePolicyConflicts.ps1 index d9b2239b1bcb..071f65cff4e7 100644 --- a/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertIntunePolicyConflicts.ps1 +++ b/Modules/CIPPAlerts/Public/Alerts/Get-CIPPAlertIntunePolicyConflicts.ps1 @@ -48,9 +48,10 @@ function Get-CIPPAlertIntunePolicyConflicts { return } - $AlertableStatuses = @() - if ($Config.AlertErrors) { $AlertableStatuses += 'error', 'failed' } - if ($Config.AlertConflicts) { $AlertableStatuses += 'conflict' } + $AlertableStatuses = @( + if ($Config.AlertErrors) { 'error'; 'failed' } + if ($Config.AlertConflicts) { 'conflict' } + ) if (-not $AlertableStatuses) { return @@ -68,7 +69,7 @@ function Get-CIPPAlertIntunePolicyConflicts { return } - $Issues = @() + $Issues = [System.Collections.Generic.List[object]]::new() if ($Config.IncludePolicies) { try { @@ -77,16 +78,16 @@ function Get-CIPPAlertIntunePolicyConflicts { foreach ($Device in $ManagedDevices) { $PolicyStates = $Device.deviceConfigurationStates | Where-Object { $_.state -and ($AlertableStatuses -contains $_.state) } foreach ($State in $PolicyStates) { - $Issues += [PSCustomObject]@{ - Message = "Policy '$($State.displayName)' is $($State.state) on device '$($Device.deviceName)' for $($Device.userPrincipalName)." - Tenant = $TenantFilter - Type = 'Policy' - PolicyName = $State.displayName - IssueStatus = $State.state - DeviceName = $Device.deviceName - UserPrincipalName = $Device.userPrincipalName - DeviceId = $Device.id - } + $Issues.Add([PSCustomObject]@{ + Message = "Policy '$($State.displayName)' is $($State.state) on device '$($Device.deviceName)' for $($Device.userPrincipalName)." + Tenant = $TenantFilter + Type = 'Policy' + PolicyName = $State.displayName + IssueStatus = $State.state + DeviceName = $Device.deviceName + UserPrincipalName = $Device.userPrincipalName + DeviceId = $Device.id + }) } } } catch { @@ -105,16 +106,16 @@ function Get-CIPPAlertIntunePolicyConflicts { } foreach ($Status in $BadStatuses) { - $Issues += [PSCustomObject]@{ - Message = "App '$($App.displayName)' install is $($Status.installState) on device '$($Status.deviceName)' for $($Status.userPrincipalName)." - Tenant = $TenantFilter - Type = 'Application' - AppName = $App.displayName - IssueStatus = $Status.installState - DeviceName = $Status.deviceName - UserPrincipalName = $Status.userPrincipalName - DeviceId = $Status.deviceId - } + $Issues.Add([PSCustomObject]@{ + Message = "App '$($App.displayName)' install is $($Status.installState) on device '$($Status.deviceName)' for $($Status.userPrincipalName)." + Tenant = $TenantFilter + Type = 'Application' + AppName = $App.displayName + IssueStatus = $Status.installState + DeviceName = $Status.deviceName + UserPrincipalName = $Status.userPrincipalName + DeviceId = $Status.deviceId + }) } } } catch { @@ -132,11 +133,11 @@ function Get-CIPPAlertIntunePolicyConflicts { $AppCount = ($Issues | Where-Object { $_.Type -eq 'Application' }).Count $AlertData = @([PSCustomObject]@{ - Message = "Found $PolicyCount policy issues and $AppCount application issues in Intune." - Tenant = $TenantFilter - PolicyIssues = $PolicyCount - AppIssues = $AppCount - Issues = $Issues + Message = "Found $PolicyCount policy issues and $AppCount application issues in Intune." + Tenant = $TenantFilter + PolicyIssues = $PolicyCount + AppIssues = $AppCount + Issues = $Issues }) } else { $AlertData = $Issues diff --git a/Modules/CIPPCore/Public/Add-CIPPBPAField.ps1 b/Modules/CIPPCore/Public/Add-CIPPBPAField.ps1 index bed52e8cc786..74c1110550a6 100644 --- a/Modules/CIPPCore/Public/Add-CIPPBPAField.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPBPAField.ps1 @@ -34,7 +34,7 @@ function Add-CIPPBPAField { $Result[$fieldName] = [string]$JsonString } 'string' { - $Result[$fieldName], [string]$FieldValue + $Result[$fieldName] = [string]$FieldValue } } Add-CIPPAzDataTableEntity @Table -Entity $Result -Force diff --git a/Modules/CIPPCore/Public/CippQueue/New-CippQueueEntry.ps1 b/Modules/CIPPCore/Public/CippQueue/New-CippQueueEntry.ps1 index 6302f05994e8..ae77578f4ae3 100644 --- a/Modules/CIPPCore/Public/CippQueue/New-CippQueueEntry.ps1 +++ b/Modules/CIPPCore/Public/CippQueue/New-CippQueueEntry.ps1 @@ -21,6 +21,7 @@ function New-CippQueueEntry { } if ($env:CIPPNG -eq 'true') { + [Craft.Services.QueueStatusBridge]::RegisterQueueMetadata($QueueEntry.RowKey, $Name, $Link, $Reference) return $QueueEntry } diff --git a/Modules/CIPPCore/Public/Compare-CIPPIntuneObject.ps1 b/Modules/CIPPCore/Public/Compare-CIPPIntuneObject.ps1 index d4a19075ac9e..fc9902d80823 100644 --- a/Modules/CIPPCore/Public/Compare-CIPPIntuneObject.ps1 +++ b/Modules/CIPPCore/Public/Compare-CIPPIntuneObject.ps1 @@ -12,7 +12,7 @@ function Compare-CIPPIntuneObject { [Parameter(Mandatory = $false)] [string[]]$CompareType = @() ) - if ($CompareType -ne 'Catalog') { + if ($CompareType -notcontains 'Catalog') { $defaultExcludeProperties = @( 'id', 'createdDateTime', diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListObjectHistory.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListObjectHistory.ps1 new file mode 100644 index 000000000000..68ed78012e63 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListObjectHistory.ps1 @@ -0,0 +1,284 @@ +function Invoke-ListObjectHistory { + <# + .SYNOPSIS + In progress concept - Rvd + Returns a transformed timeline of audit events for any tenant object over a configurable period. + + .DESCRIPTION + Aggregates change history from Graph directoryAudits and (for Exchange objects) the Unified Audit Log. + Returns a normalised timeline array with parsed property changes, actor details, and source metadata. + Supports users, groups, applications, service principals, devices, administrative units, + conditional access policies, shared mailboxes, distribution lists, and mail contacts. + + .FUNCTIONALITY + Entrypoint + .ROLE + Identity.AuditLog.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter + $ObjectId = $Request.Query.objectId ?? $Request.Query.id ?? $Request.Body.objectId ?? $Request.Body.id + $ObjectType = $Request.Query.objectType ?? $Request.Body.objectType + $Days = try { [int]($Request.Query.days ?? $Request.Body.days ?? 30) } catch { 30 } + + #region Validation + if (-not $ObjectId) { + return [HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = @{ Results = 'Error: objectId is required' } + } + } + + try { + $ObjectId = ConvertTo-CIPPODataFilterValue -Value $ObjectId -Type Guid + } catch { + return [HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = @{ Results = 'Error: objectId must be a valid GUID' } + } + } + + if (-not $TenantFilter) { + return [HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = @{ Results = 'Error: tenantFilter is required' } + } + } + + if ($Days -lt 1) { $Days = 1 } + if ($Days -gt 90) { $Days = 90 } + #endregion + + $StartTime = (Get-Date).AddDays(-$Days).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') + $TypeKey = ($ObjectType ?? '').Trim().ToLowerInvariant() + $Warnings = [System.Collections.Generic.List[string]]::new() + $Sources = [System.Collections.Generic.List[string]]::new() + + #region Resolve object and classify source lanes + $ResolveUri = if ($TypeKey -eq 'conditionalaccesspolicy') { + "https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies/$ObjectId" + } else { + "https://graph.microsoft.com/v1.0/directoryObjects/$ObjectId" + } + + $ResolvedObject = $null + $ResolvedAs = $null + $ResolvedDisplayName = $null + try { + $ResolvedObject = New-GraphGetRequest -uri $ResolveUri -tenantid $TenantFilter -ErrorAction Stop + $ODataType = $ResolvedObject.'@odata.type' -replace '^#microsoft\.graph\.', '' + $ResolvedAs = if ($TypeKey -eq 'conditionalaccesspolicy') { 'conditionalAccessPolicy' } else { $ODataType ?? 'directoryObject' } + $ResolvedDisplayName = $ResolvedObject.displayName ?? $ResolvedObject.userPrincipalName ?? $ResolvedObject.appId ?? $ObjectId + } catch { + $ResolvedAs = if ($TypeKey) { $TypeKey } else { 'directoryObject' } + $ResolvedDisplayName = $ObjectId + } + + # Classify which audit sources to query based on resolved type + $ExchangeOnlyTypes = @('mailbox', 'sharedmailbox', 'distributionlist', 'mailcontact', 'resource', 'roommailbox', 'equipmentmailbox') + $EntraOnlyTypes = @('application', 'serviceprincipal', 'device', 'administrativeunit', 'conditionalaccesspolicy') + + $QueryDirectoryAudits = $true + $QueryExchangeAudit = $false + $ExchangeAnchor = $null + + if ($TypeKey -in $ExchangeOnlyTypes) { + # Caller explicitly said this is an Exchange object — skip Graph, go Exchange only + $QueryDirectoryAudits = $false + $QueryExchangeAudit = $true + $ExchangeAnchor = $ResolvedObject.userPrincipalName ?? $ResolvedObject.mail + } elseif ($TypeKey -in $EntraOnlyTypes) { + # Pure Entra object — Graph directoryAudits only + $QueryDirectoryAudits = $true + $QueryExchangeAudit = $false + } elseif ($ResolvedObject.mail -and $ResolvedAs -in @('user', 'group')) { + # Mail-enabled user or group — query both + $QueryDirectoryAudits = $true + $QueryExchangeAudit = $true + $ExchangeAnchor = $ResolvedObject.userPrincipalName ?? $ResolvedObject.mail + } + # else: unknown type or resolution failed — default is Graph directoryAudits only + #endregion + + #region Helper: parse modifiedProperties + $ParseModifiedProperties = { + param([array]$Properties) + foreach ($Prop in $Properties) { + $OldVal = $null + $NewVal = $null + if ($Prop.oldValue -and $Prop.oldValue -ne '[]' -and $Prop.oldValue -ne 'null') { + $OldVal = try { $Prop.oldValue | ConvertFrom-Json -ErrorAction Stop } catch { $Prop.oldValue } + } + if ($Prop.newValue -and $Prop.newValue -ne '[]' -and $Prop.newValue -ne 'null') { + $NewVal = try { $Prop.newValue | ConvertFrom-Json -ErrorAction Stop } catch { $Prop.newValue } + } + if ($null -ne $OldVal -or $null -ne $NewVal) { + [PSCustomObject]@{ + property = $Prop.displayName + oldValue = $OldVal + newValue = $NewVal + } + } + } + } + #endregion + + #region Helper: normalize initiatedBy + $NormalizeActor = { + param($InitiatedBy) + if ($InitiatedBy.user) { + [PSCustomObject]@{ + displayName = $InitiatedBy.user.displayName ?? $InitiatedBy.user.userPrincipalName + id = $InitiatedBy.user.id + upn = $InitiatedBy.user.userPrincipalName + type = 'user' + } + } elseif ($InitiatedBy.app) { + [PSCustomObject]@{ + displayName = $InitiatedBy.app.displayName + id = $InitiatedBy.app.servicePrincipalId ?? $InitiatedBy.app.appId + upn = $null + type = 'app' + } + } else { + [PSCustomObject]@{ + displayName = 'Unknown' + id = $null + upn = $null + type = 'unknown' + } + } + } + #endregion + + #region Query Graph directoryAudits + [array]$DirectoryTimeline = @() + if ($QueryDirectoryAudits) { + try { + $Filter = "activityDateTime ge $StartTime and targetResources/any(s:s/id eq '$ObjectId')" + $Uri = "https://graph.microsoft.com/v1.0/auditLogs/directoryAudits?`$filter=$Filter&`$orderby=activityDateTime desc" + + Write-LogMessage -API $APIName -message "Object history: querying directoryAudits for $ObjectId (last $Days days)" -Sev 'Debug' -tenant $TenantFilter + + [array]$RawAudits = New-GraphGetRequest -uri $Uri -tenantid $TenantFilter -ComplexFilter -ErrorAction Stop + + [array]$DirectoryTimeline = @(foreach ($Event in $RawAudits) { + $TargetResource = $Event.targetResources | Where-Object { $_.id -eq $ObjectId } | Select-Object -First 1 + $TargetResource = $TargetResource ?? ($Event.targetResources | Select-Object -First 1) + + [PSCustomObject]@{ + id = $Event.id + timestamp = $Event.activityDateTime + activity = $Event.activityDisplayName + category = $Event.category + operationType = $Event.operationType + result = $Event.result + actor = & $NormalizeActor $Event.initiatedBy + target = $TargetResource.displayName ?? $TargetResource.userPrincipalName ?? $ObjectId + changes = @(& $ParseModifiedProperties ($TargetResource.modifiedProperties ?? @())) + source = 'directoryAudit' + } + }) + [void]$Sources.Add('directoryAudit') + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API $APIName -tenant $TenantFilter -message "Object history: directoryAudits failed - $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + [void]$Warnings.Add("Directory audit query failed: $($ErrorMessage.NormalizedError)") + } + } + #endregion + + #region Query Exchange Unified Audit Log + [array]$ExchangeTimeline = @() + if ($QueryExchangeAudit) { + if ($ExchangeAnchor) { + try { + $SessionId = "ObjectHistory_$(Get-Random -Minimum 10000 -Maximum 99999)" + $SearchParam = @{ + SessionCommand = 'ReturnLargeSet' + ObjectIds = @($ExchangeAnchor) + SessionId = $SessionId + StartDate = (Get-Date).AddDays(-$Days) + EndDate = (Get-Date) + ResultSize = 5000 + } + + $ExchangeLogs = [System.Collections.Generic.List[object]]::new() + $MaxPages = 10 + $Page = 0 + do { + $Batch = @(New-ExoRequest -tenantid $TenantFilter -cmdlet 'Search-UnifiedAuditLog' -cmdParams $SearchParam -Anchor $ExchangeAnchor) + foreach ($Item in $Batch) { [void]$ExchangeLogs.Add($Item) } + $Page++ + } while ($Batch.Count -eq 5000 -and $Page -lt $MaxPages) + + [array]$ExchangeTimeline = @(foreach ($Log in $ExchangeLogs) { + $AuditData = try { $Log.AuditData | ConvertFrom-Json -ErrorAction Stop } catch { $null } + if (-not $AuditData) { continue } + + [PSCustomObject]@{ + id = $AuditData.Id ?? $Log.Identity + timestamp = $Log.CreationDate ?? $AuditData.CreationTime + activity = $AuditData.Operation + category = 'ExchangeItem' + operationType = $AuditData.Operation + result = if ($AuditData.ResultStatus -eq 'Succeeded' -or $AuditData.ResultStatus -eq 'True') { 'success' } else { $AuditData.ResultStatus ?? 'success' } + actor = [PSCustomObject]@{ + displayName = $AuditData.UserId + id = $AuditData.UserId + upn = $AuditData.UserId + type = 'user' + } + target = $AuditData.ObjectId ?? $ExchangeAnchor + changes = @( + if ($AuditData.Parameters) { + foreach ($Param in $AuditData.Parameters) { + [PSCustomObject]@{ + property = $Param.Name + oldValue = $null + newValue = $Param.Value + } + } + } + ) + source = 'exchangeAudit' + } + }) + [void]$Sources.Add('exchangeAudit') + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API $APIName -tenant $TenantFilter -message "Object history: Exchange UAL failed - $($ErrorMessage.NormalizedError)" -sev Warning -LogData $ErrorMessage + [void]$Warnings.Add("Exchange audit query failed: $($ErrorMessage.NormalizedError)") + } + } else { + [void]$Warnings.Add('Exchange audit skipped: could not determine mailbox anchor (UPN/mail)') + } + } + #endregion + + #region Merge and sort timeline + $Timeline = @($DirectoryTimeline + $ExchangeTimeline | Where-Object { $_ } | Sort-Object -Property timestamp -Descending) + #endregion + + $Body = [PSCustomObject]@{ + objectId = $ObjectId + objectType = $ObjectType + resolvedObject = $ResolvedObject + resolvedAs = $ResolvedAs + resolvedDisplayName = $ResolvedDisplayName + days = $Days + activityFromUtc = $StartTime + totalEvents = $Timeline.Count + sources = @($Sources) + warnings = @($Warnings) + timeline = @($Timeline) + } + + return [HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = $Body + } +} diff --git a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPOrchestrator.ps1 b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPOrchestrator.ps1 index 31a13b68a50b..b5509bb4efc4 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPOrchestrator.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPOrchestrator.ps1 @@ -42,10 +42,7 @@ function Start-CIPPOrchestrator { if (-not $InputObject.Batch -and $InputObject.QueueFunction) { $QueueFuncName = "Push-$($InputObject.QueueFunction.FunctionName)" Write-Information "Craft: Calling QueueFunction '$QueueFuncName' to build batch for '$OrchestratorName'" - $QueueItem = [PSCustomObject]@{} - if ($InputObject.QueueFunction.Parameters) { - $QueueItem = [PSCustomObject]$InputObject.QueueFunction.Parameters - } + $QueueItem = [PSCustomObject]$InputObject.QueueFunction $BatchResult = & $QueueFuncName -Item $QueueItem $QueueBatch = @($BatchResult | Where-Object { $null -ne $_ }) if ($QueueBatch.Count -eq 0) { @@ -78,7 +75,8 @@ function Start-CIPPOrchestrator { $BatchJson, 4, $PostExecFunctionName, - $PostExecParametersJson + $PostExecParametersJson, + $InputObject.Reference ) return "Craft-$OrchestratorName" } diff --git a/Modules/CIPPCore/Public/GraphRequests/Get-GraphRequestList.ps1 b/Modules/CIPPCore/Public/GraphRequests/Get-GraphRequestList.ps1 index ad1db76305bb..134f8aeeeb79 100644 --- a/Modules/CIPPCore/Public/GraphRequests/Get-GraphRequestList.ps1 +++ b/Modules/CIPPCore/Public/GraphRequests/Get-GraphRequestList.ps1 @@ -199,7 +199,7 @@ function Get-GraphRequestList { $Type = 'Queue' Write-Information "Cached: $(($Rows | Measure-Object).Count) rows (Type: $($Type))" $QueueReference = '{0}-{1}' -f $TenantFilter, $PartitionKey - $RunningQueue = Get-CIPPQueueData -Reference $QueueReference | Where-Object { $_.Status -ne 'Completed' -and $_.Status -ne 'Failed' } + $RunningQueue = Get-CIPPQueueData -Reference $QueueReference | Where-Object { $_.Status -ne 'Completed' -and $_.Status -ne 'Failed' -and $_.Reference -eq $QueueReference } } elseif (!$SkipCache.IsPresent -and !$ClearCache.IsPresent -and !$CountOnly.IsPresent) { if ($TenantFilter -eq 'AllTenants' -or $Count -gt $SingleTenantThreshold) { $Table = Get-CIPPTable -TableName $TableName @@ -214,7 +214,7 @@ function Get-GraphRequestList { $Type = 'Cache' Write-Information "Table: $TableName | PK: $PartitionKey | Cached: $(($Rows | Measure-Object).Count) rows (Type: $($Type))" $QueueReference = '{0}-{1}' -f $TenantFilter, $PartitionKey - $RunningQueue = Get-CIPPQueueData -Reference $QueueReference | Where-Object { $_.Status -notmatch 'Completed' -and $_.Status -notmatch 'Failed' } + $RunningQueue = Get-CIPPQueueData -Reference $QueueReference | Where-Object { $_.Status -notmatch 'Completed' -and $_.Status -notmatch 'Failed' -and $_.Reference -eq $QueueReference } } } } catch { @@ -294,6 +294,7 @@ function Get-GraphRequestList { $InputObject = @{ OrchestratorName = 'GraphRequestOrchestrator' Batch = @($Batch) + Reference = $QueueReference } #Write-Information ($InputObject | ConvertTo-Json -Depth 5) $InstanceId = Start-CIPPOrchestrator -InputObject $InputObject diff --git a/Modules/CIPPCore/Public/New-CIPPAlertTemplate.ps1 b/Modules/CIPPCore/Public/New-CIPPAlertTemplate.ps1 index 221346f1bd04..deb66894da52 100644 --- a/Modules/CIPPCore/Public/New-CIPPAlertTemplate.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPAlertTemplate.ps1 @@ -280,17 +280,9 @@ function New-CIPPAlertTemplate { } if ($Format -eq 'html') { - # Escape curly braces in content variables so the -f format operator - # does not interpret data values (e.g. JSON in drift/standards) as placeholders - $FmtTitle = [string]$Title -replace '\{', '{{' -replace '\}', '}}' - $FmtIntroText = [string]$IntroText -replace '\{', '{{' -replace '\}', '}}' - $FmtButtonUrl = [string]$ButtonUrl -replace '\{', '{{' -replace '\}', '}}' - $FmtButtonText = [string]$ButtonText -replace '\{', '{{' -replace '\}', '}}' - $FmtAfterButtonText = [string]$AfterButtonText -replace '\{', '{{' -replace '\}', '}}' - $FmtAuditLogLink = [string]$AuditLogLink -replace '\{', '{{' -replace '\}', '}}' - return [pscustomobject]@{ + return [pscustomobject]@{ title = $Title - htmlcontent = $HTMLTemplate -f $FmtTitle, $FmtIntroText, $FmtButtonUrl, $FmtButtonText, $FmtAfterButtonText, $FmtAuditLogLink + htmlcontent = $HTMLTemplate -f $Title, $IntroText, $ButtonUrl, $ButtonText, $AfterButtonText, $AuditLogLink } } elseif ($Format -eq 'json') { if ($InputObject -eq 'auditlog') { diff --git a/Modules/CIPPCore/Public/New-CIPPUserTask.ps1 b/Modules/CIPPCore/Public/New-CIPPUserTask.ps1 index a91904f4724a..7fdfab465719 100644 --- a/Modules/CIPPCore/Public/New-CIPPUserTask.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPUserTask.ps1 @@ -81,13 +81,13 @@ function New-CIPPUserTask { } if ($UserObj.setManager) { - $ManagerResult = Set-CIPPManager -User $CreationResults.Username -Manager $UserObj.setManager.value -TenantFilter $UserObj.tenantFilter -Headers $Headers - $Results.Add($ManagerResult) + $ManagerResults = Set-CIPPManager -Users $CreationResults.Username -Manager $UserObj.setManager.value -TenantFilter $UserObj.tenantFilter -Headers $Headers + $Results.Add($ManagerResults.Result) } if ($UserObj.setSponsor) { - $SponsorResult = Set-CIPPSponsor -User $CreationResults.Username -Sponsor $UserObj.setSponsor.value -TenantFilter $UserObj.tenantFilter -Headers $Headers - $Results.Add($SponsorResult) + $SponsorResults = Set-CIPPSponsor -Users $CreationResults.Username -Sponsor $UserObj.setSponsor.value -TenantFilter $UserObj.tenantFilter -Headers $Headers + $Results.Add($SponsorResults.Result) } return @{ diff --git a/Modules/CIPPCore/Public/Set-CIPPAuthenticationPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPAuthenticationPolicy.ps1 index a7202202bef0..75d7971388a4 100644 --- a/Modules/CIPPCore/Public/Set-CIPPAuthenticationPolicy.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPAuthenticationPolicy.ps1 @@ -26,7 +26,7 @@ function Set-CIPPAuthenticationPolicy { } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -headers $Headers -API $APIName -tenant $Tenant -message "Could not get CurrentInfo for $AuthenticationMethodId. Error:$($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Return "Could not get CurrentInfo for $AuthenticationMethodId. Error:$($ErrorMessage.NormalizedError)" + return "Could not get CurrentInfo for $AuthenticationMethodId. Error:$($ErrorMessage.NormalizedError)" } switch ($AuthenticationMethodId) { @@ -114,7 +114,7 @@ function Set-CIPPAuthenticationPolicy { throw "Setting $AuthenticationMethodId to enabled is not allowed" } } - Default { + default { Write-LogMessage -headers $Headers -API $APIName -tenant $Tenant -message "Somehow you hit the default case with an input of $AuthenticationMethodId . You probably made a typo in the input for AuthenticationMethodId. It`'s case sensitive." -sev Error throw "Somehow you hit the default case with an input of $AuthenticationMethodId . You probably made a typo in the input for AuthenticationMethodId. It`'s case sensitive." } diff --git a/Modules/CIPPCore/Public/Set-CIPPManager.ps1 b/Modules/CIPPCore/Public/Set-CIPPManager.ps1 index c1394b04936d..b71367bceb88 100644 --- a/Modules/CIPPCore/Public/Set-CIPPManager.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPManager.ps1 @@ -1,23 +1,49 @@ function Set-CIPPManager { [CmdletBinding()] param ( - $User, - $Manager, + [Alias('User')] + [string[]] $Users, + [string] $Manager, $TenantFilter, $APIName = 'Set Manager', $Headers ) - try { - $ManagerBody = [PSCustomObject]@{'@odata.id' = "https://graph.microsoft.com/beta/users/$($Manager)" } - $ManagerBodyJSON = ConvertTo-Json -Compress -Depth 10 -InputObject $ManagerBody - $null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/users/$($User)/manager/`$ref" -tenantid $TenantFilter -type PUT -body $ManagerBodyJSON - Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Set $User's manager to $Manager" -Sev 'Info' - } catch { - $ErrorMessage = Get-CippException -Exception $_ - Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Failed to Set Manager. Error:$($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $_ - throw "Failed to set manager: $($ErrorMessage.NormalizedError)" + if ($Users.Count -eq 0) { + return @() } - return "Set $User's manager to $Manager" -} + $RequestId = 0 + $Requests = foreach ($User in $Users) { + @{ + id = ($RequestId++).ToString() + method = 'PUT' + url = "users/$User/manager/`$ref" + body = @{ '@odata.id' = "https://graph.microsoft.com/beta/users/$Manager" } + headers = @{ 'Content-Type' = 'application/json' } + } + } + + $Responses = New-GraphBulkRequest -tenantid $TenantFilter -Requests @($Requests) + + $Results = foreach ($Response in @($Responses)) { + $ResponseIndex = [int]$Response.id + $User = $Users[$ResponseIndex] + $Success = [int]$Response.status -in @(200, 204) + $ErrorMessage = if ($Response.body.error.message) { $Response.body.error.message } else { "Unknown error (Status: $($Response.status))" } + $Result = if ($Success) { "Set $User's manager to $Manager" } else { "Failed to set $User's manager: $ErrorMessage" } + $Severity = if ($Success) { 'Info' } else { 'Error' } + + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev $Severity + + [pscustomobject]@{ + User = $User + Manager = $Manager + Success = $Success + Result = $Result + Status = $Response.status + } + } + + return @($Results) +} diff --git a/Modules/CIPPCore/Public/Set-CIPPSponsor.ps1 b/Modules/CIPPCore/Public/Set-CIPPSponsor.ps1 index ad2444c01b9f..8bbb7bfb9850 100644 --- a/Modules/CIPPCore/Public/Set-CIPPSponsor.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPSponsor.ps1 @@ -1,22 +1,49 @@ function Set-CIPPSponsor { [CmdletBinding()] param ( - $User, - $Sponsor, + [Alias('User')] + [string[]] $Users, + [string] $Sponsor, $TenantFilter, $APIName = 'Set Sponsor', $Headers ) - try { - $SponsorBody = [PSCustomObject]@{'@odata.id' = "https://graph.microsoft.com/beta/users/$($Sponsor)" } - $SponsorBodyJSON = ConvertTo-Json -Compress -Depth 10 -InputObject $SponsorBody - $null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/users/$($User)/sponsors/`$ref" -tenantid $TenantFilter -type PUT -body $SponsorBodyJSON - Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Set $User's sponsor to $Sponsor" -Sev 'Info' - } catch { - $ErrorMessage = Get-CippException -Exception $_ - Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Failed to Set Sponsor. Error:$($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $_ - throw "Failed to set sponsor: $($_.Exception.Message)" + if ($Users.Count -eq 0) { + return @() } - return "Set $user's sponsor to $Sponsor" + + $RequestId = 0 + $Requests = foreach ($User in $Users) { + @{ + id = ($RequestId++).ToString() + method = 'PUT' + url = "users/$User/sponsors/`$ref" + body = @{ '@odata.id' = "https://graph.microsoft.com/beta/users/$Sponsor" } + headers = @{ 'Content-Type' = 'application/json' } + } + } + + $Responses = New-GraphBulkRequest -tenantid $TenantFilter -Requests @($Requests) + + $Results = foreach ($Response in @($Responses)) { + $ResponseIndex = [int]$Response.id + $User = $Users[$ResponseIndex] + $Success = [int]$Response.status -in @(200, 204) + $ErrorMessage = if ($Response.body.error.message) { $Response.body.error.message } else { "Unknown error (Status: $($Response.status))" } + $Result = if ($Success) { "Set $User's sponsor to $Sponsor" } else { "Failed to set $User's sponsor: $ErrorMessage" } + $Severity = if ($Success) { 'Info' } else { 'Error' } + + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev $Severity + + [pscustomobject]@{ + User = $User + Sponsor = $Sponsor + Success = $Success + Result = $Result + Status = $Response.status + } + } + + return @($Results) } diff --git a/Modules/CIPPCore/Public/Tools/Import-CommunityTemplate.ps1 b/Modules/CIPPCore/Public/Tools/Import-CommunityTemplate.ps1 index 603df66a4c96..56d3c5843e44 100644 --- a/Modules/CIPPCore/Public/Tools/Import-CommunityTemplate.ps1 +++ b/Modules/CIPPCore/Public/Tools/Import-CommunityTemplate.ps1 @@ -68,7 +68,7 @@ function Import-CommunityTemplate { $Template | Add-Member -MemberType NoteProperty -Name SHA -Value $SHA -Force $Template | Add-Member -MemberType NoteProperty -Name Source -Value $Source -Force Add-CIPPAzDataTableEntity @Table -Entity $Template -Force - + if ($Existing -and $Existing.SHA -ne $SHA) { $StatusMessage = "Updated template '$($Template.RowKey)' from source '$Source' (SHA changed)." } elseif ($Existing) { @@ -217,6 +217,21 @@ function Import-CommunityTemplate { '*managedAppPolicies*' { 'AppProtection' } '*deviceAppManagement*' { 'AppProtection' } } + + # Fallback: infer type from template content when @odata.id is missing or unrecognized + if (-not $URLName) { + $odataType = $Template.'@odata.type' + $URLName = if ($null -ne $Template.settings -and $null -ne $Template.technologies) { 'Catalog' } + elseif ($null -ne $Template.scheduledActionsForRule -or $odataType -match 'CompliancePolicy') { 'DeviceCompliancePolicies' } + elseif ($odataType -match 'windowsDriverUpdateProfile') { 'windowsDriverUpdateProfiles' } + elseif ($odataType -match 'ManagedApp|managedAppProtection') { 'AppProtection' } + elseif ($odataType -match 'deviceConfiguration|#microsoft\.graph\.\w+Configuration$') { 'Device' } + else { $null } + if ($URLName) { + Write-Information "Inferred Intune template type '$URLName' from content structure for '$($Template.displayName ?? $Template.Name)'" + } + } + $RawJson = $Template | Select-Object * -ExcludeProperty id, lastModifiedDateTime, 'assignments', '#microsoft*', '*@odata.navigationLink', '*@odata.associationLink', '*@odata.context', 'ScopeTagIds', 'supportsScopeTags', 'createdDateTime', '@odata.id', '@odata.editLink', 'lastModifiedDateTime@odata.type', 'roleScopeTagIds@odata.type', createdDateTime, 'createdDateTime@odata.type' Remove-ODataProperties -Object $RawJson $RawJson = $RawJson | ConvertTo-Json -Depth 100 -Compress @@ -288,6 +303,6 @@ function Import-CommunityTemplate { Write-Warning $StatusMessage Write-Information $_.InvocationInfo.PositionMessage } - + return $StatusMessage } diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ExecQuarantineManagement.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ExecQuarantineManagement.ps1 index deb23870dd96..44f942384722 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ExecQuarantineManagement.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ExecQuarantineManagement.ps1 @@ -18,14 +18,19 @@ function Invoke-ExecQuarantineManagement { if ($ActionType -eq 'Release') { $params['ReleaseToAll'] = $true + if ($Request.Body.Identity -is [string]) { + $params['Identity'] = $Request.Body.Identity + } else { + $params['Identities'] = $Request.Body.Identity + $params['Identity'] = '000' + } } else { $params['ActionType'] = $ActionType - } - - if ($Request.Body.Identity -is [string]) { - $params['Identity'] = $Request.Body.Identity - } else { - $params['Identities'] = $Request.Body.Identity + if ($Request.Body.Identity -is [string]) { + $params['Identities'] = @($Request.Body.Identity) + } else { + $params['Identities'] = $Request.Body.Identity + } $params['Identity'] = '000' } New-ExoRequest -tenantid $TenantFilter -cmdlet 'Release-QuarantineMessage' -cmdParams $params diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddOfficeApp.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddOfficeApp.ps1 index ff880318819f..0187c47e1f53 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddOfficeApp.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddOfficeApp.ps1 @@ -19,8 +19,21 @@ function Invoke-AddOfficeApp { try { $ExistingO365 = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/deviceAppManagement/mobileApps' -tenantid $Tenant | Where-Object { $_.displayName -eq 'Microsoft 365 Apps for Windows 10 and later' } if (!$ExistingO365) { - # Check if custom XML is provided - if ($Request.Body.useCustomXml -and $Request.Body.customXml) { + # Check if this is a template deployment with IntuneBody (saved from existing app) + if ($Request.Body.IntuneBody) { + $IntuneBody = $Request.Body.IntuneBody + if ($IntuneBody -is [string]) { + $IntuneBody = $IntuneBody | ConvertFrom-Json -Depth 100 + } + # Remove read-only properties that the Graph API won't accept on create + $ReadOnlyProps = @('id', 'createdDateTime', 'lastModifiedDateTime', 'uploadState', 'publishingState', 'isAssigned', 'roleScopeTagIds', 'dependentAppCount', 'supersedingAppCount', 'supersededAppCount', 'committedContentVersion', 'fileName', 'size') + foreach ($prop in $ReadOnlyProps) { + if ($IntuneBody.PSObject.Properties[$prop]) { + $IntuneBody.PSObject.Properties.Remove($prop) + } + } + $ObjBody = $IntuneBody + } elseif ($Request.Body.useCustomXml -and $Request.Body.customXml) { # Use custom XML configuration $ObjBody = [pscustomobject]@{ '@odata.type' = '#microsoft.graph.officeSuiteApp' @@ -76,7 +89,7 @@ function Invoke-AddOfficeApp { 'officeSuiteAppDefaultFileFormat' = 'OfficeOpenXMLFormat' 'localesToInstall' = @($Request.Body.languages.value) 'shouldUninstallOlderVersionsOfOffice' = [bool]$Request.Body.RemoveVersions - 'updateChannel' = $Request.Body.updateChannel.value + 'updateChannel' = if ($Request.Body.updateChannel.value) { $Request.Body.updateChannel.value } else { $Request.Body.updateChannel } 'useSharedComputerActivation' = [bool]$Request.Body.SharedComputerActivation 'productIds' = $products 'largeIcon' = @{ diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ExecCompareIntunePolicy.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ExecCompareIntunePolicy.ps1 index 09158ce20a60..26f924e6e195 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ExecCompareIntunePolicy.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ExecCompareIntunePolicy.ps1 @@ -157,7 +157,8 @@ function Invoke-ExecCompareIntunePolicy { } # Run the comparison - $ComparisonResults = @(Compare-CIPPIntuneObject @CompareParams) + $CompareResult = Compare-CIPPIntuneObject @CompareParams + $ComparisonResults = if ($null -eq $CompareResult) { @() } else { @($CompareResult) } $ResultBody = @{ Results = $ComparisonResults diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntunePolicy.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntunePolicy.ps1 index fd199a71aa7a..c01f718c2a5f 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntunePolicy.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntunePolicy.ps1 @@ -12,12 +12,14 @@ function Invoke-ListIntunePolicy { $TenantFilter = $Request.Query.TenantFilter $id = $Request.Query.ID $URLName = $Request.Query.URLName + $DefinitionIds = $Request.Query.DefinitionIds $UseReportDB = $Request.Query.UseReportDB $IncludeSettingDefinitions = [System.Convert]::ToBoolean($Request.Query.IncludeSettingDefinitions ?? 'false') + $IsGroupPolicyDefinitionLookup = ($URLName -ieq 'GroupPolicyDefinitions') -and -not [string]::IsNullOrWhiteSpace($DefinitionIds) try { # Return cached report data when AllTenants is requested or UseReportDB is set - if ($TenantFilter -eq 'AllTenants' -or $UseReportDB -eq 'true') { + if (-not $IsGroupPolicyDefinitionLookup -and ($TenantFilter -eq 'AllTenants' -or $UseReportDB -eq 'true')) { try { $GraphRequest = Get-CIPPIntunePolicyReport -TenantFilter $TenantFilter -ErrorAction Stop $StatusCode = [HttpStatusCode]::OK @@ -31,8 +33,30 @@ function Invoke-ListIntunePolicy { }) } - if ($ID) { - if ($URLName -ieq 'ConfigurationPolicies' -or $URLName -ieq 'configurationPolicies') { + if ($IsGroupPolicyDefinitionLookup) { + $DefinitionIdList = @($DefinitionIds -split ',' | ForEach-Object { $_.Trim() } | Where-Object { Test-IsGuid -String $_ } | Select-Object -Unique) + + if ($DefinitionIdList.Count -eq 0) { + $GraphRequest = @() + } else { + $DefinitionRequests = [System.Collections.Generic.List[object]]::new() + $DefinitionIndex = 0 + + foreach ($DefinitionId in $DefinitionIdList) { + $RequestId = "definition$DefinitionIndex" + $DefinitionRequests.Add([PSCustomObject]@{ + id = $RequestId + method = 'GET' + url = "/deviceManagement/groupPolicyDefinitions('$DefinitionId')?`$expand=presentations" + }) + $DefinitionIndex++ + } + + $DefinitionResults = New-GraphBulkRequest -Requests @($DefinitionRequests) -tenantid $TenantFilter + $GraphRequest = $DefinitionResults | Where-Object { $_.status -eq 200 -and $_.body.id } | ForEach-Object { $_.body } + } + } elseif ($ID) { + if ($URLName -ieq 'ConfigurationPolicies') { $GraphRequest = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/configurationPolicies('$ID')?`$expand=settings" -tenantid $TenantFilter if ($IncludeSettingDefinitions -and $GraphRequest.settings) { @@ -65,6 +89,47 @@ function Invoke-ListIntunePolicy { } } } + } elseif ($URLName -ieq 'GroupPolicyConfigurations') { + $GraphRequest = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/groupPolicyConfigurations('$ID')" -tenantid $TenantFilter + + if ($IncludeSettingDefinitions) { + $DefinitionValuesResponse = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/groupPolicyConfigurations('$ID')/definitionValues?`$expand=definition" -tenantid $TenantFilter + $DefinitionValues = @($DefinitionValuesResponse.value ?? $DefinitionValuesResponse) + $GraphRequest | Add-Member -NotePropertyName definitionValues -NotePropertyValue $DefinitionValues -Force + + if ($DefinitionValues.Count -gt 0) { + $PresentationRequests = [System.Collections.Generic.List[object]]::new() + $DefinitionValueLookup = @{} + $DefinitionValueIdMap = @{} + $DefinitionValueIndex = 0 + + foreach ($DefinitionValue in $DefinitionValues) { + if ($DefinitionValue.id) { + $RequestId = "definitionValue$DefinitionValueIndex" + $DefinitionValueIdMap[$RequestId] = $DefinitionValue.id + $DefinitionValueLookup[$DefinitionValue.id] = $DefinitionValue + $PresentationRequests.Add([PSCustomObject]@{ + id = $RequestId + method = 'GET' + url = "/deviceManagement/groupPolicyConfigurations('$ID')/definitionValues('$($DefinitionValue.id)')/presentationValues?`$expand=presentation" + }) + $DefinitionValueIndex++ + } + } + + if ($PresentationRequests.Count -gt 0) { + $PresentationResults = New-GraphBulkRequest -Requests @($PresentationRequests) -tenantid $TenantFilter + foreach ($PresentationResult in $PresentationResults) { + $DefinitionValueId = $DefinitionValueIdMap[$PresentationResult.id] + $DefinitionValue = $DefinitionValueLookup[$DefinitionValueId] + if ($DefinitionValue) { + $PresentationValues = @($PresentationResult.body.value ?? $PresentationResult.body) + $DefinitionValue | Add-Member -NotePropertyName presentationValues -NotePropertyValue $PresentationValues -Force + } + } + } + } + } } else { $GraphRequest = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$($URLName)('$ID')" -tenantid $TenantFilter } diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddJITAdminTemplate.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddJITAdminTemplate.ps1 index 141a2754cf0b..0f12d80773df 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddJITAdminTemplate.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddJITAdminTemplate.ps1 @@ -112,8 +112,8 @@ function Invoke-AddJITAdminTemplate { if (![string]::IsNullOrWhiteSpace($Request.Body.defaultUserName)) { $TemplateObject.defaultUserName = $Request.Body.defaultUserName } - if (![string]::IsNullOrWhiteSpace($Request.Body.defaultUsageLocation)) { - $TemplateObject.defaultUsageLocation = $Request.Body.defaultUsageLocation + if ($Request.Body.defaultUsageLocation) { + $TemplateObject.defaultUsageLocation = $Request.Body.defaultUsageLocation.value ?? $Request.Body.defaultUsageLocation } # defaultDomain is only saved for specific tenant templates (not AllTenants) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditJITAdminTemplate.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditJITAdminTemplate.ps1 index b3282e64647b..4abb488e7f03 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditJITAdminTemplate.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditJITAdminTemplate.ps1 @@ -130,8 +130,8 @@ function Invoke-EditJITAdminTemplate { if (![string]::IsNullOrWhiteSpace($Request.Body.defaultUserName)) { $TemplateObject.defaultUserName = $Request.Body.defaultUserName } - if (![string]::IsNullOrWhiteSpace($Request.Body.defaultUsageLocation)) { - $TemplateObject.defaultUsageLocation = $Request.Body.defaultUsageLocation + if ($Request.Body.defaultUsageLocation) { + $TemplateObject.defaultUsageLocation = $Request.Body.defaultUsageLocation.value ?? $Request.Body.defaultUsageLocation } # defaultDomain is only saved for specific tenant templates (not AllTenants) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditUser.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditUser.ps1 index b6f95c20f112..417b90b22cc6 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditUser.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditUser.ps1 @@ -242,13 +242,13 @@ function Invoke-EditUser { } if ($Request.body.setManager.value) { - $ManagerResult = Set-CIPPManager -User $UserPrincipalName -Manager $Request.body.setManager.value -TenantFilter $UserObj.tenantFilter -Headers $Headers - $Results.Add($ManagerResult) + $ManagerResults = Set-CIPPManager -Users $UserPrincipalName -Manager $Request.body.setManager.value -TenantFilter $UserObj.tenantFilter -Headers $Headers + $Results.Add($ManagerResults.Result) } if ($Request.body.setSponsor.value) { - $SponsorResult = Set-CIPPSponsor -User $UserPrincipalName -Sponsor $Request.body.setSponsor.value -TenantFilter $UserObj.tenantFilter -Headers $Headers - $Results.Add($SponsorResult) + $SponsorResults = Set-CIPPSponsor -Users $UserPrincipalName -Sponsor $Request.body.setSponsor.value -TenantFilter $UserObj.tenantFilter -Headers $Headers + $Results.Add($SponsorResults.Result) } return ([HttpResponseContext]@{ diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecJITAdmin.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecJITAdmin.ps1 index 2f522cc0a66d..9233c506da6a 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecJITAdmin.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecJITAdmin.ps1 @@ -63,7 +63,7 @@ function Invoke-ExecJITAdmin { 'FirstName' = $Request.Body.FirstName 'LastName' = $Request.Body.LastName 'UserPrincipalName' = $Username - 'UsageLocation' = $Request.Body.usageLocation + 'UsageLocation' = $Request.Body.usageLocation.value ?? $Request.Body.usageLocation } Expiration = $Expiration StartDate = $Start @@ -129,14 +129,42 @@ function Invoke-ExecJITAdmin { #Region TAP creation if ($Request.Body.UseTAP) { try { - if ($Start -gt (Get-Date)) { - $TapParams = @{ - startDateTime = [System.DateTimeOffset]::FromUnixTimeSeconds($Request.Body.StartDate).DateTime + $LifetimeMinutes = $null + $RequestedMinutes = $null + $ParsedRequestLifetime = $false + if (![string]::IsNullOrWhiteSpace($Request.Body.tapLifetimeInMinutes)) { + try { + $RequestedMinutes = [int]$Request.Body.tapLifetimeInMinutes + $ParsedRequestLifetime = $true + } catch { + Write-Warning "Failed to parse TAP lifetime from request: $($_.Exception.Message)" + } + } + + if ($null -eq $RequestedMinutes) { + $RequestedMinutes = [int](($Expiration - $Start).TotalMinutes) + } + + try { + $Policy = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies/authenticationMethodsPolicy/authenticationMethodConfigurations/TemporaryAccessPass' -tenantid $TenantFilter + $PolicyMax = [int]($Policy.maximumLifetimeInMinutes ?? 1440) + $PolicyMin = [Math]::Min([int]($Policy.minimumLifetimeInMinutes ?? 1), $PolicyMax) + $LifetimeMinutes = [Math]::Min([Math]::Max($RequestedMinutes, $PolicyMin), $PolicyMax) + } catch { + Write-Warning "Failed to determine TAP lifetime from policy: $($_.Exception.Message)" + if ($ParsedRequestLifetime) { + $LifetimeMinutes = $RequestedMinutes } - $TapBody = ConvertTo-Json -Depth 5 -InputObject $TapParams - } else { - $TapBody = '{}' } + + $TapParams = @{} + if ($Start -gt (Get-Date)) { + $TapParams.startDateTime = [System.DateTimeOffset]::FromUnixTimeSeconds($Request.Body.StartDate).DateTime + } + if ($LifetimeMinutes -gt 0) { + $TapParams.lifetimeInMinutes = [int]$LifetimeMinutes + } + $TapBody = if ($TapParams.Count) { ConvertTo-Json -Depth 5 -InputObject $TapParams } else { '{}' } # Write-Information "https://graph.microsoft.com/beta/users/$Username/authentication/temporaryAccessPassMethods" # Retry creating the TAP up to 10 times, since it can fail due to the user not being fully created yet. Sometimes it takes 2 reties, sometimes it takes 8+. Very annoying. -Bobby $Retries = 0 diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-PatchUser.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-PatchUser.ps1 index c4c269b9c338..b71874b17ca5 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-PatchUser.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-PatchUser.ps1 @@ -14,7 +14,7 @@ function Invoke-PatchUser { $HttpResponse = [HttpResponseContext]@{ StatusCode = [HttpStatusCode]::OK - Body = @{'Results' = @("Default response, you should never see this.") } + Body = @{'Results' = @('Default response, you should never see this.') } } try { @@ -36,22 +36,37 @@ function Invoke-PatchUser { # Group users by tenant filter $UsersByTenant = $Users | Group-Object -Property tenantFilter - $TotalSuccessCount = 0 - $AllErrorMessages = @() + $TotalPatchSuccessCount = 0 + $TotalManagerSuccessCount = 0 + $TotalSponsorSuccessCount = 0 + $AllErrorMessages = [System.Collections.Generic.List[string]]::new() + $HasManagerUpdates = @($Users | Where-Object { -not [string]::IsNullOrWhiteSpace($_.manager) }).Count -gt 0 + $HasSponsorUpdates = @($Users | Where-Object { -not [string]::IsNullOrWhiteSpace($_.sponsor) }).Count -gt 0 + $HasRelationshipUpdates = $HasManagerUpdates -or $HasSponsorUpdates # Process each tenant separately foreach ($TenantGroup in $UsersByTenant) { $tenantFilter = $TenantGroup.Name $TenantUsers = $TenantGroup.Group + $UsersWithManager = $TenantUsers | Where-Object { -not [string]::IsNullOrWhiteSpace($_.manager) } + $ManagerGroups = $UsersWithManager | Group-Object -Property manager + $UsersWithSponsor = $TenantUsers | Where-Object { -not [string]::IsNullOrWhiteSpace($_.sponsor) } + $SponsorGroups = $UsersWithSponsor | Group-Object -Property sponsor # Build bulk requests for this tenant $int = 0 - $BulkRequests = foreach ($User in $TenantUsers) { - # Remove the id and tenantFilter properties from the body since they're not user properties - $PatchBody = $User | Select-Object -Property * -ExcludeProperty id, tenantFilter + $BulkRequests = [System.Collections.Generic.List[object]]::new() + $BulkRequestUsers = [System.Collections.Generic.List[object]]::new() + foreach ($User in $TenantUsers) { + # Remove routing and relationship properties from the body since they're not normal PATCH properties. + $PatchBody = $User | Select-Object -Property * -ExcludeProperty id, tenantFilter, manager, sponsor - @{ - id = $int++ + if (@($PatchBody.PSObject.Properties).Count -eq 0) { + continue + } + + $BulkRequest = @{ + id = ($int++).ToString() method = 'PATCH' url = "users/$($User.id)" body = $PatchBody @@ -59,38 +74,107 @@ function Invoke-PatchUser { 'Content-Type' = 'application/json' } } + [void]$BulkRequests.Add($BulkRequest) + [void]$BulkRequestUsers.Add($User) } # Execute bulk request for this tenant - $BulkResults = New-GraphBulkRequest -tenantid $tenantFilter -Requests @($BulkRequests) - - # Process results for this tenant - for ($i = 0; $i -lt $BulkResults.Count; $i++) { - $result = $BulkResults[$i] - $user = $TenantUsers[$i] - - if ($result.status -eq 200 -or $result.status -eq 204) { - $TotalSuccessCount++ - Write-LogMessage -headers $Headers -API $APIName -tenant $tenantFilter -message "Successfully patched user $($user.id)" -Sev 'Info' - } else { - $errorMsg = if ($result.body.error.message) { - $result.body.error.message + if ($BulkRequests.Count -gt 0) { + $BulkResults = New-GraphBulkRequest -tenantid $tenantFilter -Requests ($BulkRequests.ToArray()) + + # Process results for this tenant + foreach ($BulkResult in @($BulkResults)) { + $ResultIndex = [int]$BulkResult.id + $User = $BulkRequestUsers[$ResultIndex] + + if ($BulkResult.status -eq 200 -or $BulkResult.status -eq 204) { + $TotalPatchSuccessCount++ + Write-LogMessage -headers $Headers -API $APIName -tenant $tenantFilter -message "Successfully patched user $($User.id)" -Sev 'Info' } else { - "Unknown error (Status: $($result.status))" + $ErrorMessage = if ($BulkResult.body.error.message) { + $BulkResult.body.error.message + } else { + "Unknown error (Status: $($BulkResult.status))" + } + [void]$AllErrorMessages.Add("Failed to patch user $($User.id) in tenant $($tenantFilter): $ErrorMessage") + Write-LogMessage -headers $Headers -API $APIName -tenant $tenantFilter -message "Failed to patch user $($User.id). Error: $ErrorMessage" -Sev 'Error' + } + } + } + + foreach ($ManagerGroup in $ManagerGroups) { + $UserIds = @($ManagerGroup.Group | ForEach-Object { $_.id }) + $ManagerUpn = $ManagerGroup.Name + + try { + $ManagerResults = Set-CIPPManager -Users $UserIds -Manager $ManagerUpn -TenantFilter $tenantFilter -Headers $Headers -APIName $APIName + + foreach ($ManagerResult in @($ManagerResults)) { + if ($ManagerResult.Success) { + $TotalManagerSuccessCount++ + } else { + [void]$AllErrorMessages.Add("Failed to set manager for $($ManagerResult.User) in tenant $($tenantFilter): $($ManagerResult.Result)") + } + } + } catch { + foreach ($UserId in $UserIds) { + [void]$AllErrorMessages.Add("Failed to set manager for $UserId in tenant $($tenantFilter): $($_.Exception.Message)") + } + } + } + + foreach ($SponsorGroup in $SponsorGroups) { + $UserIds = @($SponsorGroup.Group | ForEach-Object { $_.id }) + $SponsorUpn = $SponsorGroup.Name + + try { + $SponsorResults = Set-CIPPSponsor -Users $UserIds -Sponsor $SponsorUpn -TenantFilter $tenantFilter -Headers $Headers -APIName $APIName + + foreach ($SponsorResult in @($SponsorResults)) { + if ($SponsorResult.Success) { + $TotalSponsorSuccessCount++ + } else { + [void]$AllErrorMessages.Add("Failed to set sponsor for $($SponsorResult.User) in tenant $($tenantFilter): $($SponsorResult.Result)") + } + } + } catch { + foreach ($UserId in $UserIds) { + [void]$AllErrorMessages.Add("Failed to set sponsor for $UserId in tenant $($tenantFilter): $($_.Exception.Message)") } - $AllErrorMessages += "Failed to patch user $($user.id) in tenant $($tenantFilter): $errorMsg" - Write-LogMessage -headers $Headers -API $APIName -tenant $tenantFilter -message "Failed to patch user $($user.id). Error: $errorMsg" -Sev 'Error' } } } # Build final response + $TenantCount = ($Users | Select-Object -Property tenantFilter -Unique).Count + $RelationshipResults = [System.Collections.Generic.List[string]]::new() + if ($HasManagerUpdates) { + [void]$RelationshipResults.Add("$TotalManagerSuccessCount manager assignment$(if($TotalManagerSuccessCount -ne 1){'s'})") + } + if ($HasSponsorUpdates) { + [void]$RelationshipResults.Add("$TotalSponsorSuccessCount sponsor assignment$(if($TotalSponsorSuccessCount -ne 1){'s'})") + } + $RelationshipResultMessage = [string]::Join(' and ', $RelationshipResults.ToArray()) + + $SuccessMessage = if ($HasRelationshipUpdates -and $TotalPatchSuccessCount -gt 0) { + "Successfully patched $TotalPatchSuccessCount user$(if($TotalPatchSuccessCount -ne 1){'s'}) and updated $RelationshipResultMessage across $TenantCount tenant$(if($TenantCount -ne 1){'s'})" + } elseif ($HasRelationshipUpdates) { + "Successfully updated $RelationshipResultMessage across $TenantCount tenant$(if($TenantCount -ne 1){'s'})" + } else { + "Successfully patched $TotalPatchSuccessCount user$(if($TotalPatchSuccessCount -ne 1){'s'}) across $TenantCount tenant$(if($TenantCount -ne 1){'s'})" + } + if ($AllErrorMessages.Count -eq 0) { - $TenantCount = ($Users | Select-Object -Property tenantFilter -Unique).Count - $HttpResponse.Body = @{'Results' = @("Successfully patched $TotalSuccessCount user$(if($TotalSuccessCount -ne 1){'s'}) across $TenantCount tenant$(if($TenantCount -ne 1){'s'})") } + $HttpResponse.Body = @{'Results' = @($SuccessMessage) } } else { $HttpResponse.StatusCode = [HttpStatusCode]::BadRequest - $HttpResponse.Body = @{'Results' = $AllErrorMessages + @("Successfully patched $TotalSuccessCount of $($Users.Count) users") } + $PartialSuccessMessage = if ($HasRelationshipUpdates) { $SuccessMessage } else { "Successfully patched $TotalPatchSuccessCount of $($Users.Count) users" } + $Results = [System.Collections.Generic.List[string]]::new() + foreach ($ErrorMessage in $AllErrorMessages) { + [void]$Results.Add($ErrorMessage) + } + [void]$Results.Add($PartialSuccessMessage) + $HttpResponse.Body = @{'Results' = @($Results.ToArray()) } } } @@ -100,4 +184,4 @@ function Invoke-PatchUser { } return $HttpResponse -} \ No newline at end of file +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListCSPsku.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListCSPsku.ps1 index 094f651314e2..b317057f1870 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListCSPsku.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListCSPsku.ps1 @@ -19,11 +19,16 @@ function Invoke-ListCSPsku { } $StatusCode = [HttpStatusCode]::OK } catch { - $GraphRequest = [PSCustomObject]@{ - name = @(@{value = 'Error getting catalog' }) - sku = $_.Exception.Message + if ($_.Exception.Message -eq 'No Sherweb mapping found') { + $GraphRequest = @() + $StatusCode = [HttpStatusCode]::OK + } else { + $GraphRequest = [PSCustomObject]@{ + name = @(@{value = 'Error getting catalog' }) + sku = $_.Exception.Message + } + $StatusCode = [HttpStatusCode]::InternalServerError } - $StatusCode = [HttpStatusCode]::InternalServerError } return [HttpResponseContext]@{ diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListResellerRelationshipLink.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListResellerRelationshipLink.ps1 new file mode 100644 index 000000000000..1d48e1a8591e --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListResellerRelationshipLink.ps1 @@ -0,0 +1,37 @@ +function Invoke-ListResellerRelationshipLink { + <# + .FUNCTIONALITY + Entrypoint,AnyTenant + .ROLE + Tenant.Relationship.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $StatusCode = [HttpStatusCode]::OK + $Body = @{} + + # Get the indirect reseller relationship invite link + try { + $RelationshipRequest = New-GraphGetRequest -uri 'https://api.partnercenter.microsoft.com/v1/customers/relationshiprequests?dualRoleIndirectRelationship=false' -scope 'https://api.partnercenter.microsoft.com/.default' -NoAuthCheck $true + $Body.inviteUrl = $RelationshipRequest.url + } catch { + $Body.inviteUrl = $null + $Body.inviteUrlError = "Failed to retrieve relationship invite link: $($_.Exception.Message)" + Write-Information "ListResellerRelationshipLink: Failed to get invite URL - $($_.Exception.Message)" + } + + # Get indirect providers (for Tier 2 / indirect resellers) + try { + $RelationshipsResponse = New-GraphGetRequest -uri 'https://api.partnercenter.microsoft.com/v1/relationships?relationship_type=IsIndirectResellerOf' -scope 'https://api.partnercenter.microsoft.com/.default' -NoAuthCheck $true + $Body.indirectProviders = @($RelationshipsResponse.items) + } catch { + $Body.indirectProviders = @() + Write-Information "ListResellerRelationshipLink: Failed to get indirect providers - $($_.Exception.Message)" + } + + return [HttpResponseContext]@{ + StatusCode = $StatusCode + Body = $Body + } +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 index 981a6b02a64e..4618714eed85 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 @@ -212,6 +212,7 @@ function Invoke-ListTests { Custom = @{ Passed = @($CustomResultsForCounts | Where-Object { $_.Status -eq 'Passed' }).Count Failed = @($CustomResultsForCounts | Where-Object { $_.Status -eq 'Failed' }).Count + NeedsAttention = @($CustomResultsForCounts | Where-Object { $_.Status -eq 'Investigate' }).Count Skipped = @($CustomResultsForCounts | Where-Object { $_.Status -eq 'Skipped' }).Count Informational = @($CustomResultsForCounts | Where-Object { $_.Status -eq 'Informational' }).Count Total = $CustomTotal diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-DLP/Invoke-AddDlpCompliancePolicy.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-DLP/Invoke-AddDlpCompliancePolicy.ps1 new file mode 100644 index 000000000000..c251373790d7 --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-DLP/Invoke-AddDlpCompliancePolicy.ps1 @@ -0,0 +1,48 @@ +Function Invoke-AddDlpCompliancePolicy { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Security.DlpCompliancePolicy.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + + $RequestParams = $Request.Body.PowerShellCommand | ConvertFrom-Json | Select-Object -Property * -ExcludeProperty GUID, comments, RuleParams + $RuleParams = ($Request.Body.PowerShellCommand | ConvertFrom-Json).RuleParams + + $Tenants = ($Request.Body.selectedTenants).value + $Result = foreach ($TenantFilter in $Tenants) { + try { + $PolicyParams = $RequestParams | Select-Object -Property * -ExcludeProperty RuleParams + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'New-DlpCompliancePolicy' -cmdParams $PolicyParams -Compliance -useSystemMailbox $true + + if ($RuleParams) { + # Ensure rule references the new policy + $RuleHash = @{} + $RuleParams.PSObject.Properties | ForEach-Object { $RuleHash[$_.Name] = $_.Value } + $RuleHash['Policy'] = $RequestParams.Name + if (-not $RuleHash.ContainsKey('Name') -or [string]::IsNullOrWhiteSpace($RuleHash['Name'])) { + $RuleHash['Name'] = "$($RequestParams.Name) Rule" + } + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'New-DlpComplianceRule' -cmdParams $RuleHash -Compliance -useSystemMailbox $true + } + + "Successfully created DLP compliance policy $($RequestParams.Name) for $TenantFilter." + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Successfully created DLP compliance policy $($RequestParams.Name) for $TenantFilter." -sev Info + } catch { + $ErrorMessage = Get-CippException -Exception $_ + "Could not create DLP compliance policy for $($TenantFilter): $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Could not create DLP compliance policy for $($TenantFilter): $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + } + } + + return ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = @{Results = @($Result) } + }) + +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-DLP/Invoke-AddDlpCompliancePolicyTemplate.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-DLP/Invoke-AddDlpCompliancePolicyTemplate.ps1 new file mode 100644 index 000000000000..911c7817935f --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-DLP/Invoke-AddDlpCompliancePolicyTemplate.ps1 @@ -0,0 +1,49 @@ +Function Invoke-AddDlpCompliancePolicyTemplate { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Security.DlpCompliancePolicy.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + + try { + $GUID = (New-Guid).GUID + $JSON = if ($Request.Body.PowerShellCommand) { + $Request.Body.PowerShellCommand | ConvertFrom-Json + } else { + ([pscustomobject]$Request.Body | Select-Object Name, Comment, Mode, Workload, Enabled, ExchangeLocation, ExchangeSenderMemberOf, ExchangeSenderMemberOfException, SharePointLocation, SharePointLocationException, OneDriveLocation, OneDriveLocationException, TeamsLocation, TeamsLocationException, EndpointDlpLocation, EndpointDlpLocationException, OnPremisesScannerDlpLocation, OnPremisesScannerDlpLocationException, ThirdPartyAppDlpLocation, ThirdPartyAppDlpLocationException, PowerBIDlpLocation, PowerBIDlpLocationException, RuleParams) | ForEach-Object { + $NonEmptyProperties = $_.PSObject.Properties | Where-Object { $null -ne $_.Value } | Select-Object -ExpandProperty Name + $_ | Select-Object -Property $NonEmptyProperties + } + } + + # Allow Name to be sourced from displayName/name fields and ensure templated comments preserved + $JSON = ($JSON | Select-Object @{n = 'name'; e = { $_.Name ?? $_.name } }, @{n = 'comments'; e = { $_.Comment ?? $_.comments } }, * | ConvertTo-Json -Depth 10) + $Table = Get-CippTable -tablename 'templates' + $Table.Force = $true + Add-CIPPAzDataTableEntity @Table -Entity @{ + JSON = "$JSON" + RowKey = "$GUID" + PartitionKey = 'DlpCompliancePolicyTemplate' + } + $Result = "Successfully created DLP Compliance Policy Template: $($Request.Body.Name ?? $Request.Body.name) with GUID $GUID" + Write-LogMessage -headers $Headers -API $APIName -message $Result -Sev 'Debug' + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed to create DLP Compliance Policy Template: $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -message $Result -Sev 'Error' -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::InternalServerError + } + + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{Results = $Result } + }) + +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-DLP/Invoke-EditDlpCompliancePolicy.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-DLP/Invoke-EditDlpCompliancePolicy.ps1 new file mode 100644 index 000000000000..c0e8daee0627 --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-DLP/Invoke-EditDlpCompliancePolicy.ps1 @@ -0,0 +1,48 @@ +Function Invoke-EditDlpCompliancePolicy { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Security.DlpCompliancePolicy.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter + $Identity = $Request.Query.Identity ?? $Request.Body.Identity ?? $Request.Body.Name + $State = $Request.Query.State ?? $Request.Body.State + + try { + $Params = @{ + Identity = $Identity + } + + # If a state was passed, toggle Enabled on the policy + if ($State) { + $Params['Enabled'] = ($State -eq 'enable' -or $State -eq $true -or $State -eq 'true') + } + + # Allow passing arbitrary additional params via Body.parameters + if ($Request.Body.parameters) { + $Request.Body.parameters.PSObject.Properties | ForEach-Object { + if ($_.Name -ne 'Identity') { $Params[$_.Name] = $_.Value } + } + } + + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Set-DlpCompliancePolicy' -cmdParams $Params -Compliance -useSystemMailbox $true + $Result = "Updated DLP compliance policy $Identity" + Write-LogMessage -Headers $Request.Headers -API $APIName -tenant $TenantFilter -message $Result -Sev Info + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed updating DLP compliance policy $Identity. Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -Headers $Request.Headers -API $APIName -tenant $TenantFilter -message $Result -Sev Error -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::Forbidden + } + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{Results = $Result } + }) + +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-DLP/Invoke-ListDlpCompliancePolicy.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-DLP/Invoke-ListDlpCompliancePolicy.ps1 new file mode 100644 index 000000000000..26fcd8eafb9e --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-DLP/Invoke-ListDlpCompliancePolicy.ps1 @@ -0,0 +1,30 @@ +Function Invoke-ListDlpCompliancePolicy { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Security.DlpCompliancePolicy.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + $TenantFilter = $Request.Query.tenantFilter + + try { + $Policies = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-DlpCompliancePolicy' -Compliance | Select-Object * -ExcludeProperty *odata*, *data.type* + $Rules = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-DlpComplianceRule' -Compliance | Select-Object * -ExcludeProperty *odata*, *data.type* + $GraphRequest = $Policies | Select-Object *, + @{l = 'AssociatedRules'; e = { $name = $_.Name; @($Rules | Where-Object { $_.ParentPolicyName -eq $name }) } }, + @{l = 'RuleCount'; e = { $name = $_.Name; (@($Rules | Where-Object { $_.ParentPolicyName -eq $name })).Count } } + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + $StatusCode = [HttpStatusCode]::Forbidden + $GraphRequest = $ErrorMessage + } + + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @($GraphRequest) + }) + +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-DLP/Invoke-ListDlpCompliancePolicyTemplates.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-DLP/Invoke-ListDlpCompliancePolicyTemplates.ps1 new file mode 100644 index 000000000000..a958e9acd2ee --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-DLP/Invoke-ListDlpCompliancePolicyTemplates.ps1 @@ -0,0 +1,26 @@ +Function Invoke-ListDlpCompliancePolicyTemplates { + <# + .FUNCTIONALITY + Entrypoint,AnyTenant + .ROLE + Security.DlpCompliancePolicy.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + $Table = Get-CippTable -tablename 'templates' + $Filter = "PartitionKey eq 'DlpCompliancePolicyTemplate'" + $Templates = (Get-CIPPAzDataTableEntity @Table -Filter $Filter) | ForEach-Object { + $GUID = $_.RowKey + $data = $_.JSON | ConvertFrom-Json + $data | Add-Member -NotePropertyName 'GUID' -NotePropertyValue $GUID -Force + $data + } + + if ($Request.Query.ID) { $Templates = $Templates | Where-Object -Property GUID -EQ $Request.Query.ID } + + return ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = @($Templates) + }) + +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-DLP/Invoke-RemoveDlpCompliancePolicy.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-DLP/Invoke-RemoveDlpCompliancePolicy.ps1 new file mode 100644 index 000000000000..4af00a520304 --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-DLP/Invoke-RemoveDlpCompliancePolicy.ps1 @@ -0,0 +1,36 @@ +Function Invoke-RemoveDlpCompliancePolicy { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Security.DlpCompliancePolicy.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + + $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter + $Identity = $Request.Query.Identity ?? $Request.Body.Identity ?? $Request.Body.Name + + try { + $Params = @{ + Identity = $Identity + } + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Remove-DlpCompliancePolicy' -cmdParams $Params -Compliance -useSystemMailbox $true + $Result = "Deleted DLP compliance policy $Identity" + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev Info + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed to delete DLP compliance policy $Identity - $($ErrorMessage.NormalizedError)" + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev Error -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::Forbidden + } + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{Results = $Result } + }) + +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-DLP/Invoke-RemoveDlpCompliancePolicyTemplate.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-DLP/Invoke-RemoveDlpCompliancePolicyTemplate.ps1 new file mode 100644 index 000000000000..9504141526e5 --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-DLP/Invoke-RemoveDlpCompliancePolicyTemplate.ps1 @@ -0,0 +1,36 @@ +Function Invoke-RemoveDlpCompliancePolicyTemplate { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Security.DlpCompliancePolicy.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + + $ID = $Request.Body.ID ?? $Request.Query.ID + try { + $Table = Get-CippTable -tablename 'templates' + $SafeID = ConvertTo-CIPPODataFilterValue -Value $ID -Type Guid + $Filter = "PartitionKey eq 'DlpCompliancePolicyTemplate' and RowKey eq '$SafeID'" + $ClearRow = Get-CIPPAzDataTableEntity @Table -Filter $Filter -Property PartitionKey, RowKey + Remove-AzDataTableEntity -Force @Table -Entity $ClearRow + $Result = "Removed DLP Compliance Policy template with ID $ID" + Write-LogMessage -Headers $Headers -API $APIName -message $Result -Sev 'Info' + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed to remove DLP Compliance Policy template $ID. $($ErrorMessage.NormalizedError)" + Write-LogMessage -Headers $Headers -API $APIName -message $Result -Sev 'Error' -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::Forbidden + } + + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{'Results' = $Result } + }) + +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-Retention/Invoke-AddRetentionCompliancePolicy.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-Retention/Invoke-AddRetentionCompliancePolicy.ps1 new file mode 100644 index 000000000000..fec42ad1649a --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-Retention/Invoke-AddRetentionCompliancePolicy.ps1 @@ -0,0 +1,47 @@ +Function Invoke-AddRetentionCompliancePolicy { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Security.RetentionCompliancePolicy.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + + $RequestParams = $Request.Body.PowerShellCommand | ConvertFrom-Json | Select-Object -Property * -ExcludeProperty GUID, comments, RuleParams + $RuleParams = ($Request.Body.PowerShellCommand | ConvertFrom-Json).RuleParams + + $Tenants = ($Request.Body.selectedTenants).value + $Result = foreach ($TenantFilter in $Tenants) { + try { + $PolicyParams = $RequestParams | Select-Object -Property * -ExcludeProperty RuleParams + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'New-RetentionCompliancePolicy' -cmdParams $PolicyParams -Compliance -AsApp -useSystemMailbox $true + + if ($RuleParams) { + $RuleHash = @{} + $RuleParams.PSObject.Properties | ForEach-Object { $RuleHash[$_.Name] = $_.Value } + $RuleHash['Policy'] = $RequestParams.Name + if (-not $RuleHash.ContainsKey('Name') -or [string]::IsNullOrWhiteSpace($RuleHash['Name'])) { + $RuleHash['Name'] = "$($RequestParams.Name) Rule" + } + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'New-RetentionComplianceRule' -cmdParams $RuleHash -Compliance -AsApp -useSystemMailbox $true + } + + "Successfully created Retention compliance policy $($RequestParams.Name) for $TenantFilter." + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Successfully created Retention compliance policy $($RequestParams.Name) for $TenantFilter." -sev Info + } catch { + $ErrorMessage = Get-CippException -Exception $_ + "Could not create Retention compliance policy for $($TenantFilter): $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Could not create Retention compliance policy for $($TenantFilter): $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + } + } + + return ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = @{Results = @($Result) } + }) + +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-Retention/Invoke-AddRetentionCompliancePolicyTemplate.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-Retention/Invoke-AddRetentionCompliancePolicyTemplate.ps1 new file mode 100644 index 000000000000..79896013ae2d --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-Retention/Invoke-AddRetentionCompliancePolicyTemplate.ps1 @@ -0,0 +1,48 @@ +Function Invoke-AddRetentionCompliancePolicyTemplate { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Security.RetentionCompliancePolicy.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + + try { + $GUID = (New-Guid).GUID + $JSON = if ($Request.Body.PowerShellCommand) { + $Request.Body.PowerShellCommand | ConvertFrom-Json + } else { + ([pscustomobject]$Request.Body | Select-Object Name, Comment, Enabled, RestrictiveRetention, ExchangeLocation, ExchangeLocationException, ModernGroupLocation, ModernGroupLocationException, OneDriveLocation, OneDriveLocationException, SharePointLocation, SharePointLocationException, SkypeLocation, SkypeLocationException, PublicFolderLocation, TeamsChannelLocation, TeamsChannelLocationException, TeamsChatLocation, TeamsChatLocationException, ApplyComplianceTag, RetentionDuration, RetentionAction, RetentionDurationDisplayHint, ExpirationDateOption, RuleParams) | ForEach-Object { + $NonEmptyProperties = $_.PSObject.Properties | Where-Object { $null -ne $_.Value } | Select-Object -ExpandProperty Name + $_ | Select-Object -Property $NonEmptyProperties + } + } + + $JSON = ($JSON | Select-Object @{n = 'name'; e = { $_.Name ?? $_.name } }, @{n = 'comments'; e = { $_.Comment ?? $_.comments } }, * | ConvertTo-Json -Depth 10) + $Table = Get-CippTable -tablename 'templates' + $Table.Force = $true + Add-CIPPAzDataTableEntity @Table -Entity @{ + JSON = "$JSON" + RowKey = "$GUID" + PartitionKey = 'RetentionCompliancePolicyTemplate' + } + $Result = "Successfully created Retention Compliance Policy Template: $($Request.Body.Name ?? $Request.Body.name) with GUID $GUID" + Write-LogMessage -headers $Headers -API $APIName -message $Result -Sev 'Debug' + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed to create Retention Compliance Policy Template: $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -message $Result -Sev 'Error' -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::InternalServerError + } + + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{Results = $Result } + }) + +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-Retention/Invoke-EditRetentionCompliancePolicy.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-Retention/Invoke-EditRetentionCompliancePolicy.ps1 new file mode 100644 index 000000000000..72db204c7d83 --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-Retention/Invoke-EditRetentionCompliancePolicy.ps1 @@ -0,0 +1,46 @@ +Function Invoke-EditRetentionCompliancePolicy { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Security.RetentionCompliancePolicy.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter + $Identity = $Request.Query.Identity ?? $Request.Body.Identity ?? $Request.Body.Name + $State = $Request.Query.State ?? $Request.Body.State + + try { + $Params = @{ + Identity = $Identity + } + + if ($State) { + $Params['Enabled'] = ($State -eq 'enable' -or $State -eq $true -or $State -eq 'true') + } + + if ($Request.Body.parameters) { + $Request.Body.parameters.PSObject.Properties | ForEach-Object { + if ($_.Name -ne 'Identity') { $Params[$_.Name] = $_.Value } + } + } + + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Set-RetentionCompliancePolicy' -cmdParams $Params -Compliance -AsApp -useSystemMailbox $true + $Result = "Updated Retention compliance policy $Identity" + Write-LogMessage -Headers $Request.Headers -API $APIName -tenant $TenantFilter -message $Result -Sev Info + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed updating Retention compliance policy $Identity. Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -Headers $Request.Headers -API $APIName -tenant $TenantFilter -message $Result -Sev Error -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::Forbidden + } + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{Results = $Result } + }) + +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-Retention/Invoke-ListRetentionCompliancePolicy.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-Retention/Invoke-ListRetentionCompliancePolicy.ps1 new file mode 100644 index 000000000000..1c5290932c8f --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-Retention/Invoke-ListRetentionCompliancePolicy.ps1 @@ -0,0 +1,30 @@ +Function Invoke-ListRetentionCompliancePolicy { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Security.RetentionCompliancePolicy.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + $TenantFilter = $Request.Query.tenantFilter + + try { + $Policies = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-RetentionCompliancePolicy' -Compliance -AsApp | Select-Object * -ExcludeProperty *odata*, *data.type* + $Rules = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-RetentionComplianceRule' -Compliance -AsApp | Select-Object * -ExcludeProperty *odata*, *data.type* + $GraphRequest = $Policies | Select-Object *, + @{l = 'AssociatedRules'; e = { $name = $_.Name; @($Rules | Where-Object { $_.Policy -eq $name }) } }, + @{l = 'RuleCount'; e = { $name = $_.Name; (@($Rules | Where-Object { $_.Policy -eq $name })).Count } } + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + $StatusCode = [HttpStatusCode]::Forbidden + $GraphRequest = $ErrorMessage + } + + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @($GraphRequest) + }) + +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-Retention/Invoke-ListRetentionCompliancePolicyTemplates.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-Retention/Invoke-ListRetentionCompliancePolicyTemplates.ps1 new file mode 100644 index 000000000000..98dace219fbc --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-Retention/Invoke-ListRetentionCompliancePolicyTemplates.ps1 @@ -0,0 +1,26 @@ +Function Invoke-ListRetentionCompliancePolicyTemplates { + <# + .FUNCTIONALITY + Entrypoint,AnyTenant + .ROLE + Security.RetentionCompliancePolicy.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + $Table = Get-CippTable -tablename 'templates' + $Filter = "PartitionKey eq 'RetentionCompliancePolicyTemplate'" + $Templates = (Get-CIPPAzDataTableEntity @Table -Filter $Filter) | ForEach-Object { + $GUID = $_.RowKey + $data = $_.JSON | ConvertFrom-Json + $data | Add-Member -NotePropertyName 'GUID' -NotePropertyValue $GUID -Force + $data + } + + if ($Request.Query.ID) { $Templates = $Templates | Where-Object -Property GUID -EQ $Request.Query.ID } + + return ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = @($Templates) + }) + +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-Retention/Invoke-RemoveRetentionCompliancePolicy.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-Retention/Invoke-RemoveRetentionCompliancePolicy.ps1 new file mode 100644 index 000000000000..a6061dc75730 --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-Retention/Invoke-RemoveRetentionCompliancePolicy.ps1 @@ -0,0 +1,39 @@ +Function Invoke-RemoveRetentionCompliancePolicy { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Security.RetentionCompliancePolicy.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + + $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter + $Identity = $Request.Query.Identity ?? $Request.Body.Identity ?? $Request.Body.Name + $ForceRemoval = $Request.Body.ForceDeletion -eq $true + + try { + $Params = @{ + Identity = $Identity + } + if ($ForceRemoval) { $Params['ForceDeletion'] = $true } + + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Remove-RetentionCompliancePolicy' -cmdParams $Params -Compliance -AsApp -useSystemMailbox $true + $Result = "Deleted Retention compliance policy $Identity" + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev Info + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed to delete Retention compliance policy $Identity - $($ErrorMessage.NormalizedError)" + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev Error -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::Forbidden + } + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{Results = $Result } + }) + +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-Retention/Invoke-RemoveRetentionCompliancePolicyTemplate.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-Retention/Invoke-RemoveRetentionCompliancePolicyTemplate.ps1 new file mode 100644 index 000000000000..69d7b60eca4e --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-Retention/Invoke-RemoveRetentionCompliancePolicyTemplate.ps1 @@ -0,0 +1,36 @@ +Function Invoke-RemoveRetentionCompliancePolicyTemplate { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Security.RetentionCompliancePolicy.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + + $ID = $Request.Body.ID ?? $Request.Query.ID + try { + $Table = Get-CippTable -tablename 'templates' + $SafeID = ConvertTo-CIPPODataFilterValue -Value $ID -Type Guid + $Filter = "PartitionKey eq 'RetentionCompliancePolicyTemplate' and RowKey eq '$SafeID'" + $ClearRow = Get-CIPPAzDataTableEntity @Table -Filter $Filter -Property PartitionKey, RowKey + Remove-AzDataTableEntity -Force @Table -Entity $ClearRow + $Result = "Removed Retention Compliance Policy template with ID $ID" + Write-LogMessage -Headers $Headers -API $APIName -message $Result -Sev 'Info' + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed to remove Retention Compliance Policy template $ID. $($ErrorMessage.NormalizedError)" + Write-LogMessage -Headers $Headers -API $APIName -message $Result -Sev 'Error' -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::Forbidden + } + + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{'Results' = $Result } + }) + +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SIT/Invoke-AddSensitiveInfoType.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SIT/Invoke-AddSensitiveInfoType.ps1 new file mode 100644 index 000000000000..b8d530757c05 --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SIT/Invoke-AddSensitiveInfoType.ps1 @@ -0,0 +1,47 @@ +Function Invoke-AddSensitiveInfoType { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Security.SensitiveInfoType.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + + $RequestParams = $Request.Body.PowerShellCommand | ConvertFrom-Json | Select-Object -Property * -ExcludeProperty GUID, comments + + $Tenants = ($Request.Body.selectedTenants).value + $Result = foreach ($TenantFilter in $Tenants) { + try { + # New-DlpSensitiveInformationType expects FileData (byte array of XML rule pack) or specific simple parameters. + # We pass through whatever the user provided as JSON parameters. + $Params = @{} + $RequestParams.PSObject.Properties | ForEach-Object { + $Params[$_.Name] = $_.Value + } + + # If the template provided XML rule pack content as base64, decode it for FileData + if ($Params.ContainsKey('FileDataBase64') -and $Params['FileDataBase64']) { + $Params['FileData'] = [System.Convert]::FromBase64String($Params['FileDataBase64']) + $Params.Remove('FileDataBase64') + } + + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'New-DlpSensitiveInformationType' -cmdParams $Params -Compliance -useSystemMailbox $true + "Successfully created Sensitive Information Type $($RequestParams.Name) for $TenantFilter." + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Successfully created Sensitive Information Type $($RequestParams.Name) for $TenantFilter." -sev Info + } catch { + $ErrorMessage = Get-CippException -Exception $_ + "Could not create Sensitive Information Type for $($TenantFilter): $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Could not create Sensitive Information Type for $($TenantFilter): $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + } + } + + return ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = @{Results = @($Result) } + }) + +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SIT/Invoke-AddSensitiveInfoTypeTemplate.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SIT/Invoke-AddSensitiveInfoTypeTemplate.ps1 new file mode 100644 index 000000000000..d4f18b501fb0 --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SIT/Invoke-AddSensitiveInfoTypeTemplate.ps1 @@ -0,0 +1,48 @@ +Function Invoke-AddSensitiveInfoTypeTemplate { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Security.SensitiveInfoType.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + + try { + $GUID = (New-Guid).GUID + $JSON = if ($Request.Body.PowerShellCommand) { + $Request.Body.PowerShellCommand | ConvertFrom-Json + } else { + ([pscustomobject]$Request.Body | Select-Object Name, Description, Pattern, FileDataBase64, Locale, Publisher, Confidence, Recommended, RulePackVersion, Comment) | ForEach-Object { + $NonEmptyProperties = $_.PSObject.Properties | Where-Object { $null -ne $_.Value } | Select-Object -ExpandProperty Name + $_ | Select-Object -Property $NonEmptyProperties + } + } + + $JSON = ($JSON | Select-Object @{n = 'name'; e = { $_.Name ?? $_.name } }, @{n = 'comments'; e = { $_.Comment ?? $_.comments } }, * | ConvertTo-Json -Depth 10) + $Table = Get-CippTable -tablename 'templates' + $Table.Force = $true + Add-CIPPAzDataTableEntity @Table -Entity @{ + JSON = "$JSON" + RowKey = "$GUID" + PartitionKey = 'SensitiveInfoTypeTemplate' + } + $Result = "Successfully created Sensitive Information Type Template: $($Request.Body.Name ?? $Request.Body.name) with GUID $GUID" + Write-LogMessage -headers $Headers -API $APIName -message $Result -Sev 'Debug' + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed to create Sensitive Information Type Template: $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -message $Result -Sev 'Error' -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::InternalServerError + } + + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{Results = $Result } + }) + +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SIT/Invoke-EditSensitiveInfoType.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SIT/Invoke-EditSensitiveInfoType.ps1 new file mode 100644 index 000000000000..213ac25966fa --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SIT/Invoke-EditSensitiveInfoType.ps1 @@ -0,0 +1,41 @@ +Function Invoke-EditSensitiveInfoType { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Security.SensitiveInfoType.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter + $Identity = $Request.Query.Identity ?? $Request.Body.Identity ?? $Request.Body.Name + + try { + $Params = @{ + Identity = $Identity + } + + if ($Request.Body.parameters) { + $Request.Body.parameters.PSObject.Properties | ForEach-Object { + if ($_.Name -ne 'Identity') { $Params[$_.Name] = $_.Value } + } + } + + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Set-DlpSensitiveInformationType' -cmdParams $Params -Compliance -useSystemMailbox $true + $Result = "Updated Sensitive Information Type $Identity" + Write-LogMessage -Headers $Request.Headers -API $APIName -tenant $TenantFilter -message $Result -Sev Info + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed updating Sensitive Information Type $Identity. Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -Headers $Request.Headers -API $APIName -tenant $TenantFilter -message $Result -Sev Error -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::Forbidden + } + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{Results = $Result } + }) + +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SIT/Invoke-ListSensitiveInfoType.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SIT/Invoke-ListSensitiveInfoType.ps1 new file mode 100644 index 000000000000..5ae4a340fe49 --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SIT/Invoke-ListSensitiveInfoType.ps1 @@ -0,0 +1,33 @@ +Function Invoke-ListSensitiveInfoType { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Security.SensitiveInfoType.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + $TenantFilter = $Request.Query.tenantFilter + $IncludeBuiltIn = ($Request.Query.IncludeBuiltIn -eq 'true' -or $Request.Query.IncludeBuiltIn -eq $true) + + try { + $SITs = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-DlpSensitiveInformationType' -Compliance | Select-Object * -ExcludeProperty *odata*, *data.type* + + if (-not $IncludeBuiltIn) { + $SITs = $SITs | Where-Object { $_.Publisher -ne 'Microsoft Corporation' -and $_.Publisher -notlike 'Microsoft*' } + } + + $StatusCode = [HttpStatusCode]::OK + $GraphRequest = $SITs + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + $StatusCode = [HttpStatusCode]::Forbidden + $GraphRequest = $ErrorMessage + } + + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @($GraphRequest) + }) + +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SIT/Invoke-ListSensitiveInfoTypeTemplates.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SIT/Invoke-ListSensitiveInfoTypeTemplates.ps1 new file mode 100644 index 000000000000..fa21108ce653 --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SIT/Invoke-ListSensitiveInfoTypeTemplates.ps1 @@ -0,0 +1,26 @@ +Function Invoke-ListSensitiveInfoTypeTemplates { + <# + .FUNCTIONALITY + Entrypoint,AnyTenant + .ROLE + Security.SensitiveInfoType.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + $Table = Get-CippTable -tablename 'templates' + $Filter = "PartitionKey eq 'SensitiveInfoTypeTemplate'" + $Templates = (Get-CIPPAzDataTableEntity @Table -Filter $Filter) | ForEach-Object { + $GUID = $_.RowKey + $data = $_.JSON | ConvertFrom-Json + $data | Add-Member -NotePropertyName 'GUID' -NotePropertyValue $GUID -Force + $data + } + + if ($Request.Query.ID) { $Templates = $Templates | Where-Object -Property GUID -EQ $Request.Query.ID } + + return ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = @($Templates) + }) + +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SIT/Invoke-RemoveSensitiveInfoType.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SIT/Invoke-RemoveSensitiveInfoType.ps1 new file mode 100644 index 000000000000..3fa5502ee294 --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SIT/Invoke-RemoveSensitiveInfoType.ps1 @@ -0,0 +1,36 @@ +Function Invoke-RemoveSensitiveInfoType { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Security.SensitiveInfoType.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + + $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter + $Identity = $Request.Query.Identity ?? $Request.Body.Identity ?? $Request.Body.Name + + try { + $Params = @{ + Identity = $Identity + } + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Remove-DlpSensitiveInformationType' -cmdParams $Params -Compliance -useSystemMailbox $true + $Result = "Deleted Sensitive Information Type $Identity" + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev Info + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed to delete Sensitive Information Type $Identity - $($ErrorMessage.NormalizedError)" + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev Error -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::Forbidden + } + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{Results = $Result } + }) + +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SIT/Invoke-RemoveSensitiveInfoTypeTemplate.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SIT/Invoke-RemoveSensitiveInfoTypeTemplate.ps1 new file mode 100644 index 000000000000..23c88a9572a6 --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SIT/Invoke-RemoveSensitiveInfoTypeTemplate.ps1 @@ -0,0 +1,36 @@ +Function Invoke-RemoveSensitiveInfoTypeTemplate { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Security.SensitiveInfoType.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + + $ID = $Request.Body.ID ?? $Request.Query.ID + try { + $Table = Get-CippTable -tablename 'templates' + $SafeID = ConvertTo-CIPPODataFilterValue -Value $ID -Type Guid + $Filter = "PartitionKey eq 'SensitiveInfoTypeTemplate' and RowKey eq '$SafeID'" + $ClearRow = Get-CIPPAzDataTableEntity @Table -Filter $Filter -Property PartitionKey, RowKey + Remove-AzDataTableEntity -Force @Table -Entity $ClearRow + $Result = "Removed Sensitive Information Type template with ID $ID" + Write-LogMessage -Headers $Headers -API $APIName -message $Result -Sev 'Info' + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed to remove Sensitive Information Type template $ID. $($ErrorMessage.NormalizedError)" + Write-LogMessage -Headers $Headers -API $APIName -message $Result -Sev 'Error' -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::Forbidden + } + + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{'Results' = $Result } + }) + +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SensitivityLabel/Invoke-AddSensitivityLabel.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SensitivityLabel/Invoke-AddSensitivityLabel.ps1 new file mode 100644 index 000000000000..9f47b293933f --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SensitivityLabel/Invoke-AddSensitivityLabel.ps1 @@ -0,0 +1,49 @@ +Function Invoke-AddSensitivityLabel { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Security.SensitivityLabel.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + + $RequestParams = $Request.Body.PowerShellCommand | ConvertFrom-Json | Select-Object -Property * -ExcludeProperty GUID, comments, PolicyParams + $PolicyParams = ($Request.Body.PowerShellCommand | ConvertFrom-Json).PolicyParams + + $Tenants = ($Request.Body.selectedTenants).value + $Result = foreach ($TenantFilter in $Tenants) { + try { + $LabelParams = $RequestParams | Select-Object -Property * -ExcludeProperty PolicyParams + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'New-Label' -cmdParams $LabelParams -Compliance -useSystemMailbox $true + + if ($PolicyParams) { + $PolicyHash = @{} + $PolicyParams.PSObject.Properties | ForEach-Object { $PolicyHash[$_.Name] = $_.Value } + if (-not $PolicyHash.ContainsKey('Labels') -or -not $PolicyHash['Labels']) { + $PolicyHash['Labels'] = @($RequestParams.Name) + } + if (-not $PolicyHash.ContainsKey('Name') -or [string]::IsNullOrWhiteSpace($PolicyHash['Name'])) { + $PolicyHash['Name'] = "$($RequestParams.Name) Policy" + } + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'New-LabelPolicy' -cmdParams $PolicyHash -Compliance -useSystemMailbox $true + } + + "Successfully created sensitivity label $($RequestParams.Name) for $TenantFilter." + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Successfully created sensitivity label $($RequestParams.Name) for $TenantFilter." -sev Info + } catch { + $ErrorMessage = Get-CippException -Exception $_ + "Could not create sensitivity label for $($TenantFilter): $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Could not create sensitivity label for $($TenantFilter): $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + } + } + + return ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = @{Results = @($Result) } + }) + +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SensitivityLabel/Invoke-AddSensitivityLabelTemplate.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SensitivityLabel/Invoke-AddSensitivityLabelTemplate.ps1 new file mode 100644 index 000000000000..045a31fa3d80 --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SensitivityLabel/Invoke-AddSensitivityLabelTemplate.ps1 @@ -0,0 +1,48 @@ +Function Invoke-AddSensitivityLabelTemplate { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Security.SensitivityLabel.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + + try { + $GUID = (New-Guid).GUID + $JSON = if ($Request.Body.PowerShellCommand) { + $Request.Body.PowerShellCommand | ConvertFrom-Json + } else { + ([pscustomobject]$Request.Body | Select-Object Name, DisplayName, Comment, Tooltip, ParentId, ContentType, EncryptionEnabled, EncryptionProtectionType, EncryptionRightsDefinitions, EncryptionContentExpiredOnDateInDaysOrNever, EncryptionDoNotForward, EncryptionEncryptOnly, EncryptionOfflineAccessDays, EncryptionPromptUser, EncryptionAESKeySize, ContentMarkingHeaderEnabled, ContentMarkingHeaderText, ContentMarkingHeaderFontSize, ContentMarkingHeaderFontColor, ContentMarkingHeaderAlignment, ContentMarkingFooterEnabled, ContentMarkingFooterText, ContentMarkingFooterFontSize, ContentMarkingFooterFontColor, ContentMarkingFooterAlignment, ContentMarkingWatermarkEnabled, ContentMarkingWatermarkText, ContentMarkingWatermarkFontSize, ContentMarkingWatermarkFontColor, ContentMarkingWatermarkLayout, ApplyContentMarkingHeaderEnabled, ApplyContentMarkingFooterEnabled, ApplyWaterMarkingEnabled, SiteAndGroupProtectionEnabled, SiteAndGroupProtectionPrivacy, SiteAndGroupProtectionAllowAccessToGuestUsers, SiteAndGroupProtectionAllowEmailFromGuestUsers, SiteAndGroupProtectionAllowFullAccess, SiteAndGroupProtectionAllowLimitedAccess, SiteAndGroupProtectionBlockAccess, Conditions, AdvancedSettings, PolicyParams) | ForEach-Object { + $NonEmptyProperties = $_.PSObject.Properties | Where-Object { $null -ne $_.Value } | Select-Object -ExpandProperty Name + $_ | Select-Object -Property $NonEmptyProperties + } + } + + $JSON = ($JSON | Select-Object @{n = 'name'; e = { $_.Name ?? $_.name } }, @{n = 'comments'; e = { $_.Comment ?? $_.comments } }, * | ConvertTo-Json -Depth 10) + $Table = Get-CippTable -tablename 'templates' + $Table.Force = $true + Add-CIPPAzDataTableEntity @Table -Entity @{ + JSON = "$JSON" + RowKey = "$GUID" + PartitionKey = 'SensitivityLabelTemplate' + } + $Result = "Successfully created Sensitivity Label Template: $($Request.Body.Name ?? $Request.Body.name) with GUID $GUID" + Write-LogMessage -headers $Headers -API $APIName -message $Result -Sev 'Debug' + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed to create Sensitivity Label Template: $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -message $Result -Sev 'Error' -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::InternalServerError + } + + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{Results = $Result } + }) + +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SensitivityLabel/Invoke-EditSensitivityLabel.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SensitivityLabel/Invoke-EditSensitivityLabel.ps1 new file mode 100644 index 000000000000..acad97a18693 --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SensitivityLabel/Invoke-EditSensitivityLabel.ps1 @@ -0,0 +1,41 @@ +Function Invoke-EditSensitivityLabel { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Security.SensitivityLabel.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter + $Identity = $Request.Query.Identity ?? $Request.Body.Identity ?? $Request.Body.Name + + try { + $Params = @{ + Identity = $Identity + } + + if ($Request.Body.parameters) { + $Request.Body.parameters.PSObject.Properties | ForEach-Object { + if ($_.Name -ne 'Identity') { $Params[$_.Name] = $_.Value } + } + } + + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Set-Label' -cmdParams $Params -Compliance -useSystemMailbox $true + $Result = "Updated sensitivity label $Identity" + Write-LogMessage -Headers $Request.Headers -API $APIName -tenant $TenantFilter -message $Result -Sev Info + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed updating sensitivity label $Identity. Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -Headers $Request.Headers -API $APIName -tenant $TenantFilter -message $Result -Sev Error -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::Forbidden + } + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{Results = $Result } + }) + +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SensitivityLabel/Invoke-ListSensitivityLabel.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SensitivityLabel/Invoke-ListSensitivityLabel.ps1 new file mode 100644 index 000000000000..4c6947962223 --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SensitivityLabel/Invoke-ListSensitivityLabel.ps1 @@ -0,0 +1,35 @@ +Function Invoke-ListSensitivityLabel { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Security.SensitivityLabel.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + $TenantFilter = $Request.Query.tenantFilter + + try { + $Labels = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-Label' -Compliance | Select-Object * -ExcludeProperty *odata*, *data.type* + $Policies = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-LabelPolicy' -Compliance | Select-Object * -ExcludeProperty *odata*, *data.type* + + $GraphRequest = $Labels | Select-Object *, + @{l = 'PublishedInPolicies'; e = { + $labelGuid = $_.Guid + @($Policies | Where-Object { $_.Labels -contains $labelGuid -or $_.Labels -contains $_.ImmutableId }) | Select-Object -ExpandProperty Name + } + } + + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + $StatusCode = [HttpStatusCode]::Forbidden + $GraphRequest = $ErrorMessage + } + + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @($GraphRequest) + }) + +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SensitivityLabel/Invoke-ListSensitivityLabelTemplates.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SensitivityLabel/Invoke-ListSensitivityLabelTemplates.ps1 new file mode 100644 index 000000000000..ab066a4eb6d5 --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SensitivityLabel/Invoke-ListSensitivityLabelTemplates.ps1 @@ -0,0 +1,26 @@ +Function Invoke-ListSensitivityLabelTemplates { + <# + .FUNCTIONALITY + Entrypoint,AnyTenant + .ROLE + Security.SensitivityLabel.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + $Table = Get-CippTable -tablename 'templates' + $Filter = "PartitionKey eq 'SensitivityLabelTemplate'" + $Templates = (Get-CIPPAzDataTableEntity @Table -Filter $Filter) | ForEach-Object { + $GUID = $_.RowKey + $data = $_.JSON | ConvertFrom-Json + $data | Add-Member -NotePropertyName 'GUID' -NotePropertyValue $GUID -Force + $data + } + + if ($Request.Query.ID) { $Templates = $Templates | Where-Object -Property GUID -EQ $Request.Query.ID } + + return ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = @($Templates) + }) + +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SensitivityLabel/Invoke-RemoveSensitivityLabel.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SensitivityLabel/Invoke-RemoveSensitivityLabel.ps1 new file mode 100644 index 000000000000..225351276dc5 --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SensitivityLabel/Invoke-RemoveSensitivityLabel.ps1 @@ -0,0 +1,36 @@ +Function Invoke-RemoveSensitivityLabel { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Security.SensitivityLabel.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + + $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter + $Identity = $Request.Query.Identity ?? $Request.Body.Identity ?? $Request.Body.Name + + try { + $Params = @{ + Identity = $Identity + } + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Remove-Label' -cmdParams $Params -Compliance -useSystemMailbox $true + $Result = "Deleted sensitivity label $Identity" + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev Info + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed to delete sensitivity label $Identity - $($ErrorMessage.NormalizedError)" + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev Error -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::Forbidden + } + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{Results = $Result } + }) + +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SensitivityLabel/Invoke-RemoveSensitivityLabelTemplate.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SensitivityLabel/Invoke-RemoveSensitivityLabelTemplate.ps1 new file mode 100644 index 000000000000..c8084265db52 --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-SensitivityLabel/Invoke-RemoveSensitivityLabelTemplate.ps1 @@ -0,0 +1,36 @@ +Function Invoke-RemoveSensitivityLabelTemplate { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Security.SensitivityLabel.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + + $ID = $Request.Body.ID ?? $Request.Query.ID + try { + $Table = Get-CippTable -tablename 'templates' + $SafeID = ConvertTo-CIPPODataFilterValue -Value $ID -Type Guid + $Filter = "PartitionKey eq 'SensitivityLabelTemplate' and RowKey eq '$SafeID'" + $ClearRow = Get-CIPPAzDataTableEntity @Table -Filter $Filter -Property PartitionKey, RowKey + Remove-AzDataTableEntity -Force @Table -Entity $ClearRow + $Result = "Removed Sensitivity Label template with ID $ID" + Write-LogMessage -Headers $Headers -API $APIName -message $Result -Sev 'Info' + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed to remove Sensitivity Label template $ID. $($ErrorMessage.NormalizedError)" + Write-LogMessage -Headers $Headers -API $APIName -message $Result -Sev 'Error' -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::Forbidden + } + + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{'Results' = $Result } + }) + +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListDomainHealth.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListDomainHealth.ps1 index 8a87edb67943..2ae766dda392 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListDomainHealth.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListDomainHealth.ps1 @@ -133,6 +133,15 @@ function Invoke-ListDomainHealth { } $Body = Test-MtaSts @HttpsQuery } + 'ReadAutoDiscover' { + $AutoDiscoverQuery = @{ + Domain = $Request.Query.Domain + } + if ($Request.Query.ExpectedTarget) { + $AutoDiscoverQuery.ExpectedTarget = $Request.Query.ExpectedTarget + } + $Body = Read-AutoDiscoverRecord @AutoDiscoverQuery + } } } else { $body = [pscustomobject]@{'Results' = "Domain: $($Request.Query.Domain) is invalid" } diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListStandardsCompare.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListStandardsCompare.ps1 index 0b94eb63d919..b3f9b53f8c4c 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListStandardsCompare.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListStandardsCompare.ps1 @@ -13,6 +13,26 @@ function Invoke-ListStandardsCompare { $TenantFilter = $Request.Query.tenantFilter $TemplateFilter = $Request.Query.templateId + # Get-CIPPStandards is the authoritative source for what is currently in scope. + $StandardParams = @{} + if ($TemplateFilter) { $StandardParams.TemplateId = $TemplateFilter } + if ($TenantFilter) { $StandardParams.TenantFilter = $TenantFilter } + $StandardList = Get-CIPPStandards @StandardParams + + $ScopedTemplateGuids = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) + $ScopedQuarantineNames = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) + foreach ($Entry in $StandardList) { + switch ($Entry.Standard) { + { $_ -in @('IntuneTemplate', 'ConditionalAccessTemplate') } { + if ($Entry.Settings.TemplateList.value) { $null = $ScopedTemplateGuids.Add($Entry.Settings.TemplateList.value) } + } + 'QuarantineTemplate' { + $DisplayName = $Entry.Settings.displayName.value ?? $Entry.Settings.displayName + if ($DisplayName) { $null = $ScopedQuarantineNames.Add($DisplayName) } + } + } + } + $Filters = [system.collections.generic.list[string]]::new() if ($TenantFilter) { $Filters.Add("PartitionKey eq '{0}'" -f $TenantFilter) @@ -34,6 +54,20 @@ function Invoke-ListStandardsCompare { $FieldValue = $Standard.Value $Tenant = $Standard.PartitionKey + # Skip rows for template types no longer in scope per the current standard list. + if ($FieldName -match '^standards\.(IntuneTemplate|ConditionalAccessTemplate)\.(.+)$') { + if (-not $ScopedTemplateGuids.Contains($Matches[2])) { continue } + } elseif ($ScopedQuarantineNames.Count -gt 0 -and $FieldName -match '^standards\.QuarantineTemplate\.(.+)$') { + # Decode hex-encoded display name and check if it's still in scope + $HexEncoded = $Matches[1] + $Chars = [System.Collections.Generic.List[char]]::new() + for ($i = 0; $i -lt $HexEncoded.Length; $i += 2) { + $Chars.Add([char][Convert]::ToInt32($HexEncoded.Substring($i, 2), 16)) + } + $DecodedName = -join $Chars + if (-not $ScopedQuarantineNames.Contains($DecodedName)) { continue } + } + # decode field names that are hex encoded (e.g. QuarantineTemplates) if ($FieldName -match '^(standards\.QuarantineTemplate\.)(.+)$') { $Prefix = $Matches[1] diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-listStandardTemplates.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-listStandardTemplates.ps1 index 698fbb42deb3..de82fde161b3 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-listStandardTemplates.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-listStandardTemplates.ps1 @@ -35,6 +35,42 @@ function Invoke-listStandardTemplates { $Data.excludedTenants = @() } } + + # Re-expand TemplateList-Tags live so stale addedFields snapshots don't show removed templates + if ($Data.standards) { + foreach ($StandardName in $Data.standards.PSObject.Properties.Name) { + $StandardConfig = $Data.standards.$StandardName + $Items = if ($StandardConfig -is [System.Collections.IEnumerable] -and $StandardConfig -isnot [string]) { $StandardConfig } else { @($StandardConfig) } + foreach ($Item in $Items) { + if ($Item.'TemplateList-Tags' -and $Item.'TemplateList-Tags'.value) { + if (-not $IntuneTemplatesCache) { + $IntuneTable = Get-CippTable -tablename 'templates' + $IntuneFilter = "PartitionKey eq 'IntuneTemplate'" + $IntuneTemplatesCache = Get-CIPPAzDataTableEntity @IntuneTable -Filter $IntuneFilter + } + $PackageName = $Item.'TemplateList-Tags'.value + $LiveExpanded = @($IntuneTemplatesCache | Where-Object package -EQ $PackageName | ForEach-Object { + $TplJson = $_.JSON | ConvertFrom-Json -ErrorAction SilentlyContinue + [pscustomobject]@{ + GUID = $_.RowKey + displayName = if ($TplJson.displayName) { $TplJson.displayName } else { $_.RowKey } + name = if ($TplJson.displayName) { $TplJson.displayName } else { $_.RowKey } + } + }) + if ($Item.'TemplateList-Tags'.addedFields) { + $Item.'TemplateList-Tags'.addedFields | Add-Member -NotePropertyName 'templates' -NotePropertyValue $LiveExpanded -Force + } + if ($Item.'TemplateList-Tags'.rawData) { + $Item.'TemplateList-Tags'.rawData | Add-Member -NotePropertyName 'templates' -NotePropertyValue $LiveExpanded -Force + } + if (-not $Item.'TemplateList-Tags'.addedFields -and -not $Item.'TemplateList-Tags'.rawData) { + $Item.'TemplateList-Tags' | Add-Member -NotePropertyName 'addedFields' -NotePropertyValue ([pscustomobject]@{ templates = $LiveExpanded }) -Force + } + } + } + } + } + $Data } } | Sort-Object -Property templateName diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableSelfServiceLicenses.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableSelfServiceLicenses.ps1 index f1bcecf87877..43c37afa8fa4 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableSelfServiceLicenses.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableSelfServiceLicenses.ps1 @@ -78,7 +78,7 @@ function Invoke-CIPPStandardDisableSelfServiceLicenses { $CurrentValues.Add([PSCustomObject]@{ productName = 'Trial Autoclaim' productId = 'autoclaim' - policyValue = if ($null -eq $AutoClaimPolicy.policyValue) { 'Disabled' } else { $AutoClaimPolicy.policyValue } + policyValue = $AutoClaimPolicy.tenantPolicyValue ?? 'Disabled' }) } catch { Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to retrieve trial autoclaim policy: $($_.Exception.Message)" -sev Error @@ -178,7 +178,7 @@ function Invoke-CIPPStandardDisableSelfServiceLicenses { $CurrentValues.Add([PSCustomObject]@{ productName = 'Trial Autoclaim' productId = 'autoclaim' - policyValue = if ($null -eq $AutoClaimPolicy.policyValue) { 'Disabled' } else { $AutoClaimPolicy.policyValue } + policyValue = $AutoClaimPolicy.tenantPolicyValue ?? 'Disabled' }) } catch { Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to retrieve trial autoclaim policy after remediation: $($_.Exception.Message)" -sev Error diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardIntuneTemplate.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardIntuneTemplate.ps1 index bb1cf5dd4ca6..7af9715f79db 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardIntuneTemplate.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardIntuneTemplate.ps1 @@ -75,6 +75,28 @@ function Invoke-CIPPStandardIntuneTemplate { $RawJSON = $rawJsonFromTemplate $TemplateType = $Template.Type + # Fallback: infer type from RAWJson content when stored template has no Type + if (-not $TemplateType) { + try { + $parsedRaw = $rawJsonFromTemplate | ConvertFrom-Json -ErrorAction SilentlyContinue + $odataType = $parsedRaw.'@odata.type' + $TemplateType = if ($null -ne $parsedRaw.settings -and $null -ne $parsedRaw.technologies) { 'Catalog' } + elseif ($null -ne $parsedRaw.scheduledActionsForRule -or $odataType -match 'CompliancePolicy') { 'deviceCompliancePolicies' } + elseif ($odataType -match 'windowsDriverUpdateProfile') { 'windowsDriverUpdateProfiles' } + elseif ($odataType -match 'ManagedApp|managedAppProtection') { 'AppProtection' } + elseif ($odataType -match 'deviceConfiguration|#microsoft\.graph\.\w+Configuration$') { 'Device' } + else { $null } + } catch { + $TemplateType = $null + } + if ($TemplateType) { + Write-Information "[IntuneTemplate][$Tenant] Inferred template type '$TemplateType' from content for '$displayname'" + } else { + Write-LogMessage -API 'Standards' -tenant $tenant -message "Intune Template '$displayname' has no Type and type could not be inferred. Re-import the template to fix." -sev 'Error' + return $true + } + } + $AssignmentsMatch = $null try { $ExistingPolicy = Get-CIPPIntunePolicy -tenantFilter $Tenant -DisplayName $displayname -TemplateType $TemplateType diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardMailContacts.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardMailContacts.ps1 index a8036b8ea184..0b27c0a250b3 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardMailContacts.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardMailContacts.ps1 @@ -106,13 +106,13 @@ function Invoke-CIPPStandardMailContacts { } if ($Settings.report -eq $true) { $CurrentValue = @{ - marketingNotificationEmails = @($CurrentInfo.marketingNotificationEmails | Sort-Object) - technicalNotificationMails = @($CurrentInfo.technicalNotificationMails | Sort-Object) + marketingNotificationEmails = @($CurrentInfo.marketingNotificationEmails | Sort-Object -Unique) + technicalNotificationMails = @($CurrentInfo.technicalNotificationMails | Where-Object { [string]::IsNullOrWhiteSpace($_) -eq $false } | Sort-Object -Unique) contactEmail = $CurrentInfo.privacyProfile.contactEmail } $ExpectedValue = @{ - marketingNotificationEmails = @($Contacts.MarketingContact | Sort-Object) - technicalNotificationMails = @(@($Contacts.SecurityContact, $Contacts.TechContact) | Where-Object { $_ -ne $null } | Select-Object -Unique | Sort-Object) + marketingNotificationEmails = @($Contacts.MarketingContact | Sort-Object -Unique) + technicalNotificationMails = @(@($Contacts.SecurityContact, $Contacts.TechContact) | Where-Object { [string]::IsNullOrWhiteSpace($_) -eq $false } | Sort-Object -Unique) contactEmail = $Contacts.GeneralContact } Set-CIPPStandardsCompareField -FieldName 'standards.MailContacts' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $tenant diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardMalwareFilterPolicy.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardMalwareFilterPolicy.ps1 index 978103c47d88..661ee92e0760 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardMalwareFilterPolicy.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardMalwareFilterPolicy.ps1 @@ -220,7 +220,7 @@ function Invoke-CIPPStandardMalwareFilterPolicy { Name = $CurrentState.Name EnableFileFilter = $CurrentState.EnableFileFilter FileTypeAction = $CurrentState.FileTypeAction - FileTypes = $CurrentState.FileTypes + FileTypes = @($CurrentState.FileTypes | Sort-Object) ZapEnabled = $CurrentState.ZapEnabled QuarantineTag = $CurrentState.QuarantineTag EnableInternalSenderAdminNotifications = $CurrentState.EnableInternalSenderAdminNotifications @@ -238,7 +238,7 @@ function Invoke-CIPPStandardMalwareFilterPolicy { Name = $PolicyName EnableFileFilter = $true FileTypeAction = $FileTypeAction - FileTypes = $ExpectedFileTypes + FileTypes = @($ExpectedFileTypes | Sort-Object) ZapEnabled = $true QuarantineTag = $Settings.QuarantineTag EnableInternalSenderAdminNotifications = $Settings.EnableInternalSenderAdminNotifications diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPFileRequests.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPFileRequests.ps1 index 099d196ac80e..be1ca472b041 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPFileRequests.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPFileRequests.ps1 @@ -48,8 +48,15 @@ function Invoke-CIPPStandardSPFileRequests { return $true } + $SharingCapabilityEnum = @{ + 0L = 'Disabled' + 1L = 'External Users Only' + 2L = 'External Users and Guests (Anyone)' + 3L = 'Existing External Users Only' + } + try { - $CurrentState = Get-CIPPSPOTenant -TenantFilter $Tenant | Select-Object _ObjectIdentity_, TenantFilter, CoreRequestFilesLinkEnabled, OneDriveRequestFilesLinkEnabled, CoreRequestFilesLinkExpirationInDays, OneDriveRequestFilesLinkExpirationInDays + $CurrentState = Get-CIPPSPOTenant -TenantFilter $Tenant | Select-Object _ObjectIdentity_, TenantFilter, CoreRequestFilesLinkEnabled, OneDriveRequestFilesLinkEnabled, CoreRequestFilesLinkExpirationInDays, OneDriveRequestFilesLinkExpirationInDays, SharingCapability } catch { Write-LogMessage -API 'Standards' -tenant $tenant -message 'Failed to get current state of SPO tenant details' -sev Error return @@ -61,8 +68,8 @@ function Invoke-CIPPStandardSPFileRequests { return } - $WantedState = $Settings.state - $ExpirationDays = $Settings.expirationDays + $WantedState = [bool]$Settings.state + $ExpirationDays = if ($null -ne $Settings.expirationDays) { [int]$Settings.expirationDays } else { $null } $HumanReadableState = if ($WantedState -eq $true) { 'enabled' } else { 'disabled' } # Check if current state matches desired state @@ -83,25 +90,39 @@ function Invoke-CIPPStandardSPFileRequests { if ($Settings.remediate -eq $true) { if ($AllSettingsCorrect -eq $false) { - try { - $Properties = @{ - CoreRequestFilesLinkEnabled = $WantedState - OneDriveRequestFilesLinkEnabled = $WantedState - } - - # Add expiration settings if specified and feature is being enabled - if ($null -ne $ExpirationDays -and $WantedState -eq $true) { - $Properties['CoreRequestFilesLinkExpirationInDays'] = $ExpirationDays - $Properties['OneDriveRequestFilesLinkExpirationInDays'] = $ExpirationDays + if ($CurrentState.SharingCapability -ne 2) { + Write-LogMessage -API 'Standards' -tenant $tenant -message "Cannot set File Requests to $HumanReadableState because the Tenant sharing level is set to $($SharingCapabilityEnum[$CurrentState.SharingCapability]). The sharing level must be set to 'External Users and Guests (Anyone)' to remediate this standard. There may be a conflicting standard preventing this." -sev Error + } else { + try { + $Properties = @{ + CoreRequestFilesLinkEnabled = $WantedState + OneDriveRequestFilesLinkEnabled = $WantedState + } + + # Add expiration settings if specified and feature is being enabled + if ($null -ne $ExpirationDays -and $WantedState -eq $true) { + $Properties['CoreRequestFilesLinkExpirationInDays'] = $ExpirationDays + $Properties['OneDriveRequestFilesLinkExpirationInDays'] = $ExpirationDays + } + + $CurrentState | Set-CIPPSPOTenant -Properties $Properties + + # Reflect the just-applied state in-memory so the report block does not write + # the pre-remediation values into the drift compare field. + $CurrentState.CoreRequestFilesLinkEnabled = $WantedState + $CurrentState.OneDriveRequestFilesLinkEnabled = $WantedState + if ($null -ne $ExpirationDays -and $WantedState -eq $true) { + $CurrentState.CoreRequestFilesLinkExpirationInDays = $ExpirationDays + $CurrentState.OneDriveRequestFilesLinkExpirationInDays = $ExpirationDays + } + + $ExpirationMessage = if ($null -ne $ExpirationDays -and $WantedState -eq $true) { " with $ExpirationDays day expiration" } else { '' } + Write-LogMessage -API 'Standards' -tenant $tenant -message "Successfully set File Requests to $HumanReadableState$ExpirationMessage" -sev Info + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to set File Requests to $HumanReadableState. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage } - - $CurrentState | Set-CIPPSPOTenant -Properties $Properties - - $ExpirationMessage = if ($null -ne $ExpirationDays -and $WantedState -eq $true) { " with $ExpirationDays day expiration" } else { '' } - Write-LogMessage -API 'Standards' -tenant $tenant -message "Successfully set File Requests to $HumanReadableState$ExpirationMessage" -sev Info - } catch { - $ErrorMessage = Get-CippException -Exception $_ - Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to set File Requests to $HumanReadableState. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage } } else { $ExpirationMessage = if ($null -ne $ExpirationDays -and $WantedState -eq $true) { " with $ExpirationDays day expiration" } else { '' } @@ -136,14 +157,16 @@ function Invoke-CIPPStandardSPFileRequests { $CurrentValue = @{ CoreRequestFilesLinkEnabled = $CurrentState.CoreRequestFilesLinkEnabled OneDriveRequestFilesLinkEnabled = $CurrentState.OneDriveRequestFilesLinkEnabled - CoreRequestFilesLinkExpirationInDays = $CurrentState.CoreRequestFilesLinkExpirationInDays - OneDriveRequestFilesLinkExpirationInDays = $CurrentState.OneDriveRequestFilesLinkExpirationInDays + CoreRequestFilesLinkExpirationInDays = if ($null -ne $ExpirationDays -and $WantedState -eq $true) { $CurrentState.CoreRequestFilesLinkExpirationInDays } else { $null } + OneDriveRequestFilesLinkExpirationInDays = if ($null -ne $ExpirationDays -and $WantedState -eq $true) { $CurrentState.OneDriveRequestFilesLinkExpirationInDays } else { $null } + SharingCapability = $SharingCapabilityEnum[$CurrentState.SharingCapability] } $ExpectedValue = @{ CoreRequestFilesLinkEnabled = $WantedState OneDriveRequestFilesLinkEnabled = $WantedState - CoreRequestFilesLinkExpirationInDays = if ($null -ne $ExpirationDays -and $WantedState -eq $true) { $ExpirationDays } else { $null } - OneDriveRequestFilesLinkExpirationInDays = if ($null -ne $ExpirationDays -and $WantedState -eq $true) { $ExpirationDays } else { $null } + CoreRequestFilesLinkExpirationInDays = if ($null -ne $ExpirationDays) { $ExpirationDays } else { $CurrentState.CoreRequestFilesLinkExpirationInDays } + OneDriveRequestFilesLinkExpirationInDays = if ($null -ne $ExpirationDays) { $ExpirationDays } else { $CurrentState.OneDriveRequestFilesLinkExpirationInDays } + SharingCapability = if ($WantedState -eq $true) { 'External Users and Guests (Anyone)' } else { $SharingCapabilityEnum[$CurrentState.SharingCapability] } } Set-CIPPStandardsCompareField -FieldName 'standards.SPFileRequests' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant } diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSafeLinksPolicy.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSafeLinksPolicy.ps1 index 04527ce524d6..132389c0f15f 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSafeLinksPolicy.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSafeLinksPolicy.ps1 @@ -231,7 +231,7 @@ function Invoke-CIPPStandardSafeLinksPolicy { DeliverMessageAfterScan = $CurrentState.DeliverMessageAfterScan DisableUrlRewrite = $CurrentState.DisableUrlRewrite EnableOrganizationBranding = $CurrentState.EnableOrganizationBranding - DoNotRewriteUrls = $CurrentState.DoNotRewriteUrls + DoNotRewriteUrls = @($CurrentState.DoNotRewriteUrls | Sort-Object) } $ExpectedValue = @{ Name = $PolicyName @@ -245,7 +245,7 @@ function Invoke-CIPPStandardSafeLinksPolicy { DeliverMessageAfterScan = $true DisableUrlRewrite = $Settings.DisableUrlRewrite EnableOrganizationBranding = $Settings.EnableOrganizationBranding - DoNotRewriteUrls = $Settings.DoNotRewriteUrls.value ?? @() + DoNotRewriteUrls = @(($Settings.DoNotRewriteUrls.value ?? @()) | Sort-Object) } Set-CIPPStandardsCompareField -FieldName 'standards.SafeLinksPolicy' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant } diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSpoofWarn.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSpoofWarn.ps1 index 6fa95268d1cc..3ef10d9e5e15 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSpoofWarn.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSpoofWarn.ps1 @@ -58,6 +58,9 @@ function Invoke-CIPPStandardSpoofWarn { return } + # Sanitize AllowList — the API may return @('') instead of @() for an empty list + $CurrentInfo.AllowList = @($CurrentInfo.AllowList | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) + # Get state value using null-coalescing operator $state = $Settings.state.value ?? $Settings.state diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsFederationConfiguration.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsFederationConfiguration.ps1 index ab245b7fd939..425aa552a2b6 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsFederationConfiguration.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsFederationConfiguration.ps1 @@ -75,7 +75,7 @@ function Invoke-CIPPStandardTeamsFederationConfiguration { $AllowedDomains = $null $BlockedDomains = @() if ($null -ne $Settings.DomainList) { - $AllowedDomainsAsAList = @($Settings.DomainList).Split(',').Trim() + $AllowedDomainsAsAList = @($Settings.DomainList).Split(',').Trim() | Sort-Object } else { $AllowedDomainsAsAList = @() } @@ -85,7 +85,7 @@ function Invoke-CIPPStandardTeamsFederationConfiguration { $AllowedDomains = $AllowAllKnownDomains $AllowedDomainsAsAList = @() if ($null -ne $Settings.DomainList) { - $BlockedDomains = @($Settings.DomainList).Split(',').Trim() + $BlockedDomains = @($Settings.DomainList).Split(',').Trim() | Sort-Object } else { $BlockedDomains = @() } diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsMeetingRecordingExpiration.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsMeetingRecordingExpiration.ps1 index e84885986aee..e093179116f2 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsMeetingRecordingExpiration.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsMeetingRecordingExpiration.ps1 @@ -91,7 +91,6 @@ function Invoke-CIPPStandardTeamsMeetingRecordingExpiration { if ($Settings.report -eq $true) { Add-CIPPBPAField -FieldName 'TeamsMeetingRecordingExpiration' -FieldValue $CurrentExpirationDays -StoreAs string -Tenant $Tenant - $CurrentExpirationDays = if ($StateIsCorrect) { $true } else { $CurrentExpirationDays } $CurrentValue = @{ MeetingRecordingExpirationDays = $CurrentExpirationDays } diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTenantAllowBlockListTemplate.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTenantAllowBlockListTemplate.ps1 index 38ab5cedcbbc..c3d4522eacd2 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTenantAllowBlockListTemplate.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTenantAllowBlockListTemplate.ps1 @@ -63,16 +63,42 @@ function Invoke-CIPPStandardTenantAllowBlockListTemplate { }) if ($Settings.remediate -eq $true) { + # Track entries submitted across templates to handle overlapping entries without relying on Exchange replication + $SubmittedEntries = [System.Collections.Generic.Dictionary[string, System.Collections.Generic.HashSet[string]]]::new([System.StringComparer]::OrdinalIgnoreCase) + foreach ($TemplateData in $ResolvedTemplates) { try { $Entries = @($TemplateData.entries -split '[,;]' | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | ForEach-Object { $_.Trim() }) + $ListType = [string]$TemplateData.listType + + # Get existing entries to avoid duplicate errors that block the entire batch + if (-not $SubmittedEntries.ContainsKey($ListType)) { + $SubmittedEntries[$ListType] = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) + try { + $ExistingItems = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-TenantAllowBlockListItems' -cmdParams @{ + ListType = $ListType + } + foreach ($Item in @($ExistingItems)) { + [void]$SubmittedEntries[$ListType].Add($Item.Value) + } + } catch { + # If we can't fetch existing items, continue with empty set + } + } + + $NewEntries = @($Entries | Where-Object { -not $SubmittedEntries[$ListType].Contains($_) }) + + if ($NewEntries.Count -eq 0) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "All entries from Tenant Allow/Block List template '$($TemplateData.templateName)' already exist for $Tenant" -sev 'Info' + continue + } $ExoParams = @{ tenantid = $Tenant cmdlet = 'New-TenantAllowBlockListItems' cmdParams = @{ - Entries = $Entries - ListType = [string]$TemplateData.listType + Entries = $NewEntries + ListType = $ListType Notes = [string]$TemplateData.notes $TemplateData.listMethod = [bool]$true } @@ -85,14 +111,13 @@ function Invoke-CIPPStandardTenantAllowBlockListTemplate { } New-ExoRequest @ExoParams - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Successfully deployed Tenant Allow/Block List template '$($TemplateData.templateName)' with entries: $($TemplateData.entries)" -sev 'Info' + foreach ($Entry in $NewEntries) { + [void]$SubmittedEntries[$ListType].Add($Entry) + } + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Successfully deployed Tenant Allow/Block List template '$($TemplateData.templateName)' with entries: $($NewEntries -join ', ')" -sev 'Info' } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - if ($ErrorMessage -like '*already exists*') { - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Tenant Allow/Block List entries from template '$($TemplateData.templateName)' already exist for $Tenant" -sev 'Info' - } else { - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to deploy Tenant Allow/Block List template '$($TemplateData.templateName)' for $Tenant. Error: $ErrorMessage" -sev 'Error' - } + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to deploy Tenant Allow/Block List template '$($TemplateData.templateName)' for $Tenant. Error: $ErrorMessage" -sev 'Error' } } } diff --git a/Modules/CIPPTests/Public/Tests/CISA-Missing-Caches.md b/Modules/CIPPTests/Public/Tests/CISA-Missing-Caches.md deleted file mode 100644 index 5b7bd652195a..000000000000 --- a/Modules/CIPPTests/Public/Tests/CISA-Missing-Caches.md +++ /dev/null @@ -1,195 +0,0 @@ -# Missing CIPP Caches for CISA Tests - -This document lists the caches that need to be created to support the remaining CISA tests that cannot currently be implemented. - -## ✅ Implemented Cache Functions - -### 1. ✅ CASMailbox Cache -**Required For:** -- ✅ MS.EXO.5.1 - SMTP Authentication - -**Status**: ✅ IMPLEMENTED in Set-CIPPDBCacheCASMailbox.ps1 - ---- - -### 2. ✅ ExoSharingPolicy Cache -**Required For:** -- ✅ MS.EXO.6.1 - Contact Sharing -- ✅ MS.EXO.6.2 - Calendar Sharing - -**Status**: ✅ IMPLEMENTED in Set-CIPPDBCacheExoSharingPolicy.ps1 - ---- - -### 3. ✅ ExoAdminAuditLogConfig Cache -**Required For:** -- ✅ MS.EXO.17.1 - Audit Log -- ✅ MS.EXO.17.3 - Audit Log Retention - -**Status**: ✅ IMPLEMENTED in Set-CIPPDBCacheExoAdminAuditLogConfig.ps1 - ---- - -### 4. ✅ ExoPresetSecurityPolicy Cache -**Required For:** -- ✅ MS.EXO.11.1 - Impersonation -- ✅ MS.EXO.11.2 - Impersonation Tips -- ✅ MS.EXO.11.3 - Mailbox Intelligence - -**Status**: ✅ IMPLEMENTED in Set-CIPPDBCacheExoPresetSecurityPolicy.ps1 - ---- - -### 5. ✅ ExoTenantAllowBlockList Cache -**Required For:** -- ✅ MS.EXO.12.1 - Anti-Spam Allow List - -**Status**: ✅ IMPLEMENTED in Set-CIPPDBCacheExoTenantAllowBlockList.ps1 - ---- - -## Required New Cache Functions -**Required For:** -- MS.EXO.8.1 - DLP Solution -- MS.EXO.8.2 - DLP PII -- MS.EXO.8.4 - DLP Baseline Rules - -**SecurityCompliance Command:** -```powershell -Get-DlpCompliancePolicy | Select-Object Name, Enabled, Mode -Get-DlpComplianceRule | Select-Object Name, ParentPolicyName, ContentContainsSensitiveInformation, Disabled -``` -1 -**Cache Function Names:** -- `Set-CIPPDBCacheSccDlpPolicy` -- `Set-CIPPDBCacheSccDlpRule` - -**Properties Needed:** -- Policy: Name, Enabled, Mode -- Rule: Name, ParentPolicyName, ContentContainsSensitiveInformation, Disabled - -**Note:** Requires SecurityCompliance PowerShell connection - ---- - -### 2. SecurityCompliance ProtectionAlert Cache -**Required For:** -- MS.EXO.16.1 - Alerts - -**SecurityCompliance Command:** -```powershell -Get-ProtectionAlert | Select-Object Name, Disabled -``` - -**Cache Function Name:** `Set-CIPPDBCacheSccProtectionAlert` - -**Properties Needed:** -- Name -- Disabled - -**Note:** Requires SecurityCompliance PowerShell connection - ---- - -### 3. SecurityCompliance ActivityAlert Cache -**Required For:** -- MS.EXO.16.2 - Alert SIEM - -**SecurityCompliance Command:** -```powershell -Get-ActivityAlert | Select-Object Name, Disabled, NotificationEnabled, Type -``` - -**Cache Function Name:** `Set-CIPPDBCacheSccActivityAlert` - -**Properties Needed:** -- Name -- Disabled -- NotificationEnabled -- Type - -**Note:** Requires SecurityCompliance PowerShell connection - ---- - -## DNS-Based Tests (Cannot Be Cached) - -These tests require external DNS lookups and cannot be implemented with cached Exchange data: - -### MS.EXO.2.1 - SPF Restriction -**Requires:** DNS TXT record lookup for SPF -**Query:** `nslookup -type=txt ` - -### MS.EXO.2.2 - SPF Directive -**Requires:** DNS TXT record parsing for SPF policy -**Query:** Parse SPF record for `~all` or `-all` - -### MS.EXO.4.1 - DMARC Record Exists -**Requires:** DNS TXT record lookup for DMARC -**Query:** `nslookup -type=txt _dmarc.` - -### MS.EXO.4.2 - DMARC Reject Policy -**Requires:** DNS TXT record parsing for DMARC policy -**Query:** Parse DMARC record for `p=reject` or `p=quarantine` - -### MS.EXO.4.3 - DMARC Aggregate Reports -**Requires:** DNS TXT record parsing for DMARC rua tags -**Query:** Parse DMARC record for `rua=` email addresses - -### MS.EXO.4.4 - DMARC Reports -**Requires:** DNS TXT record parsing for DMARC report configuration -**Query:** Parse DMARC record for report targets - -### MS.EXO.7.1 - External Sender Warning -**Requires:** ExoOrganizationConfig.ExternalInOutlook property -**Note:** May already be in ExoOrganizationConfig cache - needs verification - -### MS.EXO.13.1 - Mailbox Auditing -**Requires:** ExoOrganizationConfig.AuditDisabled property -**Note:** May already be in ExoOrganizationConfig cache - needs verification - -## Manual Assessment Tests (Cannot Be Automated) - -### MS.EXO.8.3 - DLP Alternate Solution -**Reason:** Requires manual assessment of 3rd party DLP solutions - -### MS.EXO.9.4 - Email Filter Alternative -**Reason:** Requires manual assessment of 3rd party email filtering solutions - -### MS.EXO.14.4 - Spam Alternative Solution -**Reason:** Requires manual assessment of 3rd party anti-spam solutions - -### MS.EXO.17.2 - Audit Log Premium -**Reason:** Requires license validation and advanced audit policy checks beyond cached data - ---- - -## Implementation Priority - -### High Priority (Core Security Controls): -1. CASMailbox - SMTP Auth control -2. ExoAdminAuditLogConfig - Audit logging -3. ExoTenantAllowBlockList - Allow list bypass prevention - -### Medium Priority (DLP and Alerts): -4. SecurityCompliance DLP caches - Data loss prevention -5. SecurityCompliance Alert caches - Security monitoring - -### Low Priority (Advanced Features): -6. ExoSharingPolicy - External sharing controls -7. ExoPresetSecurityPolicy - Preset security policies - ---- - -## Notes on Implementation - -1. **Graph API Alternative**: Some Exchange Online cmdlets may have equivalent Graph API endpoints that could be used instead. - -## Summary - -- **New Caches Required**: 8 cache functions -- **DNS Tests**: 6 tests (architectural limitation) -- **Manual Tests**: 4 tests (cannot be automated) -- **Implementable After New Caches**: 15 additional tests -- **Current Implementation**: 13 tests -- **Total Possible with New Caches**: 28 tests (68% coverage) diff --git a/Modules/CIPPTests/Public/Tests/Custom/Invoke-CippTestCustomScripts.ps1 b/Modules/CIPPTests/Public/Tests/Custom/Invoke-CippTestCustomScripts.ps1 index 75e65e8dc70c..edd5477a2d1e 100644 --- a/Modules/CIPPTests/Public/Tests/Custom/Invoke-CippTestCustomScripts.ps1 +++ b/Modules/CIPPTests/Public/Tests/Custom/Invoke-CippTestCustomScripts.ps1 @@ -68,7 +68,7 @@ function Invoke-CippTestCustomScripts { # Auto-detected status from output, then apply explicit override if present $AutoStatus = if ($FailedRows.Count -gt 0) { 'Failed' } else { 'Passed' } - $ValidExplicitStatuses = @('Passed', 'Failed', 'Info') + $ValidExplicitStatuses = @('Passed', 'Failed', 'Info', 'Investigate') if ($ExplicitStatus -and $ExplicitStatus -in $ValidExplicitStatuses) { $AutoStatus = $ExplicitStatus } @@ -76,6 +76,7 @@ function Invoke-CippTestCustomScripts { $FinalStatus = switch ($ResultMode) { 'AlwaysPass' { 'Passed' } 'AlwaysInfo' { 'Info' } + 'AlwaysInvestigate' { 'Investigate' } default { $AutoStatus } } @@ -90,6 +91,7 @@ function Invoke-CippTestCustomScripts { $FinalStatus = switch ($ResultMode) { 'AlwaysPass' { 'Passed' } 'AlwaysInfo' { 'Info' } + 'AlwaysInvestigate' { 'Investigate' } default { 'Failed' } } Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Custom' -Status $FinalStatus -ResultMarkdown "Custom script execution failed: $($ErrorMessage.NormalizedError)" -Risk ($Script.Risk ?? 'Medium') -Name $ScriptName -Pillar $Script.Pillar -UserImpact $Script.UserImpact -ImplementationEffort $Script.ImplementationEffort -Category 'Custom Script' diff --git a/Modules/CippExtensions/Public/Sherweb/Get-SherwebCurrentSubscription.ps1 b/Modules/CippExtensions/Public/Sherweb/Get-SherwebCurrentSubscription.ps1 index f0cf0308dc7c..bfdbf623ca80 100644 --- a/Modules/CippExtensions/Public/Sherweb/Get-SherwebCurrentSubscription.ps1 +++ b/Modules/CippExtensions/Public/Sherweb/Get-SherwebCurrentSubscription.ps1 @@ -11,6 +11,10 @@ function Get-SherwebCurrentSubscription { $CustomerId = Get-ExtensionMapping -Extension 'Sherweb' | Where-Object { $_.RowKey -eq $TenantFilter } | Select-Object -ExpandProperty IntegrationId } + if ([string]::IsNullOrEmpty($CustomerId)) { + throw 'No Sherweb mapping found' + } + Write-Information "Getting current subscription for $CustomerId" $AuthHeader = Get-SherwebAuthentication $Uri = "https://api.sherweb.com/service-provider/v1/billing/subscriptions/details?customerId=$CustomerId" diff --git a/Modules/DNSHealth/1.1.6/DNSHealth.psd1 b/Modules/DNSHealth/1.1.8/DNSHealth.psd1 similarity index 92% rename from Modules/DNSHealth/1.1.6/DNSHealth.psd1 rename to Modules/DNSHealth/1.1.8/DNSHealth.psd1 index 92d590ea5bcf..a9d8c70eac4b 100644 --- a/Modules/DNSHealth/1.1.6/DNSHealth.psd1 +++ b/Modules/DNSHealth/1.1.8/DNSHealth.psd1 @@ -12,7 +12,7 @@ RootModule = 'DNSHealth.psm1' # Version number of this module. - ModuleVersion = '1.1.6' + ModuleVersion = '1.1.8' # Supported PSEditions # CompatiblePSEditions = @() @@ -69,7 +69,7 @@ # 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 = @('Read-DmarcPolicy','Read-MtaStsPolicy','Add-MailProvider','Get-MailProvider','Read-DkimRecord','Read-MtaStsRecord','Read-MXRecord','Read-NSRecord','Read-SPFRecord','Read-TlsRptRecord','Read-WhoisRecord','Remove-MailProvider','Resolve-DnsHttpsQuery','Set-DnsResolver','Test-DNSSEC','Test-HttpsCertificate','Test-MtaSts') + FunctionsToExport = @('Read-DmarcPolicy','Read-MtaStsPolicy','Add-MailProvider','Get-MailProvider','Read-AutoDiscoverRecord','Read-DkimRecord','Read-MtaStsRecord','Read-MXRecord','Read-NSRecord','Read-SPFRecord','Read-TlsRptRecord','Read-WhoisRecord','Remove-MailProvider','Resolve-DnsHttpsQuery','Set-DnsResolver','Test-DNSSEC','Test-HttpsCertificate','Test-MtaSts') # 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 = '*' diff --git a/Modules/DNSHealth/1.1.6/DNSHealth.psm1 b/Modules/DNSHealth/1.1.8/DNSHealth.psm1 similarity index 96% rename from Modules/DNSHealth/1.1.6/DNSHealth.psm1 rename to Modules/DNSHealth/1.1.8/DNSHealth.psm1 index e299b961092a..113996bb69e9 100644 --- a/Modules/DNSHealth/1.1.6/DNSHealth.psm1 +++ b/Modules/DNSHealth/1.1.8/DNSHealth.psm1 @@ -923,6 +923,112 @@ function Get-MailProvider { return $Providers } #EndRegion './Public/Records/Get-MailProvider.ps1' 95 +#Region './Public/Records/Read-AutoDiscoverRecord.ps1' -1 + +function Read-AutoDiscoverRecord { + <# + .SYNOPSIS + Check AutoDiscover DNS records for a domain + + .DESCRIPTION + Resolves AutoDiscover DNS records (CNAME, A, and SRV) and validates the configuration. + For Microsoft 365, the expected setup is a CNAME pointing to autodiscover.outlook.com. + + .PARAMETER Domain + Domain to check AutoDiscover records for + + .PARAMETER ExpectedTarget + Expected CNAME/SRV target to validate against. Defaults to autodiscover.outlook.com + + .EXAMPLE + PS> Read-AutoDiscoverRecord -Domain example.com + + .EXAMPLE + PS> Read-AutoDiscoverRecord -Domain example.com -ExpectedTarget 'autodiscover.custom.com' + + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Domain, + + [Parameter(Mandatory = $false)] + [string]$ExpectedTarget = 'autodiscover.outlook.com' + ) + + $AutoDiscoverDomain = "autodiscover.$Domain" + $SrvDomain = "_autodiscover._tcp.$Domain" + + $ValidationPasses = [System.Collections.Generic.List[string]]::new() + $ValidationWarns = [System.Collections.Generic.List[string]]::new() + $ValidationFails = [System.Collections.Generic.List[string]]::new() + + $CnameRecord = $null + $ARecord = $null + $SrvRecord = $null + $RecordType = 'None' + + # Query autodiscover hostname - A query returns CNAME in chain if present + $DnsResult = Resolve-DnsHttpsQuery -Domain $AutoDiscoverDomain + if ($DnsResult.Answer) { + $CnameRecord = ($DnsResult.Answer | Where-Object { $_.type -eq 5 }).data -replace '\.$' + if ($CnameRecord) { + $RecordType = 'CNAME' + if ($CnameRecord -eq $ExpectedTarget) { + $ValidationPasses.Add("AutoDiscover CNAME correctly points to $ExpectedTarget") | Out-Null + } else { + $ValidationWarns.Add("AutoDiscover CNAME points to $CnameRecord (expected $ExpectedTarget)") | Out-Null + } + } else { + $ARecord = @(($DnsResult.Answer | Where-Object { $_.type -eq 1 }).data) + if ($ARecord.Count -gt 0) { + $RecordType = 'A' + $ValidationWarns.Add("AutoDiscover is configured with an A record ($($ARecord -join ', ')) instead of the recommended CNAME to $ExpectedTarget") | Out-Null + } + } + } + + # Check SRV record + $SrvResult = Resolve-DnsHttpsQuery -Domain $SrvDomain -RecordType SRV + if ($SrvResult.Answer) { + $SrvData = ($SrvResult.Answer | Where-Object { $_.type -eq 33 }).data + if ($SrvData) { + # SRV data format: priority weight port target + $SrvParts = $SrvData -split '\s+' + $SrvTarget = if ($SrvParts.Count -ge 4) { $SrvParts[3] -replace '\.$' } else { $SrvData -replace '\.$' } + $SrvRecord = $SrvTarget + + if ($RecordType -eq 'None') { + $RecordType = 'SRV' + } + + if ($SrvTarget -eq $ExpectedTarget) { + $ValidationPasses.Add("AutoDiscover SRV record correctly points to $ExpectedTarget") | Out-Null + } else { + $ValidationWarns.Add("AutoDiscover SRV record points to $SrvTarget (expected $ExpectedTarget)") | Out-Null + } + } + } + + # No records found at all + if (-not $CnameRecord -and ($null -eq $ARecord -or $ARecord.Count -eq 0) -and -not $SrvRecord) { + $ValidationFails.Add("No AutoDiscover DNS records found (checked CNAME, A, and SRV for $Domain)") | Out-Null + } + + [PSCustomObject]@{ + Domain = $AutoDiscoverDomain + Record = if ($CnameRecord) { $CnameRecord } elseif ($ARecord) { $ARecord -join ', ' } elseif ($SrvRecord) { $SrvRecord } else { $null } + RecordType = $RecordType + CnameRecord = $CnameRecord + ARecord = $ARecord + SrvRecord = $SrvRecord + ExpectedTarget = $ExpectedTarget + ValidationPasses = @($ValidationPasses) + ValidationWarns = @($ValidationWarns) + ValidationFails = @($ValidationFails) + } +} +#EndRegion './Public/Records/Read-AutoDiscoverRecord.ps1' 104 #Region './Public/Records/Read-DkimRecord.ps1' -1 function Read-DkimRecord { diff --git a/Modules/DNSHealth/1.1.6/MailProviders/AppRiver.json b/Modules/DNSHealth/1.1.8/MailProviders/AppRiver.json similarity index 100% rename from Modules/DNSHealth/1.1.6/MailProviders/AppRiver.json rename to Modules/DNSHealth/1.1.8/MailProviders/AppRiver.json diff --git a/Modules/DNSHealth/1.1.6/MailProviders/BarracudaESS.json b/Modules/DNSHealth/1.1.8/MailProviders/BarracudaESS.json similarity index 100% rename from Modules/DNSHealth/1.1.6/MailProviders/BarracudaESS.json rename to Modules/DNSHealth/1.1.8/MailProviders/BarracudaESS.json diff --git a/Modules/DNSHealth/1.1.6/MailProviders/Google.json b/Modules/DNSHealth/1.1.8/MailProviders/Google.json similarity index 100% rename from Modules/DNSHealth/1.1.6/MailProviders/Google.json rename to Modules/DNSHealth/1.1.8/MailProviders/Google.json diff --git a/Modules/DNSHealth/1.1.6/MailProviders/HornetSecurity.json b/Modules/DNSHealth/1.1.8/MailProviders/HornetSecurity.json similarity index 100% rename from Modules/DNSHealth/1.1.6/MailProviders/HornetSecurity.json rename to Modules/DNSHealth/1.1.8/MailProviders/HornetSecurity.json diff --git a/Modules/DNSHealth/1.1.6/MailProviders/Intermedia.json b/Modules/DNSHealth/1.1.8/MailProviders/Intermedia.json similarity index 100% rename from Modules/DNSHealth/1.1.6/MailProviders/Intermedia.json rename to Modules/DNSHealth/1.1.8/MailProviders/Intermedia.json diff --git a/Modules/DNSHealth/1.1.6/MailProviders/Microsoft365.json b/Modules/DNSHealth/1.1.8/MailProviders/Microsoft365.json similarity index 100% rename from Modules/DNSHealth/1.1.6/MailProviders/Microsoft365.json rename to Modules/DNSHealth/1.1.8/MailProviders/Microsoft365.json diff --git a/Modules/DNSHealth/1.1.6/MailProviders/Mimecast.json b/Modules/DNSHealth/1.1.8/MailProviders/Mimecast.json similarity index 100% rename from Modules/DNSHealth/1.1.6/MailProviders/Mimecast.json rename to Modules/DNSHealth/1.1.8/MailProviders/Mimecast.json diff --git a/Modules/DNSHealth/1.1.6/MailProviders/Null.json b/Modules/DNSHealth/1.1.8/MailProviders/Null.json similarity index 100% rename from Modules/DNSHealth/1.1.6/MailProviders/Null.json rename to Modules/DNSHealth/1.1.8/MailProviders/Null.json diff --git a/Modules/DNSHealth/1.1.6/MailProviders/Proofpoint.json b/Modules/DNSHealth/1.1.8/MailProviders/Proofpoint.json similarity index 100% rename from Modules/DNSHealth/1.1.6/MailProviders/Proofpoint.json rename to Modules/DNSHealth/1.1.8/MailProviders/Proofpoint.json diff --git a/Modules/DNSHealth/1.1.6/MailProviders/Reflexion.json b/Modules/DNSHealth/1.1.8/MailProviders/Reflexion.json similarity index 100% rename from Modules/DNSHealth/1.1.6/MailProviders/Reflexion.json rename to Modules/DNSHealth/1.1.8/MailProviders/Reflexion.json diff --git a/Modules/DNSHealth/1.1.6/MailProviders/Sophos.json b/Modules/DNSHealth/1.1.8/MailProviders/Sophos.json similarity index 100% rename from Modules/DNSHealth/1.1.6/MailProviders/Sophos.json rename to Modules/DNSHealth/1.1.8/MailProviders/Sophos.json diff --git a/Modules/DNSHealth/1.1.6/MailProviders/SpamTitan.json b/Modules/DNSHealth/1.1.8/MailProviders/SpamTitan.json similarity index 100% rename from Modules/DNSHealth/1.1.6/MailProviders/SpamTitan.json rename to Modules/DNSHealth/1.1.8/MailProviders/SpamTitan.json diff --git a/Modules/DNSHealth/1.1.6/MailProviders/SymantecCloud.json b/Modules/DNSHealth/1.1.8/MailProviders/SymantecCloud.json similarity index 100% rename from Modules/DNSHealth/1.1.6/MailProviders/SymantecCloud.json rename to Modules/DNSHealth/1.1.8/MailProviders/SymantecCloud.json diff --git a/Modules/DNSHealth/1.1.6/MailProviders/_template.json b/Modules/DNSHealth/1.1.8/MailProviders/_template.json similarity index 100% rename from Modules/DNSHealth/1.1.6/MailProviders/_template.json rename to Modules/DNSHealth/1.1.8/MailProviders/_template.json diff --git a/Modules/DNSHealth/1.1.6/PSGetModuleInfo.xml b/Modules/DNSHealth/1.1.8/PSGetModuleInfo.xml similarity index 72% rename from Modules/DNSHealth/1.1.6/PSGetModuleInfo.xml rename to Modules/DNSHealth/1.1.8/PSGetModuleInfo.xml index 0a8245a31c45..a5693b78886a 100644 --- a/Modules/DNSHealth/1.1.6/PSGetModuleInfo.xml +++ b/Modules/DNSHealth/1.1.8/PSGetModuleInfo.xml @@ -7,13 +7,13 @@ DNSHealth - 1.1.6 + 1.1.8 Module CIPP DNS Health Check Module John Duprey johnduprey 2023 John Duprey -
2026-04-24T17:42:26-04:00
+
2026-05-08T15:46:09-04:00
@@ -36,18 +36,14 @@ - Cmdlet + Workflow - RoleCapability - - - - Command + Function @@ -55,6 +51,7 @@ Read-MtaStsPolicy Add-MailProvider Get-MailProvider + Read-AutoDiscoverRecord Read-DkimRecord Read-MtaStsRecord Read-MXRecord @@ -72,15 +69,15 @@ - DscResource + RoleCapability - Workflow + DscResource - Function + Command @@ -88,6 +85,7 @@ Read-MtaStsPolicy Add-MailProvider Get-MailProvider + Read-AutoDiscoverRecord Read-DkimRecord Read-MtaStsRecord Read-MXRecord @@ -104,6 +102,10 @@ + + Cmdlet + + @@ -127,24 +129,24 @@ True True 0 - 477 - 31557 - 4/24/2026 5:42:26 PM -04:00 - 4/24/2026 5:42:26 PM -04:00 - 4/24/2026 5:42:26 PM -04:00 - PSModule PSFunction_Read-DmarcPolicy PSCommand_Read-DmarcPolicy PSFunction_Read-MtaStsPolicy PSCommand_Read-MtaStsPolicy PSFunction_Add-MailProvider PSCommand_Add-MailProvider PSFunction_Get-MailProvider PSCommand_Get-MailProvider PSFunction_Read-DkimRecord PSCommand_Read-DkimRecord PSFunction_Read-MtaStsRecord PSCommand_Read-MtaStsRecord PSFunction_Read-MXRecord PSCommand_Read-MXRecord PSFunction_Read-NSRecord PSCommand_Read-NSRecord PSFunction_Read-SPFRecord PSCommand_Read-SPFRecord PSFunction_Read-TlsRptRecord PSCommand_Read-TlsRptRecord PSFunction_Read-WhoisRecord PSCommand_Read-WhoisRecord PSFunction_Remove-MailProvider PSCommand_Remove-MailProvider PSFunction_Resolve-DnsHttpsQuery PSCommand_Resolve-DnsHttpsQuery PSFunction_Set-DnsResolver PSCommand_Set-DnsResolver PSFunction_Test-DNSSEC PSCommand_Test-DNSSEC PSFunction_Test-HttpsCertificate PSCommand_Test-HttpsCertificate PSFunction_Test-MtaSts PSCommand_Test-MtaSts PSIncludes_Function + 495 + 32431 + 5/8/2026 3:46:09 PM -04:00 + 5/8/2026 3:46:09 PM -04:00 + 5/8/2026 3:46:09 PM -04:00 + PSModule PSFunction_Read-DmarcPolicy PSCommand_Read-DmarcPolicy PSFunction_Read-MtaStsPolicy PSCommand_Read-MtaStsPolicy PSFunction_Add-MailProvider PSCommand_Add-MailProvider PSFunction_Get-MailProvider PSCommand_Get-MailProvider PSFunction_Read-AutoDiscoverRecord PSCommand_Read-AutoDiscoverRecord PSFunction_Read-DkimRecord PSCommand_Read-DkimRecord PSFunction_Read-MtaStsRecord PSCommand_Read-MtaStsRecord PSFunction_Read-MXRecord PSCommand_Read-MXRecord PSFunction_Read-NSRecord PSCommand_Read-NSRecord PSFunction_Read-SPFRecord PSCommand_Read-SPFRecord PSFunction_Read-TlsRptRecord PSCommand_Read-TlsRptRecord PSFunction_Read-WhoisRecord PSCommand_Read-WhoisRecord PSFunction_Remove-MailProvider PSCommand_Remove-MailProvider PSFunction_Resolve-DnsHttpsQuery PSCommand_Resolve-DnsHttpsQuery PSFunction_Set-DnsResolver PSCommand_Set-DnsResolver PSFunction_Test-DNSSEC PSCommand_Test-DNSSEC PSFunction_Test-HttpsCertificate PSCommand_Test-HttpsCertificate PSFunction_Test-MtaSts PSCommand_Test-MtaSts PSIncludes_Function False - 2026-04-24T17:42:26Z - 1.1.6 + 2026-05-08T15:46:09Z + 1.1.8 John Duprey false Module - DNSHealth.nuspec|MailProviders\Microsoft365.json|MailProviders\Sophos.json|DNSHealth.psd1|MailProviders\SymantecCloud.json|MailProviders\SpamTitan.json|MailProviders\AppRiver.json|DNSHealth.psm1|MailProviders\Intermedia.json|MailProviders\_template.json|MailProviders\BarracudaESS.json|MailProviders\Reflexion.json|MailProviders\HornetSecurity.json|MailProviders\Google.json|MailProviders\Proofpoint.json|MailProviders\Null.json|MailProviders\Mimecast.json + DNSHealth.nuspec|MailProviders\SymantecCloud.json|MailProviders\Microsoft365.json|MailProviders\Sophos.json|DNSHealth.psd1|MailProviders\Intermedia.json|MailProviders\SpamTitan.json|MailProviders\AppRiver.json|DNSHealth.psm1|MailProviders\Reflexion.json|MailProviders\_template.json|MailProviders\BarracudaESS.json|MailProviders\Null.json|MailProviders\HornetSecurity.json|MailProviders\Google.json|MailProviders\Proofpoint.json|MailProviders\Mimecast.json a300d2b0-d468-46d1-88a3-e442a76b655b 7.0
- /Users/johnduprey/GitHub/CIPP Workspace/CIPP-API/Modules/DNSHealth/1.1.6 + /Users/johnduprey/GitHub/CIPP Workspace/CIPP-API/Modules/DNSHealth/1.1.8 diff --git a/host.json b/host.json index 804b0ab6ca5e..473ab1bfce11 100644 --- a/host.json +++ b/host.json @@ -16,7 +16,7 @@ "distributedTracingEnabled": false, "version": "None" }, - "defaultVersion": "10.4.3", + "defaultVersion": "10.4.4", "versionMatchStrategy": "Strict", "versionFailureStrategy": "Fail" } diff --git a/version_latest.txt b/version_latest.txt index 44c1a8619b2b..622a6f75b792 100644 --- a/version_latest.txt +++ b/version_latest.txt @@ -1 +1 @@ -10.4.3 +10.4.4