From 9e9b562a8648d1a375206a5786e0ad359e4efda3 Mon Sep 17 00:00:00 2001
From: MWG-Logan <2997336+MWG-Logan@users.noreply.github.com>
Date: Wed, 1 Jul 2026 19:08:18 -0400
Subject: [PATCH 01/28] fix(content): treat domain squatting 'log' action as a
distinct state
The domain squatting result handler in scripts/content.js collapsed the block | warn | log action enum into block vs. everything-else. Because only the 'block' case was checked, the 'log' action fell through to the warning branch: it showed the orange warning banner and recorded every telemetry line (protection event, CIPP report, webhook) as 'warned'. The 'log' action was therefore functionally identical to 'warn'.
Resolve the effective action as a real three-state value (block | warn | log) and derive a matching telemetry outcome (blocked | warned | logged). Preserve the semantics that warn logs telemetry and shows a banner, while log logs telemetry only and shows nothing to the user. Warn logs, log does not warn.
The detector side (getActionForSeverity in domain-squatting-detector.js) already passed 'log' through intact and is unchanged.
Fixes: #161
---
scripts/content.js | 29 +++++++++++++++++++++--------
1 file changed, 21 insertions(+), 8 deletions(-)
diff --git a/scripts/content.js b/scripts/content.js
index 453a178..cf9fe0c 100644
--- a/scripts/content.js
+++ b/scripts/content.js
@@ -4247,19 +4247,29 @@ if (window.checkExtensionLoaded) {
// Check if notifications should be shown
const showNotifications = config.showNotifications !== false;
-
- // Determine if we should block the page
- // Requires: 1) enablePageBlocking is ON, 2) domain_squatting action is "block"
+
+ // Resolve the effective action as a real three-state value:
+ // 'block' | 'warn' | 'log'. Previously this only checked for
+ // 'block', which collapsed 'log' into 'warn' (banner + "warned").
+ // Semantics: warn logs telemetry AND shows a banner; log logs
+ // telemetry only and shows nothing to the user.
+ const squattingAction = squattingData.action || 'warn';
logger.debug(` enablePageBlocking: ${config.enablePageBlocking}`);
logger.debug(` squattingData.action: ${squattingData.action}`);
- const shouldBlock = squattingData.action === 'block' &&
+ const shouldBlock = squattingAction === 'block' &&
config.enablePageBlocking !== false;
+ const outcome = shouldBlock
+ ? "blocked"
+ : squattingAction === 'log'
+ ? "logged"
+ : "warned";
logger.debug(` shouldBlock: ${shouldBlock}`);
-
+ logger.debug(` outcome: ${outcome}`);
+
// Log domain squatting detection
logProtectionEvent({
type: "threat_detected",
- action: shouldBlock ? "blocked" : "warned",
+ action: outcome,
url: location.href,
origin: currentOrigin,
reason: `Domain squatting detected: ${squattingData.techniques.map(t => t.description).join('; ')}`,
@@ -4282,7 +4292,7 @@ if (window.checkExtensionLoaded) {
})),
severity: squattingData.severity,
confidence: squattingData.confidence,
- action: shouldBlock ? "blocked" : "warned",
+ action: outcome,
reason: `Domain squatting detected: ${squattingData.techniques.map(t => t.description).join('; ')}`
});
@@ -4301,7 +4311,7 @@ if (window.checkExtensionLoaded) {
})),
severity: squattingData.severity,
confidence: squattingData.confidence,
- action: shouldBlock ? "blocked" : "warned",
+ action: outcome,
reason: `Domain squatting detected: ${squattingData.techniques.map(t => t.description).join('; ')}`
},
})
@@ -4330,6 +4340,9 @@ if (window.checkExtensionLoaded) {
}
);
return; // Stop processing, page is blocked
+ } else if (squattingAction === 'log') {
+ // Log-only action: telemetry has already been emitted above.
+ // Intentionally show nothing to the user. Log must not warn.
} else if (showNotifications) {
// Show warning banner for domain squatting (only if notifications enabled)
const techniquesDesc = squattingData.techniques.map(t =>
From a0cd545b8c90790244b033a37a441667c11e1e64 Mon Sep 17 00:00:00 2001
From: MWG-Logan <2997336+MWG-Logan@users.noreply.github.com>
Date: Wed, 1 Jul 2026 19:55:14 -0400
Subject: [PATCH 02/28] fix(allowlist): match deep-link URLs in
urlPatternToRegex
urlPatternToRegex appended a hard end-anchor to any non-wildcard pattern, so an allowlisted host or root URL only matched the bare root URL and never a real navigated deep link. An admin who allowlisted the exact host of a page (including the protocol-qualified form) still saw that page scanned or blocked, because the actual URL carries a path and the trailing anchor rejected it.
Relax only the trailing anchoring: normalize a single trailing slash and tolerate an optional trailing path, query, or fragment. The tolerated remainder must begin with /, ?, or # so suffix tricks such as https://host.evil.com/ still do not match an entry for https://host/. Leading protocol and subdomain tolerance is intentionally out of scope, so bare-domain entries still require the documented https://.../ * form.
The change is applied to both copies of urlPatternToRegex (scripts/content.js and options/options.js), which must stay in sync.
Fixes: #162
---
options/options.js | 7 +++++--
scripts/content.js | 10 ++++++++--
2 files changed, 13 insertions(+), 4 deletions(-)
diff --git a/options/options.js b/options/options.js
index b2df83d..da45355 100644
--- a/options/options.js
+++ b/options/options.js
@@ -1371,9 +1371,12 @@ class CheckOptions {
if (!escaped.startsWith('^')) {
escaped = '^' + escaped;
}
- // Add end anchor if pattern doesn't end with wildcard
+ // Add end anchor if pattern doesn't end with wildcard. Tolerate an optional
+ // trailing path, query, or fragment so a host or root URL pattern (with or
+ // without a trailing slash) also matches deep links such as https://host/path.
+ // Kept in sync with the copy in scripts/content.js.
if (!pattern.endsWith('*') && !escaped.endsWith('.*')) {
- escaped = escaped + '$';
+ escaped = escaped.replace(/\/$/, '') + '(?:[/?#].*)?$';
}
return escaped;
}
diff --git a/scripts/content.js b/scripts/content.js
index cf9fe0c..7a4427c 100644
--- a/scripts/content.js
+++ b/scripts/content.js
@@ -3411,9 +3411,15 @@ if (window.checkExtensionLoaded) {
escaped = "^" + escaped;
}
- // Add end anchor if pattern doesn't end with wildcard
+ // Add end anchor if pattern doesn't end with wildcard. Tolerate an
+ // optional trailing path, query, or fragment so that allowlisting a host
+ // or a root URL (with or without a trailing slash) also matches deep
+ // links such as https://host/path. A single trailing slash in the pattern
+ // is normalized so it does not force an exact match on the bare root URL.
+ // Suffix tricks (for example https://host.evil.com/) still do not match,
+ // because the tolerated remainder must begin with /, ?, or #.
if (!pattern.endsWith("*") && !escaped.endsWith(".*")) {
- escaped = escaped + "$";
+ escaped = escaped.replace(/\/$/, "") + "(?:[/?#].*)?$";
}
return escaped;
From 27221875cae5e059edd2dc55e66a34abce0fc6ad Mon Sep 17 00:00:00 2001
From: MWG-Logan <2997336+MWG-Logan@users.noreply.github.com>
Date: Wed, 1 Jul 2026 21:18:47 -0400
Subject: [PATCH 03/28] fix(allowlist): restrict deep-link tolerance to host
and root URL patterns
The deep-link matching added for the URL allowlist applied its tolerant trailing matcher to every non-wildcard pattern, which turned path-specific entries into prefix matches. For example an entry for https://host/safe also matched https://host/safe/anything, over-broadening a bypass allowlist entry beyond what the admin intended.
Gate the tolerant trailing matcher on host or root URL patterns only (no path segment beyond an optional single trailing slash). A pattern that includes an explicit path now stays an exact match. Host and root URL patterns still match deep links, and suffix or prefix tricks such as https://host.evil.com/ still do not match. Applied to both copies of urlPatternToRegex in scripts/content.js and options/options.js, which must stay in sync.
Addresses PR review feedback on BezaluLLC/Check#2 (review comments on the urlPatternToRegex suffix handling).
---
options/options.js | 17 ++++++++++++-----
scripts/content.js | 24 +++++++++++++++++-------
2 files changed, 29 insertions(+), 12 deletions(-)
diff --git a/options/options.js b/options/options.js
index da45355..c1a169c 100644
--- a/options/options.js
+++ b/options/options.js
@@ -1371,12 +1371,19 @@ class CheckOptions {
if (!escaped.startsWith('^')) {
escaped = '^' + escaped;
}
- // Add end anchor if pattern doesn't end with wildcard. Tolerate an optional
- // trailing path, query, or fragment so a host or root URL pattern (with or
- // without a trailing slash) also matches deep links such as https://host/path.
- // Kept in sync with the copy in scripts/content.js.
+ // Add the end anchor if the pattern does not already end with a wildcard.
+ // Only a host or root URL pattern (no path beyond an optional single
+ // trailing slash) gets the tolerant trailing matcher, so patterns that
+ // include an explicit path stay exact matches and allowlist entries are
+ // not silently broadened into prefix matches. Kept in sync with the copy
+ // in scripts/content.js.
if (!pattern.endsWith('*') && !escaped.endsWith('.*')) {
- escaped = escaped.replace(/\/$/, '') + '(?:[/?#].*)?$';
+ const afterScheme = pattern.replace(/^https?:\/\//i, '');
+ const firstSlash = afterScheme.indexOf('/');
+ const isHostOrRoot = firstSlash === -1 || firstSlash === afterScheme.length - 1;
+ escaped = isHostOrRoot
+ ? escaped.replace(/\/$/, '') + '(?:[/?#].*)?$'
+ : escaped + '$';
}
return escaped;
}
diff --git a/scripts/content.js b/scripts/content.js
index 7a4427c..bf82fab 100644
--- a/scripts/content.js
+++ b/scripts/content.js
@@ -3411,15 +3411,25 @@ if (window.checkExtensionLoaded) {
escaped = "^" + escaped;
}
- // Add end anchor if pattern doesn't end with wildcard. Tolerate an
- // optional trailing path, query, or fragment so that allowlisting a host
- // or a root URL (with or without a trailing slash) also matches deep
- // links such as https://host/path. A single trailing slash in the pattern
- // is normalized so it does not force an exact match on the bare root URL.
- // Suffix tricks (for example https://host.evil.com/) still do not match,
+ // Add the end anchor if the pattern does not already end with a wildcard.
+ //
+ // Only a host or root URL pattern (no path segment beyond an optional
+ // single trailing slash) is given the tolerant trailing matcher, so that
+ // allowlisting a host or root URL also matches deep links such as
+ // https://host/path. A pattern that includes an explicit path stays an
+ // exact match, so allowlist entries are not silently broadened into prefix
+ // matches (for example "https://host/safe" must not also allow
+ // "https://host/safe/anything"). Suffix tricks such as
+ // "https://host.evil.com/" still do not match a "https://host/" entry,
// because the tolerated remainder must begin with /, ?, or #.
if (!pattern.endsWith("*") && !escaped.endsWith(".*")) {
- escaped = escaped.replace(/\/$/, "") + "(?:[/?#].*)?$";
+ const afterScheme = pattern.replace(/^https?:\/\//i, "");
+ const firstSlash = afterScheme.indexOf("/");
+ const isHostOrRoot =
+ firstSlash === -1 || firstSlash === afterScheme.length - 1;
+ escaped = isHostOrRoot
+ ? escaped.replace(/\/$/, "") + "(?:[/?#].*)?$"
+ : escaped + "$";
}
return escaped;
From a70ae59bd0552432d298808dad5eebec90d36577 Mon Sep 17 00:00:00 2001
From: MWG-Logan <2997336+MWG-Logan@users.noreply.github.com>
Date: Wed, 1 Jul 2026 22:45:46 -0400
Subject: [PATCH 04/28] docs(domain-squatting): document the log detection
action
Reflect the three-state detection action shipped in this branch. The log action is now documented as a distinct outcome: the detection is recorded in Activity Logs and sent to reporting and webhooks, but no warning banner and no block are shown to the user.
This also corrects the earlier statement that a warning banner is shown regardless of the action setting when page blocking is disabled, which is no longer true for the log action, and updates the inline action comments from 'block or warn' to include 'log'.
Refs: #161
---
docs/features/domain-squatting-detection.md | 11 ++++++-----
1 file changed, 6 insertions(+), 5 deletions(-)
diff --git a/docs/features/domain-squatting-detection.md b/docs/features/domain-squatting-detection.md
index 078fe20..3528412 100644
--- a/docs/features/domain-squatting-detection.md
+++ b/docs/features/domain-squatting-detection.md
@@ -88,13 +88,14 @@ You don't need to do anything - the protection works automatically in the backgr
### Page Blocking Control
-Check has an **"Enable Page Blocking"** setting in the extension options that controls how suspicious pages are handled:
+Check has an **"Enable Page Blocking"** setting in the extension options that controls how suspicious pages are handled. The detection **Action** can be one of three values: `block`, `warn`, or `log`.
- **Page Blocking Enabled** + **Action: "block"** = Page is completely blocked with full-page warning
- **Page Blocking Enabled** + **Action: "warn"** = Warning banner shown, page remains accessible
-- **Page Blocking Disabled** = Warning banner shown regardless of action setting (never blocks)
+- **Action: "log"** = Detection is recorded in Activity Logs and sent to reporting and webhooks only. No banner and no block are shown to the user, regardless of the Page Blocking setting.
+- **Page Blocking Disabled** = Never blocks. A `block` or `warn` action shows a warning banner instead; a `log` action stays silent.
-This gives you control over whether you want aggressive blocking or just warnings for suspicious domains.
+This gives you control over whether you want aggressive blocking, visible warnings, or silent monitoring for suspicious domains.
### For Advanced Users and IT Departments
@@ -109,7 +110,7 @@ Edit your `rules/detection-rules.json` file to customize:
{
"domain_squatting": {
"enabled": false, // Turn detection on/off (default: false)
- "action": "block" // Action when detected: "block" or "warn"
+ "action": "block" // Action when detected: "block", "warn", or "log"
}
}
```
@@ -118,7 +119,7 @@ Edit your `rules/detection-rules.json` file to customize:
```json
{
"domain_squatting": {
- "action": "block" // "block" = full page block, "warn" = banner only
+ "action": "block" // "block" = full page block, "warn" = banner only, "log" = silent, telemetry only
}
}
```
From 2a56d171a4cf0b6b70921cda3a71f009c766c2dc Mon Sep 17 00:00:00 2001
From: Logan Cook <2997336+MWG-Logan@users.noreply.github.com>
Date: Wed, 1 Jul 2026 22:57:13 -0400
Subject: [PATCH 05/28] fix(docs): phrasing fix
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Logan Cook <2997336+MWG-Logan@users.noreply.github.com>
---
docs/features/domain-squatting-detection.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/features/domain-squatting-detection.md b/docs/features/domain-squatting-detection.md
index 3528412..ee404d2 100644
--- a/docs/features/domain-squatting-detection.md
+++ b/docs/features/domain-squatting-detection.md
@@ -92,8 +92,8 @@ Check has an **"Enable Page Blocking"** setting in the extension options that co
- **Page Blocking Enabled** + **Action: "block"** = Page is completely blocked with full-page warning
- **Page Blocking Enabled** + **Action: "warn"** = Warning banner shown, page remains accessible
-- **Action: "log"** = Detection is recorded in Activity Logs and sent to reporting and webhooks only. No banner and no block are shown to the user, regardless of the Page Blocking setting.
-- **Page Blocking Disabled** = Never blocks. A `block` or `warn` action shows a warning banner instead; a `log` action stays silent.
+- **Action: "log"** = Detection is recorded in Activity Logs and (if configured) sent to reporting and webhooks. No banner and no block are shown to the user, regardless of the Page Blocking setting.
+- **Page Blocking Disabled** = Never blocks. If **Show Notifications** is enabled, a `block` or `warn` action shows a warning banner instead; a `log` action stays silent.
This gives you control over whether you want aggressive blocking, visible warnings, or silent monitoring for suspicious domains.
From 82c71dc4efd005e5d5a409681f8363f1e70bcd9c 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 06/28] 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 cad4445..e86b04a 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 94e90c8ec023951fe997202eb53536a992e9bb13 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 07/28] 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 38ff855..a6e2bf0 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 0000000..10d14c8
--- /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 0000000..cc496f2
--- /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 0000000..674428e
--- /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 0000000..18d5842
--- /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 14c8c1c3c869525fdb6b7439ef00bc91fc1fd54b Mon Sep 17 00:00:00 2001
From: John Duprey
Date: Wed, 29 Apr 2026 18:58:54 -0400
Subject: [PATCH 08/28] 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 0000000..e36b999
--- /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 fbb4545a9245a959553a69b92f8e715f6814d53b 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 09/28] 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 1a651c6..91de282 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 0000000..d97a6f2
--- /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 0000000..3b7974a
--- /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 38b93cc6c59d56e5670b2741c9938f063560e5aa 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 10/28] 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 3b7974a..ec512a7 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 71d1172d15d9e8efa01fcf566f44fb6cd55dac0a 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 11/28] 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 d97a6f2..9cd31b9 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 4dafcbc5fa6702c8e5d88ff56bc72dab94f5f107 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 12/28] 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 91de282..0062365 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 2a8dfbb1c8d3b13e11581b573c9b9cb8a6268f48 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 13/28] 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 0062365..3c6dffd 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 7110a7d48763758f8cc6f528524f2d6c991e0a3f 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 14/28] 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 3c6dffd..17ef647 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 dc5a767feed17bc6373014cdd03fbc5315ae9143 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 15/28] 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 17ef647..0f68679 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 ec512a7..df4f539 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 d8564fcc1e252fb83eeade1583415df01506ef9d 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 16/28] 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 af90153..0ba37ea 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 9cd31b9..4900347 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 840bcbe01a94c8d34658aad4942d60788a9f081a 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 17/28] 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 4900347..1b95c7d 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 df4f539..ea88f61 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 7f424a3c0c4a3ebe4d463314737f32149ef5b9c8 Mon Sep 17 00:00:00 2001
From: Brian Simpson
Date: Fri, 24 Apr 2026 16:07:32 +0000
Subject: [PATCH 18/28] 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 0f68679..ac600b7 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 79a53bc5d075d242e8601b22a355d7d728b60f63 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 19/28] 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 a40b660..4cf45fe 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 0a6bf57..f128ac3 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 06c4027852061751f71a5f67222def0d9e330277 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 20/28] 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 4cf45fe..048c846 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 f128ac3..453a178 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 3e7fb0840ddaf285e4d1e1dbd35b27079ad57690 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 21/28] 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 057b641..20e6e2c 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 d518f0c..078fe20 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 3ac0464..0c3ef30 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 0ba37ea..5003201 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 1b95c7d..5ef84ba 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 ea88f61..502b4ee 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 af97141..6369d7a 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 bb6a250..981d70e 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 fc9509b..5f1d3a5 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 e87ff3d..f80dabc 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 491c5a9..b2df83d 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 20db576..cb22fdc 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 7a87673..075e88b 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 ad6d35d..3825ede 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 1edde81..505ec84 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 2747c3ebdf165f9915d9582409871ad1cedd9a30 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 22/28] 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 048c846..02c93b4 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": ["