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
41 changes: 41 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,3 +162,44 @@ Detailed PowerShell coding conventions are in `.github/instructions/powershell-c
- Do not create new Azure Function trigger folders — use the existing five triggers
- Do not call `Write-Output` in HTTP functions — return an `[HttpResponseContext]` (the outer trigger handles `Push-OutputBinding`)
- Do not hardcode tenant IDs or secrets — use environment variables and `Get-GraphToken`

## Commit messages

Generate all commit messages in **Conventional Commits** format:

```
<type>(<optional scope>): <description>

[optional body]

[optional footer(s)]
```

**Rules:**

- **Type** is required and must be one of:
- `feat` — a new feature
- `fix` — a bug fix
- `docs` — documentation only
- `style` — formatting, whitespace, no code-behavior change
- `refactor` — code change that neither fixes a bug nor adds a feature
- `perf` — a performance improvement
- `test` — adding or correcting tests
- `build` — build system or dependency changes
- `ci` — CI/CD configuration changes
- `chore` — routine maintenance, no production code change
- `revert` — reverts a previous commit
- **Scope** is optional and given in parentheses after the type (e.g. `feat(identity):`). Use a short, lowercase area name when it adds clarity.
- **Description** is a short, imperative-mood summary ("add", not "added"/"adds"), lowercase, no trailing period, ideally ≤ 72 characters.
- **Body** (optional) explains the *what* and *why*, not the *how*. Separate it from the description with one blank line.
- **Breaking changes** are indicated with a `!` before the colon (e.g. `feat!:`) and/or a `BREAKING CHANGE:` footer describing the change.
- Reference issues/PRs in the footer where relevant (e.g. `Closes #123`).

**Examples:**

