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:
@@ -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,
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,10 @@ export class ReportsHomeComponent implements OnInit {
|
||||
...reports[ReportType.UnsecuredWebsites],
|
||||
variant: reportRequiresPremium,
|
||||
},
|
||||
{
|
||||
...reports[ReportType.PhishingWebsitesReport],
|
||||
variant: reportRequiresPremium,
|
||||
},
|
||||
{
|
||||
...reports[ReportType.Inactive2fa],
|
||||
variant: reportRequiresPremium,
|
||||
|
||||
@@ -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()],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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: [
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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[]>;
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export abstract class PhishingApiServiceAbstraction {
|
||||
getKnownPhishingDomains: () => Promise<string[]>;
|
||||
}
|
||||
@@ -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[];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user