Skip to content

Commit eb6ec4e

Browse files
Merge branch 'dev' of https://github.com/KelvinTegelaar/CIPP-API into dev
2 parents ad19e39 + 532b20b commit eb6ec4e

225 files changed

Lines changed: 166352 additions & 73376 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/copilot-instructions.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,3 +162,44 @@ Detailed PowerShell coding conventions are in `.github/instructions/powershell-c
162162
- Do not create new Azure Function trigger folders — use the existing five triggers
163163
- Do not call `Write-Output` in HTTP functions — return an `[HttpResponseContext]` (the outer trigger handles `Push-OutputBinding`)
164164
- Do not hardcode tenant IDs or secrets — use environment variables and `Get-GraphToken`
165+
166+
## Commit messages
167+
168+
Generate all commit messages in **Conventional Commits** format:
169+
170+
```
171+
<type>(<optional scope>): <description>
172+
173+
[optional body]
174+
175+
[optional footer(s)]
176+
```
177+
178+
**Rules:**
179+
180+
- **Type** is required and must be one of:
181+
- `feat` — a new feature
182+
- `fix` — a bug fix
183+
- `docs` — documentation only
184+
- `style` — formatting, whitespace, no code-behavior change
185+
- `refactor` — code change that neither fixes a bug nor adds a feature
186+
- `perf` — a performance improvement
187+
- `test` — adding or correcting tests
188+
- `build` — build system or dependency changes
189+
- `ci` — CI/CD configuration changes
190+
- `chore` — routine maintenance, no production code change
191+
- `revert` — reverts a previous commit
192+
- **Scope** is optional and given in parentheses after the type (e.g. `feat(identity):`). Use a short, lowercase area name when it adds clarity.
193+
- **Description** is a short, imperative-mood summary ("add", not "added"/"adds"), lowercase, no trailing period, ideally ≤ 72 characters.
194+
- **Body** (optional) explains the *what* and *why*, not the *how*. Separate it from the description with one blank line.
195+
- **Breaking changes** are indicated with a `!` before the colon (e.g. `feat!:`) and/or a `BREAKING CHANGE:` footer describing the change.
196+
- Reference issues/PRs in the footer where relevant (e.g. `Closes #123`).
197+
198+
**Examples:**
199+
200+
```
201+
feat(identity): add bulk user offboarding endpoint
202+
fix(graph): handle expired token on retry
203+
docs: update authentication model overview
204+
refactor(standards)!: rename remediation parameter
205+
```
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
name: Conventional Commits Check
2+
3+
on:
4+
# Using pull_request_target instead of pull_request for secure handling of fork PRs
5+
pull_request_target:
6+
# Re-run on title edits so a corrected title clears the check
7+
types: [opened, synchronize, reopened, edited]
8+
# Only check PRs targeting the dev branch
9+
branches:
10+
- dev
11+
12+
permissions:
13+
pull-requests: write
14+
issues: write
15+
16+
jobs:
17+
conventional-commits:
18+
name: Validate Conventional Commits
19+
runs-on: ubuntu-slim
20+
steps:
21+
- name: Validate PR title
22+
uses: actions/github-script@v9
23+
with:
24+
github-token: ${{ secrets.GITHUB_TOKEN }}
25+
script: |
26+
const title = context.payload.pull_request.title || '';
27+
const pattern = /^(feat|fix|docs|style|refactor|perf|test|chore|ci|build|revert)(\(.+\))?!?: .+/i;
28+
29+
if (pattern.test(title)) {
30+
console.log(`✓ PR title follows Conventional Commits format: "${title}"`);
31+
return;
32+
}
33+
34+
console.log(`❌ PR title does not follow Conventional Commits format: "${title}"`);
35+
36+
const message = [
37+
'❌ **This PR title does not follow the [Conventional Commits](https://www.conventionalcommits.org/) format.**',
38+
'',
39+
'Expected format: `type(scope)?: description`',
40+
'',
41+
'Allowed types: `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `chore`, `ci`, `build`, `revert`',
42+
'',
43+
'Examples:',
44+
'- `feat: add SharePoint external user management`',
45+
'- `fix(auth): handle expired refresh tokens`',
46+
'- `docs: update self-hosting guide`',
47+
'',
48+
`Received: \`${title}\``,
49+
'',
50+
'🔒 This PR has been automatically closed. Please update the title to match the format above and reopen the PR.'
51+
].join('\n');
52+
53+
await github.rest.issues.createComment({
54+
...context.repo,
55+
issue_number: context.issue.number,
56+
body: message
57+
});
58+
59+
await github.rest.pulls.update({
60+
...context.repo,
61+
pull_number: context.issue.number,
62+
state: 'closed'
63+
});
64+
65+
core.setFailed(`PR title does not follow Conventional Commits format: "${title}"`);

