mirror of
https://github.com/bitwarden/browser
synced 2025-12-10 13:23:34 +00:00
[PM-26944] phishing data checksum diffing + daily patches (#16983)
* expose local db file to extension
* fetch from local db as fallback; only fetch new data on changed checksum; fetch from cdn
* check for undefined chrome runtime (for easy Storybook mocking)
* update capital letters lint
* add audit api tests
* add bash script to fetch local db info and split it to meet FF size limits
* add readme
* Rename README.md to readme.md
* remove leftover file
* remove unused methods from audit service
* remove local db logic
* wip
* revert local db build changes
* add tests; sub to updates directly; refactor teardown logic
* fix eslint crashing
* remove temp premium override
* remove unused test
* update timer value
* run prettier
* refetch all domains on app version change
* fix log statement
* harden fetching
* filter empty domains
* fix type issue
* fix typo
* fix type error
* fix cleanup
(cherry picked from commit 7ac6a67835)
This commit is contained in:
committed by
William Martin
parent
b4420d770e
commit
a1580f8aea
@@ -293,6 +293,7 @@ import { AutofillBadgeUpdaterService } from "../autofill/services/autofill-badge
|
|||||||
import AutofillService from "../autofill/services/autofill.service";
|
import AutofillService from "../autofill/services/autofill.service";
|
||||||
import { InlineMenuFieldQualificationService } from "../autofill/services/inline-menu-field-qualification.service";
|
import { InlineMenuFieldQualificationService } from "../autofill/services/inline-menu-field-qualification.service";
|
||||||
import { SafariApp } from "../browser/safariApp";
|
import { SafariApp } from "../browser/safariApp";
|
||||||
|
import { PhishingDataService } from "../dirt/phishing-detection/services/phishing-data.service";
|
||||||
import { PhishingDetectionService } from "../dirt/phishing-detection/services/phishing-detection.service";
|
import { PhishingDetectionService } from "../dirt/phishing-detection/services/phishing-detection.service";
|
||||||
import { BackgroundBrowserBiometricsService } from "../key-management/biometrics/background-browser-biometrics.service";
|
import { BackgroundBrowserBiometricsService } from "../key-management/biometrics/background-browser-biometrics.service";
|
||||||
import VaultTimeoutService from "../key-management/vault-timeout/vault-timeout.service";
|
import VaultTimeoutService from "../key-management/vault-timeout/vault-timeout.service";
|
||||||
@@ -491,6 +492,9 @@ export default class MainBackground {
|
|||||||
private popupViewCacheBackgroundService: PopupViewCacheBackgroundService;
|
private popupViewCacheBackgroundService: PopupViewCacheBackgroundService;
|
||||||
private popupRouterCacheBackgroundService: PopupRouterCacheBackgroundService;
|
private popupRouterCacheBackgroundService: PopupRouterCacheBackgroundService;
|
||||||
|
|
||||||
|
// DIRT
|
||||||
|
private phishingDataService: PhishingDataService;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
// Services
|
// Services
|
||||||
const lockedCallback = async (userId: UserId) => {
|
const lockedCallback = async (userId: UserId) => {
|
||||||
@@ -1451,15 +1455,20 @@ export default class MainBackground {
|
|||||||
|
|
||||||
this.inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService();
|
this.inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService();
|
||||||
|
|
||||||
|
this.phishingDataService = new PhishingDataService(
|
||||||
|
this.apiService,
|
||||||
|
this.taskSchedulerService,
|
||||||
|
this.globalStateProvider,
|
||||||
|
this.logService,
|
||||||
|
this.platformUtilsService,
|
||||||
|
);
|
||||||
|
|
||||||
PhishingDetectionService.initialize(
|
PhishingDetectionService.initialize(
|
||||||
this.accountService,
|
this.accountService,
|
||||||
this.auditService,
|
|
||||||
this.billingAccountProfileStateService,
|
this.billingAccountProfileStateService,
|
||||||
this.configService,
|
this.configService,
|
||||||
this.eventCollectionService,
|
|
||||||
this.logService,
|
this.logService,
|
||||||
this.storageService,
|
this.phishingDataService,
|
||||||
this.taskSchedulerService,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
this.ipcContentScriptManagerService = new IpcContentScriptManagerService(this.configService);
|
this.ipcContentScriptManagerService = new IpcContentScriptManagerService(this.configService);
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
import { MockProxy, mock } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
import {
|
||||||
|
DefaultTaskSchedulerService,
|
||||||
|
TaskSchedulerService,
|
||||||
|
} from "@bitwarden/common/platform/scheduling";
|
||||||
|
import { FakeGlobalStateProvider } from "@bitwarden/common/spec";
|
||||||
|
import { LogService } from "@bitwarden/logging";
|
||||||
|
|
||||||
|
import { PhishingDataService, PhishingData, PHISHING_DOMAINS_KEY } from "./phishing-data.service";
|
||||||
|
|
||||||
|
describe("PhishingDataService", () => {
|
||||||
|
let service: PhishingDataService;
|
||||||
|
let apiService: MockProxy<ApiService>;
|
||||||
|
let taskSchedulerService: TaskSchedulerService;
|
||||||
|
let logService: MockProxy<LogService>;
|
||||||
|
let platformUtilsService: MockProxy<PlatformUtilsService>;
|
||||||
|
const stateProvider: FakeGlobalStateProvider = new FakeGlobalStateProvider();
|
||||||
|
|
||||||
|
const setMockState = (state: PhishingData) => {
|
||||||
|
stateProvider.getFake(PHISHING_DOMAINS_KEY).stateSubject.next(state);
|
||||||
|
return state;
|
||||||
|
};
|
||||||
|
|
||||||
|
let fetchChecksumSpy: jest.SpyInstance;
|
||||||
|
let fetchDomainsSpy: jest.SpyInstance;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
apiService = mock<ApiService>();
|
||||||
|
logService = mock<LogService>();
|
||||||
|
|
||||||
|
platformUtilsService = mock<PlatformUtilsService>();
|
||||||
|
platformUtilsService.getApplicationVersion.mockResolvedValue("1.0.0");
|
||||||
|
|
||||||
|
taskSchedulerService = new DefaultTaskSchedulerService(logService);
|
||||||
|
|
||||||
|
service = new PhishingDataService(
|
||||||
|
apiService,
|
||||||
|
taskSchedulerService,
|
||||||
|
stateProvider,
|
||||||
|
logService,
|
||||||
|
platformUtilsService,
|
||||||
|
);
|
||||||
|
|
||||||
|
fetchChecksumSpy = jest.spyOn(service as any, "fetchPhishingDomainsChecksum");
|
||||||
|
fetchDomainsSpy = jest.spyOn(service as any, "fetchPhishingDomains");
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("isPhishingDomains", () => {
|
||||||
|
it("should detect a phishing domain", async () => {
|
||||||
|
setMockState({
|
||||||
|
domains: ["phish.com", "badguy.net"],
|
||||||
|
timestamp: Date.now(),
|
||||||
|
checksum: "abc123",
|
||||||
|
applicationVersion: "1.0.0",
|
||||||
|
});
|
||||||
|
const url = new URL("http://phish.com");
|
||||||
|
const result = await service.isPhishingDomain(url);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not detect a safe domain", async () => {
|
||||||
|
setMockState({
|
||||||
|
domains: ["phish.com", "badguy.net"],
|
||||||
|
timestamp: Date.now(),
|
||||||
|
checksum: "abc123",
|
||||||
|
applicationVersion: "1.0.0",
|
||||||
|
});
|
||||||
|
const url = new URL("http://safe.com");
|
||||||
|
const result = await service.isPhishingDomain(url);
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should match against root domain", async () => {
|
||||||
|
setMockState({
|
||||||
|
domains: ["phish.com", "badguy.net"],
|
||||||
|
timestamp: Date.now(),
|
||||||
|
checksum: "abc123",
|
||||||
|
applicationVersion: "1.0.0",
|
||||||
|
});
|
||||||
|
const url = new URL("http://phish.com/about");
|
||||||
|
const result = await service.isPhishingDomain(url);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not error on empty state", async () => {
|
||||||
|
setMockState(undefined as any);
|
||||||
|
const url = new URL("http://phish.com/about");
|
||||||
|
const result = await service.isPhishingDomain(url);
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getNextDomains", () => {
|
||||||
|
it("refetches all domains if applicationVersion has changed", async () => {
|
||||||
|
const prev: PhishingData = {
|
||||||
|
domains: ["a.com"],
|
||||||
|
timestamp: Date.now() - 60000,
|
||||||
|
checksum: "old",
|
||||||
|
applicationVersion: "1.0.0",
|
||||||
|
};
|
||||||
|
fetchChecksumSpy.mockResolvedValue("new");
|
||||||
|
fetchDomainsSpy.mockResolvedValue(["d.com", "e.com"]);
|
||||||
|
platformUtilsService.getApplicationVersion.mockResolvedValue("2.0.0");
|
||||||
|
|
||||||
|
const result = await service.getNextDomains(prev);
|
||||||
|
|
||||||
|
expect(result!.domains).toEqual(["d.com", "e.com"]);
|
||||||
|
expect(result!.checksum).toBe("new");
|
||||||
|
expect(result!.applicationVersion).toBe("2.0.0");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("only updates timestamp if checksum matches", async () => {
|
||||||
|
const prev: PhishingData = {
|
||||||
|
domains: ["a.com"],
|
||||||
|
timestamp: Date.now() - 60000,
|
||||||
|
checksum: "abc",
|
||||||
|
applicationVersion: "1.0.0",
|
||||||
|
};
|
||||||
|
fetchChecksumSpy.mockResolvedValue("abc");
|
||||||
|
const result = await service.getNextDomains(prev);
|
||||||
|
expect(result!.domains).toEqual(prev.domains);
|
||||||
|
expect(result!.checksum).toBe("abc");
|
||||||
|
expect(result!.timestamp).not.toBe(prev.timestamp);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("patches daily domains if cache is fresh", async () => {
|
||||||
|
const prev: PhishingData = {
|
||||||
|
domains: ["a.com"],
|
||||||
|
timestamp: Date.now() - 60000,
|
||||||
|
checksum: "old",
|
||||||
|
applicationVersion: "1.0.0",
|
||||||
|
};
|
||||||
|
fetchChecksumSpy.mockResolvedValue("new");
|
||||||
|
fetchDomainsSpy.mockResolvedValue(["b.com", "c.com"]);
|
||||||
|
const result = await service.getNextDomains(prev);
|
||||||
|
expect(result!.domains).toEqual(["a.com", "b.com", "c.com"]);
|
||||||
|
expect(result!.checksum).toBe("new");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fetches all domains if cache is old", async () => {
|
||||||
|
const prev: PhishingData = {
|
||||||
|
domains: ["a.com"],
|
||||||
|
timestamp: Date.now() - 2 * 24 * 60 * 60 * 1000,
|
||||||
|
checksum: "old",
|
||||||
|
applicationVersion: "1.0.0",
|
||||||
|
};
|
||||||
|
fetchChecksumSpy.mockResolvedValue("new");
|
||||||
|
fetchDomainsSpy.mockResolvedValue(["d.com", "e.com"]);
|
||||||
|
const result = await service.getNextDomains(prev);
|
||||||
|
expect(result!.domains).toEqual(["d.com", "e.com"]);
|
||||||
|
expect(result!.checksum).toBe("new");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
import {
|
||||||
|
catchError,
|
||||||
|
EMPTY,
|
||||||
|
first,
|
||||||
|
firstValueFrom,
|
||||||
|
map,
|
||||||
|
retry,
|
||||||
|
startWith,
|
||||||
|
Subject,
|
||||||
|
switchMap,
|
||||||
|
tap,
|
||||||
|
timer,
|
||||||
|
} from "rxjs";
|
||||||
|
|
||||||
|
import { devFlagEnabled, devFlagValue } from "@bitwarden/browser/platform/flags";
|
||||||
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
import { ScheduledTaskNames, TaskSchedulerService } from "@bitwarden/common/platform/scheduling";
|
||||||
|
import { LogService } from "@bitwarden/logging";
|
||||||
|
import { GlobalStateProvider, KeyDefinition, PHISHING_DETECTION_DISK } from "@bitwarden/state";
|
||||||
|
|
||||||
|
export type PhishingData = {
|
||||||
|
domains: string[];
|
||||||
|
timestamp: number;
|
||||||
|
checksum: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We store the application version to refetch the entire dataset on a new client release.
|
||||||
|
* This counteracts daily appends updates not removing inactive or false positive domains.
|
||||||
|
*/
|
||||||
|
applicationVersion: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PHISHING_DOMAINS_KEY = new KeyDefinition<PhishingData>(
|
||||||
|
PHISHING_DETECTION_DISK,
|
||||||
|
"phishingDomains",
|
||||||
|
{
|
||||||
|
deserializer: (value: PhishingData) =>
|
||||||
|
value ?? { domains: [], timestamp: 0, checksum: "", applicationVersion: "" },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Coordinates fetching, caching, and patching of known phishing domains */
|
||||||
|
export class PhishingDataService {
|
||||||
|
private static readonly RemotePhishingDatabaseUrl =
|
||||||
|
"https://raw.githubusercontent.com/Phishing-Database/Phishing.Database/master/phishing-domains-ACTIVE.txt";
|
||||||
|
private static readonly RemotePhishingDatabaseChecksumUrl =
|
||||||
|
"https://raw.githubusercontent.com/Phishing-Database/checksums/refs/heads/master/phishing-domains-ACTIVE.txt.md5";
|
||||||
|
private static readonly RemotePhishingDatabaseTodayUrl =
|
||||||
|
"https://raw.githubusercontent.com/Phishing-Database/Phishing.Database/refs/heads/master/phishing-domains-NEW-today.txt";
|
||||||
|
|
||||||
|
private _testDomains = this.getTestDomains();
|
||||||
|
private _cachedState = this.globalStateProvider.get(PHISHING_DOMAINS_KEY);
|
||||||
|
private _domains$ = this._cachedState.state$.pipe(
|
||||||
|
map(
|
||||||
|
(state) =>
|
||||||
|
new Set(
|
||||||
|
(state?.domains?.filter((line) => line.trim().length > 0) ?? []).concat(
|
||||||
|
this._testDomains,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// How often are new domains added to the remote?
|
||||||
|
readonly UPDATE_INTERVAL_DURATION = 24 * 60 * 60 * 1000; // 24 hours
|
||||||
|
|
||||||
|
private _triggerUpdate$ = new Subject<void>();
|
||||||
|
update$ = this._triggerUpdate$.pipe(
|
||||||
|
startWith(), // Always emit once
|
||||||
|
tap(() => this.logService.info(`[PhishingDataService] Update triggered...`)),
|
||||||
|
switchMap(() =>
|
||||||
|
this._cachedState.state$.pipe(
|
||||||
|
first(), // Only take the first value to avoid an infinite loop when updating the cache below
|
||||||
|
switchMap(async (cachedState) => {
|
||||||
|
const next = await this.getNextDomains(cachedState);
|
||||||
|
if (next) {
|
||||||
|
await this._cachedState.update(() => next);
|
||||||
|
this.logService.info(`[PhishingDataService] cache updated`);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
retry({
|
||||||
|
count: 3,
|
||||||
|
delay: (err, count) => {
|
||||||
|
this.logService.error(
|
||||||
|
`[PhishingDataService] Unable to update domains. Attempt ${count}.`,
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
return timer(5 * 60 * 1000); // 5 minutes
|
||||||
|
},
|
||||||
|
resetOnSuccess: true,
|
||||||
|
}),
|
||||||
|
catchError(
|
||||||
|
(
|
||||||
|
err: unknown /** Eslint actually crashed if you remove this type: https://github.com/cartant/eslint-plugin-rxjs/issues/122 */,
|
||||||
|
) => {
|
||||||
|
this.logService.error(
|
||||||
|
"[PhishingDataService] Retries unsuccessful. Unable to update domains.",
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
return EMPTY;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private apiService: ApiService,
|
||||||
|
private taskSchedulerService: TaskSchedulerService,
|
||||||
|
private globalStateProvider: GlobalStateProvider,
|
||||||
|
private logService: LogService,
|
||||||
|
private platformUtilsService: PlatformUtilsService,
|
||||||
|
) {
|
||||||
|
this.taskSchedulerService.registerTaskHandler(ScheduledTaskNames.phishingDomainUpdate, () => {
|
||||||
|
this._triggerUpdate$.next();
|
||||||
|
});
|
||||||
|
this.taskSchedulerService.setInterval(
|
||||||
|
ScheduledTaskNames.phishingDomainUpdate,
|
||||||
|
this.UPDATE_INTERVAL_DURATION,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
async isPhishingDomain(url: URL): Promise<boolean> {
|
||||||
|
const domains = await firstValueFrom(this._domains$);
|
||||||
|
const result = domains.has(url.hostname);
|
||||||
|
if (result) {
|
||||||
|
this.logService.debug("[PhishingDataService] Caught phishing domain:", url.hostname);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getNextDomains(prev: PhishingData | null): Promise<PhishingData | null> {
|
||||||
|
prev = prev ?? { domains: [], timestamp: 0, checksum: "", applicationVersion: "" };
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const prevAge = timestamp - prev.timestamp;
|
||||||
|
this.logService.info(`[PhishingDataService] Cache age: ${prevAge}`);
|
||||||
|
|
||||||
|
const applicationVersion = await this.platformUtilsService.getApplicationVersion();
|
||||||
|
|
||||||
|
// If checksum matches, return existing data with new timestamp & version
|
||||||
|
const remoteChecksum = await this.fetchPhishingDomainsChecksum();
|
||||||
|
if (remoteChecksum && prev.checksum === remoteChecksum) {
|
||||||
|
this.logService.info(
|
||||||
|
`[PhishingDataService] Remote checksum matches local checksum, updating timestamp only.`,
|
||||||
|
);
|
||||||
|
return { ...prev, timestamp, applicationVersion };
|
||||||
|
}
|
||||||
|
// Checksum is different, data needs to be updated.
|
||||||
|
|
||||||
|
// Approach 1: Fetch only new domains and append
|
||||||
|
const isOneDayOldMax = prevAge <= this.UPDATE_INTERVAL_DURATION;
|
||||||
|
if (isOneDayOldMax && applicationVersion === prev.applicationVersion) {
|
||||||
|
const dailyDomains: string[] = await this.fetchPhishingDomains(
|
||||||
|
PhishingDataService.RemotePhishingDatabaseTodayUrl,
|
||||||
|
);
|
||||||
|
this.logService.info(
|
||||||
|
`[PhishingDataService] ${dailyDomains.length} new phishing domains added`,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
domains: prev.domains.concat(dailyDomains),
|
||||||
|
checksum: remoteChecksum,
|
||||||
|
timestamp,
|
||||||
|
applicationVersion,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Approach 2: Fetch all domains
|
||||||
|
const domains = await this.fetchPhishingDomains(PhishingDataService.RemotePhishingDatabaseUrl);
|
||||||
|
return {
|
||||||
|
domains,
|
||||||
|
timestamp,
|
||||||
|
checksum: remoteChecksum,
|
||||||
|
applicationVersion,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fetchPhishingDomainsChecksum() {
|
||||||
|
const response = await this.apiService.nativeFetch(
|
||||||
|
new Request(PhishingDataService.RemotePhishingDatabaseChecksumUrl),
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`[PhishingDataService] Failed to fetch checksum: ${response.status}`);
|
||||||
|
}
|
||||||
|
return response.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fetchPhishingDomains(url: string) {
|
||||||
|
const response = await this.apiService.nativeFetch(new Request(url));
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`[PhishingDataService] Failed to fetch domains: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.text().then((text) => text.split("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private getTestDomains() {
|
||||||
|
const flag = devFlagEnabled("testPhishingUrls");
|
||||||
|
if (!flag) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const domains = devFlagValue("testPhishingUrls") as unknown[];
|
||||||
|
if (domains && domains instanceof Array) {
|
||||||
|
this.logService.debug(
|
||||||
|
"[PhishingDetectionService] Dev flag enabled for testing phishing detection. Adding test phishing domains:",
|
||||||
|
domains,
|
||||||
|
);
|
||||||
|
return domains as string[];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,48 +1,36 @@
|
|||||||
import { of } from "rxjs";
|
import { of } from "rxjs";
|
||||||
|
|
||||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
|
||||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
|
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
|
||||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.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 { PhishingDataService } from "./phishing-data.service";
|
||||||
import { PhishingDetectionService } from "./phishing-detection.service";
|
import { PhishingDetectionService } from "./phishing-detection.service";
|
||||||
|
|
||||||
describe("PhishingDetectionService", () => {
|
describe("PhishingDetectionService", () => {
|
||||||
let accountService: AccountService;
|
let accountService: AccountService;
|
||||||
let auditService: AuditService;
|
|
||||||
let billingAccountProfileStateService: BillingAccountProfileStateService;
|
let billingAccountProfileStateService: BillingAccountProfileStateService;
|
||||||
let configService: ConfigService;
|
let configService: ConfigService;
|
||||||
let eventCollectionService: EventCollectionService;
|
|
||||||
let logService: LogService;
|
let logService: LogService;
|
||||||
let storageService: AbstractStorageService;
|
let phishingDataService: PhishingDataService;
|
||||||
let taskSchedulerService: TaskSchedulerService;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
accountService = { getAccount$: jest.fn(() => of(null)) } as any;
|
accountService = { getAccount$: jest.fn(() => of(null)) } as any;
|
||||||
auditService = { getKnownPhishingDomains: jest.fn() } as any;
|
|
||||||
billingAccountProfileStateService = {} as any;
|
billingAccountProfileStateService = {} as any;
|
||||||
configService = { getFeatureFlag$: jest.fn(() => of(false)) } as any;
|
configService = { getFeatureFlag$: jest.fn(() => of(false)) } as any;
|
||||||
eventCollectionService = {} as any;
|
|
||||||
logService = { info: jest.fn(), debug: jest.fn(), warning: jest.fn(), error: 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;
|
phishingDataService = {} as any;
|
||||||
taskSchedulerService = { registerTaskHandler: jest.fn(), setInterval: jest.fn() } as any;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should initialize without errors", () => {
|
it("should initialize without errors", () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
PhishingDetectionService.initialize(
|
PhishingDetectionService.initialize(
|
||||||
accountService,
|
accountService,
|
||||||
auditService,
|
|
||||||
billingAccountProfileStateService,
|
billingAccountProfileStateService,
|
||||||
configService,
|
configService,
|
||||||
eventCollectionService,
|
|
||||||
logService,
|
logService,
|
||||||
storageService,
|
phishingDataService,
|
||||||
taskSchedulerService,
|
|
||||||
);
|
);
|
||||||
}).not.toThrow();
|
}).not.toThrow();
|
||||||
});
|
});
|
||||||
@@ -66,13 +54,10 @@ describe("PhishingDetectionService", () => {
|
|||||||
// Run the initialization
|
// Run the initialization
|
||||||
PhishingDetectionService.initialize(
|
PhishingDetectionService.initialize(
|
||||||
accountService,
|
accountService,
|
||||||
auditService,
|
|
||||||
billingAccountProfileStateService,
|
billingAccountProfileStateService,
|
||||||
configService,
|
configService,
|
||||||
eventCollectionService,
|
|
||||||
logService,
|
logService,
|
||||||
storageService,
|
phishingDataService,
|
||||||
taskSchedulerService,
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -105,23 +90,10 @@ describe("PhishingDetectionService", () => {
|
|||||||
// Run the initialization
|
// Run the initialization
|
||||||
PhishingDetectionService.initialize(
|
PhishingDetectionService.initialize(
|
||||||
accountService,
|
accountService,
|
||||||
auditService,
|
|
||||||
billingAccountProfileStateService,
|
billingAccountProfileStateService,
|
||||||
configService,
|
configService,
|
||||||
eventCollectionService,
|
|
||||||
logService,
|
logService,
|
||||||
storageService,
|
phishingDataService,
|
||||||
taskSchedulerService,
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
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
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,28 +1,14 @@
|
|||||||
import {
|
import { combineLatest, concatMap, delay, EMPTY, map, Subject, switchMap, takeUntil } from "rxjs";
|
||||||
combineLatest,
|
|
||||||
concatMap,
|
|
||||||
delay,
|
|
||||||
EMPTY,
|
|
||||||
map,
|
|
||||||
Subject,
|
|
||||||
Subscription,
|
|
||||||
switchMap,
|
|
||||||
} from "rxjs";
|
|
||||||
|
|
||||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
|
||||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
|
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
|
||||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.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 { BrowserApi } from "../../../platform/browser/browser-api";
|
||||||
|
|
||||||
|
import { PhishingDataService } from "./phishing-data.service";
|
||||||
import {
|
import {
|
||||||
CaughtPhishingDomain,
|
CaughtPhishingDomain,
|
||||||
isPhishingDetectionMessage,
|
isPhishingDetectionMessage,
|
||||||
@@ -32,39 +18,23 @@ import {
|
|||||||
} from "./phishing-detection.types";
|
} from "./phishing-detection.types";
|
||||||
|
|
||||||
export class PhishingDetectionService {
|
export class PhishingDetectionService {
|
||||||
private static readonly _UPDATE_INTERVAL = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
|
private static _destroy$ = new Subject<void>();
|
||||||
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 _logService: LogService;
|
||||||
private static _storageService: AbstractStorageService;
|
private static _phishingDataService: PhishingDataService;
|
||||||
private static _taskSchedulerService: TaskSchedulerService;
|
|
||||||
private static _updateCacheSubscription: Subscription | null = null;
|
|
||||||
private static _retrySubscription: Subscription | null = null;
|
|
||||||
private static _navigationEventsSubject = new Subject<PhishingDetectionNavigationEvent>();
|
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 _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(
|
static initialize(
|
||||||
accountService: AccountService,
|
accountService: AccountService,
|
||||||
auditService: AuditService,
|
|
||||||
billingAccountProfileStateService: BillingAccountProfileStateService,
|
billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||||
configService: ConfigService,
|
configService: ConfigService,
|
||||||
eventCollectionService: EventCollectionService,
|
|
||||||
logService: LogService,
|
logService: LogService,
|
||||||
storageService: AbstractStorageService,
|
phishingDataService: PhishingDataService,
|
||||||
taskSchedulerService: TaskSchedulerService,
|
|
||||||
): void {
|
): void {
|
||||||
this._auditService = auditService;
|
|
||||||
this._logService = logService;
|
this._logService = logService;
|
||||||
this._storageService = storageService;
|
this._phishingDataService = phishingDataService;
|
||||||
this._taskSchedulerService = taskSchedulerService;
|
|
||||||
|
|
||||||
logService.info("[PhishingDetectionService] Initialize called. Checking prerequisites...");
|
logService.info("[PhishingDetectionService] Initialize called. Checking prerequisites...");
|
||||||
|
|
||||||
@@ -98,21 +68,6 @@ export class PhishingDetectionService {
|
|||||||
.subscribe();
|
.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
|
* Sends a message to the phishing detection service to close the warning page
|
||||||
*/
|
*/
|
||||||
@@ -146,45 +101,12 @@ export class PhishingDetectionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
* Sets up listeners for messages from the web page and web navigation events
|
||||||
*/
|
*/
|
||||||
private static _setupListeners(): void {
|
private static _setup(): void {
|
||||||
|
this._phishingDataService.update$.pipe(takeUntil(this._destroy$)).subscribe();
|
||||||
|
|
||||||
// Setup listeners from web page/content script
|
// Setup listeners from web page/content script
|
||||||
BrowserApi.addListener(chrome.runtime.onMessage, this._handleExtensionMessage.bind(this));
|
BrowserApi.addListener(chrome.runtime.onMessage, this._handleExtensionMessage.bind(this));
|
||||||
BrowserApi.addListener(chrome.tabs.onReplaced, this._handleReplacementEvent.bind(this));
|
BrowserApi.addListener(chrome.tabs.onReplaced, this._handleReplacementEvent.bind(this));
|
||||||
@@ -192,9 +114,10 @@ export class PhishingDetectionService {
|
|||||||
|
|
||||||
// When a navigation event occurs, check if a replace event for the same tabId exists,
|
// When a navigation event occurs, check if a replace event for the same tabId exists,
|
||||||
// and call the replace handler before handling navigation.
|
// and call the replace handler before handling navigation.
|
||||||
this._navigationEvents = this._navigationEventsSubject
|
this._navigationEventsSubject
|
||||||
.pipe(
|
.pipe(
|
||||||
delay(100), // Delay slightly to allow replace events to be caught
|
delay(100), // Delay slightly to allow replace events to be caught
|
||||||
|
takeUntil(this._destroy$),
|
||||||
)
|
)
|
||||||
.subscribe(({ tabId, changeInfo, tab }) => {
|
.subscribe(({ tabId, changeInfo, tab }) => {
|
||||||
void this._processNavigation(tabId, changeInfo, tab);
|
void this._processNavigation(tabId, changeInfo, tab);
|
||||||
@@ -271,7 +194,7 @@ export class PhishingDetectionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if tab is navigating to a phishing url and handle navigation
|
// Check if tab is navigating to a phishing url and handle navigation
|
||||||
this._checkTabForPhishing(tabId, new URL(tab.url));
|
await this._checkTabForPhishing(tabId, new URL(tab.url));
|
||||||
await this._handleTabNavigation(tabId);
|
await this._handleTabNavigation(tabId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -371,11 +294,11 @@ export class PhishingDetectionService {
|
|||||||
* @param tabId Tab to check for phishing domain
|
* @param tabId Tab to check for phishing domain
|
||||||
* @param url URL of the tab to check
|
* @param url URL of the tab to check
|
||||||
*/
|
*/
|
||||||
private static _checkTabForPhishing(tabId: PhishingDetectionTabId, url: URL) {
|
private static async _checkTabForPhishing(tabId: PhishingDetectionTabId, url: URL) {
|
||||||
// Check if the tab already being tracked
|
// Check if the tab already being tracked
|
||||||
const caughtTab = this._caughtTabs.get(tabId);
|
const caughtTab = this._caughtTabs.get(tabId);
|
||||||
|
|
||||||
const isPhishing = this.isPhishingDomain(url);
|
const isPhishing = await this._phishingDataService.isPhishingDomain(url);
|
||||||
this._logService.debug(
|
this._logService.debug(
|
||||||
`[PhishingDetectionService] Checking for phishing url. Result: ${isPhishing} on ${url}`,
|
`[PhishingDetectionService] Checking for phishing url. Result: ${isPhishing} on ${url}`,
|
||||||
);
|
);
|
||||||
@@ -458,237 +381,16 @@ export class PhishingDetectionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
* Cleans up the phishing detection service
|
||||||
* Unsubscribes from all subscriptions and clears caches
|
* Unsubscribes from all subscriptions and clears caches
|
||||||
*/
|
*/
|
||||||
private static _cleanup() {
|
private static _cleanup() {
|
||||||
if (this._updateCacheSubscription) {
|
this._destroy$.next();
|
||||||
this._updateCacheSubscription.unsubscribe();
|
this._destroy$.complete();
|
||||||
this._updateCacheSubscription = null;
|
this._destroy$ = new Subject<void>();
|
||||||
}
|
|
||||||
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._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
|
// 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
|
// of static and instance methods in this class. To be fixed when refactoring
|
||||||
|
|||||||
@@ -14,10 +14,4 @@ export abstract class AuditService {
|
|||||||
* @returns A promise that resolves to an array of BreachAccountResponse objects.
|
* @returns A promise that resolves to an array of BreachAccountResponse objects.
|
||||||
*/
|
*/
|
||||||
abstract breachedAccounts: (username: string) => Promise<BreachAccountResponse[]>;
|
abstract breachedAccounts: (username: string) => Promise<BreachAccountResponse[]>;
|
||||||
/**
|
|
||||||
* Checks if a domain is known for phishing.
|
|
||||||
* @param domain The domain to check.
|
|
||||||
* @returns A promise that resolves to a boolean indicating if the domain is known for phishing.
|
|
||||||
*/
|
|
||||||
abstract getKnownPhishingDomains: () => Promise<string[]>;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,9 +80,4 @@ export class AuditService implements AuditServiceAbstraction {
|
|||||||
throw new Error();
|
throw new Error();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getKnownPhishingDomains(): Promise<string[]> {
|
|
||||||
const response = await this.apiService.send("GET", "/phishing-domains", null, true, true);
|
|
||||||
return response as string[];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,6 +107,10 @@ export const NEW_WEB_LAYOUT_BANNER_DISK = new StateDefinition("newWebLayoutBanne
|
|||||||
web: "disk-local",
|
web: "disk-local",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// DIRT
|
||||||
|
|
||||||
|
export const PHISHING_DETECTION_DISK = new StateDefinition("phishingDetection", "disk");
|
||||||
|
|
||||||
// Platform
|
// Platform
|
||||||
|
|
||||||
export const APPLICATION_ID_DISK = new StateDefinition("applicationId", "disk", {
|
export const APPLICATION_ID_DISK = new StateDefinition("applicationId", "disk", {
|
||||||
|
|||||||
Reference in New Issue
Block a user