Skip to content

Typo protection ux scan fixes#125

Open
Zacgoose wants to merge 4 commits intoCyberDrain:devfrom
Zacgoose:typo-protection-ux-scan-fixes
Open

Typo protection ux scan fixes#125
Zacgoose wants to merge 4 commits intoCyberDrain:devfrom
Zacgoose:typo-protection-ux-scan-fixes

Conversation

@Zacgoose
Copy link
Contributor

@Zacgoose Zacgoose commented Feb 8, 2026

No description provided.

…port

Add domain squatting detection with typosquatting, homoglyphs, combosquatting, Levenshtein distance, configurable page blocking, CIPP reporting, webhook integration, and unified URL allowlist configuration
Copilot AI review requested due to automatic review settings February 8, 2026 21:23
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds first-class domain-squatting (typosquatting/homoglyph/combosquatting) protection to the extension, wiring it into the scan pipeline, blocked-page UX, options UI, rules configuration, and documentation. It also tweaks scan cadence/timing to better handle dynamically loaded pages.

Changes:

  • Introduce a DomainSquattingDetector module and a check_domain_squatting background message handler, backed by new domain_squatting rules configuration.
  • Run a domain-squatting pre-check in the content script (before phishing detection) and emit logs/CIPP/webhooks; update blocked-page rendering for domain-squatting events.
  • Improve options “Configuration Overview” UX with collapsible sections and add docs for the new feature.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
scripts/modules/domain-squatting-detector.js New detector implementing multiple squatting techniques + allowlist-domain extraction.
scripts/content.js Adds domain-squatting pre-check; adjusts scan cadence and adds additional rescan triggers/timing logic.
scripts/background.js Instantiates/initializes detector and exposes check_domain_squatting message handling.
scripts/blocked.js Adds domain-squatting specific labeling + technical-details rendering.
rules/detection-rules.json Adds domain_squatting configuration block and default protected domains.
options/options.js Collapsible config UI + expand/collapse-all; adds webhook event type; enterprise-mode UX tweaks.
options/options.html Adds webhook checkbox + expand/collapse-all button; updates allowlist description.
options/options.css Styles for collapsible config sections/lists.
docs/settings/detection-rules.md Documents allowlist “dual protection” behavior.
docs/features/domain-squatting-detection.md New feature documentation page.
docs/SUMMARY.md Adds the new feature doc to the sidebar.
config/managed_schema.json Adds managed policy schema entries for domain squatting configuration.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +219 to +225
"Action": {
"title": "Action",
"description": "Action to take when domain squatting is detected",
"type": "string",
"enum": ["block", "warn", "log"],
"default": "block"
},
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the managed policy schema, the property name is "Action" (capital A) while other schema keys are lowerCamelCase. This is likely a typo and will make policy payloads inconsistent/confusing (admins will naturally try action). Rename to action (and keep enum/default) to match the rest of the schema style.

Copilot uses AI. Check for mistakes.
Comment on lines +127 to +132
**Adjust Sensitivity:**
"enabled": true
}
}
```

Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The “Adjust Sensitivity” section has a broken Markdown code block: there’s a closing ``` without a matching opening fence, and stray JSON fragment lines render incorrectly. Clean up this section so the JSON examples are inside proper fenced blocks (or remove the stray fragment) to avoid confusing readers.

