Dev to release - #134
Dev to release#134
Conversation
* Update detection-rules.json Signed-off-by: Zacgoose <107489668+Zacgoose@users.noreply.github.com> * Update detection-rules.json Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: John Duprey <jwd@johnwduprey.com> * Update detection-rules.json Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: John Duprey <jwd@johnwduprey.com> * Update rules/detection-rules.json Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Zacgoose <107489668+Zacgoose@users.noreply.github.com> * Update detection-rules.json Signed-off-by: Zacgoose <107489668+Zacgoose@users.noreply.github.com> --------- Signed-off-by: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Signed-off-by: John Duprey <jwd@johnwduprey.com> Co-authored-by: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Co-authored-by: John Duprey <jwd@johnwduprey.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: John Duprey <jwd@johnwduprey.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: John Duprey <jwd@johnwduprey.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: John Duprey <jwd@johnwduprey.com>
Updated content and detection rules manager scripts to retrieve configuration and branding data via chrome.runtime messaging to the background script, ensuring merged enterprise and local config is used. DetectionRulesManager now accepts a ConfigManager instance for improved config access. Fallbacks to local storage remain for robustness.
…port Add domain squatting detection with typosquatting, homoglyphs, combosquatting, Levenshtein distance, configurable page blocking, CIPP reporting, webhook integration, and unified URL allowlist configuration
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Zacgoose <107489668+Zacgoose@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Zacgoose <107489668+Zacgoose@users.noreply.github.com>
Introduce supportUrl, privacyPolicyUrl and aboutUrl branding properties across the project. Updates include managed schema, default configs, enterprise policy templates (REG/PS/ADMX/ADML/JSON), options and popup UI/JS, config manager merging logic (including deriving supportUrl from supportEmail when missing), and tests covering branding link behavior. Also removes the legacy companyURL field from many places and updates docs to show the new properties and examples.
Signed-off-by: Zacgoose <107489668+Zacgoose@users.noreply.github.com>
Add support/privacy/about branding URLs
Add `msn.com` and `xbox.com` and `mcas.ms` to Microsoft domain allow-list in detection rules
Typo protection ux scan fixes
Signed-off-by: Zacgoose <107489668+Zacgoose@users.noreply.github.com>
Add enterprise webhook policy parity across ADMX, deployment templates and docs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Zacgoose <107489668+Zacgoose@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Zacgoose <107489668+Zacgoose@users.noreply.github.com>
Firefox changes and feedback changes
Signed-off-by: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR prepares the project for a release by renaming “ProjectX” references to “Check”, expanding branding configuration support, and adding a new domain-squatting detection capability integrated into the background + content-script flow.
Changes:
- Add domain squatting detection (new module, ruleset config, background/content integration, UI messaging on blocked/warn flows).
- Update branding model (support/privacy/about URLs, enterprise/local merge behavior) and propagate to popup/options/blocked experiences.
- Release packaging + docs updates (version bumps, schema/policy templates, documentation restructuring).
Reviewed changes
Copilot reviewed 36 out of 36 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/config-persistence.test.js | Adds test coverage for branding-link precedence and mailto derivation. |
| scripts/modules/domain-squatting-detector.js | New detector module (Levenshtein/homoglyph/typosquat/combosquat) + allowlist-domain extraction. |
| scripts/modules/detection-rules-manager.js | Allows using ConfigManager to load merged (enterprise + local) config. |
| scripts/modules/config-manager.js | Updates default URLs, adds domainSquatting defaults, expands branding fields, updates branding merge/derivation. |
| scripts/content.js | Integrates domain-squatting check, refactors indicator evaluation portability, improves blocking/banner DOM creation, adjusts scan timing. |
| scripts/blocked.js | Fixes URL param decoding behavior and adds domain-squatting specific UI/technical-details rendering. |
| scripts/background.js | Instantiates/initializes detector, adds domain-squatting message handler, adds redirect fallback handler, updates threat event enrichment defaults. |
| rules/detection-rules.json | Bumps rules version, expands domain patterns, introduces domain_squatting config + protected domains, refines code-driven indicators. |
| popup/popup.js | Updates branding link behavior (aboutUrl support), removes company-link handling, avoids innerHTML in several places. |
| popup/popup.html | Removes clickable company link in footer. |
| package.json | Renames package and bumps version. |
| package-lock.json | Renames package metadata. |
| options/options.js | Adds domain-squatting toggle, branding URL fields, collapsible config overview UI, more CSP-safe DOM updates, policy reset logic changes. |
| options/options.html | Adds domain-squatting setting + branding URL fields; adds expand/collapse UI; removes company URL field; removes inline script. |
| options/options.css | Adds styling for collapsible config sections and expandable lists. |
| manifest.json | Bumps extension version. |
| manifest.firefox.json | Bumps extension version and adjusts Firefox-specific settings. |
| LICENSE | Renames copyright holder to “Check”. |
| enterprise/Test-Extension-Policy.ps1 | Adds domainSquatting and genericWebhook policy test data; removes companyURL branding field. |
| enterprise/Remove-Windows-Chrome-and-Edge.ps1 | Removes genericWebhook/domainSquatting policy keys; updates branding fields removed/added. |
| enterprise/README.md | Adds webhook documentation link. |
| enterprise/macos-linux/README.md | Documents webhook and domainSquatting policy support; reorganizes deployment notes. |
| enterprise/macos-linux/edge-managed-policy.json | Adds domainSquatting + new branding URL fields. |
| enterprise/macos-linux/deploy-extension-prefs.sh | Adds genericWebhook defaults and updates default rules URL. |
| enterprise/macos-linux/chrome-managed-policy.json | Adds domainSquatting + new branding URL fields. |
| enterprise/macos-linux/check-extension-config.mobileconfig | Adds genericWebhook + domainSquatting defaults; updates rules URL. |
| enterprise/firefox/policies.json | Adds domainSquatting + new branding URL fields; removes companyURL. |
| enterprise/Deploy-Windows-Chrome-and-Edge.ps1 | Adds domainSquattingEnabled and new branding URL fields; removes companyURL. |
| enterprise/Check-Extension-Policy.reg | Adds domainSquatting + genericWebhook policy examples; adds branding URL fields. |
| enterprise/admx/en-US/Check-Extension.adml | Adds ADML strings for generic webhook, domain squatting, and branding URL fields; removes companyURL strings. |
| enterprise/admx/Check-Extension.admx | Adds policies for generic webhook, domain squatting, and branding URL fields; removes companyURL policies. |
| docs/SUMMARY.md | Adds a new “Features” section linking domain squatting docs. |
| docs/settings/detection-rules.md | Documents allowlist dual-use for phishing exclusion + squatting protection. |
| docs/settings/branding.md | Updates branding docs for new link fields; removes companyURL references; formatting improvements. |
| docs/settings/about.md | Refreshes About page content/links and formatting. |
| docs/README.md | Updates marketing/install text and indicates Firefox “Coming Soon”. |
| docs/firefox-support.md | Marks page hidden/noIndex and formatting cleanup. |
| docs/features/domain-squatting-detection.md | Adds feature documentation for domain squatting detection. |
| docs/deployment/firefox-deployment.md | Marks page hidden/noIndex and formatting cleanup. |
| docs/deployment/chrome-edge-deployment-instructions/windows/manual-deployment.md | Adds webhook deployment note. |
| docs/deployment/chrome-edge-deployment-instructions/README.md | Renames page and removes embedded Firefox deployment section. |
| config/managed_schema.json | Removes companyURL; adds branding link fields and domainSquatting schema. |
| config/branding.json | Removes companyURL; adds support/privacy/about URL placeholders. |
| CHANGELOG.md | Updates repository URLs from ProjectX to Check. |
| AGENTS.md | Renames ProjectX guide to Check. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const rulesDomainSquatting = rulesConfig?.domain_squatting || {}; | ||
| const runtimeDomainSquatting = runtimeConfig?.domainSquatting || {}; | ||
|
|
||
| this.enabled = runtimeDomainSquatting.enabled !== false; | ||
| this.protectedDomains = rulesDomainSquatting.protected_domains || []; |
There was a problem hiding this comment.
initialize() sets this.enabled solely from runtimeConfig.domainSquatting.enabled and ignores rulesConfig.domain_squatting.enabled. This means a rules file that disables domain squatting (enabled: false) will still be treated as enabled as long as runtime config doesn't explicitly disable it (and your defaults set it to true). Update the merge logic to honor rules first (e.g., runtime.enabled ?? rules.enabled ?? true) so the ruleset can actually disable the feature.
| // Remove regex anchors and escaping | ||
| let cleaned = pattern.trim() | ||
| .replace(/^\^/, '') // Remove leading ^ | ||
| .replace(/\$$/, '') // Remove trailing $ | ||
| .replace(/\\/g, ''); // Remove escape characters | ||
|
|
||
| // Try to extract domain from URL pattern | ||
| // Pattern formats: | ||
| // - https://example.com/... | ||
| // - ^https://example\.com$ | ||
| // - *.example.com | ||
| // - example.com | ||
|
|
||
| // Extract hostname from URL-like patterns | ||
| const urlMatch = cleaned.match(/^(?:https?:\/\/)?([a-zA-Z0-9][\w\-\.]*[a-zA-Z0-9])/); | ||
| if (urlMatch) { |
There was a problem hiding this comment.
extractDomainsFromAllowlist() is documented as handling regex patterns, but the current extraction regex only matches hostnames that start with an alphanumeric character. Common allowlist regex patterns like ^https:\/\/.*\.example\.com\/.*$ (or patterns starting with ( / .*) won't yield any extracted domain, so those domains won't be protected. Consider handling leading wildcard/regex tokens (e.g., stripping leading (?:\(\?:)?\.?\* / \(.*\) segments) or using a more robust URL/regex hostname extraction strategy so allowlist-derived protection works for typical regex allowlist entries.
| detectCombosquat(testDomain, protectedDomain) { | ||
| // Check if protected domain is contained in test domain | ||
| if (testDomain.includes(protectedDomain) && testDomain !== protectedDomain) { | ||
| const prefix = testDomain.substring(0, testDomain.indexOf(protectedDomain)); | ||
| const suffix = testDomain.substring(testDomain.indexOf(protectedDomain) + protectedDomain.length); | ||
|
|
||
| // Common combosquatting prefixes and suffixes | ||
| const commonCombos = [ | ||
| 'secure', 'login', 'account', 'verify', 'support', 'help', 'my', | ||
| 'auth', 'sso', 'signin', 'app', 'portal', 'online', 'web', | ||
| 'mobile', 'service', 'official', 'verified', 'safe' | ||
| ]; | ||
|
|
||
| const hasCommonCombo = commonCombos.some(combo => | ||
| prefix.includes(combo) || suffix.includes(combo) | ||
| ); | ||
|
|
||
| if (hasCommonCombo) { | ||
| return { | ||
| technique: 'combosquat', | ||
| description: 'Domain adds suspicious prefix/suffix to protected domain', | ||
| pattern: 'common_combo', | ||
| prefix: prefix, | ||
| suffix: suffix, | ||
| confidence: 0.9 | ||
| }; | ||
| } | ||
|
|
||
| // Any prefix/suffix is suspicious but lower confidence | ||
| if (prefix || suffix) { | ||
| return { | ||
| technique: 'combosquat', | ||
| description: 'Domain adds prefix/suffix to protected domain', | ||
| pattern: 'generic_combo', | ||
| prefix: prefix, |
There was a problem hiding this comment.
detectCombosquat() treats any occurrence of protectedDomain as a match (testDomain.includes(protectedDomain)), which will produce false positives for legitimate domains where the protected base is just a common substring (e.g., backoffice.com would match protected base office). Consider restricting combosquatting detection to clearer boundaries (start/end, -/_ separators, or whole-label matching) and/or requiring suspicious prefix/suffix patterns rather than arbitrary substrings.
| // Handle common two-part TLDs like .co.uk, .com.au | ||
| if (parts.length >= 3 && ['co', 'com', 'net', 'org', 'gov', 'edu'].includes(parts[parts.length - 2])) { | ||
| return parts[parts.length - 3]; | ||
| } | ||
| return parts[parts.length - 2]; | ||
| } |
There was a problem hiding this comment.
extractBaseDomain() has hardcoded handling for a small set of 2-part TLDs and will mis-extract the registrable domain for many common public suffixes (e.g., example.ac.uk returns ac instead of example). This can materially affect squatting detection accuracy and increase false positives/negatives. Consider using a Public Suffix List-based approach (e.g., tldts-style logic) or expanding/centralizing suffix handling so base-domain extraction is correct across TLDs.
| ); | ||
| const resources = [...source.matchAll(resourceRegex)].map((m) => m[1]); | ||
|
|
||
| if (resources.length === 0) return false; | ||
| if (resources.length === 0) return true; | ||
|
|
There was a problem hiding this comment.
resource_from_domain returns true when no matching resources are found (resources.length === 0). In the ruleset, phi_012_suspicious_resources sets invert: true, so pages with no customcss resources will be treated as suspicious (because true becomes false after invert and triggers). This is a logic inversion bug: when there are no resources of the requested type, the check should return false ("cannot confirm all resources are from allowed domains") so invert doesn't incorrectly flag pages.
| const severityElement = document.getElementById("techSeverity"); | ||
| const severityMap = { critical: "CRITICAL", high: "HIGH", medium: "MEDIUM", low: "LOW" }; | ||
| const severityText = severityMap[details.severity] || details.severity.toUpperCase(); | ||
| setSeverityBadge(severityElement, details.severity, severityText); |
There was a problem hiding this comment.
In the domain-squatting technical-details path, const severityText = severityMap[details.severity] || details.severity.toUpperCase(); will throw if details.severity is missing/undefined. This can break the blocked page for domain-squatting events that don't provide a severity. Use a safe fallback (e.g., default to 'HIGH'/'UNKNOWN' and guard before calling toUpperCase()).
| this.elements.companyURL, | ||
| this.elements.productName, | ||
| this.elements.supportEmail, |
There was a problem hiding this comment.
enableAllPolicyManagedFields() still references this.elements.companyURL (element removed) and does not re-enable the newly added branding fields (supportUrl, privacyPolicyUrl, aboutUrl). After leaving managed mode, those fields may remain disabled/marked as policy-managed. Update allFields to include the new elements and remove the obsolete companyURL reference.
| this.elements.companyURL, | |
| this.elements.productName, | |
| this.elements.supportEmail, | |
| this.elements.productName, | |
| this.elements.supportEmail, | |
| this.elements.supportUrl, | |
| this.elements.privacyPolicyUrl, | |
| this.elements.aboutUrl, |
| @@ -1,6 +1,6 @@ | |||
| { | |||
| "name": "Check", | |||
There was a problem hiding this comment.
The npm package name is set to "Check" (uppercase). npm package names must be lowercase and URL-safe; using an uppercase name will break npm pack/publish and some tooling. Consider using a lowercase name (e.g., "check" or "check-extension") even if this repo isn't intended for publishing.
| "name": "Check", | |
| "name": "check", |
| export class DomainSquattingDetector { | ||
| constructor() { | ||
| this.protectedDomains = []; | ||
| this.enabled = true; | ||
| this.action = 'block'; | ||
| this.minimumSeverity = 'high'; | ||
| this.logDetections = true; | ||
| this.deviationThreshold = 2; // Maximum Levenshtein distance | ||
| this.algorithms = { | ||
| levenshtein: true, | ||
| homoglyph: true, | ||
| typosquat: true, | ||
| combosquat: true | ||
| }; | ||
|
|
There was a problem hiding this comment.
This PR introduces a large, security-sensitive DomainSquattingDetector module (homoglyphs/Levenshtein/typosquat/combosquat) but adds no unit tests for it. Given the high risk of false positives/negatives, add focused tests covering: base-domain extraction (incl. multi-part TLDs), allowlist-domain extraction, combosquat boundary cases, and rule/runtime precedence (enabled/action/severity).
| // Minimal config summary: just show code-driven indicator count and key settings | ||
| function renderConfigSummary(config) { | ||
| const codeDrivenDiv = document.getElementById('codeDrivenIndicators'); | ||
| const keySettingsUl = document.getElementById('configKeySettings'); | ||
| if (!config || !config.phishing_indicators) return; | ||
| // Show only code-driven indicator count | ||
| const codeDrivenCount = config.phishing_indicators.filter(r => r.code_driven).length; | ||
| const codeItem = document.createElement('div'); | ||
| codeItem.className = 'config-item'; | ||
| const codeLabel = document.createElement('strong'); | ||
| codeLabel.textContent = 'Code-Driven Indicators:'; | ||
| const codeValue = document.createElement('span'); | ||
| codeValue.className = 'config-value'; | ||
| codeValue.textContent = String(codeDrivenCount); | ||
| codeItem.appendChild(codeLabel); | ||
| codeItem.appendChild(document.createTextNode(' ')); | ||
| codeItem.appendChild(codeValue); | ||
| codeDrivenDiv.replaceChildren(codeItem); | ||
| // Show some key settings | ||
| keySettingsUl.replaceChildren(); | ||
| const appendSetting = (label, value) => { | ||
| const li = document.createElement('li'); | ||
| const strong = document.createElement('strong'); | ||
| strong.textContent = `${label}:`; | ||
| li.appendChild(strong); | ||
| li.appendChild(document.createTextNode(` ${value}`)); | ||
| keySettingsUl.appendChild(li); | ||
| }; | ||
| if (config.version) appendSetting('Rules Version', config.version); | ||
| if (config.lastUpdated) appendSetting('Last Updated', config.lastUpdated); | ||
| if (config.detection_settings && config.detection_settings.block_threshold !== undefined) { | ||
| appendSetting('Block Threshold', config.detection_settings.block_threshold); | ||
| } | ||
| if (config.detection_settings && config.detection_settings.warn_threshold !== undefined) { | ||
| appendSetting('Warn Threshold', config.detection_settings.warn_threshold); | ||
| } | ||
| } | ||
|
|
||
| // Patch into config loading logic | ||
| (function() { | ||
| const origShowConfig = window.showConfigDisplay; | ||
| window.showConfigDisplay = function(config) { | ||
| if (typeof origShowConfig === 'function') origShowConfig(config); | ||
| renderConfigSummary(config); | ||
| document.getElementById('configSummary').style.display = ''; | ||
| }; | ||
| })(); |
There was a problem hiding this comment.
The global renderConfigSummary + window.showConfigDisplay patch appears to be dead code: there is no other reference to showConfigDisplay in this file, so the wrapper never runs and the summary UI likely never updates. Either wire this into the existing config rendering flow (e.g., call renderConfigSummary(this.currentConfigData) after loading) or remove it to avoid confusion.
No description provided.