feat: Add Intune detection and setup scripts for Windows deployment#144
feat: Add Intune detection and setup scripts for Windows deployment#144bmsimp wants to merge 1 commit intoCyberDrain:devfrom
Conversation
…nt of Check extension
There was a problem hiding this comment.
Pull request overview
Adds Windows Intune-focused tooling for deploying the Check browser extension by introducing a registry-based detection script, an interactive setup script that generates configured deploy/remove/detect scripts, and updating the Windows domain deployment documentation accordingly.
Changes:
- Added an Intune detection script that validates Chrome/Edge policy registry keys against expected configuration.
- Added an interactive setup script that downloads templates and generates configured Deploy/Remove/Detect scripts for upload to Intune.
- Updated Windows domain deployment docs with Win32 app + detection-script guidance for Intune.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 9 comments.
| File | Description |
|---|---|
| enterprise/Setup-Windows-Chrome-and-Edge.ps1 | Interactive generator that downloads template scripts and applies config replacements. |
| enterprise/Detect-Windows-Chrome-and-Edge.ps1 | Intune detection script that checks registry policy keys/values for Chrome and Edge. |
| docs/deployment/chrome-edge-deployment-instructions/windows/domain-deployment.md | Updated documentation to describe Intune Win32 app packaging, detection rules, and troubleshooting. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| Write-Host "Downloading latest scripts from GitHub..." -ForegroundColor Yellow | ||
| $templates = @{} | ||
| foreach ($key in $scripts.Keys) { | ||
| try { | ||
| $templates[$key] = Invoke-WebRequest -Uri $scripts[$key].Url -UseBasicParsing -TimeoutSec 30 | Select-Object -ExpandProperty Content |
There was a problem hiding this comment.
Invoke-WebRequest -UseBasicParsing will fail on PowerShell 6/7+ (the parameter was removed). Since this setup script is intended to be run interactively on an admin workstation, consider removing -UseBasicParsing (it’s not needed on modern PowerShell) or adding a version check/fallback so downloads work in both Windows PowerShell 5.1 and PowerShell 7.
| Write-Host "Downloading latest scripts from GitHub..." -ForegroundColor Yellow | |
| $templates = @{} | |
| foreach ($key in $scripts.Keys) { | |
| try { | |
| $templates[$key] = Invoke-WebRequest -Uri $scripts[$key].Url -UseBasicParsing -TimeoutSec 30 | Select-Object -ExpandProperty Content | |
| function Invoke-DownloadWebRequest { | |
| param ( | |
| [string]$Uri | |
| ) | |
| if ($PSVersionTable.PSVersion.Major -ge 6) { | |
| return Invoke-WebRequest -Uri $Uri -TimeoutSec 30 | |
| } | |
| return Invoke-WebRequest -Uri $Uri -UseBasicParsing -TimeoutSec 30 | |
| } | |
| Write-Host "Downloading latest scripts from GitHub..." -ForegroundColor Yellow | |
| $templates = @{} | |
| foreach ($key in $scripts.Keys) { | |
| try { | |
| $templates[$key] = Invoke-DownloadWebRequest -Uri $scripts[$key].Url | Select-Object -ExpandProperty Content |
| function Format-ArrayLiteral { | ||
| param ([string[]]$Values) | ||
| if ($Values.Count -eq 0) { return '@()' } | ||
| $quoted = $Values | ForEach-Object { $escaped = $_ -replace '"', '""'; "`"$escaped`"" } |
There was a problem hiding this comment.
Format-ArrayLiteral builds array elements using double-quoted string literals, which means $ and backtick characters in allowlist/regex entries (common in regex like $ end-of-line anchors) will be interpreted/expanded when the generated Deploy/Detect scripts run. This can silently corrupt regex/URLs and break policy. Consider generating single-quoted literals (escaping embedded ' by doubling) or explicitly escaping $/` in values before writing them into the scripts.
| $quoted = $Values | ForEach-Object { $escaped = $_ -replace '"', '""'; "`"$escaped`"" } | |
| $quoted = $Values | ForEach-Object { $escaped = $_ -replace "'", "''"; "'$escaped'" } |
| @{ Pattern = '$cippServerUrl = "" #'; Value = "`$cippServerUrl = `"$cfg_cippServerUrl`" #" } | ||
| @{ Pattern = '$cippTenantId = "" #'; Value = "`$cippTenantId = `"$cfg_cippTenantId`" #" } | ||
| @{ Pattern = '$customRulesUrl = "" #'; Value = "`$customRulesUrl = `"$cfg_customRulesUrl`" #" } | ||
| @{ Pattern = '$updateInterval = 24 #'; Value = "`$updateInterval = $cfg_updateInterval #" } | ||
| @{ Pattern = '$domainSquattingEnabled = 1 #'; Value = "`$domainSquattingEnabled = $cfg_domainSquattingEnabled #" } | ||
| @{ Pattern = '$enableDebugLogging = 0 #'; Value = "`$enableDebugLogging = $cfg_enableDebugLogging #" } | ||
| @{ Pattern = '$enableGenericWebhook = 0 #'; Value = "`$enableGenericWebhook = $cfg_enableGenericWebhook #" } | ||
| @{ Pattern = '$webhookUrl = "" #'; Value = "`$webhookUrl = `"$cfg_webhookUrl`" #" } | ||
| @{ Pattern = '$companyName = "CyberDrain" #'; Value = "`$companyName = `"$cfg_companyName`" #" } |
There was a problem hiding this comment.
The replacement map writes user-provided values into double-quoted literals (e.g., for URLs and branding fields) without escaping $ or backticks, so values containing $ (query params, regex fragments, etc.) can be expanded at runtime in the generated scripts. Escaping these characters (or switching to single-quoted literals consistently) would prevent unintended interpolation and make generated scripts match the intended literal values.
| # Toolbar pin — only checked when enabled (upstream install script does not write this property when disabled) | ||
| if ($forceToolbarPin -eq 1) { | ||
| if (!(Test-RegValue $browser.ExtensionSettingsKey $browser.ToolbarProp $browser.ToolbarValue)) { exit 1 } |
There was a problem hiding this comment.
When $forceToolbarPin is set to 0, the detection script does not verify that the browser-specific toolbar policy value is absent. If a device was previously deployed with pinning enabled, the stale toolbar_pin/toolbar_state value can remain and this script will still report compliant, so Intune won’t remediate the change. Consider explicitly failing when the toolbar property exists while $forceToolbarPin -eq 0 (and ensure the deploy script removes the property in that case to avoid a reinstall loop).
| # Toolbar pin — only checked when enabled (upstream install script does not write this property when disabled) | |
| if ($forceToolbarPin -eq 1) { | |
| if (!(Test-RegValue $browser.ExtensionSettingsKey $browser.ToolbarProp $browser.ToolbarValue)) { exit 1 } | |
| # Toolbar pin — when enabled, require the expected value; when disabled, require the property to be absent | |
| if ($forceToolbarPin -eq 1) { | |
| if (!(Test-RegValue $browser.ExtensionSettingsKey $browser.ToolbarProp $browser.ToolbarValue)) { exit 1 } | |
| } else { | |
| $extensionSettingsProps = (Get-Item $browser.ExtensionSettingsKey).Property | |
| if ($null -ne $extensionSettingsProps -and $extensionSettingsProps -contains $browser.ToolbarProp) { exit 1 } |
| foreach ($browser in $browsers) { | ||
| # Verify managed storage key exists | ||
| if (!(Test-Path $browser.ManagedStorageKey)) { exit 1 } | ||
|
|
||
| $policyKey = $browser.ManagedStorageKey | ||
|
|
||
| # Core DWord settings | ||
| if (!(Test-RegValue $policyKey 'showNotifications' $showNotifications)) { exit 1 } | ||
| if (!(Test-RegValue $policyKey 'enableValidPageBadge' $enableValidPageBadge)) { exit 1 } | ||
| if (!(Test-RegValue $policyKey 'enablePageBlocking' $enablePageBlocking)) { exit 1 } | ||
| if (!(Test-RegValue $policyKey 'enableCippReporting' $enableCippReporting)) { exit 1 } | ||
| if (!(Test-RegValue $policyKey 'updateInterval' $updateInterval)) { exit 1 } | ||
| if (!(Test-RegValue $policyKey 'enableDebugLogging' $enableDebugLogging)) { exit 1 } | ||
|
|
||
| # Core String settings | ||
| if (!(Test-RegValue $policyKey 'cippServerUrl' $cippServerUrl)) { exit 1 } | ||
| if (!(Test-RegValue $policyKey 'cippTenantId' $cippTenantId)) { exit 1 } | ||
| if (!(Test-RegValue $policyKey 'customRulesUrl' $customRulesUrl)) { exit 1 } | ||
|
|
||
| # domainSquatting subkey | ||
| $domainSquattingKey = "$policyKey\domainSquatting" | ||
| if (!(Test-Path $domainSquattingKey)) { exit 1 } | ||
| if (!(Test-RegValue $domainSquattingKey 'enabled' $domainSquattingEnabled)) { exit 1 } | ||
|
|
||
| # customBranding subkey | ||
| $brandingKey = "$policyKey\customBranding" | ||
| if (!(Test-Path $brandingKey)) { exit 1 } |
There was a problem hiding this comment.
This script exits 1 on the first mismatch but doesn’t emit any details about what key/value failed. The updated docs recommend running it manually to see which check fails, but without output that’s hard to diagnose. Consider printing a short message (browser + key + expected/actual) before each failure, or refactoring to accumulate failures and output a summary before exiting 1.
| foreach ($browser in $browsers) { | |
| # Verify managed storage key exists | |
| if (!(Test-Path $browser.ManagedStorageKey)) { exit 1 } | |
| $policyKey = $browser.ManagedStorageKey | |
| # Core DWord settings | |
| if (!(Test-RegValue $policyKey 'showNotifications' $showNotifications)) { exit 1 } | |
| if (!(Test-RegValue $policyKey 'enableValidPageBadge' $enableValidPageBadge)) { exit 1 } | |
| if (!(Test-RegValue $policyKey 'enablePageBlocking' $enablePageBlocking)) { exit 1 } | |
| if (!(Test-RegValue $policyKey 'enableCippReporting' $enableCippReporting)) { exit 1 } | |
| if (!(Test-RegValue $policyKey 'updateInterval' $updateInterval)) { exit 1 } | |
| if (!(Test-RegValue $policyKey 'enableDebugLogging' $enableDebugLogging)) { exit 1 } | |
| # Core String settings | |
| if (!(Test-RegValue $policyKey 'cippServerUrl' $cippServerUrl)) { exit 1 } | |
| if (!(Test-RegValue $policyKey 'cippTenantId' $cippTenantId)) { exit 1 } | |
| if (!(Test-RegValue $policyKey 'customRulesUrl' $customRulesUrl)) { exit 1 } | |
| # domainSquatting subkey | |
| $domainSquattingKey = "$policyKey\domainSquatting" | |
| if (!(Test-Path $domainSquattingKey)) { exit 1 } | |
| if (!(Test-RegValue $domainSquattingKey 'enabled' $domainSquattingEnabled)) { exit 1 } | |
| # customBranding subkey | |
| $brandingKey = "$policyKey\customBranding" | |
| if (!(Test-Path $brandingKey)) { exit 1 } | |
| function Write-DetectionFailure { | |
| param( | |
| [string]$BrowserName, | |
| [string]$KeyPath, | |
| [string]$ValueName, | |
| [object]$ExpectedValue, | |
| [object]$ActualValue | |
| ) | |
| if ([string]::IsNullOrEmpty($ValueName)) { | |
| Write-Output "$BrowserName detection failed: missing registry key '$KeyPath'." | |
| return | |
| } | |
| Write-Output "$BrowserName detection failed for '$ValueName' at '$KeyPath': expected '$ExpectedValue', actual '$ActualValue'." | |
| } | |
| function Test-RegValueWithDetails { | |
| param( | |
| [string]$BrowserName, | |
| [string]$KeyPath, | |
| [string]$ValueName, | |
| [object]$ExpectedValue | |
| ) | |
| $matches = Test-RegValue $KeyPath $ValueName $ExpectedValue | |
| if ($matches) { | |
| return $true | |
| } | |
| $actualValue = '<missing>' | |
| if (Test-Path $KeyPath) { | |
| try { | |
| $property = Get-ItemProperty -Path $KeyPath -Name $ValueName -ErrorAction Stop | |
| $actualValue = $property.$ValueName | |
| } | |
| catch { | |
| $actualValue = '<missing>' | |
| } | |
| } | |
| Write-DetectionFailure -BrowserName $BrowserName -KeyPath $KeyPath -ValueName $ValueName -ExpectedValue $ExpectedValue -ActualValue $actualValue | |
| return $false | |
| } | |
| foreach ($browser in $browsers) { | |
| # Verify managed storage key exists | |
| if (!(Test-Path $browser.ManagedStorageKey)) { | |
| Write-DetectionFailure -BrowserName $browser.Name -KeyPath $browser.ManagedStorageKey -ValueName $null -ExpectedValue $null -ActualValue $null | |
| exit 1 | |
| } | |
| $policyKey = $browser.ManagedStorageKey | |
| # Core DWord settings | |
| if (!(Test-RegValueWithDetails $browser.Name $policyKey 'showNotifications' $showNotifications)) { exit 1 } | |
| if (!(Test-RegValueWithDetails $browser.Name $policyKey 'enableValidPageBadge' $enableValidPageBadge)) { exit 1 } | |
| if (!(Test-RegValueWithDetails $browser.Name $policyKey 'enablePageBlocking' $enablePageBlocking)) { exit 1 } | |
| if (!(Test-RegValueWithDetails $browser.Name $policyKey 'enableCippReporting' $enableCippReporting)) { exit 1 } | |
| if (!(Test-RegValueWithDetails $browser.Name $policyKey 'updateInterval' $updateInterval)) { exit 1 } | |
| if (!(Test-RegValueWithDetails $browser.Name $policyKey 'enableDebugLogging' $enableDebugLogging)) { exit 1 } | |
| # Core String settings | |
| if (!(Test-RegValueWithDetails $browser.Name $policyKey 'cippServerUrl' $cippServerUrl)) { exit 1 } | |
| if (!(Test-RegValueWithDetails $browser.Name $policyKey 'cippTenantId' $cippTenantId)) { exit 1 } | |
| if (!(Test-RegValueWithDetails $browser.Name $policyKey 'customRulesUrl' $customRulesUrl)) { exit 1 } | |
| # domainSquatting subkey | |
| $domainSquattingKey = "$policyKey\domainSquatting" | |
| if (!(Test-Path $domainSquattingKey)) { | |
| Write-DetectionFailure -BrowserName $browser.Name -KeyPath $domainSquattingKey -ValueName $null -ExpectedValue $null -ActualValue $null | |
| exit 1 | |
| } | |
| if (!(Test-RegValueWithDetails $browser.Name $domainSquattingKey 'enabled' $domainSquattingEnabled)) { exit 1 } | |
| # customBranding subkey | |
| $brandingKey = "$policyKey\customBranding" | |
| if (!(Test-Path $brandingKey)) { | |
| Write-DetectionFailure -BrowserName $browser.Name -KeyPath $brandingKey -ValueName $null -ExpectedValue $null -ActualValue $null | |
| exit 1 | |
| } |
|
|
||
| * **Installation policy** → tells the browser to force-install the extension. | ||
| * **Configuration policy** → applies your custom extension settings. | ||
| The simplest method of Intune deployment is via a win32 script. Follow the steps below to: |
There was a problem hiding this comment.
The introduction sentence is incomplete: “Follow the steps below to:” ends without listing what the reader will accomplish. Consider finishing the sentence (or replacing with a short bullet list) so the section reads cleanly.
| The simplest method of Intune deployment is via a win32 script. Follow the steps below to: | |
| The simplest method of Intune deployment is via a win32 script. Follow the steps below to deploy Check with Intune. |
| ### Step 1: Package the Scripts | ||
|
|
||
| Intune Win32 apps require an `.intunewin` package. Place your three configured scripts in a folder, then run: | ||
|
|
||
| ```powershell | ||
| .\IntuneWinAppUtil.exe -c "C:\path\to\scripts\folder" -s "Deploy-Windows-Chrome-and-Edge.ps1" -o "C:\path\to\output" | ||
| ``` | ||
|
|
||
| This creates `Deploy-Windows-Chrome-and-Edge.intunewin`. | ||
|
|
||
| ### Step 3: Configure App Information | ||
|
|
There was a problem hiding this comment.
Step numbering skips from “Step 1” to “Step 3” in the Intune instructions. Renumbering to be sequential (or adding the missing Step 2) will reduce confusion when following the guide.
| | Run script in 64-bit PowerShell host | **No** | | ||
|
|
There was a problem hiding this comment.
The Detection rules table includes “Run script in 64-bit PowerShell host”, which (depending on the Intune Win32 app UI) may not be an available setting for custom detection scripts. Also, if a 64-bit/32-bit choice is available, this detection script reads HKLM:\SOFTWARE\Policies\... and should run in 64-bit to avoid WOW6432Node redirection. Please verify the correct Intune options here and adjust the table accordingly.
| | Run script in 64-bit PowerShell host | **No** | | |
| Keep **Run script as 32-bit process on 64-bit clients** set to **No** so the detection script runs in the 64-bit PowerShell/registry context on 64-bit devices. This is important because the script checks values under `HKLM:\SOFTWARE\Policies\...`; running it as 32-bit could read redirected `WOW6432Node` paths and cause detection to fail incorrectly. |
| 2. Navigate to: **Devices → Configuration profiles** | ||
| 3. Click on **Create → Import Policy** | ||
| 4. Import the following file to deploy the extensions. This will deploy the configuration | ||
| <a href="https://raw.githubusercontent.com/CyberDrain/Check/refs/heads/main/enterprise/Setup-Windows-Chrome-and-Edge.ps1" class="button primary">Import File</a> |
There was a problem hiding this comment.
The button text says “Import File”, but the link points to a raw .ps1 download. Renaming the button to something like “Download script” would better match what the action actually does.
| <a href="https://raw.githubusercontent.com/CyberDrain/Check/refs/heads/main/enterprise/Setup-Windows-Chrome-and-Edge.ps1" class="button primary">Import File</a> | |
| <a href="https://raw.githubusercontent.com/CyberDrain/Check/refs/heads/main/enterprise/Setup-Windows-Chrome-and-Edge.ps1" class="button primary">Download script</a> |
Summary
Detect-Windows-Chrome-and-Edge.ps1-- an Intune detection script that verifies all registry keys written by the deploy script match expected configuration. Exits 0 (compliant) or 1 (drift detected), enabling Intune to automatically redeploy when settings change.Setup-Windows-Chrome-and-Edge.ps1-- an interactive configurator that downloads the latest Deploy, Remove, and Detect scripts from GitHub, walks the user through each setting with defaults and validation, and outputs ready-to-upload scripts for Intune.domain-deployment.mdwith Intune-specific deployment instructions.All config blocks mirror the existing
Deploy-Windows-Chrome-and-Edge.ps1variable names, grouping, and comment style for consistency across deployment methods.Details
Detection script features:
Test-RegValuehelper checks each registry property against expected valuesurlAllowlist,webhookEvents\events) verified bidirectionally -- checks count matches and no stale entries existSetup script features: