Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Config/CIPPTimers.json
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,16 @@
"TZOffset": true,
"IsSystem": true
},
{
"Id": "5e8a9b4c-2d6f-4a3e-b7c1-9d0e5f3a8b2c",
"Command": "Start-IntuneReportExportOrchestrator",
"Description": "Submit Intune report-export jobs ahead of nightly DB cache run",
"Cron": "0 0 2 * * *",
"Priority": 22,
"RunOnProcessor": true,
"TZOffset": true,
"IsSystem": true
},
{
"Id": "9a7f8e6d-5c4b-3a2d-1e0f-9b8c7d6e5f4a",
"Command": "Start-CIPPDBCacheOrchestrator",
Expand Down
1 change: 0 additions & 1 deletion Config/version_latest.txt

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ function Push-ExecScheduledCommand {
return
}

if ($Command.Module -notin @('CIPPCore', 'CIPPAlerts', 'CIPPStandards', 'CIPPTests', 'CIPPDB')) {
if ($Command.Module -notin @('CIPPCore', 'CIPPAlerts', 'CIPPStandards', 'CIPPTests', 'CIPPDB', 'CippExtensions')) {
$State = 'Failed'
Write-LogMessage -headers $Headers -API 'ScheduledTask' -message "Blocked attempt to schedule command from unauthorized module: $($Command.ModuleName)\$($Item.Command)" -Sev 'Warning'
$Results = "Task blocked: The command '$($Item.Command)' is not permitted to run as a scheduled task."
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
function Push-IntuneReportExportSubmit {
<#
.SYNOPSIS
Submits an Intune report export job for a tenant and stores the job id.
.FUNCTIONALITY
Entrypoint
#>
[CmdletBinding()]
param($Item)

$TenantFilter = $Item.TenantFilter
$ReportName = $Item.ReportName

if (-not $TenantFilter -or -not $ReportName) {
Write-LogMessage -API 'IntuneReportExport' -message 'Missing TenantFilter or ReportName on activity item' -sev Error
return @{ Status = 'Failed'; Reason = 'MissingInput' }
}

try {
$Select = switch ($ReportName) {
'AppInvRawData' {
@(
'ApplicationKey', 'ApplicationName', 'ApplicationPublisher', 'ApplicationVersion',
'DeviceId', 'DeviceName', 'OSDescription', 'OSVersion', 'Platform',
'UserId', 'UserName', 'EmailAddress'
)
}
default { throw "Unknown Intune report '$ReportName'" }
}

$Body = @{
reportName = $ReportName
format = 'json'
localizationType = 'replaceLocalizableValues'
select = $Select
} | ConvertTo-Json -Depth 5

$Job = New-GraphPOSTRequest `
-uri 'https://graph.microsoft.com/beta/deviceManagement/reports/exportJobs' `
-tenantid $TenantFilter `
-body $Body

if (-not $Job.id) { throw "Intune returned no job id for $ReportName" }

$JobsTable = Get-CIPPTable -tablename 'IntuneReportJobs'
$Existing = Get-CIPPAzDataTableEntity @JobsTable -Filter "PartitionKey eq '$TenantFilter' and RowKey eq '$ReportName'"
if ($Existing) {
Remove-AzDataTableEntity @JobsTable -Entity $Existing -Force -ErrorAction SilentlyContinue
}

Add-CIPPAzDataTableEntity @JobsTable -Entity @{
PartitionKey = $TenantFilter
RowKey = $ReportName
JobId = $Job.id
ReportName = $ReportName
SubmittedAt = ([DateTime]::UtcNow).ToString('o')
} -Force

Write-LogMessage -API 'IntuneReportExport' -tenant $TenantFilter -message "Submitted $ReportName export job $($Job.id)" -sev Info
return @{ Status = 'Submitted'; JobId = $Job.id; ReportName = $ReportName; TenantFilter = $TenantFilter }
} catch {
$ErrorMessage = Get-CippException -Exception $_
Write-LogMessage -API 'IntuneReportExport' -tenant $TenantFilter -message "Failed to submit $ReportName export: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage
return @{ Status = 'Failed'; ReportName = $ReportName; TenantFilter = $TenantFilter; Error = $ErrorMessage.NormalizedError }
}
}
70 changes: 23 additions & 47 deletions Modules/CIPPCore/Public/Add-CIPPGroupMember.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -45,54 +45,37 @@ function Add-CIPPGroupMember {
}
$Users = New-GraphBulkRequest -Requests @($Requests) -tenantid $TenantFilter

$SuccessfulUsers = [System.Collections.Generic.List[string]]::new()
$FailedUsers = [System.Collections.Generic.List[string]]::new()

# Accept both human-readable labels (from Invoke-EditGroup / older callers) and
# camelCase calculatedGroupType values (from the user template / add-edit-user form)
$ExoGroupTypes = @('Distribution list', 'Distribution List', 'Mail-Enabled Security', 'distributionList', 'security')

if ($GroupType -in $ExoGroupTypes) {
if ($GroupType -eq 'Distribution list' -or $GroupType -eq 'Mail-Enabled Security') {
$ExoBulkRequests = [System.Collections.Generic.List[object]]::new()
$GuidToUpn = @{}
$ExoLogs = [System.Collections.Generic.List[object]]::new()

foreach ($User in $Users) {
$UserUpn = $User.body.userPrincipalName
if (-not $UserUpn) { continue }
$OpGuid = [guid]::NewGuid().ToString()
$GuidToUpn[$OpGuid] = $UserUpn
$Params = @{ Identity = $GroupId; Member = $UserUpn; BypassSecurityGroupManagerCheck = $true }
$Params = @{ Identity = $GroupId; Member = $User.body.userPrincipalName; BypassSecurityGroupManagerCheck = $true }
$ExoBulkRequests.Add(@{
OperationGuid = $OpGuid
CmdletInput = @{
CmdletInput = @{
CmdletName = 'Add-DistributionGroupMember'
Parameters = $Params
}
})
$ExoLogs.Add(@{
message = "Added member $($User.body.userPrincipalName) to $($GroupId) group"
target = $User.body.userPrincipalName
})
}

if ($ExoBulkRequests.Count -gt 0) {
$RawExoRequest = @(New-ExoBulkRequest -tenantid $TenantFilter -cmdletArray @($ExoBulkRequests))
$RawExoRequest = New-ExoBulkRequest -tenantid $TenantFilter -cmdletArray @($ExoBulkRequests)
$LastError = $RawExoRequest | Select-Object -Last 1

# Index responses by OperationGuid so each user is correlated by position, not by error.target
$ResponseByGuid = @{}
foreach ($Response in $RawExoRequest) {
if ($Response.OperationGuid) {
$ResponseByGuid[$Response.OperationGuid] = $Response
}
foreach ($ExoError in $LastError.error) {
Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $ExoError -Sev 'Error'
throw $ExoError
}

foreach ($OpGuid in $GuidToUpn.Keys) {
$UserUpn = $GuidToUpn[$OpGuid]
$Response = $ResponseByGuid[$OpGuid]

if ($Response -and $Response.error) {
$ErrorText = if ($Response.error -is [string]) { $Response.error } else { ($Response.error | Out-String).Trim() }
Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Failed to add member $($UserUpn) to $($GroupId): $ErrorText" -Sev 'Error'
$FailedUsers.Add($UserUpn)
} else {
Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Added member $($UserUpn) to $($GroupId) group" -Sev 'Info'
$SuccessfulUsers.Add($UserUpn)
foreach ($ExoLog in $ExoLogs) {
$ExoError = $LastError | Where-Object { $ExoLog.target -in $_.target -and $_.error }
if (!$LastError -or ($LastError.error -and $LastError.target -notcontains $ExoLog.target)) {
Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $ExoLog.message -Sev 'Info'
}
}
}
Expand All @@ -108,26 +91,19 @@ function Add-CIPPGroupMember {
}
}
$AddResults = New-GraphBulkRequest -tenantid $TenantFilter -Requests @($AddRequests)
$SuccessfulUsers = [system.collections.generic.list[string]]::new()
foreach ($Result in $AddResults) {
$UserPrincipalName = $Users | Where-Object { $_.body.id -eq $Result.id } | Select-Object -ExpandProperty body | Select-Object -ExpandProperty userPrincipalName
if ($Result.status -lt 200 -or $Result.status -gt 299) {
Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Failed to add member $($UserPrincipalName): $($Result.body.error.message)" -Sev 'Error'
$FailedUsers.Add($UserPrincipalName)
$FailedUsername = $Users | Where-Object { $_.body.id -eq $Result.id } | Select-Object -ExpandProperty body | Select-Object -ExpandProperty userPrincipalName
Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Failed to add member $($FailedUsername): $($Result.body.error.message)" -Sev 'Error'
} else {
$UserPrincipalName = $Users | Where-Object { $_.body.id -eq $Result.id } | Select-Object -ExpandProperty body | Select-Object -ExpandProperty userPrincipalName
$SuccessfulUsers.Add($UserPrincipalName)
}
}
}

if ($SuccessfulUsers.Count -eq 0 -and $FailedUsers.Count -gt 0) {
$Results = "Failed to add user $($FailedUsers -join ', ') to $($GroupId)."
throw $Results
}

$Results = "Successfully added user $($SuccessfulUsers -join ', ') to $($GroupId)."
if ($FailedUsers.Count -gt 0) {
$Results = "$Results Failed to add: $($FailedUsers -join ', ')."
}
$UserList = ($SuccessfulUsers -join ', ')
$Results = "Successfully added user $UserList to $($GroupId)."
Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Results -Sev 'Info'
return $Results
} catch {
Expand Down
4 changes: 2 additions & 2 deletions Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ function Add-CIPPScheduledTask {
$ImportedModules = [System.Collections.Generic.List[string]]::new()
if (-not $Command) {
try {
foreach ($SiblingModule in @('CIPPStandards', 'CIPPAlerts', 'CIPPTests', 'CIPPDB')) {
foreach ($SiblingModule in @('CIPPStandards', 'CIPPAlerts', 'CIPPTests', 'CIPPDB', 'CippExtensions')) {
if (-not (Get-Module -Name $SiblingModule)) {
Import-Module $SiblingModule -ErrorAction SilentlyContinue
if (Get-Module -Name $SiblingModule) {
Expand All @@ -91,7 +91,7 @@ function Add-CIPPScheduledTask {
return "Error - The command '$RequestedCommand' does not exist and cannot be scheduled."
}

if ($Command.Module -notin @('CIPPCore', 'CIPPAlerts', 'CIPPStandards', 'CIPPTests', 'CIPPDB')) {
if ($Command.Module -notin @('CIPPCore', 'CIPPAlerts', 'CIPPStandards', 'CIPPTests', 'CIPPDB', 'CippExtensions')) {
Write-LogMessage -headers $Headers -API 'ScheduledTask' -message "Blocked attempt to schedule command from unauthorized module: $($Command.ModuleName)\$RequestedCommand" -Sev 'Warning'
return "Error - The command '$RequestedCommand' is not permitted to run as a scheduled task."
}
Expand Down
58 changes: 58 additions & 0 deletions Modules/CIPPCore/Public/Authentication/Add-CIPPSSOAppSecret.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
function Add-CIPPSSOAppSecret {
<#
.SYNOPSIS
Creates a client secret on the CIPP-SSO app registration with retry.
.DESCRIPTION
Adds a new password credential to the given app object via Graph. Retries up to
MaxRetries times with backoff because Entra propagation can take a few seconds
after the app is freshly created or its app-management-policy exemption is set.
Throws on final failure so callers can persist Status=error + LastError.
.PARAMETER ObjectId
Graph object ID of the application (NOT the appId/clientId).
.PARAMETER DisplayName
Display name to set on the password credential. Defaults to 'CIPP-SSO-Secret'.
.PARAMETER MaxRetries
Number of secret-creation attempts before giving up. Defaults to 5.
#>
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')]
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$ObjectId,

[Parameter(Mandatory = $false)]
[string]$DisplayName = 'CIPP-SSO-Secret',

[Parameter(Mandatory = $false)]
[int]$MaxRetries = 5
)

$SecretText = $null
$SecretAttempt = 0
$BackoffSchedule = @(2, 5, 10, 15, 30)
$LastException = $null

while ($SecretAttempt -lt $MaxRetries -and -not $SecretText) {
try {
$PasswordBody = @{ passwordCredential = @{ displayName = $DisplayName } } | ConvertTo-Json -Compress
$PasswordResult = New-GraphPOSTRequest -uri "https://graph.microsoft.com/v1.0/applications/$ObjectId/addPassword" -body $PasswordBody -type POST -NoAuthCheck $true -AsApp $true
$SecretText = $PasswordResult.secretText
Write-Information "[SSO-Secret] Client secret created on objectId $ObjectId"
} catch {
$SecretAttempt++
$LastException = $_
Write-Warning "[SSO-Secret] Secret creation attempt $SecretAttempt/$MaxRetries failed: $($_.Exception.Message)"
if ($SecretAttempt -lt $MaxRetries) {
$Delay = $BackoffSchedule[[Math]::Min($SecretAttempt - 1, $BackoffSchedule.Count - 1)]
Start-Sleep -Seconds $Delay
}
}
}

if (-not $SecretText) {
$InnerMessage = if ($LastException) { $LastException.Exception.Message } else { 'unknown error' }
throw "Failed to create client secret for CIPP-SSO after $MaxRetries attempts: $InnerMessage"
}

return $SecretText
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ function Get-CippAllowedPermissions {

# Get all available permissions and base roles configuration

$Version = (Get-Content -Path (Join-Path $env:CIPPRootPath 'Config\version_latest.txt')).trim()
$Version = (Get-Content -Path (Join-Path $env:CIPPRootPath 'version_latest.txt')).trim()
$BaseRoles = Get-Content -Path (Join-Path $env:CIPPRootPath 'Config\cipp-roles.json') | ConvertFrom-Json
$DefaultRoles = @('superadmin', 'admin', 'editor', 'readonly', 'anonymous', 'authenticated')

Expand Down
42 changes: 9 additions & 33 deletions Modules/CIPPCore/Public/Authentication/New-CIPPSSOApp.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ function New-CIPPSSOApp {
Creates a new or updates an existing Entra ID app registration for CIPP-SSO with
openid, profile, and email delegated permissions. If ExistingAppId is provided,
looks up that specific app by clientId. If the app no longer exists in the tenant,
creates a new one. Generates a client secret and returns the details needed to
configure EasyAuth.
creates a new one. Does NOT create a client secret — call Add-CIPPSSOAppSecret
for that as a separate step so the AppId can be persisted before the (sometimes
flaky) secret creation runs.
#>
[CmdletBinding()]
param(
Expand Down Expand Up @@ -120,37 +121,12 @@ function New-CIPPSSOApp {
Write-Warning "[SSO-App] App management policy update failed (secret creation may still work): $($_.Exception.Message)"
}

# Create client secret with retry
$SecretText = $null
$SecretAttempt = 0
$MaxSecretRetries = 5
while ($SecretAttempt -lt $MaxSecretRetries -and -not $SecretText) {
try {
$PasswordBody = '{"passwordCredential":{"displayName":"CIPP-SSO-Secret"}}'
$PasswordResult = New-GraphPOSTRequest -uri "https://graph.microsoft.com/v1.0/applications/$AppObjectId/addPassword" -body $PasswordBody -type POST -NoAuthCheck $true -AsApp $true
$SecretText = $PasswordResult.secretText
Write-Information "[SSO-App] Client secret created"
} catch {
$SecretAttempt++
Write-Warning "[SSO-App] Secret creation attempt $SecretAttempt/$MaxSecretRetries failed: $($_.Exception.Message)"
if ($SecretAttempt -lt $MaxSecretRetries) {
$Delay = @(2, 5, 10, 15, 30)[$SecretAttempt - 1]
Start-Sleep -Seconds $Delay
}
}
}

if (-not $SecretText) {
throw "Failed to create client secret for $AppDisplayName after $MaxSecretRetries attempts"
}

return [PSCustomObject]@{
AppId = $AppClientId
ObjectId = $AppObjectId
ClientSecret = $SecretText
TenantId = $env:TenantID
DisplayName = $AppDisplayName
State = $State
MultiTenant = $MultiTenant
AppId = $AppClientId
ObjectId = $AppObjectId
TenantId = $env:TenantID
DisplayName = $AppDisplayName
State = $State
MultiTenant = $MultiTenant
}
}
Loading