From 78f1a5bdda0d381ff14bc8ba754df9bee2e432da Mon Sep 17 00:00:00 2001
From: Zacgoose <107489668+Zacgoose@users.noreply.github.com>
Date: Wed, 8 Apr 2026 22:21:40 +0800
Subject: [PATCH 01/20] Better removal script
---
enterprise/Remove-Windows-Chrome-and-Edge.ps1 | 151 ++----------------
1 file changed, 12 insertions(+), 139 deletions(-)
diff --git a/enterprise/Remove-Windows-Chrome-and-Edge.ps1 b/enterprise/Remove-Windows-Chrome-and-Edge.ps1
index cad4445e..e86b04a6 100644
--- a/enterprise/Remove-Windows-Chrome-and-Edge.ps1
+++ b/enterprise/Remove-Windows-Chrome-and-Edge.ps1
@@ -17,155 +17,28 @@ function Remove-ExtensionSettings {
[string]$ExtensionSettingsKey
)
- # Remove properties from managed storage key
+ # Remove all managed policy values and nested keys created for this extension.
if (Test-Path $ManagedStorageKey) {
- $propertiesToRemove = @(
- "showNotifications",
- "enableValidPageBadge",
- "enablePageBlocking",
- "enableCippReporting",
- "cippServerUrl",
- "cippTenantId",
- "customRulesUrl",
- "updateInterval",
- "enableDebugLogging"
- )
-
- foreach ($property in $propertiesToRemove) {
- if (Get-ItemProperty -Path $ManagedStorageKey -Name $property -ErrorAction SilentlyContinue) {
- Remove-ItemProperty -Path $ManagedStorageKey -Name $property -Force -ErrorAction SilentlyContinue
- Write-Host "Removed property: $property from $ManagedStorageKey"
- }
- }
-
- # Remove URL allowlist subkey and all its properties
- $urlAllowlistKey = "$ManagedStorageKey\urlAllowlist"
- if (Test-Path $urlAllowlistKey) {
- # Remove all numbered properties (1, 2, 3, etc.)
- $properties = Get-ItemProperty -Path $urlAllowlistKey -ErrorAction SilentlyContinue
- if ($properties) {
- $properties.PSObject.Properties | Where-Object { $_.Name -match '^\d+$' } | ForEach-Object {
- Remove-ItemProperty -Path $urlAllowlistKey -Name $_.Name -Force -ErrorAction SilentlyContinue
- Write-Host "Removed URL allowlist property: $($_.Name) from $urlAllowlistKey"
- }
- }
- # Remove the urlAllowlist subkey if it's empty
- try {
- Remove-Item -Path $urlAllowlistKey -Force -ErrorAction SilentlyContinue
- Write-Host "Removed URL allowlist subkey: $urlAllowlistKey"
- } catch {
- # Key may not be empty or may have been removed already
- }
- }
-
- # Remove generic webhook subkey and event properties
- $genericWebhookKey = "$ManagedStorageKey\genericWebhook"
- if (Test-Path $genericWebhookKey) {
- $webhookEventsKey = "$genericWebhookKey\events"
- if (Test-Path $webhookEventsKey) {
- $eventProperties = Get-ItemProperty -Path $webhookEventsKey -ErrorAction SilentlyContinue
- if ($eventProperties) {
- $eventProperties.PSObject.Properties | Where-Object { $_.Name -match '^\d+$' } | ForEach-Object {
- Remove-ItemProperty -Path $webhookEventsKey -Name $_.Name -Force -ErrorAction SilentlyContinue
- Write-Host "Removed webhook event property: $($_.Name) from $webhookEventsKey"
- }
- }
- try {
- Remove-Item -Path $webhookEventsKey -Force -ErrorAction SilentlyContinue
- Write-Host "Removed webhook events subkey: $webhookEventsKey"
- } catch {
- # Key may not be empty or may have been removed already
- }
- }
-
- foreach ($property in @("enabled", "url")) {
- if (Get-ItemProperty -Path $genericWebhookKey -Name $property -ErrorAction SilentlyContinue) {
- Remove-ItemProperty -Path $genericWebhookKey -Name $property -Force -ErrorAction SilentlyContinue
- Write-Host "Removed generic webhook property: $property from $genericWebhookKey"
- }
- }
-
- try {
- Remove-Item -Path $genericWebhookKey -Force -ErrorAction SilentlyContinue
- Write-Host "Removed generic webhook subkey: $genericWebhookKey"
- } catch {
- # Key may not be empty or may have been removed already
- }
- }
-
- # Remove custom branding subkey and all its properties
- $customBrandingKey = "$ManagedStorageKey\customBranding"
- if (Test-Path $customBrandingKey) {
- $brandingPropertiesToRemove = @(
- "companyName",
- "productName",
- "supportEmail",
- "supportUrl",
- "privacyPolicyUrl",
- "aboutUrl",
- "primaryColor",
- "logoUrl"
- )
-
- foreach ($property in $brandingPropertiesToRemove) {
- if (Get-ItemProperty -Path $customBrandingKey -Name $property -ErrorAction SilentlyContinue) {
- Remove-ItemProperty -Path $customBrandingKey -Name $property -Force -ErrorAction SilentlyContinue
- Write-Host "Removed custom branding property: $property from $customBrandingKey"
- }
- }
-
- # Remove the customBranding subkey if it's empty
- try {
- Remove-Item -Path $customBrandingKey -Force -ErrorAction SilentlyContinue
- Write-Host "Removed custom branding subkey: $customBrandingKey"
- } catch {
- # Key may not be empty or may have been removed already
- }
- }
-
- # Remove the managed storage key if it's empty
try {
- $remainingProperties = Get-ItemProperty -Path $ManagedStorageKey -ErrorAction SilentlyContinue
- if ($remainingProperties -and $remainingProperties.PSObject.Properties.Count -eq 0) {
- Remove-Item -Path $ManagedStorageKey -Force -ErrorAction SilentlyContinue
- Write-Host "Removed managed storage key: $ManagedStorageKey"
- }
+ Remove-Item -Path $ManagedStorageKey -Recurse -Force -ErrorAction Stop
+ Write-Host "Removed managed storage key: $ManagedStorageKey"
} catch {
- # Key may not be empty or may have been removed already
+ Write-Warning "Failed to remove managed storage key: $ManagedStorageKey. Error: $($_.Exception.Message)"
}
+ } else {
+ Write-Host "Managed storage key not found (already removed): $ManagedStorageKey"
}
- # Remove properties from extension settings key
+ # Remove extension install/settings key for this extension.
if (Test-Path $ExtensionSettingsKey) {
- $extensionPropertiesToRemove = @(
- "installation_mode",
- "update_url"
- )
-
- # Add browser-specific toolbar properties
- if ($ExtensionId -eq $edgeExtensionId) {
- $extensionPropertiesToRemove += "toolbar_state"
- } elseif ($ExtensionId -eq $chromeExtensionId) {
- $extensionPropertiesToRemove += "toolbar_pin"
- }
-
- foreach ($property in $extensionPropertiesToRemove) {
- if (Get-ItemProperty -Path $ExtensionSettingsKey -Name $property -ErrorAction SilentlyContinue) {
- Remove-ItemProperty -Path $ExtensionSettingsKey -Name $property -Force -ErrorAction SilentlyContinue
- Write-Host "Removed extension setting property: $property from $ExtensionSettingsKey"
- }
- }
-
- # Remove the extension settings key if it's empty
try {
- $remainingProperties = Get-ItemProperty -Path $ExtensionSettingsKey -ErrorAction SilentlyContinue
- if ($remainingProperties -and $remainingProperties.PSObject.Properties.Count -eq 0) {
- Remove-Item -Path $ExtensionSettingsKey -Force -ErrorAction SilentlyContinue
- Write-Host "Removed extension settings key: $ExtensionSettingsKey"
- }
+ Remove-Item -Path $ExtensionSettingsKey -Recurse -Force -ErrorAction Stop
+ Write-Host "Removed extension settings key: $ExtensionSettingsKey"
} catch {
- # Key may not be empty or may have been removed already
+ Write-Warning "Failed to remove extension settings key: $ExtensionSettingsKey. Error: $($_.Exception.Message)"
}
+ } else {
+ Write-Host "Extension settings key not found (already removed): $ExtensionSettingsKey"
}
Write-Host "Completed removal of extension settings for $ExtensionId"
From ce180f942729ffdfbd00b5d4d970eaf24f61e783 Mon Sep 17 00:00:00 2001
From: Zacgoose <107489668+Zacgoose@users.noreply.github.com>
Date: Wed, 8 Apr 2026 22:45:48 +0800
Subject: [PATCH 02/20] Docs for removal
---
docs/SUMMARY.md | 7 +++++++
docs/removal/README.md | 12 ++++++++++++
docs/removal/windows/README.md | 16 ++++++++++++++++
docs/removal/windows/chrome-edge.md | 13 +++++++++++++
docs/removal/windows/firefox.md | 11 +++++++++++
5 files changed, 59 insertions(+)
create mode 100644 docs/removal/README.md
create mode 100644 docs/removal/windows/README.md
create mode 100644 docs/removal/windows/chrome-edge.md
create mode 100644 docs/removal/windows/firefox.md
diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md
index 38ff855d..a6e2bf08 100644
--- a/docs/SUMMARY.md
+++ b/docs/SUMMARY.md
@@ -17,6 +17,13 @@
- [MacOS](deployment/chrome-edge-deployment-instructions/macos.md)
- [Firefox Deployment](deployment/firefox-deployment.md)
+## Removal
+
+- [Removal Overview](removal/README.md)
+ - [Windows](removal/windows/README.md)
+ - [Chrome and Edge](removal/windows/chrome-edge.md)
+ - [Firefox](removal/windows/firefox.md)
+
## Settings
- [General](settings/general.md)
diff --git a/docs/removal/README.md b/docs/removal/README.md
new file mode 100644
index 00000000..10d14c8b
--- /dev/null
+++ b/docs/removal/README.md
@@ -0,0 +1,12 @@
+---
+description: Guides for removing Check enterprise policies and managed settings
+icon: trash
+---
+
+# Removal
+
+Use this section to remove Check deployment configuration and managed policies from supported browsers.
+
+{% content-ref url="windows/" %}
+[windows](windows/)
+{% endcontent-ref %}
diff --git a/docs/removal/windows/README.md b/docs/removal/windows/README.md
new file mode 100644
index 00000000..cc496f2f
--- /dev/null
+++ b/docs/removal/windows/README.md
@@ -0,0 +1,16 @@
+---
+description: Windows removal guidance for Check managed browser configuration
+icon: windows
+---
+
+# Windows
+
+Use the pages below to remove Check enterprise settings from Windows endpoints.
+
+{% content-ref url="chrome-edge.md" %}
+[chrome-edge.md](chrome-edge.md)
+{% endcontent-ref %}
+
+{% content-ref url="firefox.md" %}
+[firefox.md](firefox.md)
+{% endcontent-ref %}
diff --git a/docs/removal/windows/chrome-edge.md b/docs/removal/windows/chrome-edge.md
new file mode 100644
index 00000000..674428eb
--- /dev/null
+++ b/docs/removal/windows/chrome-edge.md
@@ -0,0 +1,13 @@
+# Chrome and Edge (Windows)
+
+If you need to fully remove Check managed enterprise configuration from Windows endpoints, use the uninstall script instead of manually deleting registry values.
+
+This removes all extension-specific policy values created during deployment for both Chrome and Edge, including nested settings such as domain squatting, webhook, branding, and allowlist values.
+
+## Uninstall Script
+
+1. Run the script as Administrator on the target endpoint.
+2. Use this when testing policy changes and you want a clean baseline before re-deploying.
+3. After running, restart Chrome and Edge to ensure policy refresh.
+
+Download the Uninstall Script from GitHub
diff --git a/docs/removal/windows/firefox.md b/docs/removal/windows/firefox.md
new file mode 100644
index 00000000..18d58420
--- /dev/null
+++ b/docs/removal/windows/firefox.md
@@ -0,0 +1,11 @@
+# Firefox (Windows)
+
+Firefox enterprise removal for Check is managed through the Firefox policies file.
+
+## General Removal Steps
+
+1. Remove Check entries from `%ProgramFiles%\\Mozilla Firefox\\distribution\\policies.json`.
+2. Remove extension lock and install directives related to Check.
+3. Restart Firefox to apply policy changes.
+
+For deployment and policy format details, see [Firefox Deployment](../../deployment/firefox-deployment.md).
From eb327dd9b0d0bb47e263d29c724504ea320db701 Mon Sep 17 00:00:00 2001
From: Brian Simpson <50429915+bmsimp@users.noreply.github.com>
Date: Fri, 10 Apr 2026 15:41:32 -0500
Subject: [PATCH 03/20] feat: Add interactive setup and detection scripts for
Intune deployment of Check extension
---
.../windows/domain-deployment.md | 111 ++++++-
enterprise/Detect-Windows-Chrome-and-Edge.ps1 | 170 ++++++++++
enterprise/Setup-Windows-Chrome-and-Edge.ps1 | 290 ++++++++++++++++++
3 files changed, 558 insertions(+), 13 deletions(-)
create mode 100644 enterprise/Detect-Windows-Chrome-and-Edge.ps1
create mode 100644 enterprise/Setup-Windows-Chrome-and-Edge.ps1
diff --git a/docs/deployment/chrome-edge-deployment-instructions/windows/domain-deployment.md b/docs/deployment/chrome-edge-deployment-instructions/windows/domain-deployment.md
index 1a651c61..91de282b 100644
--- a/docs/deployment/chrome-edge-deployment-instructions/windows/domain-deployment.md
+++ b/docs/deployment/chrome-edge-deployment-instructions/windows/domain-deployment.md
@@ -2,28 +2,113 @@
{% tabs %}
{% tab title="Intune" %}
-You need to create **two custom profiles** in Intune (one for Chrome, one for Edge).\
-Each profile contains **two OMA-URI settings**:
-
-* **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:
***
-#### Step 1 – Open Intune and Start a New Profile
+## Setup Script
+
+1. Download a copy of the Setup-Windows-Chrome-and-Edge.ps1 script from the Check repository on GitHub using the button below.
-1. Go to Intune Admin Center.
-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
+Import File
-Import File
+2. Run the script locally on your computer to generate the following scripts:
+ 1. Deploy-Windows-Chrome-and-Edge.ps1
+ 2. Remove-Windows-Chrome-and-Edge.ps1
+ 3. Detect-Windows-Chrome-and-Edge.ps1
+3. You will be prompted during the Setup script on how you want to configure Check. Follow the script's guidance to ensure you're accurately entering values for the script. These values will be used for both the Deploy and Detect to ensure the extension is properly deployed.
+4. Set the output location the script will use to generate the three new scripts.
+
+{% hint style="info" %} You can also download the three scripts directly from the Check GitHub repo and edit the configuration settings manually. {% endhint %}
***
-#### Step 2: Configuration
+## Adding to Intune
+
+### Prerequisites
+
+- Microsoft Intune admin access
+- The [Microsoft Win32 Content Prep Tool](https://github.com/microsoft/Microsoft-Win32-Content-Prep-Tool) (`IntuneWinAppUtil.exe`) to package scripts as `.intunewin` files
+
+### 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
+
+| Field | Value |
+|-------|-------|
+| Name | `Check by CyberDrain - Browser Extension` |
+| Description | `Deploys and configures the Check by CyberDrain phishing protection extension for Chrome and Edge browsers.` |
+| Publisher | Your company name or `CyberDrain` |
+
+### Step 4: Configure Program Settings
+
+| Field | Value |
+|-------|-------|
+| Install command | `powershell.exe -ExecutionPolicy Bypass -File Deploy-Windows-Chrome-and-Edge.ps1` |
+| Uninstall command | `powershell.exe -ExecutionPolicy Bypass -File Remove-Windows-Chrome-and-Edge.ps1` |
+| Install behavior | **System** |
+| Device restart behavior | **No specific action** |
+
+### Step 5: Configure Requirements
+
+| Field | Value |
+|-------|-------|
+| Operating system architecture | **64-bit** |
+| Minimum operating system | **Windows 10 1607** (or your minimum supported version) |
+
+### Step 6: Configure Detection Rules
+
+1. Under **Detection rules**, select **Use a custom detection script**
+2. Upload `Detect-Windows-Chrome-and-Edge.ps1`
+3. Set the following:
+
+| Field | Value |
+|-------|-------|
+| Run script as 32-bit process on 64-bit clients | **No** |
+| Enforce script signature check | **No** |
+| Run script in 64-bit PowerShell host | **No** |
+
+The detection script checks that all registry keys written by the install script exist and have the correct values. It exits with code `0` when everything matches (app detected) and code `1` when any value is missing or wrong (app not detected, triggers reinstall).
+
+### Step 7: Assign the App
+
+1. Under **Assignments**, click **Add group** under **Required**
+2. Choose your target:
+ - **All devices** — deploys to every Intune-managed Windows device
+ - **All users** — deploys to devices used by any licensed user
+ - **Select groups** — deploy to specific Azure AD / Entra ID groups
+3. Click **Review + create** > **Create**
+
+## Updating Settings
+
+When you need to change extension settings (e.g., enable page blocking, update branding):
+
+1. Re-run the setup script with new values, or manually edit the config blocks in both `Deploy-` and `Detect-` scripts
+2. Re-package with `IntuneWinAppUtil.exe`
+3. In Intune, either update the existing app or delete and recreate it with the new package
+
+Because the detection script body changes when settings change, Intune will detect the app as "not installed" on endpoints and automatically redeploy with the updated configuration.
+
+## Uninstalling
+
+To remove the extension from managed devices:
+
+- **Option A:** In Intune, change the app assignment from **Required** to **Uninstall**. Intune will run the `Remove-Windows-Chrome-and-Edge.ps1` script on targeted devices.
+- **Option B:** Delete the app from Intune entirely. Note that this stops management but does not actively remove the registry keys from devices that already have them.
+
+## Troubleshooting
-Documentation to follow
+- **Extension not appearing after deployment:** Check that the install script ran as System (not User). Verify registry keys exist under `HKLM:\SOFTWARE\Policies\Google\Chrome\ExtensionSettings\` and `HKLM:\SOFTWARE\Policies\Microsoft\Edge\ExtensionSettings\`.
+- **Intune keeps reinstalling the app:** The detection script values don't match what the install script wrote. Ensure both scripts have identical configuration values.
+- **Detection script shows as failed:** Run the detection script manually on a test device as Administrator to see which check fails (it will exit at the first mismatch).
{% endtab %}
{% tab title="Group Policy" %}
diff --git a/enterprise/Detect-Windows-Chrome-and-Edge.ps1 b/enterprise/Detect-Windows-Chrome-and-Edge.ps1
new file mode 100644
index 00000000..d97a6f29
--- /dev/null
+++ b/enterprise/Detect-Windows-Chrome-and-Edge.ps1
@@ -0,0 +1,170 @@
+# Check Extension - Intune Detection Script
+# This script verifies that the Check by CyberDrain extension is correctly configured
+# in the registry for both Chrome and Edge browsers.
+#
+# IMPORTANT: The settings below MUST match the values in your Deploy-Windows-Chrome-and-Edge.ps1.
+# If any value differs, Intune will detect the app as "not installed" and trigger a reinstall.
+#
+# Exit codes: 0 = compliant (extension correctly configured), 1 = non-compliant (drift detected)
+
+# Define extension details
+# Chrome
+$chromeExtensionId = "benimdeioplgkhanklclahllklceahbe"
+$chromeUpdateUrl = "https://clients2.google.com/service/update2/crx"
+$chromeManagedStorageKey = "HKLM:\SOFTWARE\Policies\Google\Chrome\3rdparty\extensions\$chromeExtensionId\policy"
+$chromeExtensionSettingsKey = "HKLM:\SOFTWARE\Policies\Google\Chrome\ExtensionSettings\$chromeExtensionId"
+
+#Edge
+$edgeExtensionId = "knepjpocdagponkonnbggpcnhnaikajg"
+$edgeUpdateUrl = "https://edge.microsoft.com/extensionwebstorebase/v1/crx"
+$edgeManagedStorageKey = "HKLM:\SOFTWARE\Policies\Microsoft\Edge\3rdparty\extensions\$edgeExtensionId\policy"
+$edgeExtensionSettingsKey = "HKLM:\SOFTWARE\Policies\Microsoft\Edge\ExtensionSettings\$edgeExtensionId"
+
+# Extension Configuration Settings
+$showNotifications = 1 # 0 = Unchecked, 1 = Checked (Enabled); default is 1; This will set the "Show Notifications" option in the extension settings.
+$enableValidPageBadge = 0 # 0 = Unchecked, 1 = Checked (Enabled); default is 0; This will set the "Show Valid Page Badge" option in the extension settings.
+$enablePageBlocking = 1 # 0 = Unchecked, 1 = Checked (Enabled); default is 1; This will set the "Enable Page Blocking" option in the extension settings.
+$forceToolbarPin = 1 # 0 = Not pinned, 1 = Force pinned to toolbar; default is 1
+$enableCippReporting = 0 # 0 = Unchecked, 1 = Checked (Enabled); default is 0; This will set the "Enable CIPP Reporting" option in the extension settings.
+$cippServerUrl = "" # This will set the "CIPP Server URL" option in the extension settings; default is blank; if you set $enableCippReporting to 1, you must set this to a valid URL including the protocol (e.g., https://cipp.cyberdrain.com). Can be vanity URL or the default azurestaticapps.net domain.
+$cippTenantId = "" # This will set the "Tenant ID/Domain" option in the extension settings; default is blank; if you set $enableCippReporting to 1, you must set this to a valid Tenant ID.
+$customRulesUrl = "" # This will set the "Config URL" option in the Detection Configuration settings; default is blank.
+$updateInterval = 24 # This will set the "Update Interval" option in the Detection Configuration settings; default is 24 (hours). Range: 1-168 hours (1 hour to 1 week).
+$urlAllowlist = @() # This will set the "URL Allowlist" option in the Detection Configuration settings; default is blank; if you want to add multiple URLs, add them as a comma-separated list within the brackets (e.g., @("https://example1.com", "https://example2.com")). Supports simple URLs with * wildcard (e.g., https://*.example.com) or advanced regex patterns (e.g., ^https:\/\/(www\.)?example\.com\/.*$).
+$domainSquattingEnabled = 1 # 0 = Disabled, 1 = Enabled; default is 1; controls domain squatting detection from managed policy/config.
+$enableDebugLogging = 0 # 0 = Unchecked, 1 = Checked (Enabled); default is 0; This will set the "Enable Debug Logging" option in the Activity Log settings.
+
+# Generic Webhook Settings
+$enableGenericWebhook = 0 # 0 = Disabled, 1 = Enabled; default is 0; This will enable the generic webhook for sending detection events to a custom endpoint.
+$webhookUrl = "" # This will set the "Webhook URL" option; default is blank; if you set $enableGenericWebhook to 1, you must set this to a valid URL including the protocol (e.g., https://webhook.example.com/endpoint).
+$webhookEvents = @() # This will set the "Event Types" to send to the webhook; default is blank; if you set $enableGenericWebhook to 1, you can specify which events to send. Available events: "detection_alert", "false_positive_report", "page_blocked", "rogue_app_detected", "threat_detected", "validation_event". Example: @("detection_alert", "page_blocked", "threat_detected").
+
+# Custom Branding Settings
+$companyName = "CyberDrain" # This will set the "Company Name" option in the Custom Branding settings; default is "CyberDrain".
+$productName = "Check - Phishing Protection" # This will set the "Product Name" option in the Custom Branding settings; default is "Check - Phishing Protection".
+$supportEmail = "" # This will set the "Support Email" option in the Custom Branding settings; default is blank.
+$supportUrl = "" # This will set the "Support URL" option in the Custom Branding settings; default is blank.
+$privacyPolicyUrl = "" # This will set the "Privacy URL" option in the Custom Branding settings; default is blank.
+$aboutUrl = "" # This will set the "About URL" option in the Custom Branding settings; default is blank.
+$primaryColor = "#F77F00" # This will set the "Primary Color" option in the Custom Branding settings; default is "#F77F00"; must be a valid hex color code (e.g., #FFFFFF).
+$logoUrl = "" # This will set the "Logo URL" option in the Custom Branding settings; default is blank. Must be a valid URL including the protocol (e.g., https://example.com/logo.png); protocol must be https; recommended size is 48x48 pixels with a maximum of 128x128.
+
+# Extension Settings
+$installationMode = "force_installed"
+
+# Helper to check a registry value matches expected
+function Test-RegValue {
+ param (
+ [string]$Path,
+ [string]$Name,
+ $Expected
+ )
+ $val = (Get-ItemProperty -Path $Path -Name $Name -ErrorAction SilentlyContinue).$Name
+ return ($null -ne $val -and $val -eq $Expected)
+}
+
+# Define browser configurations for detection
+$browsers = @(
+ @{
+ Name = 'Chrome'
+ ExtensionId = $chromeExtensionId
+ UpdateUrl = $chromeUpdateUrl
+ ManagedStorageKey = $chromeManagedStorageKey
+ ExtensionSettingsKey = $chromeExtensionSettingsKey
+ ToolbarProp = 'toolbar_pin'
+ ToolbarValue = 'force_pinned'
+ },
+ @{
+ Name = 'Edge'
+ ExtensionId = $edgeExtensionId
+ UpdateUrl = $edgeUpdateUrl
+ ManagedStorageKey = $edgeManagedStorageKey
+ ExtensionSettingsKey = $edgeExtensionSettingsKey
+ ToolbarProp = 'toolbar_state'
+ ToolbarValue = 'force_shown'
+ }
+)
+
+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 }
+ if (!(Test-RegValue $brandingKey 'companyName' $companyName)) { exit 1 }
+ if (!(Test-RegValue $brandingKey 'productName' $productName)) { exit 1 }
+ if (!(Test-RegValue $brandingKey 'supportEmail' $supportEmail)) { exit 1 }
+ if (!(Test-RegValue $brandingKey 'supportUrl' $supportUrl)) { exit 1 }
+ if (!(Test-RegValue $brandingKey 'privacyPolicyUrl' $privacyPolicyUrl)) { exit 1 }
+ if (!(Test-RegValue $brandingKey 'aboutUrl' $aboutUrl)) { exit 1 }
+ if (!(Test-RegValue $brandingKey 'primaryColor' $primaryColor)) { exit 1 }
+ if (!(Test-RegValue $brandingKey 'logoUrl' $logoUrl)) { exit 1 }
+
+ # genericWebhook subkey
+ $webhookKey = "$policyKey\genericWebhook"
+ if (!(Test-Path $webhookKey)) { exit 1 }
+ if (!(Test-RegValue $webhookKey 'enabled' $enableGenericWebhook)) { exit 1 }
+ if (!(Test-RegValue $webhookKey 'url' $webhookUrl)) { exit 1 }
+
+ # genericWebhook\events subkey — verify exact count and values
+ $eventsKey = "$webhookKey\events"
+ if (!(Test-Path $eventsKey)) { exit 1 }
+ if ($webhookEvents.Count -gt 0) {
+ $eventsCount = (Get-Item $eventsKey).Property.Count
+ if ($eventsCount -ne $webhookEvents.Count) { exit 1 }
+ for ($i = 0; $i -lt $webhookEvents.Count; $i++) {
+ if (!(Test-RegValue $eventsKey ($i + 1).ToString() $webhookEvents[$i])) { exit 1 }
+ }
+ } else {
+ $existingEvents = (Get-Item $eventsKey).Property
+ if ($null -ne $existingEvents -and $existingEvents.Count -gt 0) { exit 1 }
+ }
+
+ # urlAllowlist subkey — verify exact count and values
+ $allowlistKey = "$policyKey\urlAllowlist"
+ if (!(Test-Path $allowlistKey)) { exit 1 }
+ if ($urlAllowlist.Count -gt 0) {
+ $allowlistCount = (Get-Item $allowlistKey).Property.Count
+ if ($allowlistCount -ne $urlAllowlist.Count) { exit 1 }
+ for ($i = 0; $i -lt $urlAllowlist.Count; $i++) {
+ if (!(Test-RegValue $allowlistKey ($i + 1).ToString() $urlAllowlist[$i])) { exit 1 }
+ }
+ } else {
+ $existingAllowlist = (Get-Item $allowlistKey).Property
+ if ($null -ne $existingAllowlist -and $existingAllowlist.Count -gt 0) { exit 1 }
+ }
+
+ # ExtensionSettings key
+ if (!(Test-Path $browser.ExtensionSettingsKey)) { exit 1 }
+ if (!(Test-RegValue $browser.ExtensionSettingsKey 'installation_mode' $installationMode)) { exit 1 }
+ if (!(Test-RegValue $browser.ExtensionSettingsKey 'update_url' $browser.UpdateUrl)) { exit 1 }
+
+ # 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 }
+ }
+}
+
+Write-Output "Check extension is correctly configured for Chrome and Edge."
+exit 0
diff --git a/enterprise/Setup-Windows-Chrome-and-Edge.ps1 b/enterprise/Setup-Windows-Chrome-and-Edge.ps1
new file mode 100644
index 00000000..3b7974ae
--- /dev/null
+++ b/enterprise/Setup-Windows-Chrome-and-Edge.ps1
@@ -0,0 +1,290 @@
+# Check Extension - Interactive Setup Script
+# Downloads the latest Check extension deployment scripts from GitHub and walks you
+# through configuring each setting. Outputs ready-to-upload scripts for Intune.
+#
+# Usage: Run this script in PowerShell. It will prompt for each setting and generate
+# configured Deploy, Remove, and Detect scripts in your chosen output directory.
+#
+# For Intune deployment instructions, see:
+# https://docs.check.tech/deployment/chrome-edge-deployment-instructions/windows/domain-deployment#intune
+
+Write-Host ""
+Write-Host "======================================================" -ForegroundColor DarkCyan
+Write-Host " Check by CyberDrain - Intune Deployment Setup" -ForegroundColor DarkCyan
+Write-Host "======================================================" -ForegroundColor DarkCyan
+Write-Host ""
+Write-Host "This script will download the latest Check extension scripts from GitHub"
+Write-Host "and walk you through configuring each setting for your environment."
+Write-Host ""
+
+# GitHub raw URLs for the template scripts
+$baseUrl = "https://raw.githubusercontent.com/CyberDrain/Check/refs/heads/main/enterprise"
+$scripts = @{
+ Deploy = @{ Url = "$baseUrl/Deploy-Windows-Chrome-and-Edge.ps1"; FileName = "Deploy-Windows-Chrome-and-Edge.ps1" }
+ Remove = @{ Url = "$baseUrl/Remove-Windows-Chrome-and-Edge.ps1"; FileName = "Remove-Windows-Chrome-and-Edge.ps1" }
+ Detect = @{ Url = "$baseUrl/Detect-Windows-Chrome-and-Edge.ps1"; FileName = "Detect-Windows-Chrome-and-Edge.ps1" }
+}
+
+# Download templates
+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
+ Write-Host " Downloaded $($scripts[$key].FileName)" -ForegroundColor Green
+ } catch {
+ Write-Host " Failed to download $($scripts[$key].FileName): $($_.Exception.Message)" -ForegroundColor Red
+ Write-Host " Please check your internet connection and try again." -ForegroundColor Red
+ exit 1
+ }
+}
+Write-Host ""
+
+# Prompt helper functions
+function Read-Setting {
+ param (
+ [string]$Name,
+ [string]$Description,
+ [string]$Default,
+ [string]$Type = "string"
+ )
+ $prompt = "$Name - $Description"
+ if ($Default -ne "") {
+ $prompt += " [default: $Default]"
+ } else {
+ $prompt += " [default: blank]"
+ }
+ $value = Read-Host $prompt
+ if ($value -eq "") { $value = $Default }
+
+ if ($Type -eq "bool") {
+ while ($value -ne "0" -and $value -ne "1") {
+ Write-Host " Please enter 0 or 1." -ForegroundColor Yellow
+ $value = Read-Host $prompt
+ if ($value -eq "") { $value = $Default }
+ }
+ }
+ if ($Type -eq "int") {
+ while ($value -notmatch '^\d+$') {
+ Write-Host " Please enter a number." -ForegroundColor Yellow
+ $value = Read-Host $prompt
+ if ($value -eq "") { $value = $Default }
+ }
+ if ($Name -eq "updateInterval") {
+ while ([int]$value -lt 1 -or [int]$value -gt 168) {
+ Write-Host " Update interval must be between 1 and 168 hours." -ForegroundColor Yellow
+ $value = Read-Host $prompt
+ if ($value -eq "") { $value = $Default }
+ }
+ }
+ }
+ if ($Type -eq "string" -and $value -match '"') {
+ while ($value -match '"') {
+ Write-Host " Double-quote characters are not supported in this field." -ForegroundColor Yellow
+ $value = Read-Host $prompt
+ if ($value -eq "") { $value = $Default }
+ }
+ }
+ return $value
+}
+
+function Read-ArraySetting {
+ param (
+ [string]$Name,
+ [string]$Description
+ )
+ Write-Host "$Name - $Description"
+ Write-Host " Enter values one at a time. Press Enter on a blank line when done."
+ $values = @()
+ $i = 1
+ while ($true) {
+ $entry = Read-Host " [$i]"
+ if ($entry -eq "") { break }
+ $values += $entry
+ $i++
+ }
+ return $values
+}
+
+#######################################################################
+# Extension Configuration Settings
+#######################################################################
+Write-Host "--- Extension Configuration Settings ---" -ForegroundColor Cyan
+Write-Host ""
+
+$cfg_showNotifications = Read-Setting -Name "showNotifications" -Description "Show notifications (0 = Disabled, 1 = Enabled)" -Default "1" -Type "bool"
+$cfg_enableValidPageBadge = Read-Setting -Name "enableValidPageBadge" -Description "Show valid page badge (0 = Disabled, 1 = Enabled)" -Default "0" -Type "bool"
+$cfg_enablePageBlocking = Read-Setting -Name "enablePageBlocking" -Description "Enable page blocking (0 = Disabled, 1 = Enabled)" -Default "1" -Type "bool"
+$cfg_forceToolbarPin = Read-Setting -Name "forceToolbarPin" -Description "Force pin extension to toolbar (0 = Not pinned, 1 = Force pinned)" -Default "1" -Type "bool"
+$cfg_updateInterval = Read-Setting -Name "updateInterval" -Description "Update interval in hours (1-168)" -Default "24" -Type "int"
+$cfg_enableDebugLogging = Read-Setting -Name "enableDebugLogging" -Description "Enable debug logging (0 = Disabled, 1 = Enabled)" -Default "0" -Type "bool"
+$cfg_domainSquattingEnabled = Read-Setting -Name "domainSquattingEnabled" -Description "Enable domain squatting detection (0 = Disabled, 1 = Enabled)" -Default "1" -Type "bool"
+$cfg_customRulesUrl = Read-Setting -Name "customRulesUrl" -Description "Custom rules/config URL (leave blank if not used)" -Default ""
+Write-Host ""
+
+#######################################################################
+# CIPP Reporting
+#######################################################################
+Write-Host "--- CIPP Reporting ---" -ForegroundColor Cyan
+Write-Host ""
+
+$cfg_enableCippReporting = Read-Setting -Name "enableCippReporting" -Description "Enable CIPP reporting (0 = Disabled, 1 = Enabled)" -Default "0" -Type "bool"
+if ($cfg_enableCippReporting -eq "1") {
+ $cfg_cippServerUrl = Read-Setting -Name "cippServerUrl" -Description "CIPP Server URL (e.g., https://cipp.cyberdrain.com)" -Default ""
+ $cfg_cippTenantId = Read-Setting -Name "cippTenantId" -Description "Tenant ID or domain for CIPP reporting" -Default ""
+} else {
+ $cfg_cippServerUrl = ""
+ $cfg_cippTenantId = ""
+}
+Write-Host ""
+
+#######################################################################
+# Generic Webhook Settings
+#######################################################################
+Write-Host "--- Generic Webhook Settings ---" -ForegroundColor Cyan
+Write-Host ""
+
+$cfg_enableGenericWebhook = Read-Setting -Name "enableGenericWebhook" -Description "Enable generic webhook (0 = Disabled, 1 = Enabled)" -Default "0" -Type "bool"
+if ($cfg_enableGenericWebhook -eq "1") {
+ $cfg_webhookUrl = Read-Setting -Name "webhookUrl" -Description "Webhook URL (e.g., https://webhook.example.com/endpoint)" -Default ""
+ $cfg_webhookEvents = Read-ArraySetting -Name "webhookEvents" -Description "Event types to send. Available: detection_alert, false_positive_report, page_blocked, rogue_app_detected, threat_detected, validation_event"
+} else {
+ $cfg_webhookUrl = ""
+ $cfg_webhookEvents = @()
+}
+Write-Host ""
+
+#######################################################################
+# URL Allowlist
+#######################################################################
+Write-Host "--- URL Allowlist ---" -ForegroundColor Cyan
+Write-Host ""
+
+$cfg_urlAllowlist = Read-ArraySetting -Name "urlAllowlist" -Description "URLs to allowlist. Supports wildcards (e.g., https://*.example.com) and regex patterns"
+Write-Host ""
+
+#######################################################################
+# Custom Branding Settings
+#######################################################################
+Write-Host "--- Custom Branding Settings ---" -ForegroundColor Cyan
+Write-Host ""
+
+$cfg_companyName = Read-Setting -Name "companyName" -Description "Company name" -Default "CyberDrain"
+$cfg_productName = Read-Setting -Name "productName" -Description "Product name" -Default "Check - Phishing Protection"
+$cfg_supportEmail = Read-Setting -Name "supportEmail" -Description "Support email address" -Default ""
+$cfg_supportUrl = Read-Setting -Name "supportUrl" -Description "Support URL" -Default ""
+$cfg_privacyPolicyUrl = Read-Setting -Name "privacyPolicyUrl" -Description "Privacy policy URL" -Default ""
+$cfg_aboutUrl = Read-Setting -Name "aboutUrl" -Description "About URL" -Default ""
+$cfg_primaryColor = Read-Setting -Name "primaryColor" -Description "Primary color (hex code)" -Default "#F77F00"
+$cfg_logoUrl = Read-Setting -Name "logoUrl" -Description "Logo URL (https, recommended 48x48, max 128x128)" -Default ""
+Write-Host ""
+
+#######################################################################
+# Output Path
+#######################################################################
+Write-Host "--- Output ---" -ForegroundColor Cyan
+Write-Host ""
+
+$defaultOutputPath = (Get-Location).Path
+$outputPath = Read-Host "Output directory [default: $defaultOutputPath]"
+if ($outputPath -eq "") { $outputPath = $defaultOutputPath }
+
+if (!(Test-Path $outputPath)) {
+ New-Item -Path $outputPath -ItemType Directory -Force | Out-Null
+ Write-Host "Created output directory: $outputPath" -ForegroundColor Green
+}
+Write-Host ""
+
+#######################################################################
+# Build replacement map
+#######################################################################
+
+# Helper to build a PowerShell array literal string from an array
+function Format-ArrayLiteral {
+ param ([string[]]$Values)
+ if ($Values.Count -eq 0) { return '@()' }
+ $quoted = $Values | ForEach-Object { $escaped = $_ -replace '"', '""'; "`"$escaped`"" }
+ return "@($($quoted -join ', '))"
+}
+
+# Each entry: variable assignment pattern to find -> replacement value
+# Scalar replacements target the value + start of inline comment
+$replacements = @(
+ @{ Pattern = '$showNotifications = 1 #'; Value = "`$showNotifications = $cfg_showNotifications #" }
+ @{ Pattern = '$enableValidPageBadge = 0 #'; Value = "`$enableValidPageBadge = $cfg_enableValidPageBadge #" }
+ @{ Pattern = '$enablePageBlocking = 1 #'; Value = "`$enablePageBlocking = $cfg_enablePageBlocking #" }
+ @{ Pattern = '$forceToolbarPin = 1 #'; Value = "`$forceToolbarPin = $cfg_forceToolbarPin #" }
+ @{ Pattern = '$enableCippReporting = 0 #'; Value = "`$enableCippReporting = $cfg_enableCippReporting #" }
+ @{ 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`" #" }
+ @{ Pattern = '$productName = "Check - Phishing Protection" #'; Value = "`$productName = `"$cfg_productName`" #" }
+ @{ Pattern = '$supportEmail = "" #'; Value = "`$supportEmail = `"$cfg_supportEmail`" #" }
+ @{ Pattern = '$supportUrl = "" #'; Value = "`$supportUrl = `"$cfg_supportUrl`" #" }
+ @{ Pattern = '$privacyPolicyUrl = "" #'; Value = "`$privacyPolicyUrl = `"$cfg_privacyPolicyUrl`" #" }
+ @{ Pattern = '$aboutUrl = "" #'; Value = "`$aboutUrl = `"$cfg_aboutUrl`" #" }
+ @{ Pattern = '$primaryColor = "#F77F00" #'; Value = "`$primaryColor = `"$cfg_primaryColor`" #" }
+ @{ Pattern = '$logoUrl = "" #'; Value = "`$logoUrl = `"$cfg_logoUrl`" #" }
+)
+
+# Array replacements — replace the full assignment line including inline comment
+$arrayReplacements = @(
+ @{ Pattern = '$urlAllowlist = @() #'; Value = "`$urlAllowlist = $(Format-ArrayLiteral $cfg_urlAllowlist) #" }
+ @{ Pattern = '$webhookEvents = @() #'; Value = "`$webhookEvents = $(Format-ArrayLiteral $cfg_webhookEvents) #" }
+)
+
+#######################################################################
+# Apply replacements and write output files
+#######################################################################
+
+function Apply-Replacements {
+ param ([string]$Content)
+
+ foreach ($r in $replacements) {
+ $Content = $Content.Replace($r.Pattern, $r.Value)
+ }
+ foreach ($r in $arrayReplacements) {
+ $Content = $Content.Replace($r.Pattern, $r.Value)
+ }
+
+ return $Content
+}
+
+# Deploy script — apply all replacements
+$deployContent = Apply-Replacements -Content $templates['Deploy']
+$deployPath = Join-Path $outputPath $scripts['Deploy'].FileName
+Set-Content -Path $deployPath -Value $deployContent -Encoding UTF8
+Write-Host "Written: $deployPath" -ForegroundColor Green
+
+# Detect script — apply all replacements (same config block format)
+$detectContent = Apply-Replacements -Content $templates['Detect']
+$detectPath = Join-Path $outputPath $scripts['Detect'].FileName
+Set-Content -Path $detectPath -Value $detectContent -Encoding UTF8
+Write-Host "Written: $detectPath" -ForegroundColor Green
+
+# Remove script — copy as-is (no config to replace)
+$removePath = Join-Path $outputPath $scripts['Remove'].FileName
+Set-Content -Path $removePath -Value $templates['Remove'] -Encoding UTF8
+Write-Host "Written: $removePath" -ForegroundColor Green
+
+Write-Host ""
+Write-Host "======================================================" -ForegroundColor DarkCyan
+Write-Host " Setup complete!" -ForegroundColor Green
+Write-Host "======================================================" -ForegroundColor DarkCyan
+Write-Host ""
+Write-Host "Your configured scripts are in: $outputPath" -ForegroundColor Yellow
+Write-Host ""
+Write-Host "Next steps:" -ForegroundColor Yellow
+Write-Host " 1. Package the scripts as a Win32 app (.intunewin) or use Intune's script deployment"
+Write-Host " 2. Upload to Microsoft Intune admin center > Apps > Windows"
+Write-Host " 3. Set install command: powershell.exe -ExecutionPolicy Bypass -File Deploy-Windows-Chrome-and-Edge.ps1"
+Write-Host " 4. Set uninstall command: powershell.exe -ExecutionPolicy Bypass -File Remove-Windows-Chrome-and-Edge.ps1"
+Write-Host " 5. Add detection script: Detect-Windows-Chrome-and-Edge.ps1"
+Write-Host " 6. Assign to devices/users as Required"
+Write-Host ""
From 303e06caa58206a008484ad69e3a656bee3bec9d Mon Sep 17 00:00:00 2001
From: Brian Simpson <50429915+bmsimp@users.noreply.github.com>
Date: Fri, 24 Apr 2026 10:22:11 -0500
Subject: [PATCH 04/20] Update enterprise/Setup-Windows-Chrome-and-Edge.ps1
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Brian Simpson <50429915+bmsimp@users.noreply.github.com>
---
enterprise/Setup-Windows-Chrome-and-Edge.ps1 | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/enterprise/Setup-Windows-Chrome-and-Edge.ps1 b/enterprise/Setup-Windows-Chrome-and-Edge.ps1
index 3b7974ae..ec512a75 100644
--- a/enterprise/Setup-Windows-Chrome-and-Edge.ps1
+++ b/enterprise/Setup-Windows-Chrome-and-Edge.ps1
@@ -203,7 +203,7 @@ Write-Host ""
function Format-ArrayLiteral {
param ([string[]]$Values)
if ($Values.Count -eq 0) { return '@()' }
- $quoted = $Values | ForEach-Object { $escaped = $_ -replace '"', '""'; "`"$escaped`"" }
+ $quoted = $Values | ForEach-Object { $escaped = $_ -replace "'", "''"; "'$escaped'" }
return "@($($quoted -join ', '))"
}
From dd2632482ffd54e4ff08e6c70b943abf2abc34e7 Mon Sep 17 00:00:00 2001
From: Brian Simpson <50429915+bmsimp@users.noreply.github.com>
Date: Fri, 24 Apr 2026 10:23:09 -0500
Subject: [PATCH 05/20] Update enterprise/Detect-Windows-Chrome-and-Edge.ps1
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Brian Simpson <50429915+bmsimp@users.noreply.github.com>
---
enterprise/Detect-Windows-Chrome-and-Edge.ps1 | 80 ++++++++++++++++---
1 file changed, 67 insertions(+), 13 deletions(-)
diff --git a/enterprise/Detect-Windows-Chrome-and-Edge.ps1 b/enterprise/Detect-Windows-Chrome-and-Edge.ps1
index d97a6f29..9cd31b9d 100644
--- a/enterprise/Detect-Windows-Chrome-and-Edge.ps1
+++ b/enterprise/Detect-Windows-Chrome-and-Edge.ps1
@@ -85,33 +85,87 @@ $browsers = @(
}
)
+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 = ''
+ if (Test-Path $KeyPath) {
+ try {
+ $property = Get-ItemProperty -Path $KeyPath -Name $ValueName -ErrorAction Stop
+ $actualValue = $property.$ValueName
+ }
+ catch {
+ $actualValue = ''
+ }
+ }
+
+ 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)) { exit 1 }
+ 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-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 }
+ 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-RegValue $policyKey 'cippServerUrl' $cippServerUrl)) { exit 1 }
- if (!(Test-RegValue $policyKey 'cippTenantId' $cippTenantId)) { exit 1 }
- if (!(Test-RegValue $policyKey 'customRulesUrl' $customRulesUrl)) { exit 1 }
+ 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)) { exit 1 }
- if (!(Test-RegValue $domainSquattingKey 'enabled' $domainSquattingEnabled)) { exit 1 }
+ 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)) { exit 1 }
+ if (!(Test-Path $brandingKey)) {
+ Write-DetectionFailure -BrowserName $browser.Name -KeyPath $brandingKey -ValueName $null -ExpectedValue $null -ActualValue $null
+ exit 1
+ }
if (!(Test-RegValue $brandingKey 'companyName' $companyName)) { exit 1 }
if (!(Test-RegValue $brandingKey 'productName' $productName)) { exit 1 }
if (!(Test-RegValue $brandingKey 'supportEmail' $supportEmail)) { exit 1 }
From 0a92b2268be919be25bc4bef0141bafe46c2d113 Mon Sep 17 00:00:00 2001
From: Brian Simpson <50429915+bmsimp@users.noreply.github.com>
Date: Fri, 24 Apr 2026 10:23:24 -0500
Subject: [PATCH 06/20] Update
docs/deployment/chrome-edge-deployment-instructions/windows/domain-deployment.md
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Brian Simpson <50429915+bmsimp@users.noreply.github.com>
---
.../windows/domain-deployment.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/deployment/chrome-edge-deployment-instructions/windows/domain-deployment.md b/docs/deployment/chrome-edge-deployment-instructions/windows/domain-deployment.md
index 91de282b..0062365f 100644
--- a/docs/deployment/chrome-edge-deployment-instructions/windows/domain-deployment.md
+++ b/docs/deployment/chrome-edge-deployment-instructions/windows/domain-deployment.md
@@ -2,7 +2,7 @@
{% tabs %}
{% tab title="Intune" %}
-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.
***
From c0ea1f2f317ff86bebe14b9f1f75e2731476ea9b Mon Sep 17 00:00:00 2001
From: Brian Simpson <50429915+bmsimp@users.noreply.github.com>
Date: Fri, 24 Apr 2026 10:24:31 -0500
Subject: [PATCH 07/20] Update
docs/deployment/chrome-edge-deployment-instructions/windows/domain-deployment.md
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Brian Simpson <50429915+bmsimp@users.noreply.github.com>
---
.../windows/domain-deployment.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/deployment/chrome-edge-deployment-instructions/windows/domain-deployment.md b/docs/deployment/chrome-edge-deployment-instructions/windows/domain-deployment.md
index 0062365f..3c6dffd5 100644
--- a/docs/deployment/chrome-edge-deployment-instructions/windows/domain-deployment.md
+++ b/docs/deployment/chrome-edge-deployment-instructions/windows/domain-deployment.md
@@ -74,8 +74,8 @@ This creates `Deploy-Windows-Chrome-and-Edge.intunewin`.
|-------|-------|
| Run script as 32-bit process on 64-bit clients | **No** |
| Enforce script signature check | **No** |
-| 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.
The detection script checks that all registry keys written by the install script exist and have the correct values. It exits with code `0` when everything matches (app detected) and code `1` when any value is missing or wrong (app not detected, triggers reinstall).
### Step 7: Assign the App
From 3b37e2f8049cef7bc49b9a7cca2c357d783a3737 Mon Sep 17 00:00:00 2001
From: Brian Simpson <50429915+bmsimp@users.noreply.github.com>
Date: Fri, 24 Apr 2026 10:24:56 -0500
Subject: [PATCH 08/20] Update
docs/deployment/chrome-edge-deployment-instructions/windows/domain-deployment.md
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Brian Simpson <50429915+bmsimp@users.noreply.github.com>
---
.../windows/domain-deployment.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/deployment/chrome-edge-deployment-instructions/windows/domain-deployment.md b/docs/deployment/chrome-edge-deployment-instructions/windows/domain-deployment.md
index 3c6dffd5..17ef6474 100644
--- a/docs/deployment/chrome-edge-deployment-instructions/windows/domain-deployment.md
+++ b/docs/deployment/chrome-edge-deployment-instructions/windows/domain-deployment.md
@@ -10,7 +10,7 @@ The simplest method of Intune deployment is via a win32 script. Follow the steps
1. Download a copy of the Setup-Windows-Chrome-and-Edge.ps1 script from the Check repository on GitHub using the button below.
-Import File
+Download script
2. Run the script locally on your computer to generate the following scripts:
1. Deploy-Windows-Chrome-and-Edge.ps1
From 53576e600112e26ffa2d3dede9eea31d22de69ff Mon Sep 17 00:00:00 2001
From: Brian Simpson <50429915+bmsimp@users.noreply.github.com>
Date: Fri, 24 Apr 2026 10:33:42 -0500
Subject: [PATCH 09/20] fix: escape scalar values as single-quoted literals +
add missing Intune Step 2
- Setup script: mirror the Format-ArrayLiteral change for scalar
replacements by adding Format-SingleQuoted and switching all
string-valued scalar replacements to single-quoted output.
Prevents \$ / backtick interpolation in user-provided URLs and
branding fields when generated scripts run.
- Docs: add the missing "Step 2: Create the Win32 App in Intune"
between packaging and configuration, fixing the 1 -> 3 jump.
---
.../windows/domain-deployment.md | 7 ++++
enterprise/Setup-Windows-Chrome-and-Edge.ps1 | 32 ++++++++++++-------
2 files changed, 27 insertions(+), 12 deletions(-)
diff --git a/docs/deployment/chrome-edge-deployment-instructions/windows/domain-deployment.md b/docs/deployment/chrome-edge-deployment-instructions/windows/domain-deployment.md
index 17ef6474..0f686792 100644
--- a/docs/deployment/chrome-edge-deployment-instructions/windows/domain-deployment.md
+++ b/docs/deployment/chrome-edge-deployment-instructions/windows/domain-deployment.md
@@ -40,6 +40,13 @@ Intune Win32 apps require an `.intunewin` package. Place your three configured s
This creates `Deploy-Windows-Chrome-and-Edge.intunewin`.
+### Step 2: Create the Win32 App in Intune
+
+1. Open the [Microsoft Intune admin center](https://intune.microsoft.com)
+2. Navigate to **Apps** > **Windows**
+3. Click **Add** > Select **Windows app (Win32)** > **Select**
+4. Upload the `.intunewin` file created in Step 1
+
### Step 3: Configure App Information
| Field | Value |
diff --git a/enterprise/Setup-Windows-Chrome-and-Edge.ps1 b/enterprise/Setup-Windows-Chrome-and-Edge.ps1
index ec512a75..df4f5396 100644
--- a/enterprise/Setup-Windows-Chrome-and-Edge.ps1
+++ b/enterprise/Setup-Windows-Chrome-and-Edge.ps1
@@ -207,6 +207,14 @@ function Format-ArrayLiteral {
return "@($($quoted -join ', '))"
}
+# Helper to emit a single-quoted PowerShell string literal from a value.
+# Single quotes prevent $ and backtick interpolation in the generated scripts.
+function Format-SingleQuoted {
+ param ([string]$Value)
+ $escaped = $Value -replace "'", "''"
+ return "'$escaped'"
+}
+
# Each entry: variable assignment pattern to find -> replacement value
# Scalar replacements target the value + start of inline comment
$replacements = @(
@@ -215,22 +223,22 @@ $replacements = @(
@{ Pattern = '$enablePageBlocking = 1 #'; Value = "`$enablePageBlocking = $cfg_enablePageBlocking #" }
@{ Pattern = '$forceToolbarPin = 1 #'; Value = "`$forceToolbarPin = $cfg_forceToolbarPin #" }
@{ Pattern = '$enableCippReporting = 0 #'; Value = "`$enableCippReporting = $cfg_enableCippReporting #" }
- @{ Pattern = '$cippServerUrl = "" #'; Value = "`$cippServerUrl = `"$cfg_cippServerUrl`" #" }
- @{ Pattern = '$cippTenantId = "" #'; Value = "`$cippTenantId = `"$cfg_cippTenantId`" #" }
- @{ Pattern = '$customRulesUrl = "" #'; Value = "`$customRulesUrl = `"$cfg_customRulesUrl`" #" }
+ @{ Pattern = '$cippServerUrl = "" #'; Value = "`$cippServerUrl = $(Format-SingleQuoted $cfg_cippServerUrl) #" }
+ @{ Pattern = '$cippTenantId = "" #'; Value = "`$cippTenantId = $(Format-SingleQuoted $cfg_cippTenantId) #" }
+ @{ Pattern = '$customRulesUrl = "" #'; Value = "`$customRulesUrl = $(Format-SingleQuoted $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`" #" }
- @{ Pattern = '$productName = "Check - Phishing Protection" #'; Value = "`$productName = `"$cfg_productName`" #" }
- @{ Pattern = '$supportEmail = "" #'; Value = "`$supportEmail = `"$cfg_supportEmail`" #" }
- @{ Pattern = '$supportUrl = "" #'; Value = "`$supportUrl = `"$cfg_supportUrl`" #" }
- @{ Pattern = '$privacyPolicyUrl = "" #'; Value = "`$privacyPolicyUrl = `"$cfg_privacyPolicyUrl`" #" }
- @{ Pattern = '$aboutUrl = "" #'; Value = "`$aboutUrl = `"$cfg_aboutUrl`" #" }
- @{ Pattern = '$primaryColor = "#F77F00" #'; Value = "`$primaryColor = `"$cfg_primaryColor`" #" }
- @{ Pattern = '$logoUrl = "" #'; Value = "`$logoUrl = `"$cfg_logoUrl`" #" }
+ @{ Pattern = '$webhookUrl = "" #'; Value = "`$webhookUrl = $(Format-SingleQuoted $cfg_webhookUrl) #" }
+ @{ Pattern = '$companyName = "CyberDrain" #'; Value = "`$companyName = $(Format-SingleQuoted $cfg_companyName) #" }
+ @{ Pattern = '$productName = "Check - Phishing Protection" #'; Value = "`$productName = $(Format-SingleQuoted $cfg_productName) #" }
+ @{ Pattern = '$supportEmail = "" #'; Value = "`$supportEmail = $(Format-SingleQuoted $cfg_supportEmail) #" }
+ @{ Pattern = '$supportUrl = "" #'; Value = "`$supportUrl = $(Format-SingleQuoted $cfg_supportUrl) #" }
+ @{ Pattern = '$privacyPolicyUrl = "" #'; Value = "`$privacyPolicyUrl = $(Format-SingleQuoted $cfg_privacyPolicyUrl) #" }
+ @{ Pattern = '$aboutUrl = "" #'; Value = "`$aboutUrl = $(Format-SingleQuoted $cfg_aboutUrl) #" }
+ @{ Pattern = '$primaryColor = "#F77F00" #'; Value = "`$primaryColor = $(Format-SingleQuoted $cfg_primaryColor) #" }
+ @{ Pattern = '$logoUrl = "" #'; Value = "`$logoUrl = $(Format-SingleQuoted $cfg_logoUrl) #" }
)
# Array replacements — replace the full assignment line including inline comment
From a969e4ea75cce52a069aee19556eba4360c7cdb7 Mon Sep 17 00:00:00 2001
From: Brian Simpson <50429915+bmsimp@users.noreply.github.com>
Date: Fri, 24 Apr 2026 10:43:27 -0500
Subject: [PATCH 10/20] fix: coordinated toolbar pin drift detection
Deploy script previously only wrote the toolbar property when
$forceToolbarPin = 1. Flipping the setting from 1 to 0 left a stale
toolbar_pin=force_pinned / toolbar_state=force_shown on devices,
which Detect couldn't reliably catch without causing an infinite
redeploy loop.
Deploy: always write the toolbar property -- pinned values when =1,
default_unpinned / hidden when =0. Matches the CIPP Intune standard.
Detect: add ToolbarPinnedValue / ToolbarUnpinnedValue to the $browsers
array and always verify the toolbar property against the expected
value based on $forceToolbarPin.
Addresses Copilot review comment on toolbar pin drift.
---
enterprise/Deploy-Windows-Chrome-and-Edge.ps1 | 15 ++++++++-------
enterprise/Detect-Windows-Chrome-and-Edge.ps1 | 17 +++++++++--------
2 files changed, 17 insertions(+), 15 deletions(-)
diff --git a/enterprise/Deploy-Windows-Chrome-and-Edge.ps1 b/enterprise/Deploy-Windows-Chrome-and-Edge.ps1
index af90153c..0ba37eaa 100644
--- a/enterprise/Deploy-Windows-Chrome-and-Edge.ps1
+++ b/enterprise/Deploy-Windows-Chrome-and-Edge.ps1
@@ -143,14 +143,15 @@ function Configure-ExtensionSettings {
New-ItemProperty -Path $ExtensionSettingsKey -Name "installation_mode" -PropertyType String -Value $installationMode -Force | Out-Null
New-ItemProperty -Path $ExtensionSettingsKey -Name "update_url" -PropertyType String -Value $UpdateUrl -Force | Out-Null
- # Add toolbar pinning if enabled
- if ($forceToolbarPin -eq 1) {
- if ($ExtensionId -eq $edgeExtensionId) {
- New-ItemProperty -Path $ExtensionSettingsKey -Name "toolbar_state" -PropertyType String -Value "force_shown" -Force | Out-Null
- } elseif ($ExtensionId -eq $chromeExtensionId) {
- New-ItemProperty -Path $ExtensionSettingsKey -Name "toolbar_pin" -PropertyType String -Value "force_pinned" -Force | Out-Null
- }
+ # Toolbar pinning - always write a value so detection can verify either state
+ if ($ExtensionId -eq $edgeExtensionId) {
+ $toolbarProp = "toolbar_state"
+ $toolbarValue = if ($forceToolbarPin -eq 1) { "force_shown" } else { "hidden" }
+ } elseif ($ExtensionId -eq $chromeExtensionId) {
+ $toolbarProp = "toolbar_pin"
+ $toolbarValue = if ($forceToolbarPin -eq 1) { "force_pinned" } else { "default_unpinned" }
}
+ New-ItemProperty -Path $ExtensionSettingsKey -Name $toolbarProp -PropertyType String -Value $toolbarValue -Force | Out-Null
Write-Output "Configured extension settings for $ExtensionId"
}
diff --git a/enterprise/Detect-Windows-Chrome-and-Edge.ps1 b/enterprise/Detect-Windows-Chrome-and-Edge.ps1
index 9cd31b9d..49003479 100644
--- a/enterprise/Detect-Windows-Chrome-and-Edge.ps1
+++ b/enterprise/Detect-Windows-Chrome-and-Edge.ps1
@@ -71,8 +71,9 @@ $browsers = @(
UpdateUrl = $chromeUpdateUrl
ManagedStorageKey = $chromeManagedStorageKey
ExtensionSettingsKey = $chromeExtensionSettingsKey
- ToolbarProp = 'toolbar_pin'
- ToolbarValue = 'force_pinned'
+ ToolbarProp = 'toolbar_pin'
+ ToolbarPinnedValue = 'force_pinned'
+ ToolbarUnpinnedValue = 'default_unpinned'
},
@{
Name = 'Edge'
@@ -80,8 +81,9 @@ $browsers = @(
UpdateUrl = $edgeUpdateUrl
ManagedStorageKey = $edgeManagedStorageKey
ExtensionSettingsKey = $edgeExtensionSettingsKey
- ToolbarProp = 'toolbar_state'
- ToolbarValue = 'force_shown'
+ ToolbarProp = 'toolbar_state'
+ ToolbarPinnedValue = 'force_shown'
+ ToolbarUnpinnedValue = 'hidden'
}
)
@@ -214,10 +216,9 @@ foreach ($browser in $browsers) {
if (!(Test-RegValue $browser.ExtensionSettingsKey 'installation_mode' $installationMode)) { exit 1 }
if (!(Test-RegValue $browser.ExtensionSettingsKey 'update_url' $browser.UpdateUrl)) { exit 1 }
- # 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 - always verified; deploy script writes either pinned or unpinned value based on $forceToolbarPin
+ $expectedToolbar = if ($forceToolbarPin -eq 1) { $browser.ToolbarPinnedValue } else { $browser.ToolbarUnpinnedValue }
+ if (!(Test-RegValueWithDetails $browser.Name $browser.ExtensionSettingsKey $browser.ToolbarProp $expectedToolbar)) { exit 1 }
}
Write-Output "Check extension is correctly configured for Chrome and Edge."
From 6a8f614b614029aff64e8650b6b39c1dcf1a2d45 Mon Sep 17 00:00:00 2001
From: Brian Simpson <50429915+bmsimp@users.noreply.github.com>
Date: Fri, 24 Apr 2026 10:57:06 -0500
Subject: [PATCH 11/20] fix: validate template replacements + consistent
detection diagnostics
Setup: Apply-Replacements now tracks which patterns were found and
throws a detailed error listing any missing patterns. Previously a
silent miss (e.g., upstream comment reworded) would produce an output
script stuck on defaults. Callers now pass -TemplateName so the error
identifies which template failed.
Detect: convert all remaining bare Test-RegValue calls and bare exit 1
paths to use Test-RegValueWithDetails / Write-DetectionFailure so every
failure mode emits a specific reason on stdout. Affects customBranding,
genericWebhook, genericWebhook/events (incl. count mismatch and
"unexpected events" branches), urlAllowlist (same), and
ExtensionSettings. Admins running the script manually now see exactly
which check failed.
Addresses Copilot review comments on silent replacement failures and
missing diagnostic output.
---
enterprise/Detect-Windows-Chrome-and-Edge.ps1 | 68 +++++++++++++------
enterprise/Setup-Windows-Chrome-and-Edge.ps1 | 28 ++++++--
2 files changed, 69 insertions(+), 27 deletions(-)
diff --git a/enterprise/Detect-Windows-Chrome-and-Edge.ps1 b/enterprise/Detect-Windows-Chrome-and-Edge.ps1
index 49003479..1b95c7dc 100644
--- a/enterprise/Detect-Windows-Chrome-and-Edge.ps1
+++ b/enterprise/Detect-Windows-Chrome-and-Edge.ps1
@@ -168,53 +168,77 @@ foreach ($browser in $browsers) {
Write-DetectionFailure -BrowserName $browser.Name -KeyPath $brandingKey -ValueName $null -ExpectedValue $null -ActualValue $null
exit 1
}
- if (!(Test-RegValue $brandingKey 'companyName' $companyName)) { exit 1 }
- if (!(Test-RegValue $brandingKey 'productName' $productName)) { exit 1 }
- if (!(Test-RegValue $brandingKey 'supportEmail' $supportEmail)) { exit 1 }
- if (!(Test-RegValue $brandingKey 'supportUrl' $supportUrl)) { exit 1 }
- if (!(Test-RegValue $brandingKey 'privacyPolicyUrl' $privacyPolicyUrl)) { exit 1 }
- if (!(Test-RegValue $brandingKey 'aboutUrl' $aboutUrl)) { exit 1 }
- if (!(Test-RegValue $brandingKey 'primaryColor' $primaryColor)) { exit 1 }
- if (!(Test-RegValue $brandingKey 'logoUrl' $logoUrl)) { exit 1 }
+ if (!(Test-RegValueWithDetails $browser.Name $brandingKey 'companyName' $companyName)) { exit 1 }
+ if (!(Test-RegValueWithDetails $browser.Name $brandingKey 'productName' $productName)) { exit 1 }
+ if (!(Test-RegValueWithDetails $browser.Name $brandingKey 'supportEmail' $supportEmail)) { exit 1 }
+ if (!(Test-RegValueWithDetails $browser.Name $brandingKey 'supportUrl' $supportUrl)) { exit 1 }
+ if (!(Test-RegValueWithDetails $browser.Name $brandingKey 'privacyPolicyUrl' $privacyPolicyUrl)) { exit 1 }
+ if (!(Test-RegValueWithDetails $browser.Name $brandingKey 'aboutUrl' $aboutUrl)) { exit 1 }
+ if (!(Test-RegValueWithDetails $browser.Name $brandingKey 'primaryColor' $primaryColor)) { exit 1 }
+ if (!(Test-RegValueWithDetails $browser.Name $brandingKey 'logoUrl' $logoUrl)) { exit 1 }
# genericWebhook subkey
$webhookKey = "$policyKey\genericWebhook"
- if (!(Test-Path $webhookKey)) { exit 1 }
- if (!(Test-RegValue $webhookKey 'enabled' $enableGenericWebhook)) { exit 1 }
- if (!(Test-RegValue $webhookKey 'url' $webhookUrl)) { exit 1 }
+ if (!(Test-Path $webhookKey)) {
+ Write-DetectionFailure -BrowserName $browser.Name -KeyPath $webhookKey -ValueName $null -ExpectedValue $null -ActualValue $null
+ exit 1
+ }
+ if (!(Test-RegValueWithDetails $browser.Name $webhookKey 'enabled' $enableGenericWebhook)) { exit 1 }
+ if (!(Test-RegValueWithDetails $browser.Name $webhookKey 'url' $webhookUrl)) { exit 1 }
# genericWebhook\events subkey — verify exact count and values
$eventsKey = "$webhookKey\events"
- if (!(Test-Path $eventsKey)) { exit 1 }
+ if (!(Test-Path $eventsKey)) {
+ Write-DetectionFailure -BrowserName $browser.Name -KeyPath $eventsKey -ValueName $null -ExpectedValue $null -ActualValue $null
+ exit 1
+ }
if ($webhookEvents.Count -gt 0) {
$eventsCount = (Get-Item $eventsKey).Property.Count
- if ($eventsCount -ne $webhookEvents.Count) { exit 1 }
+ if ($eventsCount -ne $webhookEvents.Count) {
+ Write-Output "$($browser.Name): Registry key '$eventsKey' has $eventsCount event value(s); expected $($webhookEvents.Count)."
+ exit 1
+ }
for ($i = 0; $i -lt $webhookEvents.Count; $i++) {
- if (!(Test-RegValue $eventsKey ($i + 1).ToString() $webhookEvents[$i])) { exit 1 }
+ if (!(Test-RegValueWithDetails $browser.Name $eventsKey ($i + 1).ToString() $webhookEvents[$i])) { exit 1 }
}
} else {
$existingEvents = (Get-Item $eventsKey).Property
- if ($null -ne $existingEvents -and $existingEvents.Count -gt 0) { exit 1 }
+ if ($null -ne $existingEvents -and $existingEvents.Count -gt 0) {
+ Write-Output "$($browser.Name): Registry key '$eventsKey' has unexpected event value(s); expected none."
+ exit 1
+ }
}
# urlAllowlist subkey — verify exact count and values
$allowlistKey = "$policyKey\urlAllowlist"
- if (!(Test-Path $allowlistKey)) { exit 1 }
+ if (!(Test-Path $allowlistKey)) {
+ Write-DetectionFailure -BrowserName $browser.Name -KeyPath $allowlistKey -ValueName $null -ExpectedValue $null -ActualValue $null
+ exit 1
+ }
if ($urlAllowlist.Count -gt 0) {
$allowlistCount = (Get-Item $allowlistKey).Property.Count
- if ($allowlistCount -ne $urlAllowlist.Count) { exit 1 }
+ if ($allowlistCount -ne $urlAllowlist.Count) {
+ Write-Output "$($browser.Name): Registry key '$allowlistKey' has $allowlistCount allowlist value(s); expected $($urlAllowlist.Count)."
+ exit 1
+ }
for ($i = 0; $i -lt $urlAllowlist.Count; $i++) {
- if (!(Test-RegValue $allowlistKey ($i + 1).ToString() $urlAllowlist[$i])) { exit 1 }
+ if (!(Test-RegValueWithDetails $browser.Name $allowlistKey ($i + 1).ToString() $urlAllowlist[$i])) { exit 1 }
}
} else {
$existingAllowlist = (Get-Item $allowlistKey).Property
- if ($null -ne $existingAllowlist -and $existingAllowlist.Count -gt 0) { exit 1 }
+ if ($null -ne $existingAllowlist -and $existingAllowlist.Count -gt 0) {
+ Write-Output "$($browser.Name): Registry key '$allowlistKey' has unexpected allowlist value(s); expected none."
+ exit 1
+ }
}
# ExtensionSettings key
- if (!(Test-Path $browser.ExtensionSettingsKey)) { exit 1 }
- if (!(Test-RegValue $browser.ExtensionSettingsKey 'installation_mode' $installationMode)) { exit 1 }
- if (!(Test-RegValue $browser.ExtensionSettingsKey 'update_url' $browser.UpdateUrl)) { exit 1 }
+ if (!(Test-Path $browser.ExtensionSettingsKey)) {
+ Write-DetectionFailure -BrowserName $browser.Name -KeyPath $browser.ExtensionSettingsKey -ValueName $null -ExpectedValue $null -ActualValue $null
+ exit 1
+ }
+ if (!(Test-RegValueWithDetails $browser.Name $browser.ExtensionSettingsKey 'installation_mode' $installationMode)) { exit 1 }
+ if (!(Test-RegValueWithDetails $browser.Name $browser.ExtensionSettingsKey 'update_url' $browser.UpdateUrl)) { exit 1 }
# Toolbar pin - always verified; deploy script writes either pinned or unpinned value based on $forceToolbarPin
$expectedToolbar = if ($forceToolbarPin -eq 1) { $browser.ToolbarPinnedValue } else { $browser.ToolbarUnpinnedValue }
diff --git a/enterprise/Setup-Windows-Chrome-and-Edge.ps1 b/enterprise/Setup-Windows-Chrome-and-Edge.ps1
index df4f5396..ea88f61d 100644
--- a/enterprise/Setup-Windows-Chrome-and-Edge.ps1
+++ b/enterprise/Setup-Windows-Chrome-and-Edge.ps1
@@ -252,26 +252,44 @@ $arrayReplacements = @(
#######################################################################
function Apply-Replacements {
- param ([string]$Content)
+ param (
+ [string]$Content,
+ [string]$TemplateName
+ )
+
+ $missing = [System.Collections.Generic.List[string]]::new()
foreach ($r in $replacements) {
- $Content = $Content.Replace($r.Pattern, $r.Value)
+ if ($Content.Contains($r.Pattern)) {
+ $Content = $Content.Replace($r.Pattern, $r.Value)
+ } else {
+ $missing.Add($r.Pattern)
+ }
}
foreach ($r in $arrayReplacements) {
- $Content = $Content.Replace($r.Pattern, $r.Value)
+ if ($Content.Contains($r.Pattern)) {
+ $Content = $Content.Replace($r.Pattern, $r.Value)
+ } else {
+ $missing.Add($r.Pattern)
+ }
+ }
+
+ if ($missing.Count -gt 0) {
+ $list = ($missing | ForEach-Object { " - $_" }) -join "`n"
+ throw "Failed to customize the $TemplateName template; the following expected pattern(s) were not found:`n$list`nThe upstream template format may have changed."
}
return $Content
}
# Deploy script — apply all replacements
-$deployContent = Apply-Replacements -Content $templates['Deploy']
+$deployContent = Apply-Replacements -Content $templates['Deploy'] -TemplateName 'Deploy'
$deployPath = Join-Path $outputPath $scripts['Deploy'].FileName
Set-Content -Path $deployPath -Value $deployContent -Encoding UTF8
Write-Host "Written: $deployPath" -ForegroundColor Green
# Detect script — apply all replacements (same config block format)
-$detectContent = Apply-Replacements -Content $templates['Detect']
+$detectContent = Apply-Replacements -Content $templates['Detect'] -TemplateName 'Detect'
$detectPath = Join-Path $outputPath $scripts['Detect'].FileName
Set-Content -Path $detectPath -Value $detectContent -Encoding UTF8
Write-Host "Written: $detectPath" -ForegroundColor Green
From cbf49b396832a417c335a48ee5989e0d79de0771 Mon Sep 17 00:00:00 2001
From: Brian Simpson
Date: Fri, 24 Apr 2026 16:07:32 +0000
Subject: [PATCH 12/20] GITBOOK-72: CIPP Standard Deployment
---
.../windows/domain-deployment.md | 96 ++++++++++---------
1 file changed, 51 insertions(+), 45 deletions(-)
diff --git a/docs/deployment/chrome-edge-deployment-instructions/windows/domain-deployment.md b/docs/deployment/chrome-edge-deployment-instructions/windows/domain-deployment.md
index 0f686792..ac600b7a 100644
--- a/docs/deployment/chrome-edge-deployment-instructions/windows/domain-deployment.md
+++ b/docs/deployment/chrome-edge-deployment-instructions/windows/domain-deployment.md
@@ -6,7 +6,7 @@ The simplest method of Intune deployment is via a win32 script. Follow the steps
***
-## Setup Script
+### Setup Script
1. Download a copy of the Setup-Windows-Chrome-and-Edge.ps1 script from the Check repository on GitHub using the button below.
@@ -19,18 +19,20 @@ The simplest method of Intune deployment is via a win32 script. Follow the steps
3. You will be prompted during the Setup script on how you want to configure Check. Follow the script's guidance to ensure you're accurately entering values for the script. These values will be used for both the Deploy and Detect to ensure the extension is properly deployed.
4. Set the output location the script will use to generate the three new scripts.
-{% hint style="info" %} You can also download the three scripts directly from the Check GitHub repo and edit the configuration settings manually. {% endhint %}
+{% hint style="info" %}
+You can also download the three scripts directly from the Check GitHub repo and edit the configuration settings manually.
+{% endhint %}
***
-## Adding to Intune
+### Adding to Intune
-### Prerequisites
+#### Prerequisites
-- Microsoft Intune admin access
-- The [Microsoft Win32 Content Prep Tool](https://github.com/microsoft/Microsoft-Win32-Content-Prep-Tool) (`IntuneWinAppUtil.exe`) to package scripts as `.intunewin` files
+* Microsoft Intune admin access
+* The [Microsoft Win32 Content Prep Tool](https://github.com/microsoft/Microsoft-Win32-Content-Prep-Tool) (`IntuneWinAppUtil.exe`) to package scripts as `.intunewin` files
-### Step 1: Package the Scripts
+#### Step 1: Package the Scripts
Intune Win32 apps require an `.intunewin` package. Place your three configured scripts in a folder, then run:
@@ -40,61 +42,60 @@ Intune Win32 apps require an `.intunewin` package. Place your three configured s
This creates `Deploy-Windows-Chrome-and-Edge.intunewin`.
-### Step 2: Create the Win32 App in Intune
+#### Step 2: Create the Win32 App in Intune
1. Open the [Microsoft Intune admin center](https://intune.microsoft.com)
2. Navigate to **Apps** > **Windows**
3. Click **Add** > Select **Windows app (Win32)** > **Select**
4. Upload the `.intunewin` file created in Step 1
-### Step 3: Configure App Information
+#### Step 3: Configure App Information
-| Field | Value |
-|-------|-------|
-| Name | `Check by CyberDrain - Browser Extension` |
+| Field | Value |
+| ----------- | ------------------------------------------------------------------------------------------------------------ |
+| Name | `Check by CyberDrain - Browser Extension` |
| Description | `Deploys and configures the Check by CyberDrain phishing protection extension for Chrome and Edge browsers.` |
-| Publisher | Your company name or `CyberDrain` |
+| Publisher | Your company name or `CyberDrain` |
-### Step 4: Configure Program Settings
+#### Step 4: Configure Program Settings
-| Field | Value |
-|-------|-------|
-| Install command | `powershell.exe -ExecutionPolicy Bypass -File Deploy-Windows-Chrome-and-Edge.ps1` |
-| Uninstall command | `powershell.exe -ExecutionPolicy Bypass -File Remove-Windows-Chrome-and-Edge.ps1` |
-| Install behavior | **System** |
-| Device restart behavior | **No specific action** |
+| Field | Value |
+| ----------------------- | --------------------------------------------------------------------------------- |
+| Install command | `powershell.exe -ExecutionPolicy Bypass -File Deploy-Windows-Chrome-and-Edge.ps1` |
+| Uninstall command | `powershell.exe -ExecutionPolicy Bypass -File Remove-Windows-Chrome-and-Edge.ps1` |
+| Install behavior | **System** |
+| Device restart behavior | **No specific action** |
-### Step 5: Configure Requirements
+#### Step 5: Configure Requirements
-| Field | Value |
-|-------|-------|
-| Operating system architecture | **64-bit** |
-| Minimum operating system | **Windows 10 1607** (or your minimum supported version) |
+| Field | Value |
+| ----------------------------- | ------------------------------------------------------- |
+| Operating system architecture | **64-bit** |
+| Minimum operating system | **Windows 10 1607** (or your minimum supported version) |
-### Step 6: Configure Detection Rules
+#### Step 6: Configure Detection Rules
1. Under **Detection rules**, select **Use a custom detection script**
2. Upload `Detect-Windows-Chrome-and-Edge.ps1`
3. Set the following:
-| Field | Value |
-|-------|-------|
+| Field | Value |
+| ---------------------------------------------- | ------ |
| Run script as 32-bit process on 64-bit clients | **No** |
-| Enforce script signature check | **No** |
+| Enforce script signature check | **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.
-The detection script checks that all registry keys written by the install script exist and have the correct values. It exits with code `0` when everything matches (app detected) and code `1` when any value is missing or wrong (app not detected, triggers reinstall).
+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. The detection script checks that all registry keys written by the install script exist and have the correct values. It exits with code `0` when everything matches (app detected) and code `1` when any value is missing or wrong (app not detected, triggers reinstall).
-### Step 7: Assign the App
+#### Step 7: Assign the App
1. Under **Assignments**, click **Add group** under **Required**
2. Choose your target:
- - **All devices** — deploys to every Intune-managed Windows device
- - **All users** — deploys to devices used by any licensed user
- - **Select groups** — deploy to specific Azure AD / Entra ID groups
+ * **All devices** — deploys to every Intune-managed Windows device
+ * **All users** — deploys to devices used by any licensed user
+ * **Select groups** — deploy to specific Azure AD / Entra ID groups
3. Click **Review + create** > **Create**
-## Updating Settings
+### Updating Settings
When you need to change extension settings (e.g., enable page blocking, update branding):
@@ -104,23 +105,21 @@ When you need to change extension settings (e.g., enable page blocking, update b
Because the detection script body changes when settings change, Intune will detect the app as "not installed" on endpoints and automatically redeploy with the updated configuration.
-## Uninstalling
+### Uninstalling
To remove the extension from managed devices:
-- **Option A:** In Intune, change the app assignment from **Required** to **Uninstall**. Intune will run the `Remove-Windows-Chrome-and-Edge.ps1` script on targeted devices.
-- **Option B:** Delete the app from Intune entirely. Note that this stops management but does not actively remove the registry keys from devices that already have them.
+* **Option A:** In Intune, change the app assignment from **Required** to **Uninstall**. Intune will run the `Remove-Windows-Chrome-and-Edge.ps1` script on targeted devices.
+* **Option B:** Delete the app from Intune entirely. Note that this stops management but does not actively remove the registry keys from devices that already have them.
-## Troubleshooting
+### Troubleshooting
-- **Extension not appearing after deployment:** Check that the install script ran as System (not User). Verify registry keys exist under `HKLM:\SOFTWARE\Policies\Google\Chrome\ExtensionSettings\` and `HKLM:\SOFTWARE\Policies\Microsoft\Edge\ExtensionSettings\`.
-- **Intune keeps reinstalling the app:** The detection script values don't match what the install script wrote. Ensure both scripts have identical configuration values.
-- **Detection script shows as failed:** Run the detection script manually on a test device as Administrator to see which check fails (it will exit at the first mismatch).
+* **Extension not appearing after deployment:** Check that the install script ran as System (not User). Verify registry keys exist under `HKLM:\SOFTWARE\Policies\Google\Chrome\ExtensionSettings\` and `HKLM:\SOFTWARE\Policies\Microsoft\Edge\ExtensionSettings\`.
+* **Intune keeps reinstalling the app:** The detection script values don't match what the install script wrote. Ensure both scripts have identical configuration values.
+* **Detection script shows as failed:** Run the detection script manually on a test device as Administrator to see which check fails (it will exit at the first mismatch).
{% endtab %}
{% tab title="Group Policy" %}
-
-
1. Download the following from the Check repo on GitHub
1. [Deploy-ADMX.ps1](https://github.com/CyberDrain/Check/blob/main/enterprise/Deploy-ADMX.ps1)
2. [Check-Extension.admx](https://github.com/CyberDrain/Check/blob/main/enterprise/admx/Check-Extension.admx)
@@ -130,4 +129,11 @@ To remove the extension from managed devices:
.png>)
{% endtab %}
+
+{% tab title="CIPP Standard" %}
+You can use a CIPP standard to deploy Check. It works the same way that the [#intune](domain-deployment.md#intune "mention") instructions do but CIPP handles all the work for install and detection script building.
+
+For more, see our [Standards documentation](https://standards.cipp.app/standards/deploycheckchromeextension).
+{% endtab %}
{% endtabs %}
+
From 77d51d5a5bf81ce360e58544eb9503c8ae63cf7c Mon Sep 17 00:00:00 2001
From: John Duprey
Date: Wed, 29 Apr 2026 18:58:54 -0400
Subject: [PATCH 13/20] feat: Add PR validation GitHub Actions workflow
Introduce .github/workflows/validate-pr.yml to run automated PR checks on dev and main. The workflow includes jobs for linting (ESLint if config present), CodeQL security analysis, JSON manifest and config validation, Conventional Commits PR title validation, and verification reminders when key files (config/ or manifest) change. This centralizes PR gating and developer guidance for releases and security checks.
---
.github/workflows/validate-pr.yml | 113 ++++++++++++++++++++++++++++++
1 file changed, 113 insertions(+)
create mode 100644 .github/workflows/validate-pr.yml
diff --git a/.github/workflows/validate-pr.yml b/.github/workflows/validate-pr.yml
new file mode 100644
index 00000000..e36b9996
--- /dev/null
+++ b/.github/workflows/validate-pr.yml
@@ -0,0 +1,113 @@
+name: Validate PR
+
+on:
+ pull_request:
+ types: [opened, synchronize, reopened]
+ branches:
+ - dev
+ - main
+
+jobs:
+ lint:
+ name: Lint
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+
+ - name: Lint JavaScript
+ run: |
+ if [ -f "eslint.config.js" ] || [ -f ".eslintrc.json" ] || [ -f ".eslintrc.js" ]; then
+ npx eslint scripts options popup --max-warnings 0
+ else
+ echo "ℹ️ No ESLint configuration found. Skipping linting."
+ echo "To enable: Create eslint.config.js or .eslintrc.json at repo root"
+ fi
+
+ codeql:
+ name: CodeQL Security Analysis
+ runs-on: ubuntu-latest
+ permissions:
+ security-events: write
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v3
+ with:
+ languages: 'javascript'
+
+ - name: Autobuild
+ uses: github/codeql-action/autobuild@v3
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v3
+
+ validate-json:
+ name: Validate JSON Files
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Validate manifest.json
+ run: |
+ node -e "require('fs').readFileSync('manifest.json'); console.log('✓ manifest.json is valid JSON')"
+
+ - name: Validate Firefox manifest
+ run: |
+ node -e "require('fs').readFileSync('manifest.firefox.json'); console.log('✓ manifest.firefox.json is valid JSON')"
+
+ - name: Validate config files
+ run: |
+ for file in config/*.json rules/*.json; do
+ if [ -f "$file" ]; then
+ node -e "require('fs').readFileSync('$file'); console.log('✓ $file is valid JSON')" || exit 1
+ fi
+ done
+
+ conventional-commits:
+ name: Validate Conventional Commits
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Validate PR title
+ run: |
+ PR_TITLE="${{ github.event.pull_request.title }}"
+ if ! echo "$PR_TITLE" | grep -E "^(feat|fix|docs|style|refactor|perf|test|chore|ci)(\(.+\))?!?: .+" > /dev/null; then
+ echo "❌ PR title does not follow Conventional Commits format"
+ echo "Expected format: feat|fix|docs|style|refactor|perf|test|chore|ci(scope)?: description"
+ echo "Received: $PR_TITLE"
+ exit 1
+ fi
+ echo "✓ PR title follows Conventional Commits format"
+
+ verify-changes:
+ name: Verify File Changes
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Check for deployment configuration updates
+ run: |
+ # If config/branding.json or config/managed_schema.json changed
+ if git diff origin/main HEAD --name-only | grep -q "^config/"; then
+ echo "⚠️ Configuration files were modified"
+ echo "Reminder: Review enterprise/Deploy-Windows-Chrome-and-Edge.ps1 and enterprise/admx/Check-Extension.admx"
+ fi
+
+ # If manifest changed
+ if git diff origin/main HEAD --name-only | grep -qE "manifest.*\.json"; then
+ echo "ℹ️ Manifest was modified"
+ echo "Reminder: Test in both Chrome and Firefox"
+ fi
From 4556b6dea6da23e84c9c605091ba232df5e2426f Mon Sep 17 00:00:00 2001
From: josh-ricketts-enable
<91432846+josh-ricketts-enable@users.noreply.github.com>
Date: Wed, 20 May 2026 11:51:53 -0500
Subject: [PATCH 14/20] Update macos.md
An example of some deployment methods. If more screenshots or more official language is needed I'd be happy to assist in any way I can!
Signed-off-by: josh-ricketts-enable <91432846+josh-ricketts-enable@users.noreply.github.com>
---
.../macos.md | 128 ++++++++++++++++++
1 file changed, 128 insertions(+)
diff --git a/docs/deployment/chrome-edge-deployment-instructions/macos.md b/docs/deployment/chrome-edge-deployment-instructions/macos.md
index 0b65f0cc..623f46aa 100644
--- a/docs/deployment/chrome-edge-deployment-instructions/macos.md
+++ b/docs/deployment/chrome-edge-deployment-instructions/macos.md
@@ -3,5 +3,133 @@ icon: apple
---
# MacOS
+I'd recommend that this be deployed via your MDM if the goal is to auto-deploy it without user interaction.
+A custom .mobileconfig file can be uploaded to most MDMs for deployment if they don't have their own Google Chrome, or Microsoft Edge profile building functionality baked-in.
+
+Here's an example profile of the XML to create a mobileconfig that will install this in Microsoft Edge and Google Chrome.
+
+```
+
+
+
+
+ PayloadContent
+
+
+ ExtensionInstallForcelist
+
+ benimdeioplgkhanklclahllklceahbe
+
+ PayloadDisplayName
+ Google Chrome
+ PayloadIdentifier
+ com.google.Chrome.23E5DDCF-1EB2-4869-9510-5E47D6640A85
+ PayloadType
+ com.google.Chrome
+ PayloadUUID
+ 23E5DDCF-1EB2-4869-9510-5E47D6640A85
+ PayloadVersion
+ 1
+
+
+ ExtensionInstallForcelist
+
+ knepjpocdagponkonnbggpcnhnaikajg
+
+ PayloadDisplayName
+ Microsoft Edge
+ PayloadIdentifier
+ com.microsoft.Edge.DD4A940A-B216-4D5E-8B2C-1EF2CAFF7F38
+ PayloadType
+ com.microsoft.Edge
+ PayloadUUID
+ DD4A940A-B216-4D5E-8B2C-1EF2CAFF7F38
+ PayloadVersion
+ 1
+
+
+ PayloadDescription
+ This profile installs and enforces the 'Check' browser extension from CyberDrain on Google Chrome and Microsoft Edge web browsers.
+ PayloadDisplayName
+ Check CyberDrain
+ PayloadIdentifier
+ 020D4Z7P-7F1A-4723-89CB-1826F8BAF4B5
+ PayloadOrganization
+ YOUR ORG NAME
+ PayloadScope
+ System
+ PayloadType
+ Configuration
+ PayloadUUID
+ 020D4Z7P-7F1A-4723-89CB-1826F8BAF4B5
+ PayloadVersion
+ 1
+ RemovalDate
+ 2044-05-19T21:46:44Z
+ TargetDeviceType
+ 5
+
+
+```
+You could also deploy it in Chrome via command-line by creating the proper JSON object in the correct directory in the core /Library directory in macOS. Credit to @cezaraugusto for the script (slightly modified to simply install 'Check' if no parameter is passed...though technically you could pass any other Chrome extension ID after the script path and it would install that extension).
+
+```
+#!/bin/bash
+
+# https://developer.chrome.com/docs/extensions/mv3/external_extensions/#preferences
+# Credit to #cezaraugusto# from GithubGist for this script...slightly modified for the purposes of installing Check by Cyberdrain if no parameter is passed
+# https://gist.github.com/cezaraugusto
+# https://gist.github.com/cezaraugusto/0101d2cb251c088f398ca0f8d4495ca0
+
+extension=$1
+
+if [[ -z "$extension" ]]; then
+ extension="benimdeioplgkhanklclahllklceahbe"
+fi
+install_chrome_extension() {
+ chrome_extensions_folder="/Library/Application Support/Google/Chrome/External Extensions"
+ chrome_extensions_preferences_file="$chrome_extensions_folder/$extension.json"
+ # This URL is used by Chrome to check for updates to external extensions
+ update_services_url="https://clients2.google.com/service/update2/crx"
+
+if [[ -d "$chrome_extensions_folder" ]]; then
+ mkdir -p "$chrome_extensions_folder"
+fi
+
+ echo "{" > "$chrome_extensions_preferences_file"
+ echo " \"external_update_url\": \"$update_services_url\"" >> "$chrome_extensions_preferences_file"
+ echo "}" >> "$chrome_extensions_preferences_file"
+
+ echo "Added \"$chrome_extensions_preferences_file\""
+}
+
+if [ $# -ne 1 ]; then
+ echo "Usage: $0 "
+ exit 1
+fi
+
+install_chrome_extension "$extension"
+
+# Usage:
+# ./install_extension.sh
+# Sample: adding React Dev Tools from command-line to Chrome
+# ./install_extension.sh fmkadmapgofadopljbjfkapdkoienihi
+```
+
+This would not install the extension until the next time Chrome is launched, and then it will require the user to approve it.
+
+
+
+
+
+Due to limitations like this it really would be better to push it via an MDM.
+
+
+### I don't know if this is the kind of feedback, or instruction you're looking for with macOS. If it IS, and you just need more screenshots, better examples, and more official language I'd be happy to assist with that.
+
+
+
+
+### Original message below
Coming soon. If you have experience deploying managed MacOS browser extensions, please contribute to the [docs via GitHub](https://github.com/CyberDrain/Check/tree/dev/docs). All Mac resources in the GitHub repo should be considered inaccurate until tested.
From 8fb6e89343741eb0c5123a63270ab03335e5f995 Mon Sep 17 00:00:00 2001
From: Zacgoose <107489668+Zacgoose@users.noreply.github.com>
Date: Mon, 8 Jun 2026 20:35:43 +0800
Subject: [PATCH 15/20] better atim detection
---
rules/detection-rules.json | 35 +++++++++++++++++++++++--
scripts/content.js | 52 ++++++++++++++++++++++++++++++++++++++
2 files changed, 85 insertions(+), 2 deletions(-)
diff --git a/rules/detection-rules.json b/rules/detection-rules.json
index a40b6603..4cf45fe2 100644
--- a/rules/detection-rules.json
+++ b/rules/detection-rules.json
@@ -1,6 +1,6 @@
{
- "version": "1.2.0",
- "lastUpdated": "2026-04-07T00:00:00Z",
+ "version": "1.2.1",
+ "lastUpdated": "2026-06-08T00:00:00Z",
"description": "Phishing detection logic for identifying phishing attempts targeting Microsoft 365 login pages",
"trusted_login_patterns": [
"^https:\\/\\/login\\.microsoftonline\\.(com|us)$",
@@ -380,6 +380,27 @@
},
"action": "block",
"severity": "critical"
+ },
+ {
+ "id": "aitm_microsoft_markers_off_origin",
+ "type": "aitm_origin_validation",
+ "description": "Block when Microsoft 365 login source markers (idPartnerPL, loginfmt, urlMsaSignUp, flowToken, MS auth CDN refs) appear on a non-Microsoft origin with any credential input present. Catches adversary-in-the-middle (Evilginx/Tycoon-2FA) reverse-proxy pages whose credential capture is JS-driven and bypasses form_post_not_microsoft.",
+ "condition": {
+ "microsoft_source_markers": [
+ "idPartnerPL",
+ "loginfmt",
+ "urlMsaSignUp",
+ "flowToken",
+ "aadcdn\\.msauth\\.net",
+ "aadcdn\\.msftauth\\.net",
+ "aadcdn\\.msftauthimages\\.net"
+ ],
+ "minimum_marker_matches": 3,
+ "require_credential_input": true,
+ "require_non_trusted_host": true
+ },
+ "action": "block",
+ "severity": "critical"
}
],
"allow_rules": [
@@ -1567,6 +1588,16 @@
"action": "warn",
"category": "text_obfuscation",
"confidence": 0.9
+ },
+ {
+ "id": "phi_034_wordlist_path_kit",
+ "pattern": "^https?:\\/\\/[^/]+\\/(?:[a-z]{3,12}_){8,}[a-z]{3,12}_?(?:[/?#].*)?$",
+ "flags": "i",
+ "severity": "critical",
+ "description": "URL path matches Quantum Route Redirect / Tycoon-2FA-style single-use wordlist token (8+ underscore-joined word tokens) - high-precision phishing kit signature",
+ "action": "warn",
+ "category": "url_structure",
+ "confidence": 0.95
}
],
"legitimate_patterns": [
diff --git a/scripts/content.js b/scripts/content.js
index 0a6bf57e..f128ac3f 100644
--- a/scripts/content.js
+++ b/scripts/content.js
@@ -1684,6 +1684,58 @@ if (window.checkExtensionLoaded) {
}
break;
+ case "aitm_origin_validation": {
+ // Block AitM reverse-proxy pages: Microsoft login source markers
+ // present on a non-trusted host with credential inputs (form-less
+ // JS-driven kits bypass form_post_not_microsoft).
+ const requireNonTrustedHost =
+ rule.condition?.require_non_trusted_host !== false;
+ const isTrustedHost = isTrustedLoginDomain(window.location.href);
+ if (requireNonTrustedHost && isTrustedHost) {
+ break;
+ }
+
+ const markers = rule.condition?.microsoft_source_markers || [];
+ const minMatches = rule.condition?.minimum_marker_matches || 3;
+ const aitmPageSource = getPageSource();
+ const matchedMarkers = [];
+ for (const marker of markers) {
+ try {
+ const markerRegex = new RegExp(marker, "i");
+ if (markerRegex.test(aitmPageSource)) {
+ matchedMarkers.push(marker);
+ }
+ } catch (regexError) {
+ logger.debug(
+ `Invalid AitM marker regex "${marker}": ${regexError.message}`
+ );
+ }
+ }
+
+ if (matchedMarkers.length < minMatches) {
+ break;
+ }
+
+ if (rule.condition?.require_credential_input !== false) {
+ const hasCredentialInput =
+ document.querySelector(
+ 'input[type="password"], input[name="passwd"], input[name="loginfmt"], input[type="email"], input[name*="email" i], input[id*="password" i]'
+ ) !== null;
+ if (!hasCredentialInput) {
+ break;
+ }
+ }
+
+ ruleTriggered = true;
+ reason = `AitM detected: ${matchedMarkers.length} Microsoft login markers (${matchedMarkers
+ .slice(0, 5)
+ .join(", ")}) on non-Microsoft origin "${window.location.hostname}" with credential input present`;
+ logger.warn(
+ `BLOCKING RULE TRIGGERED: ${rule.id} ${rule.description} - ${reason}`
+ );
+ break;
+ }
+
default:
logger.warn(`Unknown blocking rule type: ${rule.type}`);
}
From 85d5f4cc104821e4b2585058028d8f2ef7ef773b Mon Sep 17 00:00:00 2001
From: Zacgoose <107489668+Zacgoose@users.noreply.github.com>
Date: Mon, 8 Jun 2026 20:49:18 +0800
Subject: [PATCH 16/20] atim
---
rules/detection-rules.json | 66 +++++++++++++++++++++++++---
scripts/content.js | 90 ++++++++++++++++++++++++++++++++++++++
2 files changed, 149 insertions(+), 7 deletions(-)
diff --git a/rules/detection-rules.json b/rules/detection-rules.json
index 4cf45fe2..048c8467 100644
--- a/rules/detection-rules.json
+++ b/rules/detection-rules.json
@@ -1,5 +1,5 @@
{
- "version": "1.2.1",
+ "version": "1.2.2",
"lastUpdated": "2026-06-08T00:00:00Z",
"description": "Phishing detection logic for identifying phishing attempts targeting Microsoft 365 login pages",
"trusted_login_patterns": [
@@ -143,6 +143,52 @@
"description": "Microsoft login page background image from CDN",
"weight": 3,
"category": "primary"
+ },
+ {
+ "id": "img_alt_microsoft",
+ "type": "source_content",
+ "pattern": "
]+alt\\s*=\\s*[\"']Microsoft[\"'][^>]*>",
+ "description": "Image with alt=\"Microsoft\" - durable visual hook used by CSS-clone phishing kits that strip canonical MS DOM hooks",
+ "weight": 3,
+ "category": "primary"
+ },
+ {
+ "id": "ms_visible_ux_combo",
+ "type": "code_driven",
+ "code_logic": {
+ "type": "all_of",
+ "operations": [
+ {
+ "type": "any_of",
+ "operations": [
+ { "type": "substring_present", "values": ["No account? Create one"] },
+ { "type": "substring_present", "values": ["Sign-in options"] }
+ ]
+ },
+ {
+ "type": "any_of",
+ "operations": [
+ { "type": "substring_present", "values": ["Can't access your account", "Can’t access your account", "Cant access your account"] },
+ { "type": "substring_present", "values": ["Terms of use"] }
+ ]
+ }
+ ]
+ },
+ "description": "Microsoft login user-visible UX text combo (two or more exact-MS-copy phrases) - durable signal for CSS-clone kits",
+ "weight": 3,
+ "category": "primary"
+ },
+ {
+ "id": "title_signin_to_your_account",
+ "type": "page_title",
+ "patterns": [
+ "^Sign in to your account$",
+ "^Sign in to your account\\s*[|\\-]\\s*Microsoft",
+ "^Sign in to your Microsoft account"
+ ],
+ "description": "Page title exactly matches the canonical Microsoft 'Sign in to your account' title",
+ "weight": 2,
+ "category": "primary"
}
],
"secondary_elements": [
@@ -319,8 +365,8 @@
{
"id": "ms_login_placeholder_text",
"type": "source_content",
- "pattern": "(?:Email,\\s*phone,?\\s*or\\s*Skype|Enter\\s+your\\s+email,\\s*phone,?\\s*or\\s*Skype|someone@example\\.com|example@example\\.com)",
- "description": "Microsoft login placeholder text patterns - highly specific to Microsoft login pages",
+ "pattern": "(?:Email,\\s*phone,?\\s*or\\s*Skype|Enter\\s+your\\s+email,\\s*phone,?\\s*or\\s*Skype|Email\\s+or\\s+phone(?!\\s+number)|placeholder\\s*=\\s*[\"']Email\\s+or\\s{1,3}phone[\"']|someone@example\\.com|example@example\\.com)",
+ "description": "Microsoft login placeholder text patterns (incl. 'Email or phone' minimalist variant used by current CSS-clone kits) - highly specific to Microsoft login pages",
"weight": 1,
"category": "secondary"
}
@@ -384,7 +430,7 @@
{
"id": "aitm_microsoft_markers_off_origin",
"type": "aitm_origin_validation",
- "description": "Block when Microsoft 365 login source markers (idPartnerPL, loginfmt, urlMsaSignUp, flowToken, MS auth CDN refs) appear on a non-Microsoft origin with any credential input present. Catches adversary-in-the-middle (Evilginx/Tycoon-2FA) reverse-proxy pages whose credential capture is JS-driven and bypasses form_post_not_microsoft.",
+ "description": "Block when Microsoft 365 login markers - structural (idPartnerPL, loginfmt, etc.) AND/OR durable visible-UX markers (img alt=\"Microsoft\", 'Sign-in options', 'No account? Create one', 'Can't access your account', 'Sign in to your account' title) - appear on a non-Microsoft origin with any credential input. Catches both AitM reverse-proxy kits and CSS-clone kits that strip canonical MS DOM hooks (which is what Quantum Route Redirect / Tycoon-2FA-style kits do).",
"condition": {
"microsoft_source_markers": [
"idPartnerPL",
@@ -393,7 +439,12 @@
"flowToken",
"aadcdn\\.msauth\\.net",
"aadcdn\\.msftauth\\.net",
- "aadcdn\\.msftauthimages\\.net"
+ "aadcdn\\.msftauthimages\\.net",
+ "
]+alt\\s*=\\s*[\"']Microsoft[\"']",
+ "Sign-in options",
+ "No account\\?\\s*Create one",
+ "Can[''']?t access your account",
+ "]*>\\s*Sign in to your account\\s*"
],
"minimum_marker_matches": 3,
"require_credential_input": true,
@@ -1591,13 +1642,14 @@
},
{
"id": "phi_034_wordlist_path_kit",
- "pattern": "^https?:\\/\\/[^/]+\\/(?:[a-z]{3,12}_){8,}[a-z]{3,12}_?(?:[/?#].*)?$",
+ "pattern": "^https?:\\/\\/[^/]+\\/(?:[a-z]{3,20}_){8,}[a-z]{3,20}_?(?:[/?#].*)?$",
"flags": "i",
"severity": "critical",
"description": "URL path matches Quantum Route Redirect / Tycoon-2FA-style single-use wordlist token (8+ underscore-joined word tokens) - high-precision phishing kit signature",
"action": "warn",
"category": "url_structure",
- "confidence": 0.95
+ "confidence": 0.95,
+ "url_only": true
}
],
"legitimate_patterns": [
diff --git a/scripts/content.js b/scripts/content.js
index f128ac3f..453a178a 100644
--- a/scripts/content.js
+++ b/scripts/content.js
@@ -2716,6 +2716,72 @@ if (window.checkExtensionLoaded) {
});
}
+ /**
+ * Run URL-only phishing indicators (those flagged with url_only: true).
+ * Fast synchronous check against the current URL, used as a pre-gate so
+ * URL-shape kits (e.g., wordlist-path morphing tokens) trigger even when
+ * the page has no DOM/content markers for Microsoft.
+ */
+ function checkUrlOnlyIndicators() {
+ try {
+ if (!detectionRules?.phishing_indicators) {
+ return { threats: [], score: 0 };
+ }
+ const currentUrl = window.location.href;
+ const threats = [];
+ let totalScore = 0;
+ for (const indicator of detectionRules.phishing_indicators) {
+ if (!indicator.url_only) continue;
+ let matches = false;
+ try {
+ if (indicator.pattern) {
+ const pattern = new RegExp(
+ indicator.pattern,
+ indicator.flags || "i"
+ );
+ matches = pattern.test(currentUrl);
+ }
+ } catch (regexErr) {
+ logger.debug(
+ `url_only indicator ${indicator.id} regex error: ${regexErr.message}`
+ );
+ continue;
+ }
+ if (matches) {
+ threats.push({
+ id: indicator.id,
+ category: indicator.category,
+ severity: indicator.severity,
+ confidence: indicator.confidence,
+ description: indicator.description,
+ action: indicator.action,
+ matchDetails: "URL (url_only pre-check)",
+ });
+ let scoreWeight = 0;
+ switch (indicator.severity) {
+ case "critical":
+ scoreWeight = 25;
+ break;
+ case "high":
+ scoreWeight = 15;
+ break;
+ case "medium":
+ scoreWeight = 10;
+ break;
+ case "low":
+ scoreWeight = 5;
+ break;
+ }
+ totalScore += scoreWeight * (indicator.confidence || 0.5);
+ }
+ }
+ return { threats, score: totalScore };
+ } catch (e) {
+ logger.warn(`checkUrlOnlyIndicators failed: ${e.message}`);
+ return { threats: [], score: 0 };
+ }
+ }
+
/**
* Process phishing indicators from detection rules
*/
@@ -4305,6 +4371,30 @@ if (window.checkExtensionLoaded) {
// Step 6: Check if page is an MS logon page (using rule file requirements)
const msDetection = detectMicrosoftElements();
if (!msDetection.isLogonPage) {
+ // Step 6a: Pre-check URL-only phishing indicators (e.g., wordlist-path
+ // kits) BEFORE the hasElements performance gate. URL-shape signals must
+ // fire even when the page has stripped all DOM hooks - this is the
+ // whole point of url_only indicators. If any critical url_only
+ // indicator hits, bypass the gate so full processPhishingIndicators
+ // and blocking rules still run.
+ const urlOnlyResult = checkUrlOnlyIndicators();
+ if (urlOnlyResult.threats.length > 0) {
+ logger.warn(
+ `🚨 URL-only indicators triggered: ${urlOnlyResult.threats
+ .map((t) => `${t.id}(${t.severity})`)
+ .join(", ")} (score ${urlOnlyResult.score})`
+ );
+ const hasCriticalUrlThreat = urlOnlyResult.threats.some(
+ (t) => t.severity === "critical" || t.action === "block"
+ );
+ if (hasCriticalUrlThreat) {
+ logger.warn(
+ "🚨 Critical URL-only indicator detected - bypassing hasElements gate for full phishing analysis"
+ );
+ msDetection.hasElements = true;
+ }
+ }
+
// Check if page has ANY Microsoft-related elements before running expensive phishing indicators
if (!msDetection.hasElements) {
logger.log(
From e568792b979e11a4cdc9973247f09e53d66caf80 Mon Sep 17 00:00:00 2001
From: Zacgoose <107489668+Zacgoose@users.noreply.github.com>
Date: Mon, 8 Jun 2026 21:45:47 +0800
Subject: [PATCH 17/20] rule loading changes
---
config/managed_schema.json | 2 +-
docs/features/domain-squatting-detection.md | 4 +-
enterprise/Check-Extension-Policy.reg | 4 +-
enterprise/Deploy-Windows-Chrome-and-Edge.ps1 | 2 +-
enterprise/Detect-Windows-Chrome-and-Edge.ps1 | 2 +-
enterprise/Setup-Windows-Chrome-and-Edge.ps1 | 2 +-
enterprise/admx/en-US/Check-Extension.adml | 8 +-
enterprise/firefox/policies.json | 2 +-
enterprise/macos-linux/README.md | 2 +-
.../check-extension-config.mobileconfig | 2 +-
options/options.js | 4 +-
scripts/background.js | 56 ++++++++++-
scripts/modules/config-manager.js | 4 +-
scripts/modules/detection-rules-manager.js | 93 ++++++++++++++++---
scripts/modules/domain-squatting-detector.js | 4 +-
15 files changed, 155 insertions(+), 36 deletions(-)
diff --git a/config/managed_schema.json b/config/managed_schema.json
index 057b6419..20e6e2c2 100644
--- a/config/managed_schema.json
+++ b/config/managed_schema.json
@@ -185,7 +185,7 @@
"title": "Enabled",
"description": "Enable or disable domain squatting detection",
"type": "boolean",
- "default": true
+ "default": false
},
"deviationThreshold": {
"title": "Deviation Threshold",
diff --git a/docs/features/domain-squatting-detection.md b/docs/features/domain-squatting-detection.md
index d518f0c3..078fe204 100644
--- a/docs/features/domain-squatting-detection.md
+++ b/docs/features/domain-squatting-detection.md
@@ -108,7 +108,7 @@ Edit your `rules/detection-rules.json` file to customize:
```json
{
"domain_squatting": {
- "enabled": true, // Turn detection on/off
+ "enabled": false, // Turn detection on/off (default: false)
"action": "block" // Action when detected: "block" or "warn"
}
}
@@ -125,7 +125,7 @@ Edit your `rules/detection-rules.json` file to customize:
Note: Page blocking also requires "Enable Page Blocking" to be turned ON in settings.
**Adjust Sensitivity:**
- "enabled": true
+ "enabled": false
}
}
```
diff --git a/enterprise/Check-Extension-Policy.reg b/enterprise/Check-Extension-Policy.reg
index 3ac04642..0c3ef307 100644
--- a/enterprise/Check-Extension-Policy.reg
+++ b/enterprise/Check-Extension-Policy.reg
@@ -21,7 +21,7 @@ Windows Registry Editor Version 5.00
"enableDebugLogging"=dword:00000000
[HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Edge\3rdparty\extensions\knepjpocdagponkonnbggpcnhnaikajg\policy\domainSquatting]
-"enabled"=dword:00000001
+"enabled"=dword:00000000
; Generic webhook configuration (optional)
[HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Edge\3rdparty\extensions\knepjpocdagponkonnbggpcnhnaikajg\policy\genericWebhook]
@@ -68,7 +68,7 @@ Windows Registry Editor Version 5.00
"enableDebugLogging"=dword:00000000
[HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Google\Chrome\3rdparty\extensions\benimdeioplgkhanklclahllklceahbe\policy\domainSquatting]
-"enabled"=dword:00000001
+"enabled"=dword:00000000
; Generic webhook configuration for Chrome (optional)
[HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Google\Chrome\3rdparty\extensions\benimdeioplgkhanklclahllklceahbe\policy\genericWebhook]
diff --git a/enterprise/Deploy-Windows-Chrome-and-Edge.ps1 b/enterprise/Deploy-Windows-Chrome-and-Edge.ps1
index 0ba37eaa..5003201a 100644
--- a/enterprise/Deploy-Windows-Chrome-and-Edge.ps1
+++ b/enterprise/Deploy-Windows-Chrome-and-Edge.ps1
@@ -22,7 +22,7 @@ $cippTenantId = "" # This will set the "Tenant ID/Domain" option in the extensio
$customRulesUrl = "" # This will set the "Config URL" option in the Detection Configuration settings; default is blank.
$updateInterval = 24 # This will set the "Update Interval" option in the Detection Configuration settings; default is 24 (hours). Range: 1-168 hours (1 hour to 1 week).
$urlAllowlist = @() # This will set the "URL Allowlist" option in the Detection Configuration settings; default is blank; if you want to add multiple URLs, add them as a comma-separated list within the brackets (e.g., @("https://example1.com", "https://example2.com")). Supports simple URLs with * wildcard (e.g., https://*.example.com) or advanced regex patterns (e.g., ^https:\/\/(www\.)?example\.com\/.*$).
-$domainSquattingEnabled = 1 # 0 = Disabled, 1 = Enabled; default is 1; controls domain squatting detection from managed policy/config.
+$domainSquattingEnabled = 0 # 0 = Disabled, 1 = Enabled; default is 0; controls domain squatting detection from managed policy/config.
$enableDebugLogging = 0 # 0 = Unchecked, 1 = Checked (Enabled); default is 0; This will set the "Enable Debug Logging" option in the Activity Log settings.
# Generic Webhook Settings
diff --git a/enterprise/Detect-Windows-Chrome-and-Edge.ps1 b/enterprise/Detect-Windows-Chrome-and-Edge.ps1
index 1b95c7dc..5ef84ba8 100644
--- a/enterprise/Detect-Windows-Chrome-and-Edge.ps1
+++ b/enterprise/Detect-Windows-Chrome-and-Edge.ps1
@@ -31,7 +31,7 @@ $cippTenantId = "" # This will set the "Tenant ID/Domain" option in the extensio
$customRulesUrl = "" # This will set the "Config URL" option in the Detection Configuration settings; default is blank.
$updateInterval = 24 # This will set the "Update Interval" option in the Detection Configuration settings; default is 24 (hours). Range: 1-168 hours (1 hour to 1 week).
$urlAllowlist = @() # This will set the "URL Allowlist" option in the Detection Configuration settings; default is blank; if you want to add multiple URLs, add them as a comma-separated list within the brackets (e.g., @("https://example1.com", "https://example2.com")). Supports simple URLs with * wildcard (e.g., https://*.example.com) or advanced regex patterns (e.g., ^https:\/\/(www\.)?example\.com\/.*$).
-$domainSquattingEnabled = 1 # 0 = Disabled, 1 = Enabled; default is 1; controls domain squatting detection from managed policy/config.
+$domainSquattingEnabled = 0 # 0 = Disabled, 1 = Enabled; default is 0; controls domain squatting detection from managed policy/config.
$enableDebugLogging = 0 # 0 = Unchecked, 1 = Checked (Enabled); default is 0; This will set the "Enable Debug Logging" option in the Activity Log settings.
# Generic Webhook Settings
diff --git a/enterprise/Setup-Windows-Chrome-and-Edge.ps1 b/enterprise/Setup-Windows-Chrome-and-Edge.ps1
index ea88f61d..502b4ee4 100644
--- a/enterprise/Setup-Windows-Chrome-and-Edge.ps1
+++ b/enterprise/Setup-Windows-Chrome-and-Edge.ps1
@@ -118,7 +118,7 @@ $cfg_enablePageBlocking = Read-Setting -Name "enablePageBlocking" -Description "
$cfg_forceToolbarPin = Read-Setting -Name "forceToolbarPin" -Description "Force pin extension to toolbar (0 = Not pinned, 1 = Force pinned)" -Default "1" -Type "bool"
$cfg_updateInterval = Read-Setting -Name "updateInterval" -Description "Update interval in hours (1-168)" -Default "24" -Type "int"
$cfg_enableDebugLogging = Read-Setting -Name "enableDebugLogging" -Description "Enable debug logging (0 = Disabled, 1 = Enabled)" -Default "0" -Type "bool"
-$cfg_domainSquattingEnabled = Read-Setting -Name "domainSquattingEnabled" -Description "Enable domain squatting detection (0 = Disabled, 1 = Enabled)" -Default "1" -Type "bool"
+$cfg_domainSquattingEnabled = Read-Setting -Name "domainSquattingEnabled" -Description "Enable domain squatting detection (0 = Disabled, 1 = Enabled)" -Default "0" -Type "bool"
$cfg_customRulesUrl = Read-Setting -Name "customRulesUrl" -Description "Custom rules/config URL (leave blank if not used)" -Default ""
Write-Host ""
diff --git a/enterprise/admx/en-US/Check-Extension.adml b/enterprise/admx/en-US/Check-Extension.adml
index af971413..6369d7ab 100644
--- a/enterprise/admx/en-US/Check-Extension.adml
+++ b/enterprise/admx/en-US/Check-Extension.adml
@@ -282,8 +282,8 @@
This policy controls domain squatting detection in the Check extension.
- When enabled (default): Typosquatting, homoglyph, and combosquatting protections are active.
- When disabled: Domain squatting detections are skipped.
+ When enabled: Typosquatting, homoglyph, and combosquatting protections are active.
+ When disabled (default): Domain squatting detections are skipped.
Enable debug logging (Chrome)
@@ -299,8 +299,8 @@
This policy controls domain squatting detection in the Check extension for Google Chrome.
- When enabled (default): Typosquatting, homoglyph, and combosquatting protections are active.
- When disabled: Domain squatting detections are skipped.
+ When enabled: Typosquatting, homoglyph, and combosquatting protections are active.
+ When disabled (default): Domain squatting detections are skipped.
Show valid page badge (Chrome)
diff --git a/enterprise/firefox/policies.json b/enterprise/firefox/policies.json
index bb6a2505..981d70e1 100644
--- a/enterprise/firefox/policies.json
+++ b/enterprise/firefox/policies.json
@@ -29,7 +29,7 @@
"updateInterval": 24,
"urlAllowlist": [],
"domainSquatting": {
- "enabled": true
+ "enabled": false
},
"enableDebugLogging": false,
"customBranding": {
diff --git a/enterprise/macos-linux/README.md b/enterprise/macos-linux/README.md
index fc9509bc..5f1d3a5c 100644
--- a/enterprise/macos-linux/README.md
+++ b/enterprise/macos-linux/README.md
@@ -166,7 +166,7 @@ All settings are based on the managed schema and include:
### Rule Management
- **`customRulesUrl`** - URL for custom detection rules
- **`updateInterval`** - Rule update interval in hours (default: 24)
-- **`domainSquatting.enabled`** - Enable/disable domain squatting detection (default: true)
+- **`domainSquatting.enabled`** - Enable/disable domain squatting detection (default: false)
### Custom Branding
- **`companyName`** - Company name for white labeling
diff --git a/enterprise/macos-linux/check-extension-config.mobileconfig b/enterprise/macos-linux/check-extension-config.mobileconfig
index e87ff3de..f80dabcf 100644
--- a/enterprise/macos-linux/check-extension-config.mobileconfig
+++ b/enterprise/macos-linux/check-extension-config.mobileconfig
@@ -73,7 +73,7 @@
Value
enabled
-
+
diff --git a/options/options.js b/options/options.js
index 491c5a9c..b2df83da 100644
--- a/options/options.js
+++ b/options/options.js
@@ -1000,7 +1000,7 @@ class CheckOptions {
if (this.elements.domainSquattingEnabled) {
this.elements.domainSquattingEnabled.checked =
- this.config?.domainSquatting?.enabled !== false;
+ this.config?.domainSquatting?.enabled === true;
}
// Handle updateInterval - ensure we always show hours in the UI
@@ -1257,7 +1257,7 @@ class CheckOptions {
// Domain squatting settings (runtime control moved from rules JSON)
domainSquatting: {
- enabled: this.elements.domainSquattingEnabled?.checked !== false,
+ enabled: this.elements.domainSquattingEnabled?.checked === true,
deviationThreshold: existingDomainSquatting.deviationThreshold ?? 2,
algorithms: {
levenshtein:
diff --git a/scripts/background.js b/scripts/background.js
index 20db5765..cb22fdc3 100644
--- a/scripts/background.js
+++ b/scripts/background.js
@@ -289,7 +289,10 @@ class CheckBackground {
constructor() {
this.configManager = new ConfigManager();
this.policyManager = new PolicyManager();
- this.detectionRulesManager = new DetectionRulesManager(this.configManager);
+ this.detectionRulesManager = new DetectionRulesManager(
+ this.configManager,
+ (rules) => this._handleDetectionRulesUpdated(rules)
+ );
this.rogueAppsManager = new RogueAppsManager();
this.domainSquattingDetector = new DomainSquattingDetector();
this.webhookManager = new WebhookManager(this.configManager);
@@ -866,6 +869,28 @@ class CheckBackground {
}
}
+ /**
+ * Invoked by DetectionRulesManager whenever a remote rules fetch succeeds
+ * (forced, on-save, or via the lazy background refresh triggered by page
+ * detection). Re-initializes downstream subsystems with the new rules so
+ * they don't lag behind the cache.
+ */
+ async _handleDetectionRulesUpdated(rules) {
+ try {
+ if (!rules || !this.domainSquattingDetector) return;
+ const runtimeConfig = await this.configManager.getConfig();
+ await this.domainSquattingDetector.initialize(rules, runtimeConfig);
+ logger.log(
+ "Domain squatting detector re-initialized after rules refresh"
+ );
+ } catch (error) {
+ logger.warn(
+ "Failed to re-init domain squatting detector after rules refresh:",
+ error?.message || error
+ );
+ }
+ }
+
async handleTabUpdate(tabId, changeInfo, tab) {
if (!this.isInitialized) return;
@@ -1487,6 +1512,10 @@ class CheckBackground {
const previousBadgeEnabled =
currentConfig?.enableValidPageBadge ||
this.policy?.EnableValidPageBadge;
+ const previousRulesUrl =
+ currentConfig?.customRulesUrl ||
+ currentConfig?.detectionRules?.customRulesUrl ||
+ null;
// Update the configuration
await this.configManager.updateConfig(message.config);
@@ -1500,6 +1529,31 @@ class CheckBackground {
updatedConfig?.enableValidPageBadge ||
this.policy?.EnableValidPageBadge;
+ // On config save, always pull fresh rules so a URL change (or any
+ // other policy save) immediately reflects new rules in cache and
+ // in the domain-squatting detector. The onRulesUpdated callback
+ // wired into DetectionRulesManager will reinit the squatting
+ // detector once the fetch completes, so we don't need to do that
+ // here for the success path - but we still re-init synchronously
+ // below to cover the case where the network fetch fails.
+ const newRulesUrl =
+ updatedConfig?.customRulesUrl ||
+ updatedConfig?.detectionRules?.customRulesUrl ||
+ null;
+ this.detectionRulesManager.updateDetectionRules().catch((err) => {
+ logger.warn(
+ "Pull-on-save detection-rules update failed:",
+ err?.message || err
+ );
+ });
+ if (previousRulesUrl !== newRulesUrl) {
+ logger.log(
+ `customRulesUrl changed (${previousRulesUrl || ""} -> ${
+ newRulesUrl || ""
+ }) - background rules pull triggered`
+ );
+ }
+
// Update domain squatting detector with new configuration
// If URL allowlist changed, reinitialize detector to extract new domains
if (this.domainSquattingDetector) {
diff --git a/scripts/modules/config-manager.js b/scripts/modules/config-manager.js
index 7a87673b..075e88b2 100644
--- a/scripts/modules/config-manager.js
+++ b/scripts/modules/config-manager.js
@@ -112,7 +112,7 @@ export class ConfigManager {
updateInterval: 24,
enableDebugLogging: false,
domainSquatting: {
- enabled: true,
+ enabled: false,
deviationThreshold: 2,
algorithms: {
levenshtein: true,
@@ -306,7 +306,7 @@ export class ConfigManager {
// Domain squatting runtime settings
domainSquatting: {
- enabled: true,
+ enabled: false,
deviationThreshold: 2,
algorithms: {
levenshtein: true,
diff --git a/scripts/modules/detection-rules-manager.js b/scripts/modules/detection-rules-manager.js
index ad6d35d3..3825ede4 100644
--- a/scripts/modules/detection-rules-manager.js
+++ b/scripts/modules/detection-rules-manager.js
@@ -7,7 +7,7 @@ import { chrome, storage } from "../browser-polyfill.js";
import logger from "../utils/logger.js";
export class DetectionRulesManager {
- constructor(configManager = null) {
+ constructor(configManager = null, onRulesUpdated = null) {
this.cachedRules = null;
this.lastUpdate = 0;
this.updateInterval = 24 * 60 * 60 * 1000; // Default: 24 hours
@@ -18,6 +18,9 @@ export class DetectionRulesManager {
this.config = null;
this.configManager = configManager;
this.initialized = false;
+ this.onRulesUpdated = onRulesUpdated;
+ this._refreshInFlight = null;
+ this._usingFallback = false;
}
async initialize() {
@@ -161,7 +164,9 @@ export class DetectionRulesManager {
rules = await response.json();
logger.log("Successfully fetched detection rules from remote URL");
- // Save to cache
+ // Persist only successful remote fetches so the cache always reflects
+ // the true remote state - never poisoned by bundled fallback content.
+ this._usingFallback = false;
await this.saveToCache(rules);
return rules;
} catch (error) {
@@ -169,9 +174,14 @@ export class DetectionRulesManager {
}
}
- // Fallback to local rules
+ // Remote fetch failed (or no remote URL configured): serve the bundled
+ // rules in-memory but DO NOT persist them. lastUpdate stays at 0 so the
+ // next getDetectionRules() call treats the cache as stale and retries the
+ // remote URL - the persistent cache remains a record of remote-only state.
try {
- logger.log("Falling back to local detection rules");
+ logger.log(
+ "Falling back to bundled detection rules (in-memory only; will retry remote on next refresh)"
+ );
const response = await fetch(this.fallbackUrl);
if (!response.ok) {
@@ -179,13 +189,14 @@ export class DetectionRulesManager {
}
rules = await response.json();
- logger.log("Successfully loaded local detection rules");
+ logger.log("Loaded bundled detection rules");
- // Save to cache as fallback
- await this.saveToCache(rules);
+ this.cachedRules = rules;
+ this.lastUpdate = 0; // Force next access to re-attempt the remote fetch
+ this._usingFallback = true;
return rules;
} catch (error) {
- logger.error("Failed to load local detection rules:", error.message);
+ logger.error("Failed to load bundled detection rules:", error.message);
throw error;
}
}
@@ -194,6 +205,19 @@ export class DetectionRulesManager {
try {
const rules = await this.fetchDetectionRules();
+ // Notify the background-script wiring so dependent subsystems (e.g. the
+ // domain-squatting detector) can re-initialize with the new rules.
+ if (typeof this.onRulesUpdated === "function") {
+ try {
+ await this.onRulesUpdated(rules);
+ } catch (callbackError) {
+ logger.warn(
+ "onRulesUpdated callback threw:",
+ callbackError?.message || callbackError
+ );
+ }
+ }
+
// Notify other parts of the extension that rules have been updated
if (
typeof chrome !== "undefined" &&
@@ -217,24 +241,63 @@ export class DetectionRulesManager {
}
}
+ /**
+ * Kick off a non-blocking refresh of the detection rules. Used by
+ * getDetectionRules() to trigger a background refresh whenever a page
+ * detection requests rules and the cache is past the configured interval,
+ * without making the requesting page wait for the network round-trip.
+ * Guarded against overlapping in-flight refreshes.
+ */
+ _scheduleBackgroundRefresh(reason) {
+ if (this._refreshInFlight) {
+ return this._refreshInFlight;
+ }
+ logger.log(
+ `Scheduling background detection-rules refresh (${reason || "expired"})`
+ );
+ this._refreshInFlight = this.updateDetectionRules()
+ .catch((err) => {
+ logger.warn(
+ "Background detection-rules refresh failed:",
+ err?.message || err
+ );
+ })
+ .finally(() => {
+ this._refreshInFlight = null;
+ });
+ return this._refreshInFlight;
+ }
+
async getDetectionRules() {
- // Return cached rules if available and fresh
- if (this.cachedRules) {
- const now = Date.now();
+ const now = Date.now();
+
+ // Fast path: in-memory cache exists from a previous successful fetch.
+ if (this.cachedRules && this.lastUpdate > 0) {
const cacheAge = now - this.lastUpdate;
if (cacheAge < this.updateInterval) {
+ // Fresh - return immediately, no network.
return this.cachedRules;
}
+
+ // Cache is past the configured refresh interval. Return the (still usable)
+ // cached rules immediately so page detection isn't blocked, and kick off
+ // a non-blocking remote refresh whose result lands on the NEXT request.
+ this._scheduleBackgroundRefresh(
+ `cache age ${Math.round(cacheAge / 60000)}m > interval ${Math.round(
+ this.updateInterval / 60000
+ )}m`
+ );
+ return this.cachedRules;
}
- // Need to fetch fresh rules
+ // No usable cache yet (cold start or running on bundled fallback after a
+ // remote failure). Block on a real fetch so callers don't get null.
try {
return await this.fetchDetectionRules();
} catch (error) {
- // Return cached rules as last resort, even if expired
if (this.cachedRules) {
- logger.warn("Using expired cached rules due to fetch failure");
+ logger.warn("Using bundled cached rules due to fetch failure");
return this.cachedRules;
}
throw error;
@@ -257,6 +320,8 @@ export class DetectionRulesManager {
isExpired: this.lastUpdate
? Date.now() - this.lastUpdate > this.updateInterval
: true,
+ usingFallback: this._usingFallback,
+ refreshInFlight: !!this._refreshInFlight,
};
}
}
diff --git a/scripts/modules/domain-squatting-detector.js b/scripts/modules/domain-squatting-detector.js
index 1edde814..505ec845 100644
--- a/scripts/modules/domain-squatting-detector.js
+++ b/scripts/modules/domain-squatting-detector.js
@@ -8,7 +8,7 @@ import logger from '../utils/logger.js';
export class DomainSquattingDetector {
constructor() {
this.protectedDomains = [];
- this.enabled = true;
+ this.enabled = false;
this.action = 'block';
this.minimumSeverity = 'high';
this.logDetections = true;
@@ -141,7 +141,7 @@ export class DomainSquattingDetector {
const rulesDomainSquatting = rulesConfig?.domain_squatting || {};
const runtimeDomainSquatting = runtimeConfig?.domainSquatting || {};
- this.enabled = runtimeDomainSquatting.enabled !== false;
+ this.enabled = runtimeDomainSquatting.enabled === true;
this.protectedDomains = rulesDomainSquatting.protected_domains || [];
this.deviationThreshold =
runtimeDomainSquatting.deviationThreshold ||
From 835a5419e5b35222f9d0f2cbec87d21df496231d Mon Sep 17 00:00:00 2001
From: Zacgoose <107489668+Zacgoose@users.noreply.github.com>
Date: Tue, 9 Jun 2026 08:03:38 +0800
Subject: [PATCH 18/20] Update detection-rules.json
---
rules/detection-rules.json | 92 +++++++++++++++++++++++++++++++++++++-
1 file changed, 91 insertions(+), 1 deletion(-)
diff --git a/rules/detection-rules.json b/rules/detection-rules.json
index 048c8467..02c93b4c 100644
--- a/rules/detection-rules.json
+++ b/rules/detection-rules.json
@@ -1,5 +1,5 @@
{
- "version": "1.2.2",
+ "version": "1.2.3",
"lastUpdated": "2026-06-08T00:00:00Z",
"description": "Phishing detection logic for identifying phishing attempts targeting Microsoft 365 login pages",
"trusted_login_patterns": [
@@ -189,6 +189,50 @@
"description": "Page title exactly matches the canonical Microsoft 'Sign in to your account' title",
"weight": 2,
"category": "primary"
+ },
+ {
+ "id": "canvas_streaming_ms_kit",
+ "type": "code_driven",
+ "code_logic": {
+ "type": "all_of",
+ "operations": [
+ { "type": "substring_present", "values": ["