Suggested change
**Adjust Sensitivity:**
"enabled": true
}
}
```

Copilot uses AI. Check for mistakes.
Comment on lines +3966 to +3969
const domainSquattingResult = await chrome.runtime.sendMessage({
type: "check_domain_squatting",
domain: window.location.hostname
});
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Domain squatting is checked via chrome.runtime.sendMessage on every runProtection scan, but the hostname doesn’t change during a page session. This can cause repeated background messaging/logging and repeated warnings across rescans. Cache the result per window.location.hostname (or add a one-time domainSquattingChecked flag) so the expensive check runs at most once per page load unless the hostname actually changes.

Copilot uses AI. Check for mistakes.
Comment on lines +4053 to +4066
await showBlockingOverlay(
`Domain Squatting: This site closely resembles "${squattingData.protectedDomain}" but is not the legitimate site`,
{
type: "domain_squatting",
severity: squattingData.severity,
testDomain: squattingData.testDomain,
protectedDomain: squattingData.protectedDomain,
techniques: squattingData.techniques,
confidence: squattingData.confidence,
reason: `Domain squatting detected: ${techniquesDesc}`,
detectionMethod: "domain-squatting",
detectionTime: Date.now()
}
);
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The domain-squatting block path passes type/severity/testDomain/protectedDomain/techniques/confidence to showBlockingOverlay(), but showBlockingOverlay() currently serializes only phishing-centric fields (score, threshold, threats, etc.) into the details query param. As a result, blocked.js won’t receive details.type === "domain_squatting", and the new domain-squatting UI/technical details won’t render. Extend showBlockingOverlay() to include/passthrough these domain-squatting fields in blockingDetails when present.

Copilot uses AI. Check for mistakes.
Comment on lines +6627 to +6665
// Intercept fetch
const originalFetch = window.fetch;
window.fetch = function(...args) {
pendingRequests++;
lastNetworkActivity = Date.now();
logger.debug(`🌐 Fetch request started (pending: ${pendingRequests})`);

return originalFetch.apply(this, arguments).finally(() => {
pendingRequests--;
lastNetworkActivity = Date.now();
logger.debug(`🌐 Fetch request completed (pending: ${pendingRequests})`);
checkNetworkIdle();
});
};

// Intercept XMLHttpRequest
const originalOpen = XMLHttpRequest.prototype.open;
const originalSend = XMLHttpRequest.prototype.send;

XMLHttpRequest.prototype.open = function(...args) {
this._check_tracked = true;
return originalOpen.apply(this, args);
};

XMLHttpRequest.prototype.send = function(...args) {
if (this._check_tracked) {
pendingRequests++;
lastNetworkActivity = Date.now();
logger.debug(`🌐 XHR request started (pending: ${pendingRequests})`);

this.addEventListener('loadend', () => {
pendingRequests--;
lastNetworkActivity = Date.now();
logger.debug(`🌐 XHR request completed (pending: ${pendingRequests})`);
checkNetworkIdle();
});
}
return originalSend.apply(this, args);
};
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setupNetworkMonitoring() overrides window.fetch and XMLHttpRequest to infer “network idle”, but scripts/content.js is a content script (runs in an isolated world). Overriding these globals won’t observe the page’s own network requests, so “network idle” will not reflect actual page loading behavior. If the goal is to track page network activity, use chrome.scripting.executeScript({ world: 'MAIN' ... }) to inject monitoring into the main world, or track requests from the background via webRequest/tabs APIs.

Suggested change
// Intercept fetch
const originalFetch = window.fetch;
window.fetch = function(...args) {
pendingRequests++;
lastNetworkActivity = Date.now();
logger.debug(`🌐 Fetch request started (pending: ${pendingRequests})`);
return originalFetch.apply(this, arguments).finally(() => {
pendingRequests--;
lastNetworkActivity = Date.now();
logger.debug(`🌐 Fetch request completed (pending: ${pendingRequests})`);
checkNetworkIdle();
});
};
// Intercept XMLHttpRequest
const originalOpen = XMLHttpRequest.prototype.open;
const originalSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function(...args) {
this._check_tracked = true;
return originalOpen.apply(this, args);
};
XMLHttpRequest.prototype.send = function(...args) {
if (this._check_tracked) {
pendingRequests++;
lastNetworkActivity = Date.now();
logger.debug(`🌐 XHR request started (pending: ${pendingRequests})`);
this.addEventListener('loadend', () => {
pendingRequests--;
lastNetworkActivity = Date.now();
logger.debug(`🌐 XHR request completed (pending: ${pendingRequests})`);
checkNetworkIdle();
});
}
return originalSend.apply(this, args);
};
// Listen for network activity messages from the page's main world
window.addEventListener(
"message",
function(event) {
// Ensure messages originate from this page and have our marker
if (!event || event.source !== window || !event.data) {
return;
}
const data = event.data;
if (!data || !data.__m365NetworkActivity) {
return;
}
if (typeof data.delta === "number") {
pendingRequests += data.delta;
// Guard against negative counts in case of unexpected sequences
if (pendingRequests < 0) {
pendingRequests = 0;
}
lastNetworkActivity = Date.now();
if (data.kind === "fetchStart" || data.kind === "xhrStart") {
logger.debug(
`🌐 ${data.kind} request started (pending: ${pendingRequests})`
);
} else if (data.kind === "fetchEnd" || data.kind === "xhrEnd") {
logger.debug(
`🌐 ${data.kind} request completed (pending: ${pendingRequests})`
);
}
checkNetworkIdle();
}
},
false
);
// Inject a script into the page's main world to monitor network activity
const script = document.createElement("script");
script.textContent =
"(" +
function() {
try {
var originalFetch = window.fetch;
if (typeof originalFetch === "function") {
window.fetch = function() {
window.postMessage(
{
__m365NetworkActivity: true,
delta: +1,
kind: "fetchStart",
},
"*"
);
return originalFetch
.apply(this, arguments)
.finally(function() {
window.postMessage(
{
__m365NetworkActivity: true,
delta: -1,
kind: "fetchEnd",
},
"*"
);
});
};
}
var originalOpen = XMLHttpRequest.prototype.open;
var originalSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function() {
this.__m365Tracked = true;
return originalOpen.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function() {
if (this.__m365Tracked) {
window.postMessage(
{
__m365NetworkActivity: true,
delta: +1,
kind: "xhrStart",
},
"*"
);
this.addEventListener("loadend", function() {
window.postMessage(
{
__m365NetworkActivity: true,
delta: -1,
kind: "xhrEnd",
},
"*"
);
});
}
return originalSend.apply(this, arguments);
};
} catch (e) {
// Fail-safe: do not break the page if monitoring cannot be injected
}
} +
")();";
(document.head || document.documentElement).appendChild(script);
if (script.parentNode) {
script.parentNode.removeChild(script);
}

Copilot uses AI. Check for mistakes.
Comment on lines +3673 to +3679
// 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 = '';
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This patch hooks config summary rendering by monkey-patching window.showConfigDisplay, but the options page uses CheckOptions.updateConfigDisplay() / displayConfigInCard() (there is no showConfigDisplay elsewhere). As written, renderConfigSummary() may never run. Call renderConfigSummary(this.currentConfigData) inside updateConfigDisplay() (or after displayConfigInCard()), or expose a real hook instead of patching a non-existent global.

Suggested change
// 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 = '';
// Patch into config loading logic by wrapping CheckOptions.updateConfigDisplay
(function() {
const proto = CheckOptions && CheckOptions.prototype;
if (!proto || typeof proto.updateConfigDisplay !== 'function') {
return;
}
const origUpdateConfigDisplay = proto.updateConfigDisplay;
proto.updateConfigDisplay = function(...args) {
const result = origUpdateConfigDisplay.apply(this, args);
// Render minimal config summary based on the current config data
try {
if (this && this.currentConfigData) {
renderConfigSummary(this.currentConfigData);
const summaryEl = document.getElementById('configSummary');
if (summaryEl) {
summaryEl.style.display = '';
}
}
} catch (e) {
console.error('Failed to render config summary:', e);
}
return result;

Copilot uses AI. Check for mistakes.
Comment on lines +136 to +163
async initialize(config, urlAllowlist = []) {
try {
if (config.domain_squatting) {
this.enabled = config.domain_squatting.enabled !== false;
this.protectedDomains = config.domain_squatting.protected_domains || [];
this.deviationThreshold = config.domain_squatting.deviation_threshold || 2;

if (config.domain_squatting.algorithms) {
this.algorithms = { ...this.algorithms, ...config.domain_squatting.algorithms };
}
}

// Extract domains from URL allowlist patterns
const allowlistDomains = this.extractDomainsFromAllowlist(urlAllowlist);
if (allowlistDomains.length > 0) {
// Merge with protected domains from rules (avoid duplicates)
const allDomains = [...new Set([...this.protectedDomains, ...allowlistDomains])];
this.protectedDomains = allDomains;
logger.log(`Added ${allowlistDomains.length} domains from URL allowlist`);
}

logger.log('DomainSquattingDetector initialized:', {
enabled: this.enabled,
protectedDomains: this.protectedDomains.length,
fromRules: config.domain_squatting?.protected_domains?.length || 0,
fromAllowlist: allowlistDomains.length,
deviationThreshold: this.deviationThreshold
});
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

initialize(config, urlAllowlist) assumes config is a non-null object. If config is undefined/null (e.g., when background.js calls initialize with cachedRules not yet loaded), if (config.domain_squatting) and the later log line fromRules: config.domain_squatting?... will throw and the detector won’t initialize. Guard with config?.domain_squatting and default to {} (or early-return) so reinitialization can safely run even when rules aren’t available yet.

Copilot uses AI. Check for mistakes.
Comment on lines +197 to +206
// Extract domain without subdomain and TLD for comparison
const testBase = this.extractBaseDomain(testDomain);

for (const protectedDomain of this.protectedDomains) {
const protectedBase = this.extractBaseDomain(protectedDomain);

// Skip if domains are identical
if (testBase === protectedBase) {
continue;
}
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

checkDomain() compares only the “base” label returned by extractBaseDomain() and skips when testBase === protectedBase. This makes the detector miss common squatting variants that only change the TLD (e.g., microsoft.net vs microsoft.com) or public-suffix differences, because both bases become microsoft and the check is skipped. Consider comparing the registrable domain (eTLD+1) instead of a single label, or at least include the TLD in comparisons and only skip when the full registrable domains match.

Copilot uses AI. Check for mistakes.
Zacgoose and others added 2 commits February 8, 2026 13:33
Co-authored-by: Copilot <[email protected]>
Signed-off-by: Zacgoose <[email protected]>
Co-authored-by: Copilot <[email protected]>
Signed-off-by: Zacgoose <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant