mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +00:00
[PM-19814] Phishing Detection Warning Popup UI (#16064)
* Add PhishingDetectionService
* Add a tab listener.
* Get the known phishing domain from the server
* Get the known phishing domain from the server
* Add phishing detection content script.
* Revert "Add phishing detection content script."
This reverts commit ce64d3435a.
* Fix conflicts
* Add build configs.
* Decouple the phishing detection content script logic from the rest of the app.
* move the call to background
* Add communication between the content script and background service.
* Update code to use Log service.
* Resolve conflict
* Add changes for phishing domain report
* Fix initializer order issue.
* Fix domain error.
* Account for no responses.
* Add exit functionality for onclick.
* Wrapped phishing detection feature behind feature flag (#13915)
* push changes for alert
* Removed browser logic for checking feature flag
* move the alert as dialog
* Add functionality to navigate back in history.
* [PM-19814] Add redirect to warning page when a phishing domain is detected.
* [PM-19814] Add the phishing warning page to the Angular popup.
* [PM-19814] Add functionality to display phishing host.
* [PM-19814] Add exit button and learn more link.
* [PM-19814] Add phishing detection feature flag.
* [PM-19814] Move phishing service to phishing directory
* [PM-19814] Add UI to display phishing URL.
* [PM-19814] Disable the URL input and populate it with the phishing URL.
* [PM-19814] Add phishing icon
* [PM-19814] Temporarily remove phishing reporting feature. It can be released separately in another ticket.
* [PM-19814] Clean up
* [PM-19814] Add types to the handlers.
* [PM-19814] Remove logic for handling authentication since the endpoint will be unauthenticated.
* [PM-19814] Fixed as many type issues as possible; added @ts-strict-ignore to the remaining ones.
* [PM-19814] Fix race condition in feature flag check.
* [PM-19814] Update wording for the marketing request.
* [PM-19814] Move phishing detection check from content script to webRequest.onCompleted listener.
* [PM-19814] Use webNavigation.onCompleted for redirect to ensure that the redirect only happens when they land on the page.
* [PM-19814] Remove unused code.
* [PM-19814] Fix merge conflict and update text based on product owner’s request
* [PM-19814] Fix merge conflict
* [PM-19814] Update text
* Resolve the message catalog entries
* Update file for consistent import and exports
* Update imports
* Update another import for BrowserPopupUtils
* Update the rest of the imports for BrowserPopupUtils
* Updates messages
* Rename files
* Current phishing block changes
* Use globalthis for chrome
* Add types file
* Update browser api to include tab navigation and close tab functions
* Update phishing detection to track multiple tabs and not trust info from content script
* Change chrome to browser.
* Fixed phishing detection checking previous url instead of current on navigation. Updated def flag for testing urls.
* Move phishing icon
* Fix chrome specific issues. Add comments to where BrowserApi should be used
* Fix command errors. Typecheck messages. Added guard for phishing detection messages
* Use concat map instead of merge map
* Unformat webfonts.scss file
* Fix lint and import errors
* Move phishing blocker files to dirt folder
* Rename background folder to services
* Add code ownership for phishing blocker
* Update text to use locales on phishing blocker learn more page
* Change navigation from using webapi to browser on updated event for safari support
* Update icon usage
* Fix type issues and add test file
* Fix linting error in test
---------
Co-authored-by: Jimmy Vo <huynhmaivo82@gmail.com>
Co-authored-by: Cy Okeke <cokeke@bitwarden.com>
Co-authored-by: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com>
Co-authored-by: Conner Turnbull <cturnbull@bitwarden.com>
Co-authored-by: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com>
Co-authored-by: Tom <144813356+ttalty@users.noreply.github.com>
This commit is contained in:
@@ -5511,6 +5511,18 @@
|
||||
"hasItemsVaultNudgeTitle": {
|
||||
"message": "Welcome to your vault!"
|
||||
},
|
||||
"phishingPageTitle":{
|
||||
"message": "Phishing website"
|
||||
},
|
||||
"phishingPageCloseTab": {
|
||||
"message": "Close tab"
|
||||
},
|
||||
"phishingPageContinue": {
|
||||
"message": "Continue"
|
||||
},
|
||||
"phishingPageLearnWhy": {
|
||||
"message": "Why are you seeing this?"
|
||||
},
|
||||
"hasItemsVaultNudgeBodyOne": {
|
||||
"message": "Autofill items for the current page"
|
||||
},
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { MockProxy, mock } from "jest-mock-extended";
|
||||
|
||||
import { ChangePasswordService } from "@bitwarden/angular/auth/password-management/change-password";
|
||||
import BrowserPopupUtils from "@bitwarden/browser/platform/browser/browser-popup-utils";
|
||||
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||
import BrowserPopupUtils from "../../../platform/browser/browser-popup-utils";
|
||||
|
||||
import { ExtensionChangePasswordService } from "./extension-change-password.service";
|
||||
|
||||
|
||||
@@ -2,12 +2,12 @@ import {
|
||||
DefaultChangePasswordService,
|
||||
ChangePasswordService,
|
||||
} from "@bitwarden/angular/auth/password-management/change-password";
|
||||
import BrowserPopupUtils from "@bitwarden/browser/platform/browser/browser-popup-utils";
|
||||
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||
import BrowserPopupUtils from "../../../platform/browser/browser-popup-utils";
|
||||
|
||||
export class ExtensionChangePasswordService
|
||||
extends DefaultChangePasswordService
|
||||
|
||||
@@ -286,6 +286,7 @@ import { AutofillBadgeUpdaterService } from "../autofill/services/autofill-badge
|
||||
import AutofillService from "../autofill/services/autofill.service";
|
||||
import { InlineMenuFieldQualificationService } from "../autofill/services/inline-menu-field-qualification.service";
|
||||
import { SafariApp } from "../browser/safariApp";
|
||||
import { PhishingDetectionService } from "../dirt/phishing-detection/services/phishing-detection.service";
|
||||
import { BackgroundBrowserBiometricsService } from "../key-management/biometrics/background-browser-biometrics.service";
|
||||
import VaultTimeoutService from "../key-management/vault-timeout/vault-timeout.service";
|
||||
import { BrowserActionsService } from "../platform/actions/browser-actions.service";
|
||||
@@ -1402,6 +1403,15 @@ export default class MainBackground {
|
||||
|
||||
this.inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService();
|
||||
|
||||
PhishingDetectionService.initialize(
|
||||
this.configService,
|
||||
this.auditService,
|
||||
this.logService,
|
||||
this.storageService,
|
||||
this.taskSchedulerService,
|
||||
this.eventCollectionService,
|
||||
);
|
||||
|
||||
this.ipcContentScriptManagerService = new IpcContentScriptManagerService(this.configService);
|
||||
this.ipcService = new IpcBackgroundService(this.platformUtilsService, this.logService);
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<span>{{ "phishingPageLearnWhy"| i18n}}</span>
|
||||
<a href="http://bitwarden.com/help/phishing-blocked/" bitLink block buttonType="primary">
|
||||
{{ "learnMore" | i18n }}
|
||||
</a>
|
||||
@@ -0,0 +1,16 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { CommonModule } from "@angular/common";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { ButtonModule } from "@bitwarden/components";
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
templateUrl: "learn-more-component.html",
|
||||
imports: [CommonModule, CommonModule, JslibModule, ButtonModule],
|
||||
})
|
||||
export class LearnMoreComponent {
|
||||
constructor() {}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<div class="tw-flex tw-flex-col tw-gap-2">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "phishingPageTitle" | i18n }}</bit-label>
|
||||
<input bitInput disabled type="text" [value]="phishingHost" />
|
||||
</bit-form-field>
|
||||
|
||||
<button type="button" (click)="closeTab()" bitButton buttonType="primary">
|
||||
{{ "phishingPageCloseTab" | i18n }}
|
||||
</button>
|
||||
<button type="button" (click)="continueAnyway()" bitButton buttonType="danger">
|
||||
{{ "phishingPageContinue" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -0,0 +1,58 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { CommonModule } from "@angular/common";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Component, OnDestroy } from "@angular/core";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { ActivatedRoute, RouterModule } from "@angular/router";
|
||||
import { Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import {
|
||||
AsyncActionsModule,
|
||||
ButtonModule,
|
||||
CheckboxModule,
|
||||
FormFieldModule,
|
||||
IconModule,
|
||||
LinkModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { PhishingDetectionService } from "../services/phishing-detection.service";
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
templateUrl: "phishing-warning.component.html",
|
||||
imports: [
|
||||
CommonModule,
|
||||
IconModule,
|
||||
JslibModule,
|
||||
LinkModule,
|
||||
FormFieldModule,
|
||||
AsyncActionsModule,
|
||||
CheckboxModule,
|
||||
ButtonModule,
|
||||
RouterModule,
|
||||
],
|
||||
})
|
||||
export class PhishingWarning implements OnDestroy {
|
||||
phishingHost = "";
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(private activatedRoute: ActivatedRoute) {
|
||||
this.activatedRoute.queryParamMap.pipe(takeUntil(this.destroy$)).subscribe((params) => {
|
||||
this.phishingHost = params.get("phishingHost") || "";
|
||||
});
|
||||
}
|
||||
|
||||
async closeTab() {
|
||||
await PhishingDetectionService.requestClosePhishingWarningPage();
|
||||
}
|
||||
async continueAnyway() {
|
||||
await PhishingDetectionService.requestContinueToDangerousUrl();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
import { TaskSchedulerService } from "@bitwarden/common/platform/scheduling/task-scheduler.service";
|
||||
|
||||
import { PhishingDetectionService } from "./phishing-detection.service";
|
||||
|
||||
describe("PhishingDetectionService", () => {
|
||||
let auditService: AuditService;
|
||||
let logService: LogService;
|
||||
let storageService: AbstractStorageService;
|
||||
let taskSchedulerService: TaskSchedulerService;
|
||||
let configService: ConfigService;
|
||||
let eventCollectionService: EventCollectionService;
|
||||
|
||||
beforeEach(() => {
|
||||
auditService = { getKnownPhishingDomains: jest.fn() } as any;
|
||||
logService = { info: jest.fn(), debug: jest.fn(), warning: jest.fn(), error: jest.fn() } as any;
|
||||
storageService = { get: jest.fn(), save: jest.fn() } as any;
|
||||
taskSchedulerService = { registerTaskHandler: jest.fn(), setInterval: jest.fn() } as any;
|
||||
configService = { getFeatureFlag$: jest.fn(() => of(false)) } as any;
|
||||
eventCollectionService = {} as any;
|
||||
});
|
||||
|
||||
it("should initialize without errors", () => {
|
||||
expect(() => {
|
||||
PhishingDetectionService.initialize(
|
||||
configService,
|
||||
auditService,
|
||||
logService,
|
||||
storageService,
|
||||
taskSchedulerService,
|
||||
eventCollectionService,
|
||||
);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("should detect phishing domains", () => {
|
||||
PhishingDetectionService["_knownPhishingDomains"].add("phishing.com");
|
||||
const url = new URL("https://phishing.com");
|
||||
expect(PhishingDetectionService.isPhishingDomain(url)).toBe(true);
|
||||
const safeUrl = new URL("https://safe.com");
|
||||
expect(PhishingDetectionService.isPhishingDomain(safeUrl)).toBe(false);
|
||||
});
|
||||
|
||||
// Add more tests for other methods as needed
|
||||
});
|
||||
@@ -0,0 +1,685 @@
|
||||
import { concatMap, delay, Subject, Subscription } from "rxjs";
|
||||
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
import { devFlagEnabled, devFlagValue } from "@bitwarden/common/platform/misc/flags";
|
||||
import { ScheduledTaskNames } from "@bitwarden/common/platform/scheduling";
|
||||
import { TaskSchedulerService } from "@bitwarden/common/platform/scheduling/task-scheduler.service";
|
||||
|
||||
import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||
|
||||
import {
|
||||
CaughtPhishingDomain,
|
||||
isPhishingDetectionMessage,
|
||||
PhishingDetectionMessage,
|
||||
PhishingDetectionNavigationEvent,
|
||||
PhishingDetectionTabId,
|
||||
} from "./phishing-detection.types";
|
||||
|
||||
export class PhishingDetectionService {
|
||||
private static readonly _UPDATE_INTERVAL = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
|
||||
private static readonly _RETRY_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
||||
private static readonly _MAX_RETRIES = 3;
|
||||
private static readonly _STORAGE_KEY = "phishing_domains_cache";
|
||||
private static _auditService: AuditService;
|
||||
private static _logService: LogService;
|
||||
private static _storageService: AbstractStorageService;
|
||||
private static _taskSchedulerService: TaskSchedulerService;
|
||||
private static _updateCacheSubscription: Subscription | null = null;
|
||||
private static _retrySubscription: Subscription | null = null;
|
||||
private static _navigationEventsSubject = new Subject<PhishingDetectionNavigationEvent>();
|
||||
private static _navigationEvents: Subscription | null = null;
|
||||
private static _knownPhishingDomains = new Set<string>();
|
||||
private static _caughtTabs: Map<PhishingDetectionTabId, CaughtPhishingDomain> = new Map();
|
||||
private static _isInitialized = false;
|
||||
private static _isUpdating = false;
|
||||
private static _retryCount = 0;
|
||||
private static _lastUpdateTime: number = 0;
|
||||
|
||||
static initialize(
|
||||
configService: ConfigService,
|
||||
auditService: AuditService,
|
||||
logService: LogService,
|
||||
storageService: AbstractStorageService,
|
||||
taskSchedulerService: TaskSchedulerService,
|
||||
eventCollectionService: EventCollectionService,
|
||||
): void {
|
||||
this._auditService = auditService;
|
||||
this._logService = logService;
|
||||
this._storageService = storageService;
|
||||
this._taskSchedulerService = taskSchedulerService;
|
||||
|
||||
logService.info("[PhishingDetectionService] Initialize called");
|
||||
|
||||
configService
|
||||
.getFeatureFlag$(FeatureFlag.PhishingDetection)
|
||||
.pipe(
|
||||
concatMap(async (enabled) => {
|
||||
if (!enabled) {
|
||||
logService.info(
|
||||
"[PhishingDetectionService] Phishing detection feature flag is disabled.",
|
||||
);
|
||||
this._cleanup();
|
||||
} else {
|
||||
// Enable phishing detection service
|
||||
logService.info("[PhishingDetectionService] Enabling phishing detection service");
|
||||
await this._setup();
|
||||
}
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given URL is a known phishing domain
|
||||
*
|
||||
* @param url The URL to check
|
||||
* @returns True if the URL is a known phishing domain, false otherwise
|
||||
*/
|
||||
static isPhishingDomain(url: URL): boolean {
|
||||
const result = this._knownPhishingDomains.has(url.hostname);
|
||||
if (result) {
|
||||
this._logService.debug("[PhishingDetectionService] Caught phishing domain:", url.hostname);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message to the phishing detection service to close the warning page
|
||||
*/
|
||||
static requestClosePhishingWarningPage(): void {
|
||||
void chrome.runtime.sendMessage({ command: PhishingDetectionMessage.Close });
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message to the phishing detection service to continue to the caught url
|
||||
*/
|
||||
static async requestContinueToDangerousUrl() {
|
||||
void chrome.runtime.sendMessage({ command: PhishingDetectionMessage.Continue });
|
||||
}
|
||||
|
||||
/**
|
||||
* Continues to the dangerous URL if the user has requested it
|
||||
*
|
||||
* @param tabId The ID of the tab to continue to the dangerous URL
|
||||
*/
|
||||
static async _continueToDangerousUrl(tabId: PhishingDetectionTabId): Promise<void> {
|
||||
const caughtTab = this._caughtTabs.get(tabId);
|
||||
if (caughtTab) {
|
||||
this._logService.info(
|
||||
"[PhishingDetectionService] Continuing to known phishing domain: ",
|
||||
caughtTab,
|
||||
caughtTab.url.href,
|
||||
);
|
||||
await BrowserApi.navigateTabToUrl(tabId, caughtTab.url);
|
||||
} else {
|
||||
this._logService.warning("[PhishingDetectionService] No caught domain to continue to");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the phishing detection service, setting up listeners and registering tasks
|
||||
*/
|
||||
private static async _setup(): Promise<void> {
|
||||
if (this._isInitialized) {
|
||||
this._logService.info("[PhishingDetectionService] Already initialized, skipping setup.");
|
||||
return;
|
||||
}
|
||||
|
||||
this._isInitialized = true;
|
||||
this._setupListeners();
|
||||
|
||||
// Register the update task
|
||||
this._taskSchedulerService.registerTaskHandler(
|
||||
ScheduledTaskNames.phishingDomainUpdate,
|
||||
async () => {
|
||||
try {
|
||||
await this._fetchKnownPhishingDomains();
|
||||
} catch (error) {
|
||||
this._logService.error(
|
||||
"[PhishingDetectionService] Failed to update phishing domains in task handler:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Initial load of cached domains
|
||||
await this._loadCachedDomains();
|
||||
|
||||
// Set up periodic updates every 24 hours
|
||||
this._setupPeriodicUpdates();
|
||||
this._logService.debug("[PhishingDetectionService] Phishing detection feature is initialized.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up listeners for messages from the web page and web navigation events
|
||||
*/
|
||||
private static _setupListeners(): void {
|
||||
// Setup listeners from web page/content script
|
||||
BrowserApi.addListener(chrome.runtime.onMessage, this._handleExtensionMessage.bind(this));
|
||||
BrowserApi.addListener(chrome.tabs.onReplaced, this._handleReplacementEvent.bind(this));
|
||||
BrowserApi.addListener(chrome.tabs.onUpdated, this._handleNavigationEvent.bind(this));
|
||||
|
||||
// When a navigation event occurs, check if a replace event for the same tabId exists,
|
||||
// and call the replace handler before handling navigation.
|
||||
this._navigationEvents = this._navigationEventsSubject
|
||||
.pipe(
|
||||
delay(100), // Delay slightly to allow replace events to be caught
|
||||
)
|
||||
.subscribe(({ tabId, changeInfo, tab }) => {
|
||||
void this._processNavigation(tabId, changeInfo, tab);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles messages from the phishing warning page
|
||||
*
|
||||
* @returns true if the message was handled, false otherwise
|
||||
*/
|
||||
private static _handleExtensionMessage(
|
||||
message: unknown,
|
||||
sender: chrome.runtime.MessageSender,
|
||||
): boolean {
|
||||
if (!isPhishingDetectionMessage(message)) {
|
||||
return false;
|
||||
}
|
||||
const isValidSender = sender && sender.tab && sender.tab.id;
|
||||
const senderTabId = isValidSender ? sender?.tab?.id : null;
|
||||
|
||||
// Only process messages from tab navigation
|
||||
if (senderTabId == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handle Dangerous Continue to Phishing Domain
|
||||
if (message.command === PhishingDetectionMessage.Continue) {
|
||||
this._logService.debug(
|
||||
"[PhishingDetectionService] User requested continue to phishing domain on tab: ",
|
||||
senderTabId,
|
||||
);
|
||||
|
||||
this._setCaughtTabContinue(senderTabId);
|
||||
void this._continueToDangerousUrl(senderTabId);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Handle Close Phishing Warning Page
|
||||
if (message.command === PhishingDetectionMessage.Close) {
|
||||
this._logService.debug(
|
||||
"[PhishingDetectionService] User requested to close phishing warning page on tab: ",
|
||||
senderTabId,
|
||||
);
|
||||
|
||||
void BrowserApi.closeTab(senderTabId);
|
||||
this._removeCaughtTab(senderTabId);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter out navigation events that are to warning pages or not complete, check for phishing domains,
|
||||
* then handle the navigation appropriately.
|
||||
*/
|
||||
private static async _processNavigation(
|
||||
tabId: number,
|
||||
changeInfo: chrome.tabs.TabChangeInfo,
|
||||
tab: chrome.tabs.Tab,
|
||||
): Promise<void> {
|
||||
if (changeInfo.status !== "complete" || !tab.url) {
|
||||
// Not a complete navigation or no URL to check
|
||||
return;
|
||||
}
|
||||
// Check if navigating to a warning page to ignore
|
||||
const isWarningPage = this._isWarningPage(tabId, tab.url);
|
||||
if (isWarningPage) {
|
||||
this._logService.debug(
|
||||
`[PhishingDetectionService] Ignoring navigation to warning page for tab ${tabId}: ${tab.url}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if tab is navigating to a phishing url and handle navigation
|
||||
this._checkTabForPhishing(tabId, new URL(tab.url));
|
||||
await this._handleTabNavigation(tabId);
|
||||
}
|
||||
|
||||
private static _handleNavigationEvent(
|
||||
tabId: number,
|
||||
changeInfo: chrome.tabs.TabChangeInfo,
|
||||
tab: chrome.tabs.Tab,
|
||||
): boolean {
|
||||
this._navigationEventsSubject.next({ tabId, changeInfo, tab });
|
||||
|
||||
// Return value for supporting BrowserApi event listener signature
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a replace event in Safari when redirecting to a warning page
|
||||
*
|
||||
* @returns true if the replacement was handled, false otherwise
|
||||
*/
|
||||
private static _handleReplacementEvent(newTabId: number, originalTabId: number): boolean {
|
||||
if (this._caughtTabs.has(originalTabId)) {
|
||||
this._logService.debug(
|
||||
`[PhishingDetectionService] Handling original tab ${originalTabId} changing to new tab ${newTabId}`,
|
||||
);
|
||||
|
||||
// Handle replacement
|
||||
const originalCaughtTab = this._caughtTabs.get(originalTabId);
|
||||
if (originalCaughtTab) {
|
||||
this._caughtTabs.set(newTabId, originalCaughtTab);
|
||||
this._caughtTabs.delete(originalTabId);
|
||||
} else {
|
||||
this._logService.debug(
|
||||
`[PhishingDetectionService] Original caught tab not found, ignoring replacement.`,
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a tab to the caught tabs map with the requested continue status set to false
|
||||
*
|
||||
* @param tabId The ID of the tab that was caught
|
||||
* @param url The URL of the tab that was caught
|
||||
* @param redirectedTo The URL that the tab was redirected to
|
||||
*/
|
||||
private static _addCaughtTab(tabId: PhishingDetectionTabId, url: URL) {
|
||||
const redirectedTo = this._createWarningPageUrl(url);
|
||||
const newTab = { url, warningPageUrl: redirectedTo, requestedContinue: false };
|
||||
|
||||
this._caughtTabs.set(tabId, newTab);
|
||||
this._logService.debug("[PhishingDetectionService] Tracking new tab:", tabId, newTab);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a tab from the caught tabs map
|
||||
*
|
||||
* @param tabId The ID of the tab to remove
|
||||
*/
|
||||
private static _removeCaughtTab(tabId: PhishingDetectionTabId) {
|
||||
this._logService.debug("[PhishingDetectionService] Removing tab from tracking: ", tabId);
|
||||
this._caughtTabs.delete(tabId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the requested continue status for a caught tab
|
||||
*
|
||||
* @param tabId The ID of the tab to set the continue status for
|
||||
*/
|
||||
private static _setCaughtTabContinue(tabId: PhishingDetectionTabId) {
|
||||
const caughtTab = this._caughtTabs.get(tabId);
|
||||
if (caughtTab) {
|
||||
this._caughtTabs.set(tabId, {
|
||||
url: caughtTab.url,
|
||||
warningPageUrl: caughtTab.warningPageUrl,
|
||||
requestedContinue: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the tab should continue to a dangerous domain
|
||||
*
|
||||
* @param tabId Tab to check if a domain was caught
|
||||
* @returns True if the user requested to continue to the phishing domain
|
||||
*/
|
||||
private static _continueToCaughtDomain(tabId: PhishingDetectionTabId) {
|
||||
const caughtDomain = this._caughtTabs.get(tabId);
|
||||
const hasRequestedContinue = caughtDomain?.requestedContinue;
|
||||
return caughtDomain && hasRequestedContinue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the tab is going to a phishing domain and updates the caught tabs map
|
||||
*
|
||||
* @param tabId Tab to check for phishing domain
|
||||
* @param url URL of the tab to check
|
||||
*/
|
||||
private static _checkTabForPhishing(tabId: PhishingDetectionTabId, url: URL) {
|
||||
// Check if the tab already being tracked
|
||||
const caughtTab = this._caughtTabs.get(tabId);
|
||||
|
||||
const isPhishing = this.isPhishingDomain(url);
|
||||
this._logService.debug(
|
||||
`[PhishingDetectionService] Checking for phishing url. Result: ${isPhishing} on ${url}`,
|
||||
);
|
||||
|
||||
// Add a new caught tab
|
||||
if (!caughtTab && isPhishing) {
|
||||
this._addCaughtTab(tabId, url);
|
||||
}
|
||||
|
||||
// The tab was caught before but has an updated url
|
||||
if (caughtTab && caughtTab.url.href !== url.href) {
|
||||
if (isPhishing) {
|
||||
this._logService.debug(
|
||||
"[PhishingDetectionService] Caught tab going to a new phishing domain:",
|
||||
caughtTab.url,
|
||||
);
|
||||
// The tab can be treated as a new tab, clear the old one and reset
|
||||
this._removeCaughtTab(tabId);
|
||||
this._addCaughtTab(tabId, url);
|
||||
} else {
|
||||
this._logService.debug(
|
||||
"[PhishingDetectionService] Caught tab navigating away from a phishing domain",
|
||||
);
|
||||
// The tab is safe
|
||||
this._removeCaughtTab(tabId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a phishing tab for redirection to a warning page if the user has not requested to continue
|
||||
*
|
||||
* @param tabId Tab to handle
|
||||
* @param url URL of the tab
|
||||
*/
|
||||
private static async _handleTabNavigation(tabId: PhishingDetectionTabId) {
|
||||
const caughtTab = this._caughtTabs.get(tabId);
|
||||
|
||||
if (caughtTab && !this._continueToCaughtDomain(tabId)) {
|
||||
await this._redirectToWarningPage(tabId);
|
||||
}
|
||||
}
|
||||
|
||||
private static _isWarningPage(tabId: number, url: string): boolean {
|
||||
const caughtTab = this._caughtTabs.get(tabId);
|
||||
return !!caughtTab && caughtTab.warningPageUrl.href === url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs the phishing warning page URL with the caught URL as a query parameter
|
||||
*
|
||||
* @param caughtUrl The URL that was caught as phishing
|
||||
* @returns The complete URL to the phishing warning page
|
||||
*/
|
||||
private static _createWarningPageUrl(caughtUrl: URL) {
|
||||
const phishingWarningPage = BrowserApi.getRuntimeURL(
|
||||
"popup/index.html#/security/phishing-warning",
|
||||
);
|
||||
const pageWithViewData = `${phishingWarningPage}?phishingHost=${caughtUrl.hostname}`;
|
||||
this._logService.debug(
|
||||
"[PhishingDetectionService] Created phishing warning page url:",
|
||||
pageWithViewData,
|
||||
);
|
||||
return new URL(pageWithViewData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirects the tab to the phishing warning page
|
||||
*
|
||||
* @param tabId The ID of the tab to redirect
|
||||
*/
|
||||
private static async _redirectToWarningPage(tabId: number) {
|
||||
const tabToRedirect = this._caughtTabs.get(tabId);
|
||||
|
||||
if (tabToRedirect) {
|
||||
this._logService.info("[PhishingDetectionService] Redirecting to warning page");
|
||||
await BrowserApi.navigateTabToUrl(tabId, tabToRedirect.warningPageUrl);
|
||||
} else {
|
||||
this._logService.warning("[PhishingDetectionService] No caught tab found for redirection");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up periodic updates for phishing domains
|
||||
*/
|
||||
private static _setupPeriodicUpdates() {
|
||||
// Clean up any existing subscriptions
|
||||
if (this._updateCacheSubscription) {
|
||||
this._updateCacheSubscription.unsubscribe();
|
||||
}
|
||||
if (this._retrySubscription) {
|
||||
this._retrySubscription.unsubscribe();
|
||||
}
|
||||
|
||||
this._updateCacheSubscription = this._taskSchedulerService.setInterval(
|
||||
ScheduledTaskNames.phishingDomainUpdate,
|
||||
this._UPDATE_INTERVAL,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules a retry for updating phishing domains if the update fails
|
||||
*/
|
||||
private static _scheduleRetry() {
|
||||
// If we've exceeded max retries, stop retrying
|
||||
if (this._retryCount >= this._MAX_RETRIES) {
|
||||
this._logService.warning(
|
||||
`[PhishingDetectionService] Max retries (${this._MAX_RETRIES}) reached for phishing domain update. Will try again in ${this._UPDATE_INTERVAL / (1000 * 60 * 60)} hours.`,
|
||||
);
|
||||
this._retryCount = 0;
|
||||
if (this._retrySubscription) {
|
||||
this._retrySubscription.unsubscribe();
|
||||
this._retrySubscription = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Clean up existing retry subscription if any
|
||||
if (this._retrySubscription) {
|
||||
this._retrySubscription.unsubscribe();
|
||||
}
|
||||
|
||||
// Increment retry count
|
||||
this._retryCount++;
|
||||
|
||||
// Schedule a retry in 5 minutes
|
||||
this._retrySubscription = this._taskSchedulerService.setInterval(
|
||||
ScheduledTaskNames.phishingDomainUpdate,
|
||||
this._RETRY_INTERVAL,
|
||||
);
|
||||
|
||||
this._logService.info(
|
||||
`[PhishingDetectionService] Scheduled retry ${this._retryCount}/${this._MAX_RETRIES} for phishing domain update in ${this._RETRY_INTERVAL / (1000 * 60)} minutes`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles adding test phishing URLs from dev flags for testing purposes
|
||||
*/
|
||||
private static _handleTestUrls() {
|
||||
if (devFlagEnabled("testPhishingUrls")) {
|
||||
const testPhishingUrls = devFlagValue("testPhishingUrls");
|
||||
this._logService.debug(
|
||||
"[PhishingDetectionService] Dev flag enabled for testing phishing detection. Adding test phishing domains:",
|
||||
testPhishingUrls,
|
||||
);
|
||||
if (testPhishingUrls && testPhishingUrls instanceof Array) {
|
||||
testPhishingUrls.forEach((domain) => {
|
||||
if (domain && typeof domain === "string") {
|
||||
this._knownPhishingDomains.add(domain);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads cached phishing domains from storage
|
||||
* If no cache exists or it is expired, fetches the latest domains
|
||||
*/
|
||||
private static async _loadCachedDomains() {
|
||||
try {
|
||||
const cachedData = await this._storageService.get<{ domains: string[]; timestamp: number }>(
|
||||
this._STORAGE_KEY,
|
||||
);
|
||||
if (cachedData) {
|
||||
this._logService.info("[PhishingDetectionService] Phishing cachedData exists");
|
||||
const phishingDomains = cachedData.domains || [];
|
||||
|
||||
this._setKnownPhishingDomains(phishingDomains);
|
||||
this._handleTestUrls();
|
||||
}
|
||||
|
||||
// If cache is empty or expired, trigger an immediate update
|
||||
if (
|
||||
this._knownPhishingDomains.size === 0 ||
|
||||
Date.now() - this._lastUpdateTime >= this._UPDATE_INTERVAL
|
||||
) {
|
||||
await this._fetchKnownPhishingDomains();
|
||||
}
|
||||
} catch (error) {
|
||||
this._logService.error(
|
||||
"[PhishingDetectionService] Failed to load cached phishing domains:",
|
||||
error,
|
||||
);
|
||||
this._handleTestUrls();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the latest known phishing domains from the audit service
|
||||
* Updates the cache and handles retries if necessary
|
||||
*/
|
||||
static async _fetchKnownPhishingDomains(): Promise<void> {
|
||||
let domains: string[] = [];
|
||||
|
||||
// Prevent concurrent updates
|
||||
if (this._isUpdating) {
|
||||
this._logService.warning(
|
||||
"[PhishingDetectionService] Update already in progress, skipping...",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this._logService.info("[PhishingDetectionService] Starting phishing domains update...");
|
||||
this._isUpdating = true;
|
||||
domains = await this._auditService.getKnownPhishingDomains();
|
||||
this._setKnownPhishingDomains(domains);
|
||||
|
||||
await this._saveDomains();
|
||||
|
||||
this._resetRetry();
|
||||
this._isUpdating = false;
|
||||
|
||||
this._logService.info("[PhishingDetectionService] Successfully fetched domains");
|
||||
} catch (error) {
|
||||
this._logService.error(
|
||||
"[PhishingDetectionService] Failed to fetch known phishing domains.",
|
||||
error,
|
||||
);
|
||||
|
||||
this._scheduleRetry();
|
||||
this._isUpdating = false;
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the known phishing domains to storage
|
||||
* Caches the updated domains and updates the last update time
|
||||
*/
|
||||
private static async _saveDomains() {
|
||||
try {
|
||||
// Cache the updated domains
|
||||
await this._storageService.save(this._STORAGE_KEY, {
|
||||
domains: Array.from(this._knownPhishingDomains),
|
||||
timestamp: this._lastUpdateTime,
|
||||
});
|
||||
this._logService.info(
|
||||
`[PhishingDetectionService] Updated phishing domains cache with ${this._knownPhishingDomains.size} domains`,
|
||||
);
|
||||
} catch (error) {
|
||||
this._logService.error(
|
||||
"[PhishingDetectionService] Failed to save known phishing domains.",
|
||||
error,
|
||||
);
|
||||
this._scheduleRetry();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the retry count and clears the retry subscription
|
||||
*/
|
||||
private static _resetRetry(): void {
|
||||
this._logService.info(
|
||||
`[PhishingDetectionService] Resetting retry count and clearing retry subscription.`,
|
||||
);
|
||||
// Reset retry count and clear retry subscription on success
|
||||
this._retryCount = 0;
|
||||
if (this._retrySubscription) {
|
||||
this._retrySubscription.unsubscribe();
|
||||
this._retrySubscription = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds phishing domains to the known phishing domains set
|
||||
* Clears old domains to prevent memory leaks
|
||||
*
|
||||
* @param domains Array of phishing domains to add
|
||||
*/
|
||||
private static _setKnownPhishingDomains(domains: string[]): void {
|
||||
this._logService.debug(
|
||||
`[PhishingDetectionService] Tracking ${domains.length} phishing domains`,
|
||||
);
|
||||
|
||||
// Clear old domains to prevent memory leaks
|
||||
this._knownPhishingDomains.clear();
|
||||
|
||||
domains.forEach((domain: string) => {
|
||||
if (domain) {
|
||||
this._knownPhishingDomains.add(domain);
|
||||
}
|
||||
});
|
||||
this._lastUpdateTime = Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up the phishing detection service
|
||||
* Unsubscribes from all subscriptions and clears caches
|
||||
*/
|
||||
private static _cleanup() {
|
||||
if (this._updateCacheSubscription) {
|
||||
this._updateCacheSubscription.unsubscribe();
|
||||
this._updateCacheSubscription = null;
|
||||
}
|
||||
if (this._retrySubscription) {
|
||||
this._retrySubscription.unsubscribe();
|
||||
this._retrySubscription = null;
|
||||
}
|
||||
if (this._navigationEvents) {
|
||||
this._navigationEvents.unsubscribe();
|
||||
this._navigationEvents = null;
|
||||
}
|
||||
this._knownPhishingDomains.clear();
|
||||
this._caughtTabs.clear();
|
||||
this._lastUpdateTime = 0;
|
||||
this._isUpdating = false;
|
||||
this._isInitialized = false;
|
||||
this._retryCount = 0;
|
||||
|
||||
// Manually type cast to satisfy the listener signature due to the mixture
|
||||
// of static and instance methods in this class. To be fixed when refactoring
|
||||
// this class to be instance-based while providing a singleton instance in usage
|
||||
BrowserApi.removeListener(
|
||||
chrome.runtime.onMessage,
|
||||
PhishingDetectionService._handleExtensionMessage as (...args: readonly unknown[]) => unknown,
|
||||
);
|
||||
BrowserApi.removeListener(
|
||||
chrome.tabs.onReplaced,
|
||||
PhishingDetectionService._handleReplacementEvent as (...args: readonly unknown[]) => unknown,
|
||||
);
|
||||
BrowserApi.removeListener(
|
||||
chrome.tabs.onUpdated,
|
||||
PhishingDetectionService._handleNavigationEvent as (...args: readonly unknown[]) => unknown,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
export const PhishingDetectionMessage = Object.freeze({
|
||||
Close: "phishing-detection-close",
|
||||
Continue: "phishing-detection-continue",
|
||||
} as const);
|
||||
|
||||
export type PhishingDetectionMessageTypes =
|
||||
(typeof PhishingDetectionMessage)[keyof typeof PhishingDetectionMessage];
|
||||
|
||||
export function isPhishingDetectionMessage(
|
||||
input: unknown,
|
||||
): input is { command: PhishingDetectionMessageTypes } {
|
||||
if (!!input && typeof input === "object" && "command" in input) {
|
||||
const command = (input as Record<string, unknown>)["command"];
|
||||
if (typeof command === "string") {
|
||||
return Object.values(PhishingDetectionMessage).includes(
|
||||
command as PhishingDetectionMessageTypes,
|
||||
);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export type PhishingDetectionTabId = number;
|
||||
|
||||
export type CaughtPhishingDomain = {
|
||||
url: URL;
|
||||
warningPageUrl: URL;
|
||||
requestedContinue: boolean;
|
||||
};
|
||||
|
||||
export type PhishingDetectionNavigationEvent = {
|
||||
tabId: number;
|
||||
changeInfo: chrome.tabs.TabChangeInfo;
|
||||
tab: chrome.tabs.Tab;
|
||||
};
|
||||
@@ -212,6 +212,47 @@ export class BrowserApi {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes a browser tab with the given id
|
||||
*
|
||||
* @param tabId The id of the tab to close
|
||||
*/
|
||||
static async closeTab(tabId: number): Promise<void> {
|
||||
if (tabId) {
|
||||
if (BrowserApi.isWebExtensionsApi) {
|
||||
browser.tabs.remove(tabId).catch((error) => {
|
||||
throw new Error("[BrowserApi] Failed to remove current tab: " + error.message);
|
||||
});
|
||||
} else if (BrowserApi.isChromeApi) {
|
||||
chrome.tabs.remove(tabId).catch((error) => {
|
||||
throw new Error("[BrowserApi] Failed to remove current tab: " + error.message);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates a browser tab to the given URL
|
||||
*
|
||||
* @param tabId The id of the tab to navigate
|
||||
* @param url The URL to navigate to
|
||||
*/
|
||||
static async navigateTabToUrl(tabId: number, url: URL): Promise<void> {
|
||||
if (tabId) {
|
||||
if (BrowserApi.isWebExtensionsApi) {
|
||||
browser.tabs.update(tabId, { url: url.href }).catch((error) => {
|
||||
throw new Error("Failed to navigate tab to URL: " + error.message);
|
||||
});
|
||||
} else if (BrowserApi.isChromeApi) {
|
||||
chrome.tabs.update(tabId, { url: url.href }, () => {
|
||||
if (chrome.runtime.lastError) {
|
||||
throw new Error("Failed to navigate tab to URL: " + chrome.runtime.lastError.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static async tabsQuery(options: chrome.tabs.QueryInfo): Promise<chrome.tabs.Tab[]> {
|
||||
return new Promise((resolve) => {
|
||||
chrome.tabs.query(options, (tabs) => {
|
||||
@@ -233,7 +274,7 @@ export class BrowserApi {
|
||||
* Drop-in replacement for {@link BrowserApi.tabsQueryFirst}.
|
||||
*
|
||||
* Safari sometimes returns >1 tabs unexpectedly even when
|
||||
* specificing a `windowId` or `currentWindow: true` query option.
|
||||
* specifying a `windowId` or `currentWindow: true` query option.
|
||||
*
|
||||
* For all of these calls,
|
||||
* ```
|
||||
@@ -320,6 +361,14 @@ export class BrowserApi {
|
||||
chrome.tabs.sendMessage<TabMessage, T>(tabId, message, options, responseCallback);
|
||||
}
|
||||
|
||||
static getRuntimeURL(path: string): string {
|
||||
if (BrowserApi.isWebExtensionsApi) {
|
||||
return browser.runtime.getURL(path);
|
||||
} else if (BrowserApi.isChromeApi) {
|
||||
return chrome.runtime.getURL(path);
|
||||
}
|
||||
}
|
||||
|
||||
static async onWindowCreated(callback: (win: chrome.windows.Window) => any) {
|
||||
// FIXME: Make sure that is does not cause a memory leak in Safari or use BrowserApi.AddListener
|
||||
// and test that it doesn't break.
|
||||
|
||||
@@ -17,7 +17,7 @@ export const PopupWidthOptions = Object.freeze({
|
||||
type PopupWidthOptions = typeof PopupWidthOptions;
|
||||
export type PopupWidthOption = keyof PopupWidthOptions;
|
||||
|
||||
class BrowserPopupUtils {
|
||||
export default class BrowserPopupUtils {
|
||||
/**
|
||||
* Identifies if the popup is within the sidebar.
|
||||
*
|
||||
@@ -288,5 +288,3 @@ class BrowserPopupUtils {
|
||||
return parsedUrl.toString();
|
||||
}
|
||||
}
|
||||
|
||||
export default BrowserPopupUtils;
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
UserLockIcon,
|
||||
VaultIcon,
|
||||
LockIcon,
|
||||
DeactivatedOrg,
|
||||
} from "@bitwarden/assets/svg";
|
||||
import {
|
||||
LoginComponent,
|
||||
@@ -40,7 +41,7 @@ import {
|
||||
NewDeviceVerificationComponent,
|
||||
} from "@bitwarden/auth/angular";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { AnonLayoutWrapperData } from "@bitwarden/components";
|
||||
import { AnonLayoutWrapperComponent, AnonLayoutWrapperData } from "@bitwarden/components";
|
||||
import { LockComponent, ConfirmKeyConnectorDomainComponent } from "@bitwarden/key-management-ui";
|
||||
|
||||
import { AccountSwitcherComponent } from "../auth/popup/account-switching/account-switcher.component";
|
||||
@@ -53,6 +54,8 @@ import { BlockedDomainsComponent } from "../autofill/popup/settings/blocked-doma
|
||||
import { ExcludedDomainsComponent } from "../autofill/popup/settings/excluded-domains.component";
|
||||
import { NotificationsSettingsComponent } from "../autofill/popup/settings/notifications.component";
|
||||
import { PremiumV2Component } from "../billing/popup/settings/premium-v2.component";
|
||||
import { LearnMoreComponent } from "../dirt/phishing-detection/pages/learn-more-component";
|
||||
import { PhishingWarning } from "../dirt/phishing-detection/pages/phishing-warning.component";
|
||||
import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component";
|
||||
import BrowserPopupUtils from "../platform/browser/browser-popup-utils";
|
||||
import { popupRouterCacheGuard } from "../platform/popup/view-cache/popup-router-cache.service";
|
||||
@@ -672,6 +675,32 @@ const routes: Routes = [
|
||||
canActivate: [authGuard],
|
||||
data: { elevation: 2 } satisfies RouteDataProperties,
|
||||
},
|
||||
{
|
||||
path: "security",
|
||||
component: AnonLayoutWrapperComponent,
|
||||
children: [
|
||||
{
|
||||
path: "phishing-warning",
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: PhishingWarning,
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
component: LearnMoreComponent,
|
||||
outlet: "secondary",
|
||||
},
|
||||
],
|
||||
data: {
|
||||
pageIcon: DeactivatedOrg,
|
||||
pageTitle: "Bitwarden blocked it!",
|
||||
pageSubtitle: "Bitwarden blocked a known phishing site from loading.",
|
||||
showReadonlyHostname: true,
|
||||
} satisfies AnonLayoutWrapperData,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@Injectable()
|
||||
|
||||
Reference in New Issue
Block a user