Skip to content

Commit d5319ab

Browse files
authored
Merge pull request #902 from KelvinTegelaar/dev
[pull] dev from KelvinTegelaar:dev
2 parents 2a3a94e + 4c58339 commit d5319ab

2 files changed

Lines changed: 247 additions & 11 deletions

File tree

Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployCheckChromeExtension.ps1

Lines changed: 54 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,13 @@ function Invoke-CIPPStandardDeployCheckChromeExtension {
2828
{"type":"textField","name":"standards.DeployCheckChromeExtension.webhookUrl","label":"Webhook URL","placeholder":"https://webhook.example.com/endpoint","required":false}
2929
{"type":"autoComplete","multiple":true,"creatable":true,"required":false,"label":"Webhook Events","name":"standards.DeployCheckChromeExtension.webhookEvents","placeholder":"e.g. pageBlocked, pageAllowed"}
3030
{"type":"autoComplete","multiple":true,"creatable":true,"required":false,"label":"URL Allowlist","name":"standards.DeployCheckChromeExtension.urlAllowlist","placeholder":"e.g. https://example.com/*"}
31+
{"type":"switch","name":"standards.DeployCheckChromeExtension.domainSquattingEnabled","label":"Enable domain squatting detection","defaultValue":true}
3132
{"type":"textField","name":"standards.DeployCheckChromeExtension.companyName","label":"Company Name","placeholder":"YOUR-COMPANY","required":false}
32-
{"type":"textField","name":"standards.DeployCheckChromeExtension.companyURL","label":"Company URL","placeholder":"https://yourcompany.com","required":false}
3333
{"type":"textField","name":"standards.DeployCheckChromeExtension.productName","label":"Product Name","placeholder":"YOUR-PRODUCT-NAME","required":false}
3434
{"type":"textField","name":"standards.DeployCheckChromeExtension.supportEmail","label":"Support Email","placeholder":"support@yourcompany.com","required":false}
35+
{"type":"textField","name":"standards.DeployCheckChromeExtension.supportUrl","label":"Support URL","placeholder":"https://support.yourcompany.com","required":false}
36+
{"type":"textField","name":"standards.DeployCheckChromeExtension.privacyPolicyUrl","label":"Privacy Policy URL","placeholder":"https://yourcompany.com/privacy","required":false}
37+
{"type":"textField","name":"standards.DeployCheckChromeExtension.aboutUrl","label":"About URL","placeholder":"https://yourcompany.com/about","required":false}
3538
{"type":"textField","name":"standards.DeployCheckChromeExtension.primaryColor","label":"Primary Color","placeholder":"#F77F00","required":false}
3639
{"type":"textField","name":"standards.DeployCheckChromeExtension.logoUrl","label":"Logo URL","placeholder":"https://yourcompany.com/logo.png","required":false}
3740
{"name":"AssignTo","label":"Who should this app be assigned to?","type":"radio","options":[{"label":"Do not assign","value":"On"},{"label":"Assign to all users","value":"allLicensedUsers"},{"label":"Assign to all devices","value":"AllDevices"},{"label":"Assign to all users and devices","value":"AllDevicesAndUsers"},{"label":"Assign to Custom Group","value":"customGroup"}]}
@@ -90,10 +93,13 @@ function Invoke-CIPPStandardDeployCheckChromeExtension {
9093
$WebhookUrl = $Settings.webhookUrl ?? ''
9194
$WebhookEvents = @($Settings.webhookEvents | ForEach-Object { $_.value ?? $_ } | Where-Object { $_ })
9295
$UrlAllowlist = @($Settings.urlAllowlist | ForEach-Object { $_.value ?? $_ } | Where-Object { $_ })
96+
$DomainSquattingEnabled = [int][bool]($Settings.domainSquattingEnabled ?? $true)
9397
$CompanyName = $Settings.companyName ?? ''
94-
$CompanyURL = $Settings.companyURL ?? ''
9598
$ProductName = $Settings.productName ?? ''
9699
$SupportEmail = $Settings.supportEmail ?? ''
100+
$SupportUrl = $Settings.supportUrl ?? ''
101+
$PrivacyPolicyUrl = $Settings.privacyPolicyUrl ?? ''
102+
$AboutUrl = $Settings.aboutUrl ?? ''
97103
$PrimaryColor = if ($Settings.primaryColor) { $Settings.primaryColor } else { '#F77F00' }
98104
$LogoUrl = $Settings.logoUrl ?? ''
99105

@@ -140,13 +146,21 @@ foreach (`$b in `$browsers) {
140146
New-ItemProperty -Path `$b.ManagedStorageKey -Name 'updateInterval' -PropertyType DWord -Value $UpdateInterval -Force | Out-Null
141147
New-ItemProperty -Path `$b.ManagedStorageKey -Name 'enableDebugLogging' -PropertyType DWord -Value $EnableDebugLogging -Force | Out-Null
142148
149+
# Managed storage - domainSquatting subkey
150+
`$domainSquattingKey = "`$(`$b.ManagedStorageKey)\domainSquatting"
151+
if (!(Test-Path `$domainSquattingKey)) { New-Item -Path `$domainSquattingKey -Force | Out-Null }
152+
New-ItemProperty -Path `$domainSquattingKey -Name 'enabled' -PropertyType DWord -Value $DomainSquattingEnabled -Force | Out-Null
153+
143154
# Managed storage - customBranding subkey
144155
`$brandingKey = "`$(`$b.ManagedStorageKey)\customBranding"
145156
if (!(Test-Path `$brandingKey)) { New-Item -Path `$brandingKey -Force | Out-Null }
146157
New-ItemProperty -Path `$brandingKey -Name 'companyName' -PropertyType String -Value '$($CompanyName -replace "'", "''")' -Force | Out-Null
147-
New-ItemProperty -Path `$brandingKey -Name 'companyURL' -PropertyType String -Value '$($CompanyURL -replace "'", "''")' -Force | Out-Null
158+
148159
New-ItemProperty -Path `$brandingKey -Name 'productName' -PropertyType String -Value '$($ProductName -replace "'", "''")' -Force | Out-Null
149-
New-ItemProperty -Path `$brandingKey -Name 'supportEmail' -PropertyType String -Value '$($SupportEmail -replace "'", "''")' -Force | Out-Null
160+
New-ItemProperty -Path `$brandingKey -Name 'supportEmail' -PropertyType String -Value '$($SupportEmail -replace "'", "''")' -Force | Out-Null
161+
New-ItemProperty -Path `$brandingKey -Name 'supportUrl' -PropertyType String -Value '$($SupportUrl -replace "'", "''")' -Force | Out-Null
162+
New-ItemProperty -Path `$brandingKey -Name 'privacyPolicyUrl' -PropertyType String -Value '$($PrivacyPolicyUrl -replace "'", "''")' -Force | Out-Null
163+
New-ItemProperty -Path `$brandingKey -Name 'aboutUrl' -PropertyType String -Value '$($AboutUrl -replace "'", "''")' -Force | Out-Null
150164
New-ItemProperty -Path `$brandingKey -Name 'primaryColor' -PropertyType String -Value '$PrimaryColor' -Force | Out-Null
151165
New-ItemProperty -Path `$brandingKey -Name 'logoUrl' -PropertyType String -Value '$($LogoUrl -replace "'", "''")' -Force | Out-Null
152166
@@ -251,13 +265,21 @@ foreach (`$key in @(`$chromeKey, `$edgeKey)) {
251265
if (!(Test-RegValue `$key 'cippTenantId' '$CippTenantId')) { exit 1 }
252266
if (!(Test-RegValue `$key 'customRulesUrl' '$CustomRulesUrl')) { exit 1 }
253267
268+
# domainSquatting subkey
269+
`$domainSquattingKey = "`$key\domainSquatting"
270+
if (!(Test-Path `$domainSquattingKey)) { exit 1 }
271+
if (!(Test-RegValue `$domainSquattingKey 'enabled' $DomainSquattingEnabled)) { exit 1 }
272+
254273
# customBranding subkey
255274
`$brandingKey = "`$key\customBranding"
256275
if (!(Test-Path `$brandingKey)) { exit 1 }
257276
if (!(Test-RegValue `$brandingKey 'companyName' '$($CompanyName -replace "'", "''")')) { exit 1 }
258-
if (!(Test-RegValue `$brandingKey 'companyURL' '$($CompanyURL -replace "'", "''")')) { exit 1 }
277+
259278
if (!(Test-RegValue `$brandingKey 'productName' '$($ProductName -replace "'", "''")')) { exit 1 }
260-
if (!(Test-RegValue `$brandingKey 'supportEmail' '$($SupportEmail -replace "'", "''")')) { exit 1 }
279+
if (!(Test-RegValue `$brandingKey 'supportEmail' '$($SupportEmail -replace "'", "''")')) { exit 1 }
280+
if (!(Test-RegValue `$brandingKey 'supportUrl' '$($SupportUrl -replace "'", "''")')) { exit 1 }
281+
if (!(Test-RegValue `$brandingKey 'privacyPolicyUrl' '$($PrivacyPolicyUrl -replace "'", "''")')) { exit 1 }
282+
if (!(Test-RegValue `$brandingKey 'aboutUrl' '$($AboutUrl -replace "'", "''")')) { exit 1 }
261283
if (!(Test-RegValue `$brandingKey 'primaryColor' '$PrimaryColor')) { exit 1 }
262284
if (!(Test-RegValue `$brandingKey 'logoUrl' '$($LogoUrl -replace "'", "''")')) { exit 1 }
263285
@@ -312,6 +334,16 @@ Write-Output 'Check Chrome Extension is correctly configured.'
312334
exit 0
313335
"@
314336

337+
##########################################################################
338+
# Compute a settings fingerprint from the install script so we can skip
339+
# redeploy when nothing has changed.
340+
##########################################################################
341+
$Sha256 = [System.Security.Cryptography.SHA256]::Create()
342+
$SettingsHash = ([System.BitConverter]::ToString(
343+
$Sha256.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($InstallScript))
344+
) -replace '-', '').Substring(0, 16)
345+
$AppDescription = "Deploys and configures the Check by CyberDrain phishing protection extension for Chrome and Edge browsers. Managed by CIPP. [cfg:$SettingsHash]"
346+
315347
##########################################################################
316348
# Legacy OMA-URI policy cleanup
317349
##########################################################################
@@ -325,7 +357,7 @@ exit 0
325357
# Check for existing Win32 app
326358
##########################################################################
327359
$Baseuri = 'https://graph.microsoft.com/beta/deviceAppManagement/mobileApps'
328-
$ExistingApps = New-GraphGetRequest -Uri "$Baseuri`?`$filter=displayName eq '$AppDisplayName'&`$select=id,displayName" -tenantid $Tenant | Where-Object {
360+
$ExistingApps = New-GraphGetRequest -Uri "$Baseuri`?`$filter=displayName eq '$AppDisplayName'&`$select=id,displayName,description" -tenantid $Tenant | Where-Object {
329361
$_.'@odata.type' -eq '#microsoft.graph.win32LobApp'
330362
}
331363
$AppExists = ($null -ne $ExistingApps -and @($ExistingApps).Count -gt 0)
@@ -358,9 +390,20 @@ exit 0
358390
}
359391

360392
if ($AppExists) {
361-
# App exists — delete and recreate to pick up any script changes
362-
foreach ($ExistingApp in @($ExistingApps)) {
363-
$null = New-GraphPostRequest -Uri "$Baseuri/$($ExistingApp.id)" -Type DELETE -tenantid $Tenant
393+
# Check if the settings hash matches — skip redeploy if nothing changed
394+
$ExistingHash = $null
395+
$ExistingApp = @($ExistingApps)[0]
396+
if ($ExistingApp.description -match '\[cfg:([0-9A-Fa-f]{16})\]') {
397+
$ExistingHash = $Matches[1]
398+
}
399+
400+
if ($ExistingHash -eq $SettingsHash) {
401+
Write-LogMessage -API 'Standards' -tenant $Tenant -message "$AppDisplayName settings unchanged — skipping redeploy" -sev Info
402+
return
403+
}
404+
405+
foreach ($App in @($ExistingApps)) {
406+
$null = New-GraphPostRequest -Uri "$Baseuri/$($App.id)" -Type DELETE -tenantid $Tenant
364407
Write-LogMessage -API 'Standards' -tenant $Tenant -message "Removed existing $AppDisplayName app to redeploy with updated settings" -sev Info
365408
}
366409
Start-Sleep -Seconds 2
@@ -369,7 +412,7 @@ exit 0
369412
# Deploy the Win32 script app
370413
$AppProperties = [PSCustomObject]@{
371414
displayName = $AppDisplayName
372-
description = 'Deploys and configures the Check by CyberDrain phishing protection extension for Chrome and Edge browsers. Managed by CIPP.'
415+
description = $AppDescription
373416
publisher = 'CIPP'
374417
installScript = $InstallScript
375418
uninstallScript = $UninstallScript
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
#!/usr/bin/env pwsh
2+
<#
3+
.SYNOPSIS
4+
Tests OData filter injection and the ConvertTo-CIPPODataFilterValue sanitization helper.
5+
6+
.DESCRIPTION
7+
Part 1: Unit tests for ConvertTo-CIPPODataFilterValue — verifies escaping/validation behavior.
8+
Part 2: Live injection tests against the local dev API (localhost:7071).
9+
10+
.NOTES
11+
Requires the local Azure Functions host to be running for Part 2.
12+
Run: func host start (from CIPP-API root)
13+
#>
14+
[CmdletBinding()]
15+
param(
16+
[string]$BaseUrl = 'http://localhost:7071/api',
17+
[switch]$SkipLiveTests
18+
)
19+
20+
# ---------------------------------------------------------------------------
21+
# Load the sanitization function from source
22+
# ---------------------------------------------------------------------------
23+
$helperPath = Join-Path $PSScriptRoot '../Modules/CIPPCore/Public/ConvertTo-CIPPODataFilterValue.ps1'
24+
if (-not (Test-Path $helperPath)) {
25+
Write-Error "ConvertTo-CIPPODataFilterValue.ps1 not found at: $helperPath"
26+
exit 1
27+
}
28+
. $helperPath
29+
30+
$pass = 0
31+
$fail = 0
32+
33+
function Assert-Equal {
34+
param([string]$Label, $Got, $Expected)
35+
if ($Got -eq $Expected) {
36+
Write-Host " [PASS] $Label" -ForegroundColor Green
37+
$script:pass++
38+
} else {
39+
Write-Host " [FAIL] $Label" -ForegroundColor Red
40+
Write-Host " Expected: $Expected"
41+
Write-Host " Got: $Got"
42+
$script:fail++
43+
}
44+
}
45+
46+
function Assert-Throws {
47+
param([string]$Label, [scriptblock]$Block)
48+
try {
49+
& $Block | Out-Null
50+
Write-Host " [FAIL] $Label (expected throw, but did not)" -ForegroundColor Red
51+
$script:fail++
52+
} catch {
53+
Write-Host " [PASS] $Label (threw: $($_.Exception.Message))" -ForegroundColor Green
54+
$script:pass++
55+
}
56+
}
57+
58+
# ---------------------------------------------------------------------------
59+
# Part 1: Unit tests — ConvertTo-CIPPODataFilterValue
60+
# ---------------------------------------------------------------------------
61+
Write-Host "`n=== Part 1: Sanitization Unit Tests ===" -ForegroundColor Cyan
62+
63+
Write-Host "`n-- String escaping --"
64+
# Classic OData injection: single quote should be doubled
65+
Assert-Equal 'Injection payload escaped' `
66+
(ConvertTo-CIPPODataFilterValue -Value "' or PartitionKey ne '" -Type String) `
67+
"'' or PartitionKey ne ''"
68+
69+
Assert-Equal 'Normal string passthrough' `
70+
(ConvertTo-CIPPODataFilterValue -Value 'hello world' -Type String) `
71+
'hello world'
72+
73+
Assert-Equal 'Multiple single quotes' `
74+
(ConvertTo-CIPPODataFilterValue -Value "O'Brien's" -Type String) `
75+
"O''Brien''s"
76+
77+
Assert-Equal 'Empty string' `
78+
(ConvertTo-CIPPODataFilterValue -Value '' -Type String) `
79+
''
80+
81+
Write-Host "`n-- GUID validation --"
82+
Assert-Equal 'Valid GUID' `
83+
(ConvertTo-CIPPODataFilterValue -Value '12345678-1234-1234-1234-123456789abc' -Type Guid) `
84+
'12345678-1234-1234-1234-123456789abc'
85+
86+
Assert-Throws 'Invalid GUID throws' {
87+
ConvertTo-CIPPODataFilterValue -Value "' or '1' eq '1" -Type Guid
88+
}
89+
Assert-Throws 'GUID with extra chars throws' {
90+
ConvertTo-CIPPODataFilterValue -Value '12345678-1234-1234-1234-123456789abc; DROP' -Type Guid
91+
}
92+
93+
Write-Host "`n-- Date validation --"
94+
Assert-Equal 'Valid date yyyy-MM-dd' `
95+
(ConvertTo-CIPPODataFilterValue -Value '2026-04-01' -Type Date) `
96+
'2026-04-01'
97+
98+
Assert-Equal 'Valid date yyyyMMdd' `
99+
(ConvertTo-CIPPODataFilterValue -Value '20260401' -Type Date) `
100+
'20260401'
101+
102+
Assert-Equal 'Valid ISO 8601 datetime UTC' `
103+
(ConvertTo-CIPPODataFilterValue -Value '2026-04-01T12:00:00Z' -Type Date) `
104+
'2026-04-01T12:00:00Z'
105+
106+
Assert-Equal 'Valid ISO 8601 datetime with offset' `
107+
(ConvertTo-CIPPODataFilterValue -Value '2026-04-01T12:00:00+00:00' -Type Date) `
108+
'2026-04-01T12:00:00+00:00'
109+
110+
Assert-Throws 'Invalid date throws' {
111+
ConvertTo-CIPPODataFilterValue -Value "20260401' or '1' eq '1" -Type Date
112+
}
113+
114+
Write-Host "`n-- Integer validation --"
115+
Assert-Equal 'Valid integer' `
116+
(ConvertTo-CIPPODataFilterValue -Value '42' -Type Integer) `
117+
'42'
118+
119+
Assert-Throws 'Integer with injection throws' {
120+
ConvertTo-CIPPODataFilterValue -Value '42 or 1 eq 1' -Type Integer
121+
}
122+
123+
# ---------------------------------------------------------------------------
124+
# Part 2: Live injection tests against local dev API
125+
# ---------------------------------------------------------------------------
126+
if ($SkipLiveTests) {
127+
Write-Host "`n=== Part 2: Live Tests (skipped) ===" -ForegroundColor Yellow
128+
} else {
129+
Write-Host "`n=== Part 2: Live Injection Tests ($BaseUrl) ===" -ForegroundColor Cyan
130+
Write-Host " Note: These require the local Functions host to be running.`n"
131+
132+
$headers = @{
133+
'x-ms-client-principal' = [Convert]::ToBase64String(
134+
[Text.Encoding]::UTF8.GetBytes(
135+
'{"identityProvider":"aad","userId":"test","userDetails":"test@test.com","userRoles":["authenticated","superadmin"]}'
136+
)
137+
)
138+
'x-ms-client-principal-idp' = 'aad'
139+
'x-ms-client-principal-name' = 'test@test.com'
140+
}
141+
142+
function Invoke-TestRequest {
143+
param([string]$Label, [string]$Url, [int]$ExpectedStatus, [string]$ExpectedInjectionWarning = '')
144+
try {
145+
$response = Invoke-WebRequest -Uri $Url -Headers $headers -SkipHttpErrorCheck -ErrorAction Stop
146+
$statusOk = $response.StatusCode -eq $ExpectedStatus
147+
$symbol = if ($statusOk) { '[PASS]' } else { '[FAIL]' }
148+
$color = if ($statusOk) { 'Green' } else { 'Red' }
149+
Write-Host " $symbol $Label (HTTP $($response.StatusCode))" -ForegroundColor $color
150+
if (-not $statusOk) { $script:fail++ } else { $script:pass++ }
151+
152+
if ($ExpectedInjectionWarning -and $response.StatusCode -eq 200) {
153+
$body = $response.Content | ConvertFrom-Json -ErrorAction SilentlyContinue
154+
$count = if ($body -is [array]) { $body.Count } else { ($body | Measure-Object).Count }
155+
Write-Host " -> Returned $count item(s) — $ExpectedInjectionWarning" -ForegroundColor Yellow
156+
}
157+
158+
return $response
159+
} catch {
160+
Write-Host " [SKIP] $Label — could not reach $BaseUrl ($($_.Exception.Message))" -ForegroundColor DarkYellow
161+
}
162+
}
163+
164+
# Normal request — expect 404 for non-existent ID
165+
Invoke-TestRequest `
166+
-Label 'Normal: ListContactTemplates?ID=nonexistent -> 404' `
167+
-Url "$BaseUrl/ListContactTemplates?ID=nonexistent" `
168+
-ExpectedStatus 404
169+
170+
# Injection attempt — with unpatched code this returns 200 + cross-partition data
171+
# With patched code (single quote doubled) it should return 404 (no match for escaped value)
172+
$injectionPayload = [Uri]::EscapeDataString("' or PartitionKey ne '")
173+
Invoke-TestRequest `
174+
-Label "Injection: ListContactTemplates?ID=' or PartitionKey ne ' -> should be 404 (patched)" `
175+
-Url "$BaseUrl/ListContactTemplates?ID=$injectionPayload" `
176+
-ExpectedStatus 404 `
177+
-ExpectedInjectionWarning 'INJECTION SUCCEEDED if >0 items returned'
178+
179+
# Sanitized: normal-looking template ID (adjust to a real one in your test data if available)
180+
Invoke-TestRequest `
181+
-Label 'Sanitized: ListContactTemplates with valid name -> 200 or 404' `
182+
-Url "$BaseUrl/ListContactTemplates" `
183+
-ExpectedStatus 200
184+
}
185+
186+
# ---------------------------------------------------------------------------
187+
# Summary
188+
# ---------------------------------------------------------------------------
189+
Write-Host "`n=== Results ===" -ForegroundColor Cyan
190+
Write-Host " Passed: $pass" -ForegroundColor Green
191+
Write-Host " Failed: $fail" -ForegroundColor $(if ($fail -gt 0) { 'Red' } else { 'Green' })
192+
193+
if ($fail -gt 0) { exit 1 } else { exit 0 }

0 commit comments

Comments
 (0)