1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-10 05:30:01 +00:00

Add changes for phishing domain report

This commit is contained in:
Cy Okeke
2025-03-18 17:24:46 +01:00
parent 36d06899ac
commit b08f30a25b
14 changed files with 278 additions and 24 deletions

View File

@@ -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,

View File

@@ -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<string[]> {
const response = await this.apiService.send("GET", "/phishing-domains", null, false, true);
return response as string[];
}
}

View File

@@ -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

View File

@@ -0,0 +1,100 @@
<app-header></app-header>
<bit-container>
<p>{{ "phishingWebsiteReportDesc" | i18n }}</p>
<div *ngIf="!hasLoaded && loading">
<i
class="bwi bwi-spinner bwi-spin tw-text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
</div>
<div class="tw-mt-4" *ngIf="hasLoaded">
<bit-callout type="success" title="{{ 'goodNews' | i18n }}" *ngIf="!ciphers.length">
{{ "noPhishingWebsitesFound" | i18n }}
</bit-callout>
<ng-container *ngIf="ciphers.length">
<bit-callout type="danger" title="{{ 'phishingWebsitesFound' | i18n }}">
{{ "phishingWebsitesFoundReportDesc" | i18n: (ciphers.length | number) : vaultMsg }}
</bit-callout>
<bit-toggle-group
*ngIf="showFilterToggle && !isAdminConsoleActive"
[selected]="filterOrgStatus$ | async"
(selectedChange)="filterOrgToggle($event)"
[attr.aria-label]="'addAccessFilter' | i18n"
>
<ng-container *ngFor="let status of filterStatus">
<bit-toggle [value]="status">
{{ getName(status) }}
<span bitBadge variant="info"> {{ getCount(status) }} </span>
</bit-toggle>
</ng-container>
</bit-toggle-group>
<bit-table [dataSource]="dataSource">
<ng-container header *ngIf="!isAdminConsoleActive">
<tr bitRow>
<th bitCell></th>
<th bitCell>{{ "name" | i18n }}</th>
<th bitCell>{{ "owner" | i18n }}</th>
<th bitCell></th>
</tr>
</ng-container>
<ng-template body let-rows$>
<tr bitRow *ngFor="let r of rows$ | async">
<td bitCell>
<app-vault-icon [cipher]="r"></app-vault-icon>
</td>
<td bitCell>
<ng-container *ngIf="!organization || canManageCipher(r); else cantManage">
<a
bitLink
href="#"
appStopClick
(click)="selectCipher(r)"
title="{{ 'editItemWithName' | i18n: r.name }}"
>{{ r.name }}</a
>
</ng-container>
<ng-template #cantManage>
<span>{{ r.name }}</span>
</ng-template>
<ng-container *ngIf="!organization && r.organizationId">
<i
class="bwi bwi-collection"
appStopProp
title="{{ 'shared' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "shared" | i18n }}</span>
</ng-container>
<ng-container *ngIf="r.hasAttachments">
<i
class="bwi bwi-paperclip"
appStopProp
title="{{ 'attachments' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "attachments" | i18n }}</span>
</ng-container>
<br />
<small>{{ r.subTitle }}</small>
</td>
<td bitCell>
<app-org-badge
*ngIf="!organization"
[disabled]="disabled"
[organizationId]="r.organizationId"
[organizationName]="r.organizationId | orgNameFromId: (organizations$ | async)"
appStopProp
>
</app-org-badge>
</td>
</tr>
</ng-template>
</bit-table>
</ng-container>
</div>
<ng-template #cipherAddEdit></ng-template>
</bit-container>

View File

@@ -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<void>[] = [];
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;
}
}

View File

@@ -47,6 +47,10 @@ export class ReportsHomeComponent implements OnInit {
...reports[ReportType.UnsecuredWebsites],
variant: reportRequiresPremium,
},
{
...reports[ReportType.PhishingWebsitesReport],
variant: reportRequiresPremium,
},
{
...reports[ReportType.Inactive2fa],
variant: reportRequiresPremium,

View File

@@ -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()],
},
],
},
];

View File

@@ -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: [
{

View File

@@ -15,6 +15,7 @@ export enum ReportType {
Inactive2fa = "inactive2fa",
DataBreach = "dataBreach",
MemberAccessReport = "memberAccessReport",
PhishingWebsitesReport = "phishingWebsiteReport",
}
type ReportWithoutVariant = Omit<ReportEntry, "variant">;
@@ -62,4 +63,10 @@ export const reports: Record<ReportType, ReportWithoutVariant> = {
route: "member-access-report",
icon: MemberAccess,
},
[ReportType.PhishingWebsitesReport]: {
title: "phishingWebsiteReport",
description: "phishingWebsiteReportDesc",
route: "phishing-website-report",
icon: MemberAccess,
},
};

View File

@@ -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"
},

View File

@@ -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";

View File

@@ -5,4 +5,5 @@ import { BreachAccountResponse } from "../models/response/breach-account.respons
export abstract class AuditService {
passwordLeaked: (password: string) => Promise<number>;
breachedAccounts: (username: string) => Promise<BreachAccountResponse[]>;
getKnownPhishingDomains: () => Promise<string[]>;
}

View File

@@ -1,3 +0,0 @@
export abstract class PhishingApiServiceAbstraction {
getKnownPhishingDomains: () => Promise<string[]>;
}

View File

@@ -41,4 +41,9 @@ export class AuditService implements AuditServiceAbstraction {
throw new Error();
}
}
async getKnownPhishingDomains(): Promise<string[]> {
const response = await this.apiService.send("GET", "/phishing-domains", null, true, true);
return response as string[];
}
}