```
feat(identity): add bulk user offboarding endpoint
fix(graph): handle expired token on retry
docs: update authentication model overview
refactor(standards)!: rename remediation parameter
```
2 changes: 1 addition & 1 deletion .github/workflows/Conventional_Commits.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const title = context.payload.pull_request.title || '';
const pattern = /^(feat|fix|docs|style|refactor|perf|test|chore|ci|build|revert)(\(.+\))?!?: .+/;
const pattern = /^(feat|fix|docs|style|refactor|perf|test|chore|ci|build|revert)(\(.+\))?!?: .+/i;
if (pattern.test(title)) {
console.log(`✓ PR title follows Conventional Commits format: "${title}"`);
Expand Down
2 changes: 1 addition & 1 deletion Modules/CIPPCore/Public/Add-CIPPApplicationPermission.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ function Add-CIPPApplicationPermission {
if (!$svcPrincipalId) { continue }

foreach ($SingleResource in $App.ResourceAccess | Where-Object -Property Type -EQ 'Role') {
if ($SingleResource.id -in $CurrentRoles.appRoleId) { continue }
if ($CurrentRoles | Where-Object { $_.appRoleId -eq $SingleResource.id -and $_.resourceId -eq $svcPrincipalId.id }) { continue }
[pscustomobject]@{
principalId = $($ourSVCPrincipal.id)
resourceId = $($svcPrincipalId.id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,13 @@ function Get-CIPPRolePermissions {
try {
$ValidPermissions = Get-CippHttpPermissions
if (@($ValidPermissions).Count -gt 0) {
$Permissions = @($Permissions | Where-Object { $ValidPermissions -contains $_ })
$ValidBases = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
foreach ($ValidPermission in $ValidPermissions) {
$null = $ValidBases.Add(($ValidPermission -replace '\.(ReadWrite|Read)$', ''))
}
$Permissions = @($Permissions | Where-Object {
$ValidBases.Contains(($_ -replace '\.(ReadWrite|Read)$', ''))
})
}
} catch {
Write-Warning "Unable to resolve valid permissions to filter role '$RoleName': $($_.Exception.Message)"
Expand Down
8 changes: 6 additions & 2 deletions Modules/CIPPCore/Public/Get-CIPPAuthentication.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,13 @@ function Get-CIPPAuthentication {
}
}

if (-not $env:SAMCertificate) {
# First run on this instance: provision the certificate now.
if (-not $env:SAMCertificate -and $env:SAMCertProvisionAttempted -ne 'true') {
# First run on this instance: provision the certificate now, at most once per
# process. The guard also breaks a recursion loop: Update-CIPPSAMCertificate
# calls Get-GraphToken, which re-enters this function when the AppCache
# ApplicationId does not match the environment.
# Set-CIPPSAMCertificate refreshes $env:SAMCertificate on success.
$env:SAMCertProvisionAttempted = 'true'
Write-Information 'No SAM certificate found, provisioning one now'
$CertResult = Update-CIPPSAMCertificate -ErrorAction Stop
Write-LogMessage -message "Provisioned SAM certificate during authentication load. Thumbprint: $($CertResult.Thumbprint), storage mode: $($CertResult.StorageMode)" -Sev 'Info' -API 'CIPP Authentication'
Expand Down
4 changes: 4 additions & 0 deletions Modules/CIPPCore/Public/Get-CippKeyVaultSecret.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ function Get-CippKeyVaultSecret {
break
} catch {
$lastError = $_
# 404 is definitive - the secret does not exist and retrying cannot change that
if ($_.Exception.Message -match '404|SecretNotFound') {
throw "Failed to retrieve secret '$Name' from vault '$VaultName': $($_.Exception.Message)"
}
if ($i -lt ($maxRetries - 1)) {
Start-Sleep -Seconds $retryDelay
$retryDelay *= 2 # Exponential backoff
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,12 @@ function Invoke-ExecDeployAppTemplate {
try {
$Config = $App.config
if ($Config -is [string]) {
$Config = $Config | ConvertFrom-Json -Depth 100
# Parse case-sensitive to survive templates carrying both 'applicationName'
# and 'ApplicationName', then collapse them via a case-insensitive dictionary.
$Parsed = $Config | ConvertFrom-Json -Depth 100 -AsHashtable
$Config = [ordered]@{}
foreach ($Key in $Parsed.Keys) { $Config[$Key] = $Parsed[$Key] }
$Config = [PSCustomObject]$Config
}

$AppType = "$($App.appType ?? $App.AppType)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ function Invoke-ExecCompareIntunePolicy {
'windowsQualityUpdatePolicies' = 'windowsQualityUpdatePolicies'
'windowsQualityUpdateProfiles' = 'windowsQualityUpdateProfiles'
'Intents' = 'Intents'
'ManagedAppPolicies' = 'AppProtection'
}

try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,11 @@ function Invoke-ListIntunePolicy {
method = 'GET'
url = "/deviceManagement/intents?`$top=1000"
}
@{
id = 'ManagedAppPolicies'
method = 'GET'
url = '/deviceAppManagement/managedAppPolicies?$orderby=displayName'
}
)

$BulkResults = New-GraphBulkRequest -Requests $BulkRequests -tenantid $TenantFilter
Expand All @@ -236,6 +241,7 @@ function Invoke-ListIntunePolicy {
$URLName = $_.Id
$_.body.Value | ForEach-Object {
$AssignmentContext = $_.'assignments@odata.context'
$PolicyODataType = $_.'@odata.type'
$policyTypeName = switch -Wildcard ($AssignmentContext) {
'*microsoft.graph.windowsIdentityProtectionConfiguration*' { 'Identity Protection' }
'*microsoft.graph.windows10EndpointProtectionConfiguration*' { 'Endpoint Protection' }
Expand Down Expand Up @@ -264,6 +270,14 @@ function Invoke-ListIntunePolicy {
$policyTypeName = switch ($URLName) {
'deviceCompliancePolicies' { 'Compliance Policy' }
'Intents' { 'Endpoint Security' }
'ManagedAppPolicies' {
switch -Wildcard ($PolicyODataType) {
'*iosManagedAppProtection*' { 'iOS App Protection' }
'*androidManagedAppProtection*' { 'Android App Protection' }
'*windowsManagedAppProtection*' { 'Windows App Protection' }
default { 'App Protection' }
}
}
default { $AssignmentContext }
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,13 +123,15 @@ function Invoke-CIPPStandardTeamsFederationConfiguration {
$BlockedDomainsMatches = $true
}
'AllowSpecificExternal' {
$AllowedDomainsMatches = -not (Compare-Object -ReferenceObject $AllowedDomainsAsAList -DifferenceObject $CurrentAllowedDomains)
# Both lists are already Sort-Object'd; compare as joined strings. Avoids Compare-Object,
# whose parameter binder coerces an empty array @() to $null and then throws.
$AllowedDomainsMatches = (@($AllowedDomainsAsAList) -join ',') -eq (@($CurrentAllowedDomains) -join ',')
$BlockedDomainsMatches = (!$CurrentBlockedDomains -or @($CurrentBlockedDomains).Count -eq 0)
}
'BlockSpecificExternal' {
# Allowed should be AllowAllKnownDomains, blocked domains already parsed above
$AllowedDomainsMatches = $IsCurrentAllowAllKnownDomains
$BlockedDomainsMatches = -not (Compare-Object -ReferenceObject $BlockedDomains -DifferenceObject $CurrentBlockedDomains)
$BlockedDomainsMatches = (@($BlockedDomains) -join ',') -eq (@($CurrentBlockedDomains) -join ',')
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,20 @@ function Invoke-NinjaOneDocumentTemplate {
} else {
$DocumentTemplate = (Invoke-WebRequest -Uri "https://$($Configuration.Instance)/api/v2/document-templates/$($ID)" -Method GET -Headers @{Authorization = "Bearer $($token.access_token)" } -ContentType 'application/json').content | ConvertFrom-Json -Depth 100
}

$MatchedCount = ($DocumentTemplate | Measure-Object).count
if ($MatchedCount -eq 1) {
# Matched a single document template
$NinjaDocumentTemplate = $DocumentTemplate
} elseif ($MatchedCount -eq 0) {
# Create a new Document Template
$Body = $Template | ConvertTo-Json -Depth 100
Write-Host "Ninja Body: $body"
$NinjaDocumentTemplate = (Invoke-WebRequest -Uri "https://$($Configuration.Instance)/api/v2/document-templates/" -Method POST -Headers @{Authorization = "Bearer $($token.access_token)" } -ContentType 'application/json' -Body $Body).content | ConvertFrom-Json -Depth 100
} else {
# Matched multiple templates. Should be impossible but lets check anyway :D
Throw 'Multiple Documents Matched the Provided Criteria'
$NinjaDocumentTemplate = $DocumentTemplate | Sort-Object { [int64]$_.id } | Select-Object -First 1
Write-Warning "Multiple NinjaOne document templates named '$($Template.name)' found ($MatchedCount). Using the oldest (id $($NinjaDocumentTemplate.id)). Remove the duplicate template(s) in NinjaOne."
}

return $NinjaDocumentTemplate

}
}
Loading