Modules/CIPPCore/Public/Add-CIPPApplicationPermission.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ function Add-CIPPApplicationPermission {
122122
if (!$svcPrincipalId) { continue }
123123

124124
foreach ($SingleResource in $App.ResourceAccess | Where-Object -Property Type -EQ 'Role') {
125-
if ($SingleResource.id -in $CurrentRoles.appRoleId) { continue }
125+
if ($CurrentRoles | Where-Object { $_.appRoleId -eq $SingleResource.id -and $_.resourceId -eq $svcPrincipalId.id }) { continue }
126126
[pscustomobject]@{
127127
principalId = $($ourSVCPrincipal.id)
128128
resourceId = $($svcPrincipalId.id)

Modules/CIPPCore/Public/Authentication/Get-CIPPRolePermissions.ps1

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,13 @@ function Get-CIPPRolePermissions {
2525
try {
2626
$ValidPermissions = Get-CippHttpPermissions
2727
if (@($ValidPermissions).Count -gt 0) {
28-
$Permissions = @($Permissions | Where-Object { $ValidPermissions -contains $_ })
28+
$ValidBases = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
29+
foreach ($ValidPermission in $ValidPermissions) {
30+
$null = $ValidBases.Add(($ValidPermission -replace '\.(ReadWrite|Read)$', ''))
31+
}
32+
$Permissions = @($Permissions | Where-Object {
33+
$ValidBases.Contains(($_ -replace '\.(ReadWrite|Read)$', ''))
34+
})
2935
}
3036
} catch {
3137
Write-Warning "Unable to resolve valid permissions to filter role '$RoleName': $($_.Exception.Message)"

Modules/CIPPCore/Public/Get-CIPPAuthentication.ps1

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,13 @@ function Get-CIPPAuthentication {
4949
}
5050
}
5151

52-
if (-not $env:SAMCertificate) {
53-
# First run on this instance: provision the certificate now.
52+
if (-not $env:SAMCertificate -and $env:SAMCertProvisionAttempted -ne 'true') {
53+
# First run on this instance: provision the certificate now, at most once per
54+
# process. The guard also breaks a recursion loop: Update-CIPPSAMCertificate
55+
# calls Get-GraphToken, which re-enters this function when the AppCache
56+
# ApplicationId does not match the environment.
5457
# Set-CIPPSAMCertificate refreshes $env:SAMCertificate on success.
58+
$env:SAMCertProvisionAttempted = 'true'
5559
Write-Information 'No SAM certificate found, provisioning one now'
5660
$CertResult = Update-CIPPSAMCertificate -ErrorAction Stop
5761
Write-LogMessage -message "Provisioned SAM certificate during authentication load. Thumbprint: $($CertResult.Thumbprint), storage mode: $($CertResult.StorageMode)" -Sev 'Info' -API 'CIPP Authentication'

Modules/CIPPCore/Public/Get-CippKeyVaultSecret.ps1

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ function Get-CippKeyVaultSecret {
6060
break
6161
} catch {
6262
$lastError = $_
63+
# 404 is definitive - the secret does not exist and retrying cannot change that
64+
if ($_.Exception.Message -match '404|SecretNotFound') {
65+
throw "Failed to retrieve secret '$Name' from vault '$VaultName': $($_.Exception.Message)"
66+
}
6367
if ($i -lt ($maxRetries - 1)) {
6468
Start-Sleep -Seconds $retryDelay
6569
$retryDelay *= 2 # Exponential backoff

Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-ExecDeployAppTemplate.ps1

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,12 @@ function Invoke-ExecDeployAppTemplate {
5151
try {
5252
$Config = $App.config
5353
if ($Config -is [string]) {
54-
$Config = $Config | ConvertFrom-Json -Depth 100
54+
# Parse case-sensitive to survive templates carrying both 'applicationName'
55+
# and 'ApplicationName', then collapse them via a case-insensitive dictionary.
56+
$Parsed = $Config | ConvertFrom-Json -Depth 100 -AsHashtable
57+
$Config = [ordered]@{}
58+
foreach ($Key in $Parsed.Keys) { $Config[$Key] = $Parsed[$Key] }
59+
$Config = [PSCustomObject]$Config
5560
}
5661

5762
$AppType = "$($App.appType ?? $App.AppType)"

Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ExecCompareIntunePolicy.ps1

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ function Invoke-ExecCompareIntunePolicy {
2222
'windowsQualityUpdatePolicies' = 'windowsQualityUpdatePolicies'
2323
'windowsQualityUpdateProfiles' = 'windowsQualityUpdateProfiles'
2424
'Intents' = 'Intents'
25+
'ManagedAppPolicies' = 'AppProtection'
2526
}
2627

2728
try {

Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntunePolicy.ps1

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,11 @@ function Invoke-ListIntunePolicy {
225225
method = 'GET'
226226
url = "/deviceManagement/intents?`$top=1000"
227227
}
228+
@{
229+
id = 'ManagedAppPolicies'
230+
method = 'GET'
231+
url = '/deviceAppManagement/managedAppPolicies?$orderby=displayName'
232+
}
228233
)
229234

230235
$BulkResults = New-GraphBulkRequest -Requests $BulkRequests -tenantid $TenantFilter
@@ -236,6 +241,7 @@ function Invoke-ListIntunePolicy {
236241
$URLName = $_.Id
237242
$_.body.Value | ForEach-Object {
238243
$AssignmentContext = $_.'assignments@odata.context'
244+
$PolicyODataType = $_.'@odata.type'
239245
$policyTypeName = switch -Wildcard ($AssignmentContext) {
240246
'*microsoft.graph.windowsIdentityProtectionConfiguration*' { 'Identity Protection' }
241247
'*microsoft.graph.windows10EndpointProtectionConfiguration*' { 'Endpoint Protection' }
@@ -264,6 +270,14 @@ function Invoke-ListIntunePolicy {
264270
$policyTypeName = switch ($URLName) {
265271
'deviceCompliancePolicies' { 'Compliance Policy' }
266272
'Intents' { 'Endpoint Security' }
273+
'ManagedAppPolicies' {
274+
switch -Wildcard ($PolicyODataType) {
275+
'*iosManagedAppProtection*' { 'iOS App Protection' }
276+
'*androidManagedAppProtection*' { 'Android App Protection' }
277+
'*windowsManagedAppProtection*' { 'Windows App Protection' }
278+
default { 'App Protection' }
279+
}
280+
}
267281
default { $AssignmentContext }
268282
}
269283
}

Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsFederationConfiguration.ps1

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,13 +123,15 @@ function Invoke-CIPPStandardTeamsFederationConfiguration {
123123
$BlockedDomainsMatches = $true
124124
}
125125
'AllowSpecificExternal' {
126-
$AllowedDomainsMatches = -not (Compare-Object -ReferenceObject $AllowedDomainsAsAList -DifferenceObject $CurrentAllowedDomains)
126+
# Both lists are already Sort-Object'd; compare as joined strings. Avoids Compare-Object,
127+
# whose parameter binder coerces an empty array @() to $null and then throws.
128+
$AllowedDomainsMatches = (@($AllowedDomainsAsAList) -join ',') -eq (@($CurrentAllowedDomains) -join ',')
127129
$BlockedDomainsMatches = (!$CurrentBlockedDomains -or @($CurrentBlockedDomains).Count -eq 0)
128130
}
129131
'BlockSpecificExternal' {
130132
# Allowed should be AllowAllKnownDomains, blocked domains already parsed above
131133
$AllowedDomainsMatches = $IsCurrentAllowAllKnownDomains
132-
$BlockedDomainsMatches = -not (Compare-Object -ReferenceObject $BlockedDomains -DifferenceObject $CurrentBlockedDomains)
134+
$BlockedDomainsMatches = (@($BlockedDomains) -join ',') -eq (@($CurrentBlockedDomains) -join ',')
133135
}
134136
}
135137

0 commit comments

Comments
 (0)