From 8ae71fabafb88e3e313847a4bca8c63432ef730c Mon Sep 17 00:00:00 2001 From: Jason Ng Date: Wed, 1 May 2024 10:39:22 -0400 Subject: [PATCH] [AC-1586] individual reports filter (#8598) * add filtering to individual reports --- .../exposed-passwords-report.component.ts | 12 +- .../inactive-two-factor-report.component.ts | 12 +- .../reused-passwords-report.component.ts | 5 +- .../unsecured-websites-report.component.ts | 5 +- .../tools/weak-passwords-report.component.ts | 4 + .../reports/pages/cipher-report.component.ts | 124 +++++++++++++++++- .../exposed-passwords-report.component.html | 25 +++- ...exposed-passwords-report.component.spec.ts | 6 +- .../exposed-passwords-report.component.ts | 14 +- .../inactive-two-factor-report.component.html | 25 +++- ...active-two-factor-report.component.spec.ts | 8 +- .../inactive-two-factor-report.component.ts | 13 +- .../reused-passwords-report.component.html | 27 +++- .../reused-passwords-report.component.spec.ts | 8 +- .../reused-passwords-report.component.ts | 12 +- .../unsecured-websites-report.component.html | 26 +++- ...nsecured-websites-report.component.spec.ts | 8 +- .../unsecured-websites-report.component.ts | 18 ++- .../weak-passwords-report.component.html | 25 +++- .../weak-passwords-report.component.spec.ts | 6 +- .../pages/weak-passwords-report.component.ts | 16 ++- apps/web/src/locales/en/messages.json | 40 ++++-- 22 files changed, 374 insertions(+), 65 deletions(-) diff --git a/apps/web/src/app/admin-console/organizations/tools/exposed-passwords-report.component.ts b/apps/web/src/app/admin-console/organizations/tools/exposed-passwords-report.component.ts index 54a4bb18b7..d354459ee9 100644 --- a/apps/web/src/app/admin-console/organizations/tools/exposed-passwords-report.component.ts +++ b/apps/web/src/app/admin-console/organizations/tools/exposed-passwords-report.component.ts @@ -4,6 +4,7 @@ import { ActivatedRoute } from "@angular/router"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -27,11 +28,20 @@ export class ExposedPasswordsReportComponent extends BaseExposedPasswordsReportC organizationService: OrganizationService, private route: ActivatedRoute, passwordRepromptService: PasswordRepromptService, + i18nService: I18nService, ) { - super(cipherService, auditService, organizationService, modalService, passwordRepromptService); + super( + cipherService, + auditService, + organizationService, + modalService, + passwordRepromptService, + i18nService, + ); } async ngOnInit() { + this.isAdminConsoleActive = true; // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe this.route.parent.parent.params.subscribe(async (params) => { this.organization = await this.organizationService.get(params.organizationId); diff --git a/apps/web/src/app/admin-console/organizations/tools/inactive-two-factor-report.component.ts b/apps/web/src/app/admin-console/organizations/tools/inactive-two-factor-report.component.ts index 906451397f..67d4e963b0 100644 --- a/apps/web/src/app/admin-console/organizations/tools/inactive-two-factor-report.component.ts +++ b/apps/web/src/app/admin-console/organizations/tools/inactive-two-factor-report.component.ts @@ -3,6 +3,7 @@ import { ActivatedRoute } from "@angular/router"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -24,11 +25,20 @@ export class InactiveTwoFactorReportComponent extends BaseInactiveTwoFactorRepor logService: LogService, passwordRepromptService: PasswordRepromptService, organizationService: OrganizationService, + i18nService: I18nService, ) { - super(cipherService, organizationService, modalService, logService, passwordRepromptService); + super( + cipherService, + organizationService, + modalService, + logService, + passwordRepromptService, + i18nService, + ); } async ngOnInit() { + this.isAdminConsoleActive = true; // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe this.route.parent.parent.params.subscribe(async (params) => { this.organization = await this.organizationService.get(params.organizationId); diff --git a/apps/web/src/app/admin-console/organizations/tools/reused-passwords-report.component.ts b/apps/web/src/app/admin-console/organizations/tools/reused-passwords-report.component.ts index 03418a7279..c8ceb023af 100644 --- a/apps/web/src/app/admin-console/organizations/tools/reused-passwords-report.component.ts +++ b/apps/web/src/app/admin-console/organizations/tools/reused-passwords-report.component.ts @@ -3,6 +3,7 @@ import { ActivatedRoute } from "@angular/router"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -25,11 +26,13 @@ export class ReusedPasswordsReportComponent extends BaseReusedPasswordsReportCom private route: ActivatedRoute, organizationService: OrganizationService, passwordRepromptService: PasswordRepromptService, + i18nService: I18nService, ) { - super(cipherService, organizationService, modalService, passwordRepromptService); + super(cipherService, organizationService, modalService, passwordRepromptService, i18nService); } async ngOnInit() { + this.isAdminConsoleActive = true; // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe this.route.parent.parent.params.subscribe(async (params) => { this.organization = await this.organizationService.get(params.organizationId); diff --git a/apps/web/src/app/admin-console/organizations/tools/unsecured-websites-report.component.ts b/apps/web/src/app/admin-console/organizations/tools/unsecured-websites-report.component.ts index 6e1f38e645..2a905b3665 100644 --- a/apps/web/src/app/admin-console/organizations/tools/unsecured-websites-report.component.ts +++ b/apps/web/src/app/admin-console/organizations/tools/unsecured-websites-report.component.ts @@ -3,6 +3,7 @@ import { ActivatedRoute } from "@angular/router"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { PasswordRepromptService } from "@bitwarden/vault"; @@ -22,11 +23,13 @@ export class UnsecuredWebsitesReportComponent extends BaseUnsecuredWebsitesRepor private route: ActivatedRoute, organizationService: OrganizationService, passwordRepromptService: PasswordRepromptService, + i18nService: I18nService, ) { - super(cipherService, organizationService, modalService, passwordRepromptService); + super(cipherService, organizationService, modalService, passwordRepromptService, i18nService); } async ngOnInit() { + this.isAdminConsoleActive = true; // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe this.route.parent.parent.params.subscribe(async (params) => { this.organization = await this.organizationService.get(params.organizationId); diff --git a/apps/web/src/app/admin-console/organizations/tools/weak-passwords-report.component.ts b/apps/web/src/app/admin-console/organizations/tools/weak-passwords-report.component.ts index a79691c01b..8820e596e3 100644 --- a/apps/web/src/app/admin-console/organizations/tools/weak-passwords-report.component.ts +++ b/apps/web/src/app/admin-console/organizations/tools/weak-passwords-report.component.ts @@ -3,6 +3,7 @@ import { ActivatedRoute } from "@angular/router"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; @@ -27,6 +28,7 @@ export class WeakPasswordsReportComponent extends BaseWeakPasswordsReportCompone private route: ActivatedRoute, organizationService: OrganizationService, passwordRepromptService: PasswordRepromptService, + i18nService: I18nService, ) { super( cipherService, @@ -34,10 +36,12 @@ export class WeakPasswordsReportComponent extends BaseWeakPasswordsReportCompone organizationService, modalService, passwordRepromptService, + i18nService, ); } async ngOnInit() { + this.isAdminConsoleActive = true; // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe this.route.parent.parent.params.subscribe(async (params) => { this.organization = await this.organizationService.get(params.organizationId); diff --git a/apps/web/src/app/tools/reports/pages/cipher-report.component.ts b/apps/web/src/app/tools/reports/pages/cipher-report.component.ts index 0d67b7a769..041307122b 100644 --- a/apps/web/src/app/tools/reports/pages/cipher-report.component.ts +++ b/apps/web/src/app/tools/reports/pages/cipher-report.component.ts @@ -1,9 +1,11 @@ -import { Directive, ViewChild, ViewContainerRef } from "@angular/core"; -import { Observable } from "rxjs"; +import { Directive, ViewChild, ViewContainerRef, OnDestroy } from "@angular/core"; +import { BehaviorSubject, Observable, Subject, takeUntil } from "rxjs"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { PasswordRepromptService } from "@bitwarden/vault"; @@ -12,27 +14,111 @@ import { AddEditComponent } from "../../../vault/individual-vault/add-edit.compo import { AddEditComponent as OrgAddEditComponent } from "../../../vault/org-vault/add-edit.component"; @Directive() -export class CipherReportComponent { +export class CipherReportComponent implements OnDestroy { @ViewChild("cipherAddEdit", { read: ViewContainerRef, static: true }) cipherAddEditModalRef: ViewContainerRef; + isAdminConsoleActive = false; loading = false; hasLoaded = false; ciphers: CipherView[] = []; + allCiphers: CipherView[] = []; organization: Organization; + organizations: Organization[]; organizations$: Observable; + filterStatus: any = [0]; + showFilterToggle: boolean = false; + vaultMsg: string = "vault"; + currentFilterStatus: number | string; + protected filterOrgStatus$ = new BehaviorSubject(0); + private destroyed$: Subject = new Subject(); + constructor( + protected cipherService: CipherService, private modalService: ModalService, protected passwordRepromptService: PasswordRepromptService, protected organizationService: OrganizationService, + protected i18nService: I18nService, ) { this.organizations$ = this.organizationService.organizations$; + this.organizations$.pipe(takeUntil(this.destroyed$)).subscribe((orgs) => { + this.organizations = orgs; + }); + } + + ngOnDestroy(): void { + this.destroyed$.next(); + this.destroyed$.complete(); + } + + getName(filterId: string | number) { + let orgName: any; + + if (filterId === 0) { + orgName = this.i18nService.t("all"); + } else if (filterId === 1) { + orgName = this.i18nService.t("me"); + } else { + this.organizations.filter((org: Organization) => { + if (org.id === filterId) { + orgName = org.name; + return org; + } + }); + } + return orgName; + } + + getCount(filterId: string | number) { + let orgFilterStatus: any; + let cipherCount; + + if (filterId === 0) { + cipherCount = this.allCiphers.length; + } else if (filterId === 1) { + cipherCount = this.allCiphers.filter((c: any) => c.orgFilterStatus === null).length; + } else { + this.organizations.filter((org: Organization) => { + if (org.id === filterId) { + orgFilterStatus = org.id; + return org; + } + }); + cipherCount = this.allCiphers.filter( + (c: any) => c.orgFilterStatus === orgFilterStatus, + ).length; + } + return cipherCount; + } + + async filterOrgToggle(status: any) { + this.currentFilterStatus = status; + await this.setCiphers(); + if (status === 0) { + return; + } else if (status === 1) { + this.ciphers = this.ciphers.filter((c: any) => c.orgFilterStatus == null); + } else { + this.ciphers = this.ciphers.filter((c: any) => c.orgFilterStatus === status); + } } async load() { this.loading = true; - await this.setCiphers(); + // when a user fixes an item in a report we want to persist the filter they had + // if they fix the last item of that filter we will go back to the "All" filter + if (this.currentFilterStatus) { + if (this.ciphers.length > 2) { + this.filterOrgStatus$.next(this.currentFilterStatus); + await this.filterOrgToggle(this.currentFilterStatus); + } else { + this.filterOrgStatus$.next(0); + await this.filterOrgToggle(0); + } + } else { + await this.setCiphers(); + } this.loading = false; this.hasLoaded = true; } @@ -76,7 +162,7 @@ export class CipherReportComponent { } protected async setCiphers() { - this.ciphers = []; + this.allCiphers = []; } protected async repromptCipher(c: CipherView) { @@ -85,4 +171,32 @@ export class CipherReportComponent { (await this.passwordRepromptService.showPasswordPrompt()) ); } + + protected async getAllCiphers(): Promise { + return await this.cipherService.getAllDecrypted(); + } + + protected filterCiphersByOrg(ciphersList: CipherView[]) { + this.allCiphers = [...ciphersList]; + + this.ciphers = ciphersList.map((ciph: any) => { + ciph.orgFilterStatus = ciph.organizationId; + + if (this.filterStatus.indexOf(ciph.organizationId) === -1 && ciph.organizationId != null) { + this.filterStatus.push(ciph.organizationId); + } else if (this.filterStatus.indexOf(1) === -1 && ciph.organizationId == null) { + this.filterStatus.splice(1, 0, 1); + } + return ciph; + }); + + if (this.filterStatus.length > 2) { + this.showFilterToggle = true; + this.vaultMsg = "vaults"; + } else { + // If a user fixes an item and there is only one item left remove the filter toggle and change the vault message to singular + this.showFilterToggle = false; + this.vaultMsg = "vault"; + } + } } diff --git a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.html b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.html index af80e2e62b..30801a42fd 100644 --- a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.html +++ b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.html @@ -11,9 +11,32 @@ - {{ "exposedPasswordsFoundDesc" | i18n: (ciphers.length | number) }} + {{ "exposedPasswordsFoundReportDesc" | i18n: (ciphers.length | number) : vaultMsg }} + + + + {{ getName(status) }} + {{ getCount(status) }} + + + + + + + + + +
{{ "name" | i18n }}{{ "owner" | i18n }}
diff --git a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.spec.ts index d1cf89eb08..7b73ad8305 100644 --- a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.spec.ts +++ b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.spec.ts @@ -1,6 +1,7 @@ // eslint-disable-next-line no-restricted-imports import { ComponentFixture, TestBed } from "@angular/core/testing"; import { mock, MockProxy } from "jest-mock-extended"; +import { of } from "rxjs"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { ModalService } from "@bitwarden/angular/services/modal.service"; @@ -17,9 +18,12 @@ describe("ExposedPasswordsReportComponent", () => { let component: ExposedPasswordsReportComponent; let fixture: ComponentFixture; let auditService: MockProxy; + let organizationService: MockProxy; beforeEach(() => { auditService = mock(); + organizationService = mock(); + organizationService.organizations$ = of([]); // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises TestBed.configureTestingModule({ @@ -35,7 +39,7 @@ describe("ExposedPasswordsReportComponent", () => { }, { provide: OrganizationService, - useValue: mock(), + useValue: organizationService, }, { provide: ModalService, diff --git a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.ts b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.ts index 631e9ef8a8..39414487d7 100644 --- a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.ts +++ b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.ts @@ -3,6 +3,7 @@ import { Component, OnInit } from "@angular/core"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; 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"; @@ -24,8 +25,9 @@ export class ExposedPasswordsReportComponent extends CipherReportComponent imple protected organizationService: OrganizationService, modalService: ModalService, passwordRepromptService: PasswordRepromptService, + i18nService: I18nService, ) { - super(modalService, passwordRepromptService, organizationService); + super(cipherService, modalService, passwordRepromptService, organizationService, i18nService); } async ngOnInit() { @@ -36,7 +38,9 @@ export class ExposedPasswordsReportComponent extends CipherReportComponent imple const allCiphers = await this.getAllCiphers(); const exposedPasswordCiphers: CipherView[] = []; const promises: Promise[] = []; - allCiphers.forEach((ciph) => { + this.filterStatus = [0]; + + allCiphers.forEach((ciph: any) => { const { type, login, isDeleted, edit, viewPassword, id } = ciph; if ( type !== CipherType.Login || @@ -48,6 +52,7 @@ export class ExposedPasswordsReportComponent extends CipherReportComponent imple ) { return; } + const promise = this.auditService.passwordLeaked(login.password).then((exposedCount) => { if (exposedCount > 0) { exposedPasswordCiphers.push(ciph); @@ -57,11 +62,8 @@ export class ExposedPasswordsReportComponent extends CipherReportComponent imple promises.push(promise); }); await Promise.all(promises); - this.ciphers = [...exposedPasswordCiphers]; - } - protected getAllCiphers(): Promise { - return this.cipherService.getAllDecrypted(); + this.filterCiphersByOrg(exposedPasswordCiphers); } protected canManageCipher(c: CipherView): boolean { diff --git a/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.html b/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.html index d81fc2d413..ae03a3bcb8 100644 --- a/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.html +++ b/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.html @@ -16,9 +16,32 @@ - {{ "inactive2faFoundDesc" | i18n: (ciphers.length | number) }} + {{ "inactive2faFoundReportDesc" | i18n: (ciphers.length | number) : vaultMsg }} + + + + {{ getName(status) }} + {{ getCount(status) }} + + + + + + + + + +
{{ "name" | i18n }}{{ "owner" | i18n }}
diff --git a/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.spec.ts index 97321480fa..528f6306e0 100644 --- a/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.spec.ts +++ b/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.spec.ts @@ -1,6 +1,7 @@ // eslint-disable-next-line no-restricted-imports import { ComponentFixture, TestBed } from "@angular/core/testing"; -import { mock } from "jest-mock-extended"; +import { MockProxy, mock } from "jest-mock-extended"; +import { of } from "rxjs"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { ModalService } from "@bitwarden/angular/services/modal.service"; @@ -16,8 +17,11 @@ import { cipherData } from "./reports-ciphers.mock"; describe("InactiveTwoFactorReportComponent", () => { let component: InactiveTwoFactorReportComponent; let fixture: ComponentFixture; + let organizationService: MockProxy; beforeEach(() => { + organizationService = mock(); + organizationService.organizations$ = of([]); // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises TestBed.configureTestingModule({ @@ -29,7 +33,7 @@ describe("InactiveTwoFactorReportComponent", () => { }, { provide: OrganizationService, - useValue: mock(), + useValue: organizationService, }, { provide: ModalService, diff --git a/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.ts b/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.ts index 15b79981b6..956607c8fb 100644 --- a/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.ts +++ b/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.ts @@ -2,6 +2,7 @@ import { Component, OnInit } from "@angular/core"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -26,8 +27,9 @@ export class InactiveTwoFactorReportComponent extends CipherReportComponent impl modalService: ModalService, private logService: LogService, passwordRepromptService: PasswordRepromptService, + i18nService: I18nService, ) { - super(modalService, passwordRepromptService, organizationService); + super(cipherService, modalService, passwordRepromptService, organizationService, i18nService); } async ngOnInit() { @@ -45,6 +47,7 @@ export class InactiveTwoFactorReportComponent extends CipherReportComponent impl const allCiphers = await this.getAllCiphers(); const inactive2faCiphers: CipherView[] = []; const docs = new Map(); + this.filterStatus = [0]; allCiphers.forEach((ciph) => { const { type, login, isDeleted, edit, id, viewPassword } = ciph; @@ -58,6 +61,7 @@ export class InactiveTwoFactorReportComponent extends CipherReportComponent impl ) { return; } + for (let i = 0; i < login.uris.length; i++) { const u = login.uris[i]; if (u.uri != null && u.uri !== "") { @@ -75,15 +79,12 @@ export class InactiveTwoFactorReportComponent extends CipherReportComponent impl } } }); - this.ciphers = [...inactive2faCiphers]; + + this.filterCiphersByOrg(inactive2faCiphers); this.cipherDocs = docs; } } - protected getAllCiphers(): Promise { - return this.cipherService.getAllDecrypted(); - } - private async load2fa() { if (this.services.size > 0) { return; diff --git a/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.html b/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.html index cde2e59ea8..549773ba8c 100644 --- a/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.html +++ b/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.html @@ -16,9 +16,34 @@ - {{ "reusedPasswordsFoundDesc" | i18n: (ciphers.length | number) }} + {{ "reusedPasswordsFoundReportDesc" | i18n: (ciphers.length | number) : vaultMsg }} + + + + + {{ getName(status) }} + {{ getCount(status) }} + + + + + + + + + + +
{{ "name" | i18n }}{{ "owner" | i18n }}
diff --git a/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.spec.ts index 450e42805a..29e20c11af 100644 --- a/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.spec.ts +++ b/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.spec.ts @@ -1,6 +1,7 @@ // eslint-disable-next-line no-restricted-imports import { ComponentFixture, TestBed } from "@angular/core/testing"; -import { mock } from "jest-mock-extended"; +import { MockProxy, mock } from "jest-mock-extended"; +import { of } from "rxjs"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { ModalService } from "@bitwarden/angular/services/modal.service"; @@ -15,8 +16,11 @@ import { ReusedPasswordsReportComponent } from "./reused-passwords-report.compon describe("ReusedPasswordsReportComponent", () => { let component: ReusedPasswordsReportComponent; let fixture: ComponentFixture; + let organizationService: MockProxy; beforeEach(() => { + organizationService = mock(); + organizationService.organizations$ = of([]); // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises TestBed.configureTestingModule({ @@ -28,7 +32,7 @@ describe("ReusedPasswordsReportComponent", () => { }, { provide: OrganizationService, - useValue: mock(), + useValue: organizationService, }, { provide: ModalService, diff --git a/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.ts b/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.ts index f785186c15..cbc2ea11b5 100644 --- a/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.ts +++ b/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.ts @@ -2,6 +2,7 @@ import { Component, OnInit } from "@angular/core"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; 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"; @@ -22,8 +23,9 @@ export class ReusedPasswordsReportComponent extends CipherReportComponent implem protected organizationService: OrganizationService, modalService: ModalService, passwordRepromptService: PasswordRepromptService, + i18nService: I18nService, ) { - super(modalService, passwordRepromptService, organizationService); + super(cipherService, modalService, passwordRepromptService, organizationService, i18nService); } async ngOnInit() { @@ -34,6 +36,8 @@ export class ReusedPasswordsReportComponent extends CipherReportComponent implem const allCiphers = await this.getAllCiphers(); const ciphersWithPasswords: CipherView[] = []; this.passwordUseMap = new Map(); + this.filterStatus = [0]; + allCiphers.forEach((ciph) => { const { type, login, isDeleted, edit, viewPassword } = ciph; if ( @@ -46,6 +50,7 @@ export class ReusedPasswordsReportComponent extends CipherReportComponent implem ) { return; } + ciphersWithPasswords.push(ciph); if (this.passwordUseMap.has(login.password)) { this.passwordUseMap.set(login.password, this.passwordUseMap.get(login.password) + 1); @@ -57,11 +62,8 @@ export class ReusedPasswordsReportComponent extends CipherReportComponent implem (c) => this.passwordUseMap.has(c.login.password) && this.passwordUseMap.get(c.login.password) > 1, ); - this.ciphers = reusedPasswordCiphers; - } - protected getAllCiphers(): Promise { - return this.cipherService.getAllDecrypted(); + this.filterCiphersByOrg(reusedPasswordCiphers); } protected canManageCipher(c: CipherView): boolean { diff --git a/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.html b/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.html index 616bdbba0b..ced0ff9731 100644 --- a/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.html +++ b/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.html @@ -16,9 +16,33 @@ - {{ "unsecuredWebsitesFoundDesc" | i18n: (ciphers.length | number) }} + {{ "unsecuredWebsitesFoundReportDesc" | i18n: (ciphers.length | number) : vaultMsg }} + + + + + {{ getName(status) }} + {{ getCount(status) }} + + + + + + + + + +
{{ "name" | i18n }}{{ "owner" | i18n }}
diff --git a/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.spec.ts index 5cdf640c55..3b7c6d350f 100644 --- a/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.spec.ts +++ b/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.spec.ts @@ -1,6 +1,7 @@ // eslint-disable-next-line no-restricted-imports import { ComponentFixture, TestBed } from "@angular/core/testing"; -import { mock } from "jest-mock-extended"; +import { MockProxy, mock } from "jest-mock-extended"; +import { of } from "rxjs"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { ModalService } from "@bitwarden/angular/services/modal.service"; @@ -15,8 +16,11 @@ import { UnsecuredWebsitesReportComponent } from "./unsecured-websites-report.co describe("UnsecuredWebsitesReportComponent", () => { let component: UnsecuredWebsitesReportComponent; let fixture: ComponentFixture; + let organizationService: MockProxy; beforeEach(() => { + organizationService = mock(); + organizationService.organizations$ = of([]); // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises TestBed.configureTestingModule({ @@ -28,7 +32,7 @@ describe("UnsecuredWebsitesReportComponent", () => { }, { provide: OrganizationService, - useValue: mock(), + useValue: organizationService, }, { provide: ModalService, diff --git a/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.ts b/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.ts index 2de70e928b..769eb058cd 100644 --- a/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.ts +++ b/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.ts @@ -2,9 +2,9 @@ import { Component, OnInit } from "@angular/core"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; 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 { PasswordRepromptService } from "@bitwarden/vault"; import { CipherReportComponent } from "./cipher-report.component"; @@ -21,8 +21,9 @@ export class UnsecuredWebsitesReportComponent extends CipherReportComponent impl protected organizationService: OrganizationService, modalService: ModalService, passwordRepromptService: PasswordRepromptService, + i18nService: I18nService, ) { - super(modalService, passwordRepromptService, organizationService); + super(cipherService, modalService, passwordRepromptService, organizationService, i18nService); } async ngOnInit() { @@ -31,18 +32,15 @@ export class UnsecuredWebsitesReportComponent extends CipherReportComponent impl async setCiphers() { const allCiphers = await this.getAllCiphers(); + this.filterStatus = [0]; const unsecuredCiphers = allCiphers.filter((c) => { if (c.type !== CipherType.Login || !c.login.hasUris || c.isDeleted) { return false; } - return c.login.uris.some((u) => u.uri != null && u.uri.indexOf("http://") === 0); - }); - this.ciphers = unsecuredCiphers.filter( - (c) => (!this.organization && c.edit) || (this.organization && !c.edit), - ); - } - protected getAllCiphers(): Promise { - return this.cipherService.getAllDecrypted(); + return c.login.uris.some((u: any) => u.uri != null && u.uri.indexOf("http://") === 0); + }); + + this.filterCiphersByOrg(unsecuredCiphers); } } diff --git a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.html b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.html index b4c77b2fa1..a943c8c29e 100644 --- a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.html +++ b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.html @@ -16,9 +16,32 @@ - {{ "weakPasswordsFoundDesc" | i18n: (ciphers.length | number) }} + {{ "weakPasswordsFoundReportDesc" | i18n: (ciphers.length | number) : vaultMsg }} + + + + {{ getName(status) }} + {{ getCount(status) }} + + + + + + + + + +
{{ "name" | i18n }}{{ "owner" | i18n }}
diff --git a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.spec.ts index f1446c4209..dbc367b108 100644 --- a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.spec.ts +++ b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.spec.ts @@ -1,6 +1,7 @@ // eslint-disable-next-line no-restricted-imports import { ComponentFixture, TestBed } from "@angular/core/testing"; import { mock, MockProxy } from "jest-mock-extended"; +import { of } from "rxjs"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { ModalService } from "@bitwarden/angular/services/modal.service"; @@ -17,9 +18,12 @@ describe("WeakPasswordsReportComponent", () => { let component: WeakPasswordsReportComponent; let fixture: ComponentFixture; let passwordStrengthService: MockProxy; + let organizationService: MockProxy; beforeEach(() => { passwordStrengthService = mock(); + organizationService = mock(); + organizationService.organizations$ = of([]); // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises TestBed.configureTestingModule({ @@ -35,7 +39,7 @@ describe("WeakPasswordsReportComponent", () => { }, { provide: OrganizationService, - useValue: mock(), + useValue: organizationService, }, { provide: ModalService, diff --git a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.ts b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.ts index a7ed119e19..4d179b58f3 100644 --- a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.ts +++ b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.ts @@ -2,6 +2,7 @@ import { Component, OnInit } from "@angular/core"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -29,8 +30,9 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen protected organizationService: OrganizationService, modalService: ModalService, passwordRepromptService: PasswordRepromptService, + i18nService: I18nService, ) { - super(modalService, passwordRepromptService, organizationService); + super(cipherService, modalService, passwordRepromptService, organizationService, i18nService); } async ngOnInit() { @@ -38,7 +40,10 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen } async setCiphers() { - const allCiphers = await this.getAllCiphers(); + const allCiphers: any = await this.getAllCiphers(); + this.passwordStrengthCache = new Map(); + this.weakPasswordCiphers = []; + this.filterStatus = [0]; this.findWeakPasswords(allCiphers); } @@ -55,6 +60,7 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen ) { return; } + const hasUserName = this.isUserNameNotEmpty(ciph); const cacheKey = this.getCacheKey(ciph); if (!this.passwordStrengthCache.has(cacheKey)) { @@ -87,6 +93,7 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen this.passwordStrengthCache.set(cacheKey, result.score); } const score = this.passwordStrengthCache.get(cacheKey); + if (score != null && score <= 2) { this.passwordStrengthMap.set(id, this.scoreKey(score)); this.weakPasswordCiphers.push(ciph); @@ -98,11 +105,8 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen this.passwordStrengthCache.get(this.getCacheKey(b)) ); }); - this.ciphers = [...this.weakPasswordCiphers]; - } - protected getAllCiphers(): Promise { - return this.cipherService.getAllDecrypted(); + this.filterCiphersByOrg(this.weakPasswordCiphers); } protected canManageCipher(c: CipherView): boolean { diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 3a590622b8..e37ccaf536 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Unsecured websites found" }, - "unsecuredWebsitesFoundDesc": { - "message": "We found $COUNT$ items in your vault with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", + "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": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Logins without two-step login found" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Exposed passwords found" }, - "exposedPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault that have passwords that were exposed in known data breaches. You should change them to use a new password.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Weak passwords found" }, - "weakPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault with passwords that are not strong. You should update them to use stronger passwords.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Reused passwords found" }, - "reusedPasswordsFoundDesc": { - "message": "We found $COUNT$ passwords that are being reused in your vault. You should change them to a unique value.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } },