diff --git a/.github/workflows/validate-pr.yml b/.github/workflows/validate-pr.yml new file mode 100644 index 00000000..e36b9996 --- /dev/null +++ b/.github/workflows/validate-pr.yml @@ -0,0 +1,113 @@ +name: Validate PR + +on: + pull_request: + types: [opened, synchronize, reopened] + branches: + - dev + - main + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Lint JavaScript + run: | + if [ -f "eslint.config.js" ] || [ -f ".eslintrc.json" ] || [ -f ".eslintrc.js" ]; then + npx eslint scripts options popup --max-warnings 0 + else + echo "ℹ️ No ESLint configuration found. Skipping linting." + echo "To enable: Create eslint.config.js or .eslintrc.json at repo root" + fi + + codeql: + name: CodeQL Security Analysis + runs-on: ubuntu-latest + permissions: + security-events: write + steps: + - uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: 'javascript' + + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + + validate-json: + name: Validate JSON Files + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Validate manifest.json + run: | + node -e "require('fs').readFileSync('manifest.json'); console.log('✓ manifest.json is valid JSON')" + + - name: Validate Firefox manifest + run: | + node -e "require('fs').readFileSync('manifest.firefox.json'); console.log('✓ manifest.firefox.json is valid JSON')" + + - name: Validate config files + run: | + for file in config/*.json rules/*.json; do + if [ -f "$file" ]; then + node -e "require('fs').readFileSync('$file'); console.log('✓ $file is valid JSON')" || exit 1 + fi + done + + conventional-commits: + name: Validate Conventional Commits + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Validate PR title + run: | + PR_TITLE="${{ github.event.pull_request.title }}" + if ! echo "$PR_TITLE" | grep -E "^(feat|fix|docs|style|refactor|perf|test|chore|ci)(\(.+\))?!?: .+" > /dev/null; then + echo "❌ PR title does not follow Conventional Commits format" + echo "Expected format: feat|fix|docs|style|refactor|perf|test|chore|ci(scope)?: description" + echo "Received: $PR_TITLE" + exit 1 + fi + echo "✓ PR title follows Conventional Commits format" + + verify-changes: + name: Verify File Changes + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Check for deployment configuration updates + run: | + # If config/branding.json or config/managed_schema.json changed + if git diff origin/main HEAD --name-only | grep -q "^config/"; then + echo "⚠️ Configuration files were modified" + echo "Reminder: Review enterprise/Deploy-Windows-Chrome-and-Edge.ps1 and enterprise/admx/Check-Extension.admx" + fi + + # If manifest changed + if git diff origin/main HEAD --name-only | grep -qE "manifest.*\.json"; then + echo "ℹ️ Manifest was modified" + echo "Reminder: Test in both Chrome and Firefox" + fi diff --git a/config/managed_schema.json b/config/managed_schema.json index 057b6419..20e6e2c2 100644 --- a/config/managed_schema.json +++ b/config/managed_schema.json @@ -185,7 +185,7 @@ "title": "Enabled", "description": "Enable or disable domain squatting detection", "type": "boolean", - "default": true + "default": false }, "deviationThreshold": { "title": "Deviation Threshold", diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 38ff855d..a6e2bf08 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -17,6 +17,13 @@ - [MacOS](deployment/chrome-edge-deployment-instructions/macos.md) - [Firefox Deployment](deployment/firefox-deployment.md) +## Removal + +- [Removal Overview](removal/README.md) + - [Windows](removal/windows/README.md) + - [Chrome and Edge](removal/windows/chrome-edge.md) + - [Firefox](removal/windows/firefox.md) + ## Settings - [General](settings/general.md) diff --git a/docs/deployment/chrome-edge-deployment-instructions/macos.md b/docs/deployment/chrome-edge-deployment-instructions/macos.md index 0b65f0cc..45c2bec7 100644 --- a/docs/deployment/chrome-edge-deployment-instructions/macos.md +++ b/docs/deployment/chrome-edge-deployment-instructions/macos.md @@ -3,5 +3,127 @@ icon: apple --- # MacOS +I'd recommend that this be deployed via your MDM if the goal is to auto-deploy it without user interaction. -Coming soon. If you have experience deploying managed MacOS browser extensions, please contribute to the [docs via GitHub](https://github.com/CyberDrain/Check/tree/dev/docs). All Mac resources in the GitHub repo should be considered inaccurate until tested. +A custom .mobileconfig file can be uploaded to most MDMs for deployment if they don't have their own Google Chrome, or Microsoft Edge profile building functionality baked-in. + +Here's an example profile of the XML to create a mobileconfig that will install this in Microsoft Edge and Google Chrome. + +``` + + + + + PayloadContent + + + ExtensionInstallForcelist + + benimdeioplgkhanklclahllklceahbe + + PayloadDisplayName + Google Chrome + PayloadIdentifier + com.google.Chrome.23E5DDCF-1EB2-4869-9510-5E47D6640A85 + PayloadType + com.google.Chrome + PayloadUUID + 23E5DDCF-1EB2-4869-9510-5E47D6640A85 + PayloadVersion + 1 + + + ExtensionInstallForcelist + + knepjpocdagponkonnbggpcnhnaikajg + + PayloadDisplayName + Microsoft Edge + PayloadIdentifier + com.microsoft.Edge.DD4A940A-B216-4D5E-8B2C-1EF2CAFF7F38 + PayloadType + com.microsoft.Edge + PayloadUUID + DD4A940A-B216-4D5E-8B2C-1EF2CAFF7F38 + PayloadVersion + 1 + + + PayloadDescription + This profile installs and enforces the 'Check' browser extension from CyberDrain on Google Chrome and Microsoft Edge web browsers. + PayloadDisplayName + Check CyberDrain + PayloadIdentifier + 020D4Z7P-7F1A-4723-89CB-1826F8BAF4B5 + PayloadOrganization + YOUR ORG NAME + PayloadScope + System + PayloadType + Configuration + PayloadUUID + 020D4Z7P-7F1A-4723-89CB-1826F8BAF4B5 + PayloadVersion + 1 + RemovalDate + 2044-05-19T21:46:44Z + TargetDeviceType + 5 + + +``` +You could also deploy it in Chrome via command-line by creating the proper JSON object in the correct directory in the core /Library directory in macOS. Credit to @cezaraugusto for the script (slightly modified to simply install 'Check' if no parameter is passed...though technically you could pass any other Chrome extension ID after the script path and it would install that extension). + +``` +#!/bin/bash + +# https://developer.chrome.com/docs/extensions/mv3/external_extensions/#preferences +# Credit to #cezaraugusto# from GithubGist for this script...slightly modified for the purposes of installing Check by Cyberdrain if no parameter is passed +# https://gist.github.com/cezaraugusto +# https://gist.github.com/cezaraugusto/0101d2cb251c088f398ca0f8d4495ca0 + +extension=$1 + +if [[ -z "$extension" ]]; then + extension="benimdeioplgkhanklclahllklceahbe" +fi +install_chrome_extension() { + chrome_extensions_folder="/Library/Application Support/Google/Chrome/External Extensions" + chrome_extensions_preferences_file="$chrome_extensions_folder/$extension.json" + # This URL is used by Chrome to check for updates to external extensions + update_services_url="https://clients2.google.com/service/update2/crx" + +if [[ -d "$chrome_extensions_folder" ]]; then + mkdir -p "$chrome_extensions_folder" +fi + + echo "{" > "$chrome_extensions_preferences_file" + echo " \"external_update_url\": \"$update_services_url\"" >> "$chrome_extensions_preferences_file" + echo "}" >> "$chrome_extensions_preferences_file" + + echo "Added \"$chrome_extensions_preferences_file\"" +} + +if [ $# -ne 1 ]; then + echo "Usage: $0 " + exit 1 +fi + +install_chrome_extension "$extension" + +# Usage: +# ./install_extension.sh +# Sample: adding React Dev Tools from command-line to Chrome +# ./install_extension.sh fmkadmapgofadopljbjfkapdkoienihi +``` + +This would not install the extension until the next time Chrome is launched, and then it will require the user to approve it. + +SCR-20260520-krbi + + + +Due to limitations like this it really would be better to push it via an MDM. + + +If you have experience deploying managed MacOS browser extensions, please contribute to the [docs via GitHub](https://github.com/CyberDrain/Check/tree/dev/docs). All Mac resources in the GitHub repo should be considered inaccurate until tested. diff --git a/docs/deployment/chrome-edge-deployment-instructions/windows/domain-deployment.md b/docs/deployment/chrome-edge-deployment-instructions/windows/domain-deployment.md index 1a651c61..ac600b7a 100644 --- a/docs/deployment/chrome-edge-deployment-instructions/windows/domain-deployment.md +++ b/docs/deployment/chrome-edge-deployment-instructions/windows/domain-deployment.md @@ -2,33 +2,124 @@ {% 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 deploy Check with Intune. *** -#### 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. + +Download script -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 +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. -Import File +{% 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 -Documentation to follow -{% endtab %} +#### Prerequisites -{% tab title="Group Policy" %} +* 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 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 | +| ----------- | ------------------------------------------------------------------------------------------------------------ | +| 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** | +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 + +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 + +* **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)​ @@ -38,4 +129,11 @@ Documentation to follow ![](<../../../.gitbook/assets/image (2).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 %} + diff --git a/docs/features/domain-squatting-detection.md b/docs/features/domain-squatting-detection.md index d518f0c3..078fe204 100644 --- a/docs/features/domain-squatting-detection.md +++ b/docs/features/domain-squatting-detection.md @@ -108,7 +108,7 @@ Edit your `rules/detection-rules.json` file to customize: ```json { "domain_squatting": { - "enabled": true, // Turn detection on/off + "enabled": false, // Turn detection on/off (default: false) "action": "block" // Action when detected: "block" or "warn" } } @@ -125,7 +125,7 @@ Edit your `rules/detection-rules.json` file to customize: Note: Page blocking also requires "Enable Page Blocking" to be turned ON in settings. **Adjust Sensitivity:** - "enabled": true + "enabled": false } } ``` diff --git a/docs/removal/README.md b/docs/removal/README.md new file mode 100644 index 00000000..10d14c8b --- /dev/null +++ b/docs/removal/README.md @@ -0,0 +1,12 @@ +--- +description: Guides for removing Check enterprise policies and managed settings +icon: trash +--- + +# Removal + +Use this section to remove Check deployment configuration and managed policies from supported browsers. + +{% content-ref url="windows/" %} +[windows](windows/) +{% endcontent-ref %} diff --git a/docs/removal/windows/README.md b/docs/removal/windows/README.md new file mode 100644 index 00000000..cc496f2f --- /dev/null +++ b/docs/removal/windows/README.md @@ -0,0 +1,16 @@ +--- +description: Windows removal guidance for Check managed browser configuration +icon: windows +--- + +# Windows + +Use the pages below to remove Check enterprise settings from Windows endpoints. + +{% content-ref url="chrome-edge.md" %} +[chrome-edge.md](chrome-edge.md) +{% endcontent-ref %} + +{% content-ref url="firefox.md" %} +[firefox.md](firefox.md) +{% endcontent-ref %} diff --git a/docs/removal/windows/chrome-edge.md b/docs/removal/windows/chrome-edge.md new file mode 100644 index 00000000..674428eb --- /dev/null +++ b/docs/removal/windows/chrome-edge.md @@ -0,0 +1,13 @@ +# Chrome and Edge (Windows) + +If you need to fully remove Check managed enterprise configuration from Windows endpoints, use the uninstall script instead of manually deleting registry values. + +This removes all extension-specific policy values created during deployment for both Chrome and Edge, including nested settings such as domain squatting, webhook, branding, and allowlist values. + +## Uninstall Script + +1. Run the script as Administrator on the target endpoint. +2. Use this when testing policy changes and you want a clean baseline before re-deploying. +3. After running, restart Chrome and Edge to ensure policy refresh. + +Download the Uninstall Script from GitHub diff --git a/docs/removal/windows/firefox.md b/docs/removal/windows/firefox.md new file mode 100644 index 00000000..18d58420 --- /dev/null +++ b/docs/removal/windows/firefox.md @@ -0,0 +1,11 @@ +# Firefox (Windows) + +Firefox enterprise removal for Check is managed through the Firefox policies file. + +## General Removal Steps + +1. Remove Check entries from `%ProgramFiles%\\Mozilla Firefox\\distribution\\policies.json`. +2. Remove extension lock and install directives related to Check. +3. Restart Firefox to apply policy changes. + +For deployment and policy format details, see [Firefox Deployment](../../deployment/firefox-deployment.md). diff --git a/enterprise/Check-Extension-Policy.reg b/enterprise/Check-Extension-Policy.reg index 3ac04642..0c3ef307 100644 --- a/enterprise/Check-Extension-Policy.reg +++ b/enterprise/Check-Extension-Policy.reg @@ -21,7 +21,7 @@ Windows Registry Editor Version 5.00 "enableDebugLogging"=dword:00000000 [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Edge\3rdparty\extensions\knepjpocdagponkonnbggpcnhnaikajg\policy\domainSquatting] -"enabled"=dword:00000001 +"enabled"=dword:00000000 ; Generic webhook configuration (optional) [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Edge\3rdparty\extensions\knepjpocdagponkonnbggpcnhnaikajg\policy\genericWebhook] @@ -68,7 +68,7 @@ Windows Registry Editor Version 5.00 "enableDebugLogging"=dword:00000000 [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Google\Chrome\3rdparty\extensions\benimdeioplgkhanklclahllklceahbe\policy\domainSquatting] -"enabled"=dword:00000001 +"enabled"=dword:00000000 ; Generic webhook configuration for Chrome (optional) [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Google\Chrome\3rdparty\extensions\benimdeioplgkhanklclahllklceahbe\policy\genericWebhook] diff --git a/enterprise/Deploy-Windows-Chrome-and-Edge.ps1 b/enterprise/Deploy-Windows-Chrome-and-Edge.ps1 index af90153c..5003201a 100644 --- a/enterprise/Deploy-Windows-Chrome-and-Edge.ps1 +++ b/enterprise/Deploy-Windows-Chrome-and-Edge.ps1 @@ -22,7 +22,7 @@ $cippTenantId = "" # This will set the "Tenant ID/Domain" option in the extensio $customRulesUrl = "" # This will set the "Config URL" option in the Detection Configuration settings; default is blank. $updateInterval = 24 # This will set the "Update Interval" option in the Detection Configuration settings; default is 24 (hours). Range: 1-168 hours (1 hour to 1 week). $urlAllowlist = @() # This will set the "URL Allowlist" option in the Detection Configuration settings; default is blank; if you want to add multiple URLs, add them as a comma-separated list within the brackets (e.g., @("https://example1.com", "https://example2.com")). Supports simple URLs with * wildcard (e.g., https://*.example.com) or advanced regex patterns (e.g., ^https:\/\/(www\.)?example\.com\/.*$). -$domainSquattingEnabled = 1 # 0 = Disabled, 1 = Enabled; default is 1; controls domain squatting detection from managed policy/config. +$domainSquattingEnabled = 0 # 0 = Disabled, 1 = Enabled; default is 0; controls domain squatting detection from managed policy/config. $enableDebugLogging = 0 # 0 = Unchecked, 1 = Checked (Enabled); default is 0; This will set the "Enable Debug Logging" option in the Activity Log settings. # Generic Webhook Settings @@ -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 new file mode 100644 index 00000000..5ef84ba8 --- /dev/null +++ b/enterprise/Detect-Windows-Chrome-and-Edge.ps1 @@ -0,0 +1,249 @@ +# 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 = 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 +$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' + ToolbarPinnedValue = 'force_pinned' + ToolbarUnpinnedValue = 'default_unpinned' + }, + @{ + Name = 'Edge' + ExtensionId = $edgeExtensionId + UpdateUrl = $edgeUpdateUrl + ManagedStorageKey = $edgeManagedStorageKey + ExtensionSettingsKey = $edgeExtensionSettingsKey + ToolbarProp = 'toolbar_state' + ToolbarPinnedValue = 'force_shown' + ToolbarUnpinnedValue = 'hidden' + } +) + +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)) { + Write-DetectionFailure -BrowserName $browser.Name -KeyPath $browser.ManagedStorageKey -ValueName $null -ExpectedValue $null -ActualValue $null + exit 1 + } + + $policyKey = $browser.ManagedStorageKey + + # Core DWord settings + if (!(Test-RegValueWithDetails $browser.Name $policyKey 'showNotifications' $showNotifications)) { exit 1 } + if (!(Test-RegValueWithDetails $browser.Name $policyKey 'enableValidPageBadge' $enableValidPageBadge)) { exit 1 } + if (!(Test-RegValueWithDetails $browser.Name $policyKey 'enablePageBlocking' $enablePageBlocking)) { exit 1 } + if (!(Test-RegValueWithDetails $browser.Name $policyKey 'enableCippReporting' $enableCippReporting)) { exit 1 } + if (!(Test-RegValueWithDetails $browser.Name $policyKey 'updateInterval' $updateInterval)) { exit 1 } + if (!(Test-RegValueWithDetails $browser.Name $policyKey 'enableDebugLogging' $enableDebugLogging)) { exit 1 } + + # Core String settings + if (!(Test-RegValueWithDetails $browser.Name $policyKey 'cippServerUrl' $cippServerUrl)) { exit 1 } + if (!(Test-RegValueWithDetails $browser.Name $policyKey 'cippTenantId' $cippTenantId)) { exit 1 } + if (!(Test-RegValueWithDetails $browser.Name $policyKey 'customRulesUrl' $customRulesUrl)) { exit 1 } + + # domainSquatting subkey + $domainSquattingKey = "$policyKey\domainSquatting" + if (!(Test-Path $domainSquattingKey)) { + Write-DetectionFailure -BrowserName $browser.Name -KeyPath $domainSquattingKey -ValueName $null -ExpectedValue $null -ActualValue $null + exit 1 + } + if (!(Test-RegValueWithDetails $browser.Name $domainSquattingKey 'enabled' $domainSquattingEnabled)) { exit 1 } + + # customBranding subkey + $brandingKey = "$policyKey\customBranding" + if (!(Test-Path $brandingKey)) { + Write-DetectionFailure -BrowserName $browser.Name -KeyPath $brandingKey -ValueName $null -ExpectedValue $null -ActualValue $null + exit 1 + } + 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)) { + 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)) { + 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) { + 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-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) { + 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)) { + 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) { + 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-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) { + Write-Output "$($browser.Name): Registry key '$allowlistKey' has unexpected allowlist value(s); expected none." + exit 1 + } + } + + # ExtensionSettings key + 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 } + if (!(Test-RegValueWithDetails $browser.Name $browser.ExtensionSettingsKey $browser.ToolbarProp $expectedToolbar)) { exit 1 } +} + +Write-Output "Check extension is correctly configured for Chrome and Edge." +exit 0 diff --git a/enterprise/Remove-Windows-Chrome-and-Edge.ps1 b/enterprise/Remove-Windows-Chrome-and-Edge.ps1 index cad4445e..e86b04a6 100644 --- a/enterprise/Remove-Windows-Chrome-and-Edge.ps1 +++ b/enterprise/Remove-Windows-Chrome-and-Edge.ps1 @@ -17,155 +17,28 @@ function Remove-ExtensionSettings { [string]$ExtensionSettingsKey ) - # Remove properties from managed storage key + # Remove all managed policy values and nested keys created for this extension. if (Test-Path $ManagedStorageKey) { - $propertiesToRemove = @( - "showNotifications", - "enableValidPageBadge", - "enablePageBlocking", - "enableCippReporting", - "cippServerUrl", - "cippTenantId", - "customRulesUrl", - "updateInterval", - "enableDebugLogging" - ) - - foreach ($property in $propertiesToRemove) { - if (Get-ItemProperty -Path $ManagedStorageKey -Name $property -ErrorAction SilentlyContinue) { - Remove-ItemProperty -Path $ManagedStorageKey -Name $property -Force -ErrorAction SilentlyContinue - Write-Host "Removed property: $property from $ManagedStorageKey" - } - } - - # Remove URL allowlist subkey and all its properties - $urlAllowlistKey = "$ManagedStorageKey\urlAllowlist" - if (Test-Path $urlAllowlistKey) { - # Remove all numbered properties (1, 2, 3, etc.) - $properties = Get-ItemProperty -Path $urlAllowlistKey -ErrorAction SilentlyContinue - if ($properties) { - $properties.PSObject.Properties | Where-Object { $_.Name -match '^\d+$' } | ForEach-Object { - Remove-ItemProperty -Path $urlAllowlistKey -Name $_.Name -Force -ErrorAction SilentlyContinue - Write-Host "Removed URL allowlist property: $($_.Name) from $urlAllowlistKey" - } - } - # Remove the urlAllowlist subkey if it's empty - try { - Remove-Item -Path $urlAllowlistKey -Force -ErrorAction SilentlyContinue - Write-Host "Removed URL allowlist subkey: $urlAllowlistKey" - } catch { - # Key may not be empty or may have been removed already - } - } - - # Remove generic webhook subkey and event properties - $genericWebhookKey = "$ManagedStorageKey\genericWebhook" - if (Test-Path $genericWebhookKey) { - $webhookEventsKey = "$genericWebhookKey\events" - if (Test-Path $webhookEventsKey) { - $eventProperties = Get-ItemProperty -Path $webhookEventsKey -ErrorAction SilentlyContinue - if ($eventProperties) { - $eventProperties.PSObject.Properties | Where-Object { $_.Name -match '^\d+$' } | ForEach-Object { - Remove-ItemProperty -Path $webhookEventsKey -Name $_.Name -Force -ErrorAction SilentlyContinue - Write-Host "Removed webhook event property: $($_.Name) from $webhookEventsKey" - } - } - try { - Remove-Item -Path $webhookEventsKey -Force -ErrorAction SilentlyContinue - Write-Host "Removed webhook events subkey: $webhookEventsKey" - } catch { - # Key may not be empty or may have been removed already - } - } - - foreach ($property in @("enabled", "url")) { - if (Get-ItemProperty -Path $genericWebhookKey -Name $property -ErrorAction SilentlyContinue) { - Remove-ItemProperty -Path $genericWebhookKey -Name $property -Force -ErrorAction SilentlyContinue - Write-Host "Removed generic webhook property: $property from $genericWebhookKey" - } - } - - try { - Remove-Item -Path $genericWebhookKey -Force -ErrorAction SilentlyContinue - Write-Host "Removed generic webhook subkey: $genericWebhookKey" - } catch { - # Key may not be empty or may have been removed already - } - } - - # Remove custom branding subkey and all its properties - $customBrandingKey = "$ManagedStorageKey\customBranding" - if (Test-Path $customBrandingKey) { - $brandingPropertiesToRemove = @( - "companyName", - "productName", - "supportEmail", - "supportUrl", - "privacyPolicyUrl", - "aboutUrl", - "primaryColor", - "logoUrl" - ) - - foreach ($property in $brandingPropertiesToRemove) { - if (Get-ItemProperty -Path $customBrandingKey -Name $property -ErrorAction SilentlyContinue) { - Remove-ItemProperty -Path $customBrandingKey -Name $property -Force -ErrorAction SilentlyContinue - Write-Host "Removed custom branding property: $property from $customBrandingKey" - } - } - - # Remove the customBranding subkey if it's empty - try { - Remove-Item -Path $customBrandingKey -Force -ErrorAction SilentlyContinue - Write-Host "Removed custom branding subkey: $customBrandingKey" - } catch { - # Key may not be empty or may have been removed already - } - } - - # Remove the managed storage key if it's empty try { - $remainingProperties = Get-ItemProperty -Path $ManagedStorageKey -ErrorAction SilentlyContinue - if ($remainingProperties -and $remainingProperties.PSObject.Properties.Count -eq 0) { - Remove-Item -Path $ManagedStorageKey -Force -ErrorAction SilentlyContinue - Write-Host "Removed managed storage key: $ManagedStorageKey" - } + Remove-Item -Path $ManagedStorageKey -Recurse -Force -ErrorAction Stop + Write-Host "Removed managed storage key: $ManagedStorageKey" } catch { - # Key may not be empty or may have been removed already + Write-Warning "Failed to remove managed storage key: $ManagedStorageKey. Error: $($_.Exception.Message)" } + } else { + Write-Host "Managed storage key not found (already removed): $ManagedStorageKey" } - # Remove properties from extension settings key + # Remove extension install/settings key for this extension. if (Test-Path $ExtensionSettingsKey) { - $extensionPropertiesToRemove = @( - "installation_mode", - "update_url" - ) - - # Add browser-specific toolbar properties - if ($ExtensionId -eq $edgeExtensionId) { - $extensionPropertiesToRemove += "toolbar_state" - } elseif ($ExtensionId -eq $chromeExtensionId) { - $extensionPropertiesToRemove += "toolbar_pin" - } - - foreach ($property in $extensionPropertiesToRemove) { - if (Get-ItemProperty -Path $ExtensionSettingsKey -Name $property -ErrorAction SilentlyContinue) { - Remove-ItemProperty -Path $ExtensionSettingsKey -Name $property -Force -ErrorAction SilentlyContinue - Write-Host "Removed extension setting property: $property from $ExtensionSettingsKey" - } - } - - # Remove the extension settings key if it's empty try { - $remainingProperties = Get-ItemProperty -Path $ExtensionSettingsKey -ErrorAction SilentlyContinue - if ($remainingProperties -and $remainingProperties.PSObject.Properties.Count -eq 0) { - Remove-Item -Path $ExtensionSettingsKey -Force -ErrorAction SilentlyContinue - Write-Host "Removed extension settings key: $ExtensionSettingsKey" - } + Remove-Item -Path $ExtensionSettingsKey -Recurse -Force -ErrorAction Stop + Write-Host "Removed extension settings key: $ExtensionSettingsKey" } catch { - # Key may not be empty or may have been removed already + Write-Warning "Failed to remove extension settings key: $ExtensionSettingsKey. Error: $($_.Exception.Message)" } + } else { + Write-Host "Extension settings key not found (already removed): $ExtensionSettingsKey" } Write-Host "Completed removal of extension settings for $ExtensionId" diff --git a/enterprise/Setup-Windows-Chrome-and-Edge.ps1 b/enterprise/Setup-Windows-Chrome-and-Edge.ps1 new file mode 100644 index 00000000..502b4ee4 --- /dev/null +++ b/enterprise/Setup-Windows-Chrome-and-Edge.ps1 @@ -0,0 +1,316 @@ +# 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 "0" -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 ', '))" +} + +# 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 = @( + @{ 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 = $(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 = $(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 +$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, + [string]$TemplateName + ) + + $missing = [System.Collections.Generic.List[string]]::new() + + foreach ($r in $replacements) { + if ($Content.Contains($r.Pattern)) { + $Content = $Content.Replace($r.Pattern, $r.Value) + } else { + $missing.Add($r.Pattern) + } + } + foreach ($r in $arrayReplacements) { + 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'] -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'] -TemplateName '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 "" diff --git a/enterprise/admx/en-US/Check-Extension.adml b/enterprise/admx/en-US/Check-Extension.adml index af971413..6369d7ab 100644 --- a/enterprise/admx/en-US/Check-Extension.adml +++ b/enterprise/admx/en-US/Check-Extension.adml @@ -282,8 +282,8 @@ This policy controls domain squatting detection in the Check extension. - When enabled (default): Typosquatting, homoglyph, and combosquatting protections are active. - When disabled: Domain squatting detections are skipped. + When enabled: Typosquatting, homoglyph, and combosquatting protections are active. + When disabled (default): Domain squatting detections are skipped. Enable debug logging (Chrome) @@ -299,8 +299,8 @@ This policy controls domain squatting detection in the Check extension for Google Chrome. - When enabled (default): Typosquatting, homoglyph, and combosquatting protections are active. - When disabled: Domain squatting detections are skipped. + When enabled: Typosquatting, homoglyph, and combosquatting protections are active. + When disabled (default): Domain squatting detections are skipped. Show valid page badge (Chrome) diff --git a/enterprise/firefox/policies.json b/enterprise/firefox/policies.json index bb6a2505..981d70e1 100644 --- a/enterprise/firefox/policies.json +++ b/enterprise/firefox/policies.json @@ -29,7 +29,7 @@ "updateInterval": 24, "urlAllowlist": [], "domainSquatting": { - "enabled": true + "enabled": false }, "enableDebugLogging": false, "customBranding": { diff --git a/enterprise/macos-linux/README.md b/enterprise/macos-linux/README.md index fc9509bc..5f1d3a5c 100644 --- a/enterprise/macos-linux/README.md +++ b/enterprise/macos-linux/README.md @@ -166,7 +166,7 @@ All settings are based on the managed schema and include: ### Rule Management - **`customRulesUrl`** - URL for custom detection rules - **`updateInterval`** - Rule update interval in hours (default: 24) -- **`domainSquatting.enabled`** - Enable/disable domain squatting detection (default: true) +- **`domainSquatting.enabled`** - Enable/disable domain squatting detection (default: false) ### Custom Branding - **`companyName`** - Company name for white labeling diff --git a/enterprise/macos-linux/check-extension-config.mobileconfig b/enterprise/macos-linux/check-extension-config.mobileconfig index e87ff3de..f80dabcf 100644 --- a/enterprise/macos-linux/check-extension-config.mobileconfig +++ b/enterprise/macos-linux/check-extension-config.mobileconfig @@ -73,7 +73,7 @@ Value enabled - + diff --git a/options/options.js b/options/options.js index 491c5a9c..b2df83da 100644 --- a/options/options.js +++ b/options/options.js @@ -1000,7 +1000,7 @@ class CheckOptions { if (this.elements.domainSquattingEnabled) { this.elements.domainSquattingEnabled.checked = - this.config?.domainSquatting?.enabled !== false; + this.config?.domainSquatting?.enabled === true; } // Handle updateInterval - ensure we always show hours in the UI @@ -1257,7 +1257,7 @@ class CheckOptions { // Domain squatting settings (runtime control moved from rules JSON) domainSquatting: { - enabled: this.elements.domainSquattingEnabled?.checked !== false, + enabled: this.elements.domainSquattingEnabled?.checked === true, deviationThreshold: existingDomainSquatting.deviationThreshold ?? 2, algorithms: { levenshtein: diff --git a/rules/detection-rules.json b/rules/detection-rules.json index a40b6603..7edd3975 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.3", + "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)$", @@ -143,6 +143,96 @@ "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" + }, + { + "id": "canvas_streaming_ms_kit", + "type": "code_driven", + "code_logic": { + "type": "all_of", + "operations": [ + { "type": "substring_present", "values": ["Outlook", + "Microsoft 365", + "Office 365", + "Microsoft account", + "Sign in to your account" + ] + } + ] + } + ] + }, + "description": "Remote-browser-streaming phishing kit signature: full-viewport + Socket.IO/WebSocket transport + Microsoft/Outlook/Office reference (favicon hotlink or exact title). Catches BrowserClone/Castle/Pixel-stream-style kits that paint Microsoft login UI as pixels to defeat DOM-based detection.", + "weight": 3, + "category": "primary" } ], "secondary_elements": [ @@ -319,8 +409,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" } @@ -380,6 +470,32 @@ }, "action": "block", "severity": "critical" + }, + { + "id": "aitm_microsoft_markers_off_origin", + "type": "aitm_origin_validation", + "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", + "loginfmt", + "urlMsaSignUp", + "flowToken", + "aadcdn\\.msauth\\.net", + "aadcdn\\.msftauth\\.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, + "require_non_trusted_host": true + }, + "action": "block", + "severity": "critical" } ], "allow_rules": [ @@ -1567,6 +1683,63 @@ "action": "warn", "category": "text_obfuscation", "confidence": 0.9 + }, + { + "id": "phi_036_canvas_streaming_ms_impersonation", + "code_driven": true, + "code_logic": { + "type": "all_of", + "operations": [ + { "type": "substring_present", "values": ["Outlook", + "Microsoft 365", + "Office 365", + "Microsoft account", + "Sign in to your account" + ] + } + ] + } + ] + }, + "severity": "critical", + "description": "Remote-browser-streaming phishing kit detected: page is a -only UI streamed via Socket.IO/WebSocket from a server-side headless browser, with Microsoft/Outlook/Office branding (favicon hotlink or impersonating title). Defeats DOM-based detection because the rendered UI exists only as pixels.", + "action": "warn", + "category": "remote_browser_phishing", + "confidence": 0.9 + }, + { + "id": "phi_034_wordlist_path_kit", + "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, + "url_only": true } ], "legitimate_patterns": [ @@ -1831,7 +2004,7 @@ "rogue_apps_detection": { "description": "Dynamic detection of known rogue OAuth applications", "enabled": true, - "source_url": "https://raw.githubusercontent.com/huntresslabs/rogueapps/refs/heads/main/public/rogueapps.json", + "source_url": "https://huntresslabs.github.io/rogueapps/rogueapps.json", "cache_duration": 86400000, "update_interval": 43200000, "detection_action": "warn", diff --git a/scripts/background.js b/scripts/background.js index 20db5765..cb22fdc3 100644 --- a/scripts/background.js +++ b/scripts/background.js @@ -289,7 +289,10 @@ class CheckBackground { constructor() { this.configManager = new ConfigManager(); this.policyManager = new PolicyManager(); - this.detectionRulesManager = new DetectionRulesManager(this.configManager); + this.detectionRulesManager = new DetectionRulesManager( + this.configManager, + (rules) => this._handleDetectionRulesUpdated(rules) + ); this.rogueAppsManager = new RogueAppsManager(); this.domainSquattingDetector = new DomainSquattingDetector(); this.webhookManager = new WebhookManager(this.configManager); @@ -866,6 +869,28 @@ class CheckBackground { } } + /** + * Invoked by DetectionRulesManager whenever a remote rules fetch succeeds + * (forced, on-save, or via the lazy background refresh triggered by page + * detection). Re-initializes downstream subsystems with the new rules so + * they don't lag behind the cache. + */ + async _handleDetectionRulesUpdated(rules) { + try { + if (!rules || !this.domainSquattingDetector) return; + const runtimeConfig = await this.configManager.getConfig(); + await this.domainSquattingDetector.initialize(rules, runtimeConfig); + logger.log( + "Domain squatting detector re-initialized after rules refresh" + ); + } catch (error) { + logger.warn( + "Failed to re-init domain squatting detector after rules refresh:", + error?.message || error + ); + } + } + async handleTabUpdate(tabId, changeInfo, tab) { if (!this.isInitialized) return; @@ -1487,6 +1512,10 @@ class CheckBackground { const previousBadgeEnabled = currentConfig?.enableValidPageBadge || this.policy?.EnableValidPageBadge; + const previousRulesUrl = + currentConfig?.customRulesUrl || + currentConfig?.detectionRules?.customRulesUrl || + null; // Update the configuration await this.configManager.updateConfig(message.config); @@ -1500,6 +1529,31 @@ class CheckBackground { updatedConfig?.enableValidPageBadge || this.policy?.EnableValidPageBadge; + // On config save, always pull fresh rules so a URL change (or any + // other policy save) immediately reflects new rules in cache and + // in the domain-squatting detector. The onRulesUpdated callback + // wired into DetectionRulesManager will reinit the squatting + // detector once the fetch completes, so we don't need to do that + // here for the success path - but we still re-init synchronously + // below to cover the case where the network fetch fails. + const newRulesUrl = + updatedConfig?.customRulesUrl || + updatedConfig?.detectionRules?.customRulesUrl || + null; + this.detectionRulesManager.updateDetectionRules().catch((err) => { + logger.warn( + "Pull-on-save detection-rules update failed:", + err?.message || err + ); + }); + if (previousRulesUrl !== newRulesUrl) { + logger.log( + `customRulesUrl changed (${previousRulesUrl || ""} -> ${ + newRulesUrl || "" + }) - background rules pull triggered` + ); + } + // Update domain squatting detector with new configuration // If URL allowlist changed, reinitialize detector to extract new domains if (this.domainSquattingDetector) { diff --git a/scripts/content.js b/scripts/content.js index 0a6bf57e..453a178a 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}`); } @@ -2664,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 */ @@ -4253,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( diff --git a/scripts/modules/config-manager.js b/scripts/modules/config-manager.js index 7a87673b..075e88b2 100644 --- a/scripts/modules/config-manager.js +++ b/scripts/modules/config-manager.js @@ -112,7 +112,7 @@ export class ConfigManager { updateInterval: 24, enableDebugLogging: false, domainSquatting: { - enabled: true, + enabled: false, deviationThreshold: 2, algorithms: { levenshtein: true, @@ -306,7 +306,7 @@ export class ConfigManager { // Domain squatting runtime settings domainSquatting: { - enabled: true, + enabled: false, deviationThreshold: 2, algorithms: { levenshtein: true, diff --git a/scripts/modules/detection-rules-manager.js b/scripts/modules/detection-rules-manager.js index ad6d35d3..3825ede4 100644 --- a/scripts/modules/detection-rules-manager.js +++ b/scripts/modules/detection-rules-manager.js @@ -7,7 +7,7 @@ import { chrome, storage } from "../browser-polyfill.js"; import logger from "../utils/logger.js"; export class DetectionRulesManager { - constructor(configManager = null) { + constructor(configManager = null, onRulesUpdated = null) { this.cachedRules = null; this.lastUpdate = 0; this.updateInterval = 24 * 60 * 60 * 1000; // Default: 24 hours @@ -18,6 +18,9 @@ export class DetectionRulesManager { this.config = null; this.configManager = configManager; this.initialized = false; + this.onRulesUpdated = onRulesUpdated; + this._refreshInFlight = null; + this._usingFallback = false; } async initialize() { @@ -161,7 +164,9 @@ export class DetectionRulesManager { rules = await response.json(); logger.log("Successfully fetched detection rules from remote URL"); - // Save to cache + // Persist only successful remote fetches so the cache always reflects + // the true remote state - never poisoned by bundled fallback content. + this._usingFallback = false; await this.saveToCache(rules); return rules; } catch (error) { @@ -169,9 +174,14 @@ export class DetectionRulesManager { } } - // Fallback to local rules + // Remote fetch failed (or no remote URL configured): serve the bundled + // rules in-memory but DO NOT persist them. lastUpdate stays at 0 so the + // next getDetectionRules() call treats the cache as stale and retries the + // remote URL - the persistent cache remains a record of remote-only state. try { - logger.log("Falling back to local detection rules"); + logger.log( + "Falling back to bundled detection rules (in-memory only; will retry remote on next refresh)" + ); const response = await fetch(this.fallbackUrl); if (!response.ok) { @@ -179,13 +189,14 @@ export class DetectionRulesManager { } rules = await response.json(); - logger.log("Successfully loaded local detection rules"); + logger.log("Loaded bundled detection rules"); - // Save to cache as fallback - await this.saveToCache(rules); + this.cachedRules = rules; + this.lastUpdate = 0; // Force next access to re-attempt the remote fetch + this._usingFallback = true; return rules; } catch (error) { - logger.error("Failed to load local detection rules:", error.message); + logger.error("Failed to load bundled detection rules:", error.message); throw error; } } @@ -194,6 +205,19 @@ export class DetectionRulesManager { try { const rules = await this.fetchDetectionRules(); + // Notify the background-script wiring so dependent subsystems (e.g. the + // domain-squatting detector) can re-initialize with the new rules. + if (typeof this.onRulesUpdated === "function") { + try { + await this.onRulesUpdated(rules); + } catch (callbackError) { + logger.warn( + "onRulesUpdated callback threw:", + callbackError?.message || callbackError + ); + } + } + // Notify other parts of the extension that rules have been updated if ( typeof chrome !== "undefined" && @@ -217,24 +241,63 @@ export class DetectionRulesManager { } } + /** + * Kick off a non-blocking refresh of the detection rules. Used by + * getDetectionRules() to trigger a background refresh whenever a page + * detection requests rules and the cache is past the configured interval, + * without making the requesting page wait for the network round-trip. + * Guarded against overlapping in-flight refreshes. + */ + _scheduleBackgroundRefresh(reason) { + if (this._refreshInFlight) { + return this._refreshInFlight; + } + logger.log( + `Scheduling background detection-rules refresh (${reason || "expired"})` + ); + this._refreshInFlight = this.updateDetectionRules() + .catch((err) => { + logger.warn( + "Background detection-rules refresh failed:", + err?.message || err + ); + }) + .finally(() => { + this._refreshInFlight = null; + }); + return this._refreshInFlight; + } + async getDetectionRules() { - // Return cached rules if available and fresh - if (this.cachedRules) { - const now = Date.now(); + const now = Date.now(); + + // Fast path: in-memory cache exists from a previous successful fetch. + if (this.cachedRules && this.lastUpdate > 0) { const cacheAge = now - this.lastUpdate; if (cacheAge < this.updateInterval) { + // Fresh - return immediately, no network. return this.cachedRules; } + + // Cache is past the configured refresh interval. Return the (still usable) + // cached rules immediately so page detection isn't blocked, and kick off + // a non-blocking remote refresh whose result lands on the NEXT request. + this._scheduleBackgroundRefresh( + `cache age ${Math.round(cacheAge / 60000)}m > interval ${Math.round( + this.updateInterval / 60000 + )}m` + ); + return this.cachedRules; } - // Need to fetch fresh rules + // No usable cache yet (cold start or running on bundled fallback after a + // remote failure). Block on a real fetch so callers don't get null. try { return await this.fetchDetectionRules(); } catch (error) { - // Return cached rules as last resort, even if expired if (this.cachedRules) { - logger.warn("Using expired cached rules due to fetch failure"); + logger.warn("Using bundled cached rules due to fetch failure"); return this.cachedRules; } throw error; @@ -257,6 +320,8 @@ export class DetectionRulesManager { isExpired: this.lastUpdate ? Date.now() - this.lastUpdate > this.updateInterval : true, + usingFallback: this._usingFallback, + refreshInFlight: !!this._refreshInFlight, }; } } diff --git a/scripts/modules/domain-squatting-detector.js b/scripts/modules/domain-squatting-detector.js index 1edde814..505ec845 100644 --- a/scripts/modules/domain-squatting-detector.js +++ b/scripts/modules/domain-squatting-detector.js @@ -8,7 +8,7 @@ import logger from '../utils/logger.js'; export class DomainSquattingDetector { constructor() { this.protectedDomains = []; - this.enabled = true; + this.enabled = false; this.action = 'block'; this.minimumSeverity = 'high'; this.logDetections = true; @@ -141,7 +141,7 @@ export class DomainSquattingDetector { const rulesDomainSquatting = rulesConfig?.domain_squatting || {}; const runtimeDomainSquatting = runtimeConfig?.domainSquatting || {}; - this.enabled = runtimeDomainSquatting.enabled !== false; + this.enabled = runtimeDomainSquatting.enabled === true; this.protectedDomains = rulesDomainSquatting.protected_domains || []; this.deviationThreshold = runtimeDomainSquatting.deviationThreshold ||