diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 5ad7cf80f31..5b1a01e57e0 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -271,7 +271,6 @@ import BrowserMemoryStorageService from "../platform/services/browser-memory-sto import { BrowserScriptInjectorService } from "../platform/services/browser-script-injector.service"; import I18nService from "../platform/services/i18n.service"; import { LocalBackedSessionStorageService } from "../platform/services/local-backed-session-storage.service"; -import { PhishingApiService } from "../platform/services/phishing-api.service"; import { PhishingDetectionService } from "../platform/services/phishing-detection.service"; import { BackgroundPlatformUtilsService } from "../platform/services/platform-utils/background-platform-utils.service"; import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service"; @@ -711,9 +710,8 @@ export default class MainBackground { this.vaultTimeoutSettingsService, ); - const phishingApiService = new PhishingApiService(this.apiService); PhishingDetectionService.initialize( - phishingApiService, + this.auditService, this.logService, this.storageService, this.taskSchedulerService, diff --git a/apps/browser/src/platform/services/phishing-api.service.ts b/apps/browser/src/platform/services/phishing-api.service.ts deleted file mode 100644 index f02402e093a..00000000000 --- a/apps/browser/src/platform/services/phishing-api.service.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { PhishingApiServiceAbstraction } from "@bitwarden/common/abstractions/phishing-api.service.abstraction"; - -export class PhishingApiService implements PhishingApiServiceAbstraction { - constructor(private apiService: ApiService) {} - - async getKnownPhishingDomains(): Promise { - const response = await this.apiService.send("GET", "/phishing-domains", null, false, true); - return response as string[]; - } -} diff --git a/apps/browser/src/platform/services/phishing-detection.service.ts b/apps/browser/src/platform/services/phishing-detection.service.ts index 2d9d59241b7..a40ccd54c82 100644 --- a/apps/browser/src/platform/services/phishing-detection.service.ts +++ b/apps/browser/src/platform/services/phishing-detection.service.ts @@ -1,6 +1,6 @@ import { Subscription } from "rxjs"; -import { PhishingApiServiceAbstraction } from "@bitwarden/common/abstractions/phishing-api.service.abstraction"; +import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -16,7 +16,7 @@ export class PhishingDetectionService { 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 phishingApiService: PhishingApiServiceAbstraction; + private static auditService: AuditService; private static logService: LogService; private static storageService: AbstractStorageService; private static taskSchedulerService: TaskSchedulerService; @@ -26,12 +26,12 @@ export class PhishingDetectionService { private static retryCount = 0; static initialize( - phishingApiService: PhishingApiServiceAbstraction, + auditService: AuditService, logService: LogService, storageService: AbstractStorageService, taskSchedulerService: TaskSchedulerService, ) { - PhishingDetectionService.phishingApiService = phishingApiService; + PhishingDetectionService.auditService = auditService; PhishingDetectionService.logService = logService; PhishingDetectionService.storageService = storageService; PhishingDetectionService.taskSchedulerService = taskSchedulerService; @@ -145,7 +145,7 @@ export class PhishingDetectionService { this.isUpdating = true; try { this.logService.info("Starting phishing domains update..."); - const domains = await PhishingDetectionService.phishingApiService.getKnownPhishingDomains(); + const domains = await PhishingDetectionService.auditService.getKnownPhishingDomains(); this.logService.info("Received phishing domains response"); // Clear old domains to prevent memory leaks diff --git a/apps/web/src/app/tools/reports/pages/phishing-website-report.component.html b/apps/web/src/app/tools/reports/pages/phishing-website-report.component.html new file mode 100644 index 00000000000..00d1501a3f8 --- /dev/null +++ b/apps/web/src/app/tools/reports/pages/phishing-website-report.component.html @@ -0,0 +1,100 @@ + + + +

{{ "phishingWebsiteReportDesc" | i18n }}

+
+ + {{ "loading" | i18n }} +
+
+ + {{ "noPhishingWebsitesFound" | i18n }} + + + + {{ "phishingWebsitesFoundReportDesc" | i18n: (ciphers.length | number) : vaultMsg }} + + + + + + {{ getName(status) }} + {{ getCount(status) }} + + + + + + + + {{ "name" | i18n }} + {{ "owner" | i18n }} + + + + + + + + + + + {{ r.name }} + + + {{ r.name }} + + + + {{ "shared" | i18n }} + + + + {{ "attachments" | i18n }} + +
+ {{ r.subTitle }} + + + + + + +
+
+
+
+ +
diff --git a/apps/web/src/app/tools/reports/pages/phishing-website-report.component.ts b/apps/web/src/app/tools/reports/pages/phishing-website-report.component.ts new file mode 100644 index 00000000000..a339f39e8eb --- /dev/null +++ b/apps/web/src/app/tools/reports/pages/phishing-website-report.component.ts @@ -0,0 +1,119 @@ +import { Component, OnInit } from "@angular/core"; + +import { CollectionService } from "@bitwarden/admin-console/common"; +import { AuditService } from "@bitwarden/common/abstractions/audit.service"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { SyncService } from "@bitwarden/common/platform/sync"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherType } from "@bitwarden/common/vault/enums"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { DialogService } from "@bitwarden/components"; +import { PasswordRepromptService, CipherFormConfigService } from "@bitwarden/vault"; + +import { AdminConsoleCipherFormConfigService } from "../../../vault/org-vault/services/admin-console-cipher-form-config.service"; + +import { CipherReportComponent } from "./cipher-report.component"; + +type ReportResult = CipherView & { exposedXTimes: number }; + +@Component({ + selector: "app-phishing-webiste-report", + templateUrl: "phishing-website-report.component.html", +}) +export class PhishingWebsiteReport extends CipherReportComponent implements OnInit { + disabled = true; + + constructor( + protected cipherService: CipherService, + protected organizationService: OrganizationService, + dialogService: DialogService, + accountService: AccountService, + passwordRepromptService: PasswordRepromptService, + i18nService: I18nService, + protected auditService: AuditService, + syncService: SyncService, + private collectionService: CollectionService, + cipherFormConfigService: CipherFormConfigService, + adminConsoleCipherFormConfigService: AdminConsoleCipherFormConfigService, + ) { + super( + cipherService, + dialogService, + passwordRepromptService, + organizationService, + accountService, + i18nService, + syncService, + cipherFormConfigService, + adminConsoleCipherFormConfigService, + ); + } + + async ngOnInit() { + await super.load(); + } + + async setCiphers() { + const allCiphers = await this.getAllCiphers(); + const visitedPhishingWebsites: ReportResult[] = []; + const promises: Promise[] = []; + this.filterStatus = [0]; + allCiphers.forEach((ciph) => { + const { type, login, isDeleted, edit, viewPassword } = ciph; + if ( + type !== CipherType.Login || + login.password == null || + login.password === "" || + isDeleted || + (!this.organization && !edit) || + !viewPassword + ) { + return; + } + + const promise = this.auditService.passwordLeaked(login.password).then((exposedCount) => { + if (exposedCount > 0) { + const row = { ...ciph, exposedXTimes: exposedCount } as ReportResult; + visitedPhishingWebsites.push(row); + } + }); + promises.push(promise); + }); + await Promise.all(promises); + this.filterCiphersByOrg(visitedPhishingWebsites); + this.dataSource.sort = { column: "exposedXTimes", direction: "desc" }; + } + + /** + * Cipher needs to be a Login type, contain Uris, and not be deleted + * @param cipher Current cipher with unsecured uri + */ + private cipherContainsUnsecured(cipher: CipherView): boolean { + if ( + cipher.type !== CipherType.Login || + !cipher.login.hasUris || + cipher.isDeleted || + (!this.organization && !cipher.edit) + ) { + return false; + } + + const containsUnsecured = cipher.login.uris.some( + (u: any) => u.uri != null && u.uri.indexOf("http://") === 0, + ); + return containsUnsecured; + } + + /** + * Provides a way to determine if someone with permissions to run an organizational report is also able to view/edit ciphers within the results + * Default to true for indivduals running reports on their own vault. + * @param c CipherView + * @returns boolean + */ + protected canManageCipher(c: CipherView): boolean { + // this will only ever be false from the org view; + return true; + } +} diff --git a/apps/web/src/app/tools/reports/pages/reports-home.component.ts b/apps/web/src/app/tools/reports/pages/reports-home.component.ts index 604d66f6858..f6691c7405a 100644 --- a/apps/web/src/app/tools/reports/pages/reports-home.component.ts +++ b/apps/web/src/app/tools/reports/pages/reports-home.component.ts @@ -47,6 +47,10 @@ export class ReportsHomeComponent implements OnInit { ...reports[ReportType.UnsecuredWebsites], variant: reportRequiresPremium, }, + { + ...reports[ReportType.PhishingWebsitesReport], + variant: reportRequiresPremium, + }, { ...reports[ReportType.Inactive2fa], variant: reportRequiresPremium, diff --git a/apps/web/src/app/tools/reports/reports-routing.module.ts b/apps/web/src/app/tools/reports/reports-routing.module.ts index 941e6eb7d3d..d86121a605e 100644 --- a/apps/web/src/app/tools/reports/reports-routing.module.ts +++ b/apps/web/src/app/tools/reports/reports-routing.module.ts @@ -8,6 +8,7 @@ import { hasPremiumGuard } from "../../billing/guards/has-premium.guard"; import { BreachReportComponent } from "./pages/breach-report.component"; import { ExposedPasswordsReportComponent } from "./pages/exposed-passwords-report.component"; import { InactiveTwoFactorReportComponent } from "./pages/inactive-two-factor-report.component"; +import { PhishingWebsiteReport } from "./pages/phishing-website-report.component"; import { ReportsHomeComponent } from "./pages/reports-home.component"; import { ReusedPasswordsReportComponent } from "./pages/reused-passwords-report.component"; import { UnsecuredWebsitesReportComponent } from "./pages/unsecured-websites-report.component"; @@ -61,6 +62,12 @@ const routes: Routes = [ data: { titleId: "inactive2faReport" }, canActivate: [hasPremiumGuard()], }, + { + path: "phishing-website-report", + component: PhishingWebsiteReport, + data: { titleId: "phishingWebsiteReport" }, + canActivate: [hasPremiumGuard()], + }, ], }, ]; diff --git a/apps/web/src/app/tools/reports/reports.module.ts b/apps/web/src/app/tools/reports/reports.module.ts index 358768e71ee..9b14b777150 100644 --- a/apps/web/src/app/tools/reports/reports.module.ts +++ b/apps/web/src/app/tools/reports/reports.module.ts @@ -14,6 +14,7 @@ import { AdminConsoleCipherFormConfigService } from "../../vault/org-vault/servi import { BreachReportComponent } from "./pages/breach-report.component"; import { ExposedPasswordsReportComponent } from "./pages/exposed-passwords-report.component"; import { InactiveTwoFactorReportComponent } from "./pages/inactive-two-factor-report.component"; +import { PhishingWebsiteReport } from "./pages/phishing-website-report.component"; import { ReportsHomeComponent } from "./pages/reports-home.component"; import { ReusedPasswordsReportComponent } from "./pages/reused-passwords-report.component"; import { UnsecuredWebsitesReportComponent } from "./pages/unsecured-websites-report.component"; @@ -41,6 +42,7 @@ import { ReportsSharedModule } from "./shared"; ReusedPasswordsReportComponent, UnsecuredWebsitesReportComponent, WeakPasswordsReportComponent, + PhishingWebsiteReport, ], providers: [ { diff --git a/apps/web/src/app/tools/reports/reports.ts b/apps/web/src/app/tools/reports/reports.ts index 500ae23e5cf..a0769006700 100644 --- a/apps/web/src/app/tools/reports/reports.ts +++ b/apps/web/src/app/tools/reports/reports.ts @@ -15,6 +15,7 @@ export enum ReportType { Inactive2fa = "inactive2fa", DataBreach = "dataBreach", MemberAccessReport = "memberAccessReport", + PhishingWebsitesReport = "phishingWebsiteReport", } type ReportWithoutVariant = Omit; @@ -62,4 +63,10 @@ export const reports: Record = { route: "member-access-report", icon: MemberAccess, }, + [ReportType.PhishingWebsitesReport]: { + title: "phishingWebsiteReport", + description: "phishingWebsiteReportDesc", + route: "phishing-website-report", + icon: MemberAccess, + }, }; diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 229cca65ae5..e24a4dd1d37 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -2504,6 +2504,9 @@ "unsecuredWebsitesFound": { "message": "Unsecured websites found" }, + "phishingWebsitesFound":{ + "message": "Phishing websites found" + }, "unsecuredWebsitesFoundReportDesc": { "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { @@ -2517,9 +2520,26 @@ } } }, + + "phishingWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that are classified as phishing webistes.", + "placeholders": { + "count": { + "content": "$1", + "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" + } + } + }, "noUnsecuredWebsites": { "message": "No items in your vault have unsecured URIs." }, + "noPhishingWebsitesFound": { + "message": "No websites in your vault have been classified as phishing website." + }, "inactive2faReport": { "message": "Inactive two-step login" }, @@ -2548,6 +2568,12 @@ "instructions": { "message": "Instructions" }, + "phishingWebsiteReport":{ + "message": "Phishing websites" + }, + "phishingWebsiteReportDesc":{ + "message": "List of phishing websites visited. These websites could expose your passwords." + }, "exposedPasswordsReport": { "message": "Exposed passwords" }, diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 9ee49a30689..3996deab961 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -1,5 +1,4 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore +// FIXME: Update this file to be typ@bitwarden/common/src/services/phishing-api.serviceALE_ID, NgModule } from "@angular/core"; import { ErrorHandler, LOCALE_ID, NgModule } from "@angular/core"; import { Subject } from "rxjs"; diff --git a/libs/common/src/abstractions/audit.service.ts b/libs/common/src/abstractions/audit.service.ts index a54beb59a78..ab47bb08444 100644 --- a/libs/common/src/abstractions/audit.service.ts +++ b/libs/common/src/abstractions/audit.service.ts @@ -5,4 +5,5 @@ import { BreachAccountResponse } from "../models/response/breach-account.respons export abstract class AuditService { passwordLeaked: (password: string) => Promise; breachedAccounts: (username: string) => Promise; + getKnownPhishingDomains: () => Promise; } diff --git a/libs/common/src/abstractions/phishing-api.service.abstraction.ts b/libs/common/src/abstractions/phishing-api.service.abstraction.ts deleted file mode 100644 index a65c9e6deb2..00000000000 --- a/libs/common/src/abstractions/phishing-api.service.abstraction.ts +++ /dev/null @@ -1,3 +0,0 @@ -export abstract class PhishingApiServiceAbstraction { - getKnownPhishingDomains: () => Promise; -} diff --git a/libs/common/src/services/audit.service.ts b/libs/common/src/services/audit.service.ts index 60486cef0a8..2752d2413b1 100644 --- a/libs/common/src/services/audit.service.ts +++ b/libs/common/src/services/audit.service.ts @@ -41,4 +41,9 @@ export class AuditService implements AuditServiceAbstraction { throw new Error(); } } + + async getKnownPhishingDomains(): Promise { + const response = await this.apiService.send("GET", "/phishing-domains", null, true, true); + return response as string[]; + } }