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 415bd6516c2..7f8e3197a54 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,7 +4,6 @@ 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 { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.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,12 +24,11 @@ export class ExposedPasswordsReportComponent extends BaseExposedPasswordsReportC cipherService: CipherService, auditService: AuditService, modalService: ModalService, - messagingService: MessagingService, - private organizationService: OrganizationService, + organizationService: OrganizationService, private route: ActivatedRoute, passwordRepromptService: PasswordRepromptService ) { - super(cipherService, auditService, modalService, messagingService, passwordRepromptService); + super(cipherService, auditService, organizationService, modalService, passwordRepromptService); } async ngOnInit() { 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 4cc68b1f9d0..7d60439f3f2 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 @@ -4,7 +4,6 @@ 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 { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.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"; @@ -21,13 +20,12 @@ export class InactiveTwoFactorReportComponent extends BaseInactiveTwoFactorRepor constructor( cipherService: CipherService, modalService: ModalService, - messagingService: MessagingService, private route: ActivatedRoute, logService: LogService, passwordRepromptService: PasswordRepromptService, - private organizationService: OrganizationService + organizationService: OrganizationService ) { - super(cipherService, modalService, messagingService, logService, passwordRepromptService); + super(cipherService, organizationService, modalService, logService, passwordRepromptService); } async ngOnInit() { 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 93652239a11..c85178f7211 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,8 +3,6 @@ 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 { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.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"; @@ -24,13 +22,11 @@ export class ReusedPasswordsReportComponent extends BaseReusedPasswordsReportCom constructor( cipherService: CipherService, modalService: ModalService, - messagingService: MessagingService, - stateService: StateService, private route: ActivatedRoute, - private organizationService: OrganizationService, + organizationService: OrganizationService, passwordRepromptService: PasswordRepromptService ) { - super(cipherService, modalService, messagingService, stateService, passwordRepromptService); + super(cipherService, organizationService, modalService, passwordRepromptService); } async ngOnInit() { 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 e8fbc0ed5e6..bee951063f0 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,7 +3,6 @@ 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 { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.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"; @@ -20,12 +19,11 @@ export class UnsecuredWebsitesReportComponent extends BaseUnsecuredWebsitesRepor constructor( cipherService: CipherService, modalService: ModalService, - messagingService: MessagingService, private route: ActivatedRoute, - private organizationService: OrganizationService, + organizationService: OrganizationService, passwordRepromptService: PasswordRepromptService ) { - super(cipherService, modalService, messagingService, passwordRepromptService); + super(cipherService, organizationService, modalService, passwordRepromptService); } async ngOnInit() { 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 540f71d91dc..1902d24fe7d 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,7 +3,6 @@ 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 { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.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"; @@ -25,16 +24,15 @@ export class WeakPasswordsReportComponent extends BaseWeakPasswordsReportCompone cipherService: CipherService, passwordStrengthService: PasswordStrengthServiceAbstraction, modalService: ModalService, - messagingService: MessagingService, private route: ActivatedRoute, - private organizationService: OrganizationService, + organizationService: OrganizationService, passwordRepromptService: PasswordRepromptService ) { super( cipherService, passwordStrengthService, + organizationService, modalService, - messagingService, passwordRepromptService ); } diff --git a/apps/web/src/app/reports/pages/cipher-report.component.ts b/apps/web/src/app/reports/pages/cipher-report.component.ts index 80c41b0c252..984b547fb75 100644 --- a/apps/web/src/app/reports/pages/cipher-report.component.ts +++ b/apps/web/src/app/reports/pages/cipher-report.component.ts @@ -1,8 +1,9 @@ import { Directive, ViewChild, ViewContainerRef } from "@angular/core"; +import { Observable } 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 { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.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"; @@ -19,13 +20,15 @@ export class CipherReportComponent { hasLoaded = false; ciphers: CipherView[] = []; organization: Organization; + organizations$: Observable; constructor( private modalService: ModalService, - protected messagingService: MessagingService, - public requiresPaid: boolean, - protected passwordRepromptService: PasswordRepromptService - ) {} + protected passwordRepromptService: PasswordRepromptService, + protected organizationService: OrganizationService + ) { + this.organizations$ = this.organizationService.organizations$; + } async load() { this.loading = true; diff --git a/apps/web/src/app/reports/pages/exposed-passwords-report.component.html b/apps/web/src/app/reports/pages/exposed-passwords-report.component.html index e23fb4a086d..f88d3a082e6 100644 --- a/apps/web/src/app/reports/pages/exposed-passwords-report.component.html +++ b/apps/web/src/app/reports/pages/exposed-passwords-report.component.html @@ -49,6 +49,16 @@

{{ "exposedPasswordsReport" | i18n }}


{{ c.subTitle }} + + + + {{ "exposedXTimes" | i18n : (exposedPasswordMap.get(c.id) | number) }} diff --git a/apps/web/src/app/reports/pages/exposed-passwords-report.component.spec.ts b/apps/web/src/app/reports/pages/exposed-passwords-report.component.spec.ts new file mode 100644 index 00000000000..656abbff2af --- /dev/null +++ b/apps/web/src/app/reports/pages/exposed-passwords-report.component.spec.ts @@ -0,0 +1,79 @@ +// eslint-disable-next-line no-restricted-imports +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { mock, MockProxy } from "jest-mock-extended"; + +import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; +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 { PasswordRepromptService } from "@bitwarden/vault"; + +import { ExposedPasswordsReportComponent } from "./exposed-passwords-report.component"; +import { cipherData } from "./reports-ciphers.mock"; + +describe("ExposedPasswordsReportComponent", () => { + let component: ExposedPasswordsReportComponent; + let fixture: ComponentFixture; + let auditService: MockProxy; + + beforeEach(() => { + auditService = mock(); + TestBed.configureTestingModule({ + declarations: [ExposedPasswordsReportComponent, I18nPipe], + providers: [ + { + provide: CipherService, + useValue: mock(), + }, + { + provide: AuditService, + useValue: auditService, + }, + { + provide: OrganizationService, + useValue: mock(), + }, + { + provide: ModalService, + useValue: mock(), + }, + { + provide: PasswordRepromptService, + useValue: mock(), + }, + { + provide: I18nService, + useValue: mock(), + }, + ], + schemas: [], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ExposedPasswordsReportComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should initialize component", () => { + expect(component).toBeTruthy(); + }); + + it('should get only ciphers with exposed passwords that the user has "Can Edit" access to', async () => { + const expectedIdOne: any = "cbea34a8-bde4-46ad-9d19-b05001228ab2"; + const expectedIdTwo = "cbea34a8-bde4-46ad-9d19-b05001228cd3"; + + jest.spyOn(auditService, "passwordLeaked").mockReturnValue(Promise.resolve(1234)); + jest.spyOn(component as any, "getAllCiphers").mockReturnValue(Promise.resolve(cipherData)); + await component.setCiphers(); + + expect(component.ciphers.length).toEqual(2); + expect(component.ciphers[0].id).toEqual(expectedIdOne); + expect(component.ciphers[0].edit).toEqual(true); + expect(component.ciphers[1].id).toEqual(expectedIdTwo); + expect(component.ciphers[1].edit).toEqual(true); + }); +}); diff --git a/apps/web/src/app/reports/pages/exposed-passwords-report.component.ts b/apps/web/src/app/reports/pages/exposed-passwords-report.component.ts index dec2f707d4a..54a634b2510 100644 --- a/apps/web/src/app/reports/pages/exposed-passwords-report.component.ts +++ b/apps/web/src/app/reports/pages/exposed-passwords-report.component.ts @@ -2,7 +2,7 @@ import { Component, OnInit } from "@angular/core"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums/cipher-type"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -16,15 +16,16 @@ import { CipherReportComponent } from "./cipher-report.component"; }) export class ExposedPasswordsReportComponent extends CipherReportComponent implements OnInit { exposedPasswordMap = new Map(); + disabled = true; constructor( protected cipherService: CipherService, protected auditService: AuditService, + protected organizationService: OrganizationService, modalService: ModalService, - messagingService: MessagingService, passwordRepromptService: PasswordRepromptService ) { - super(modalService, messagingService, true, passwordRepromptService); + super(modalService, passwordRepromptService, organizationService); } async ngOnInit() { @@ -35,25 +36,28 @@ export class ExposedPasswordsReportComponent extends CipherReportComponent imple const allCiphers = await this.getAllCiphers(); const exposedPasswordCiphers: CipherView[] = []; const promises: Promise[] = []; - allCiphers.forEach((c) => { + allCiphers.forEach((ciph) => { + const { type, login, isDeleted, edit, viewPassword, id } = ciph; if ( - c.type !== CipherType.Login || - c.login.password == null || - c.login.password === "" || - c.isDeleted + type !== CipherType.Login || + login.password == null || + login.password === "" || + isDeleted || + (!this.organization && !edit) || + !viewPassword ) { return; } - const promise = this.auditService.passwordLeaked(c.login.password).then((exposedCount) => { + const promise = this.auditService.passwordLeaked(login.password).then((exposedCount) => { if (exposedCount > 0) { - exposedPasswordCiphers.push(c); - this.exposedPasswordMap.set(c.id, exposedCount); + exposedPasswordCiphers.push(ciph); + this.exposedPasswordMap.set(id, exposedCount); } }); promises.push(promise); }); await Promise.all(promises); - this.ciphers = exposedPasswordCiphers; + this.ciphers = [...exposedPasswordCiphers]; } protected getAllCiphers(): Promise { diff --git a/apps/web/src/app/reports/pages/inactive-two-factor-report.component.html b/apps/web/src/app/reports/pages/inactive-two-factor-report.component.html index eca2d3e3ab7..076f2f03ed5 100644 --- a/apps/web/src/app/reports/pages/inactive-two-factor-report.component.html +++ b/apps/web/src/app/reports/pages/inactive-two-factor-report.component.html @@ -59,6 +59,16 @@


{{ c.subTitle }} + + + + { + let component: InactiveTwoFactorReportComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [InactiveTwoFactorReportComponent, I18nPipe], + providers: [ + { + provide: CipherService, + useValue: mock(), + }, + { + provide: OrganizationService, + useValue: mock(), + }, + { + provide: ModalService, + useValue: mock(), + }, + { + provide: LogService, + useValue: mock(), + }, + { + provide: PasswordRepromptService, + useValue: mock(), + }, + { + provide: I18nService, + useValue: mock(), + }, + ], + schemas: [], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(InactiveTwoFactorReportComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should initialize component", () => { + expect(component).toBeTruthy(); + }); + + it('should get only ciphers with domains in the 2fa directory that they have "Can Edit" access to', async () => { + const expectedIdOne: any = "cbea34a8-bde4-46ad-9d19-b05001228xy4"; + const expectedIdTwo: any = "cbea34a8-bde4-46ad-9d19-b05001227nm5"; + component.services.set( + "101domain.com", + "https://help.101domain.com/account-management/account-security/enabling-disabling-two-factor-verification" + ); + component.services.set( + "123formbuilder.com", + "https://www.123formbuilder.com/docs/multi-factor-authentication-login" + ); + + jest.spyOn(component as any, "getAllCiphers").mockReturnValue(Promise.resolve(cipherData)); + await component.setCiphers(); + + expect(component.ciphers.length).toEqual(2); + expect(component.ciphers[0].id).toEqual(expectedIdOne); + expect(component.ciphers[0].edit).toEqual(true); + expect(component.ciphers[1].id).toEqual(expectedIdTwo); + expect(component.ciphers[1].edit).toEqual(true); + }); +}); diff --git a/apps/web/src/app/reports/pages/inactive-two-factor-report.component.ts b/apps/web/src/app/reports/pages/inactive-two-factor-report.component.ts index ad257d7e596..18d669ab54c 100644 --- a/apps/web/src/app/reports/pages/inactive-two-factor-report.component.ts +++ b/apps/web/src/app/reports/pages/inactive-two-factor-report.component.ts @@ -1,8 +1,8 @@ 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 { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums/cipher-type"; @@ -18,15 +18,16 @@ import { CipherReportComponent } from "./cipher-report.component"; export class InactiveTwoFactorReportComponent extends CipherReportComponent implements OnInit { services = new Map(); cipherDocs = new Map(); + disabled = true; constructor( protected cipherService: CipherService, + protected organizationService: OrganizationService, modalService: ModalService, - messagingService: MessagingService, private logService: LogService, passwordRepromptService: PasswordRepromptService ) { - super(modalService, messagingService, true, passwordRepromptService); + super(modalService, passwordRepromptService, organizationService); } async ngOnInit() { @@ -43,33 +44,34 @@ export class InactiveTwoFactorReportComponent extends CipherReportComponent impl if (this.services.size > 0) { const allCiphers = await this.getAllCiphers(); const inactive2faCiphers: CipherView[] = []; - const promises: Promise[] = []; const docs = new Map(); - allCiphers.forEach((c) => { + + allCiphers.forEach((ciph) => { + const { type, login, isDeleted, edit, id } = ciph; if ( - c.type !== CipherType.Login || - (c.login.totp != null && c.login.totp !== "") || - !c.login.hasUris || - c.isDeleted + type !== CipherType.Login || + (login.totp != null && login.totp !== "") || + !login.hasUris || + isDeleted || + (!this.organization && !edit) ) { return; } - for (let i = 0; i < c.login.uris.length; i++) { - const u = c.login.uris[i]; + for (let i = 0; i < login.uris.length; i++) { + const u = login.uris[i]; if (u.uri != null && u.uri !== "") { const uri = u.uri.replace("www.", ""); const domain = Utils.getDomain(uri); if (domain != null && this.services.has(domain)) { if (this.services.get(domain) != null) { - docs.set(c.id, this.services.get(domain)); + docs.set(id, this.services.get(domain)); } - inactive2faCiphers.push(c); + inactive2faCiphers.push(ciph); } } } }); - await Promise.all(promises); - this.ciphers = inactive2faCiphers; + this.ciphers = [...inactive2faCiphers]; this.cipherDocs = docs; } } diff --git a/apps/web/src/app/reports/pages/reports-ciphers.mock.ts b/apps/web/src/app/reports/pages/reports-ciphers.mock.ts new file mode 100644 index 00000000000..195c9afa9d2 --- /dev/null +++ b/apps/web/src/app/reports/pages/reports-ciphers.mock.ts @@ -0,0 +1,128 @@ +export const cipherData: any[] = [ + { + initializerKey: 1, + id: "cbea34a8-bde4-46ad-9d19-b05001228ab1", + organizationId: null, + folderId: null, + name: "Cannot Be Edited", + notes: null, + isDeleted: false, + type: 1, + favorite: false, + organizationUseTotp: false, + login: { + password: "123", + }, + edit: false, + viewPassword: true, + collectionIds: [], + revisionDate: "2023-08-03T17:40:59.793Z", + creationDate: "2023-08-03T17:40:59.793Z", + deletedDate: null, + reprompt: 0, + localData: null, + }, + { + initializerKey: 1, + id: "cbea34a8-bde4-46ad-9d19-b05001228ab2", + organizationId: null, + folderId: null, + name: "Can Be Edited id ending 2", + notes: null, + isDeleted: false, + type: 1, + favorite: false, + organizationUseTotp: false, + login: { + password: "123", + hasUris: true, + uris: [ + { + uri: "http://nothing.com", + }, + ], + }, + edit: true, + viewPassword: true, + collectionIds: [], + revisionDate: "2023-08-03T17:40:59.793Z", + creationDate: "2023-08-03T17:40:59.793Z", + deletedDate: null, + reprompt: 0, + localData: null, + }, + { + initializerKey: 1, + id: "cbea34a8-bde4-46ad-9d19-b05001228cd3", + organizationId: null, + folderId: null, + name: "Can Be Edited id ending 3", + notes: null, + type: 1, + favorite: false, + organizationUseTotp: false, + login: { + password: "123", + hasUris: true, + uris: [ + { + uri: "http://example.com", + }, + ], + }, + edit: true, + viewPassword: true, + collectionIds: [], + revisionDate: "2023-08-03T17:40:59.793Z", + creationDate: "2023-08-03T17:40:59.793Z", + deletedDate: null, + reprompt: 0, + localData: null, + }, + { + initializerKey: 1, + id: "cbea34a8-bde4-46ad-9d19-b05001228xy4", + organizationId: null, + folderId: null, + name: "Can Be Edited id ending 4", + notes: null, + type: 1, + favorite: false, + organizationUseTotp: false, + login: { + hasUris: true, + uris: [{ uri: "101domain.com" }], + }, + edit: true, + viewPassword: true, + collectionIds: [], + revisionDate: "2023-08-03T17:40:59.793Z", + creationDate: "2023-08-03T17:40:59.793Z", + deletedDate: null, + reprompt: 0, + localData: null, + }, + { + initializerKey: 1, + id: "cbea34a8-bde4-46ad-9d19-b05001227nm5", + organizationId: null, + folderId: null, + name: "Can Be Edited id ending 5", + notes: null, + type: 1, + favorite: false, + organizationUseTotp: false, + login: { + hasUris: true, + uris: [{ uri: "123formbuilder.com" }], + }, + edit: true, + viewPassword: true, + collectionIds: [], + revisionDate: "2023-08-03T17:40:59.793Z", + creationDate: "2023-08-03T17:40:59.793Z", + deletedDate: null, + reprompt: 0, + localData: null, + }, +]; diff --git a/apps/web/src/app/reports/pages/reused-passwords-report.component.html b/apps/web/src/app/reports/pages/reused-passwords-report.component.html index e423365ef89..c4b80a88f31 100644 --- a/apps/web/src/app/reports/pages/reused-passwords-report.component.html +++ b/apps/web/src/app/reports/pages/reused-passwords-report.component.html @@ -64,6 +64,16 @@


{{ c.subTitle }} + + + + {{ "reusedXTimes" | i18n : passwordUseMap.get(c.login.password) }} diff --git a/apps/web/src/app/reports/pages/reused-passwords-report.component.spec.ts b/apps/web/src/app/reports/pages/reused-passwords-report.component.spec.ts new file mode 100644 index 00000000000..01ed07a5682 --- /dev/null +++ b/apps/web/src/app/reports/pages/reused-passwords-report.component.spec.ts @@ -0,0 +1,70 @@ +// eslint-disable-next-line no-restricted-imports +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { mock } from "jest-mock-extended"; + +import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; +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 { PasswordRepromptService } from "@bitwarden/vault"; + +import { cipherData } from "./reports-ciphers.mock"; +import { ReusedPasswordsReportComponent } from "./reused-passwords-report.component"; + +describe("ReusedPasswordsReportComponent", () => { + let component: ReusedPasswordsReportComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [ReusedPasswordsReportComponent, I18nPipe], + providers: [ + { + provide: CipherService, + useValue: mock(), + }, + { + provide: OrganizationService, + useValue: mock(), + }, + { + provide: ModalService, + useValue: mock(), + }, + { + provide: PasswordRepromptService, + useValue: mock(), + }, + { + provide: I18nService, + useValue: mock(), + }, + ], + schemas: [], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ReusedPasswordsReportComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should initialize component", () => { + expect(component).toBeTruthy(); + }); + + it('should get ciphers with reused passwords that the user has "Can Edit" access to', async () => { + const expectedIdOne: any = "cbea34a8-bde4-46ad-9d19-b05001228ab2"; + const expectedIdTwo = "cbea34a8-bde4-46ad-9d19-b05001228cd3"; + jest.spyOn(component as any, "getAllCiphers").mockReturnValue(Promise.resolve(cipherData)); + await component.setCiphers(); + + expect(component.ciphers.length).toEqual(2); + expect(component.ciphers[0].id).toEqual(expectedIdOne); + expect(component.ciphers[0].edit).toEqual(true); + expect(component.ciphers[1].id).toEqual(expectedIdTwo); + expect(component.ciphers[1].edit).toEqual(true); + }); +}); diff --git a/apps/web/src/app/reports/pages/reused-passwords-report.component.ts b/apps/web/src/app/reports/pages/reused-passwords-report.component.ts index 431b571fa16..c8aa305b8d8 100644 --- a/apps/web/src/app/reports/pages/reused-passwords-report.component.ts +++ b/apps/web/src/app/reports/pages/reused-passwords-report.component.ts @@ -1,8 +1,7 @@ import { Component, OnInit } from "@angular/core"; import { ModalService } from "@bitwarden/angular/services/modal.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums/cipher-type"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -16,15 +15,15 @@ import { CipherReportComponent } from "./cipher-report.component"; }) export class ReusedPasswordsReportComponent extends CipherReportComponent implements OnInit { passwordUseMap: Map; + disabled = true; constructor( protected cipherService: CipherService, + protected organizationService: OrganizationService, modalService: ModalService, - messagingService: MessagingService, - stateService: StateService, passwordRepromptService: PasswordRepromptService ) { - super(modalService, messagingService, true, passwordRepromptService); + super(modalService, passwordRepromptService, organizationService); } async ngOnInit() { @@ -35,20 +34,23 @@ export class ReusedPasswordsReportComponent extends CipherReportComponent implem const allCiphers = await this.getAllCiphers(); const ciphersWithPasswords: CipherView[] = []; this.passwordUseMap = new Map(); - allCiphers.forEach((c) => { + allCiphers.forEach((ciph) => { + const { type, login, isDeleted, edit, viewPassword } = ciph; if ( - c.type !== CipherType.Login || - c.login.password == null || - c.login.password === "" || - c.isDeleted + type !== CipherType.Login || + login.password == null || + login.password === "" || + isDeleted || + (!this.organization && !edit) || + !viewPassword ) { return; } - ciphersWithPasswords.push(c); - if (this.passwordUseMap.has(c.login.password)) { - this.passwordUseMap.set(c.login.password, this.passwordUseMap.get(c.login.password) + 1); + ciphersWithPasswords.push(ciph); + if (this.passwordUseMap.has(login.password)) { + this.passwordUseMap.set(login.password, this.passwordUseMap.get(login.password) + 1); } else { - this.passwordUseMap.set(c.login.password, 1); + this.passwordUseMap.set(login.password, 1); } }); const reusedPasswordCiphers = ciphersWithPasswords.filter( diff --git a/apps/web/src/app/reports/pages/unsecured-websites-report.component.html b/apps/web/src/app/reports/pages/unsecured-websites-report.component.html index 99530b753c7..61feed5cefa 100644 --- a/apps/web/src/app/reports/pages/unsecured-websites-report.component.html +++ b/apps/web/src/app/reports/pages/unsecured-websites-report.component.html @@ -59,6 +59,16 @@


{{ c.subTitle }} + + + + diff --git a/apps/web/src/app/reports/pages/unsecured-websites-report.component.spec.ts b/apps/web/src/app/reports/pages/unsecured-websites-report.component.spec.ts new file mode 100644 index 00000000000..3bc8346d7a3 --- /dev/null +++ b/apps/web/src/app/reports/pages/unsecured-websites-report.component.spec.ts @@ -0,0 +1,70 @@ +// eslint-disable-next-line no-restricted-imports +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { mock } from "jest-mock-extended"; + +import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; +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 { PasswordRepromptService } from "@bitwarden/vault"; + +import { cipherData } from "./reports-ciphers.mock"; +import { UnsecuredWebsitesReportComponent } from "./unsecured-websites-report.component"; + +describe("UnsecuredWebsitesReportComponent", () => { + let component: UnsecuredWebsitesReportComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [UnsecuredWebsitesReportComponent, I18nPipe], + providers: [ + { + provide: CipherService, + useValue: mock(), + }, + { + provide: OrganizationService, + useValue: mock(), + }, + { + provide: ModalService, + useValue: mock(), + }, + { + provide: PasswordRepromptService, + useValue: mock(), + }, + { + provide: I18nService, + useValue: mock(), + }, + ], + schemas: [], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(UnsecuredWebsitesReportComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should initialize component", () => { + expect(component).toBeTruthy(); + }); + + it('should get only unsecured ciphers that the user has "Can Edit" access to', async () => { + const expectedIdOne: any = "cbea34a8-bde4-46ad-9d19-b05001228ab2"; + const expectedIdTwo = "cbea34a8-bde4-46ad-9d19-b05001228cd3"; + jest.spyOn(component as any, "getAllCiphers").mockReturnValue(Promise.resolve(cipherData)); + await component.setCiphers(); + + expect(component.ciphers.length).toEqual(2); + expect(component.ciphers[0].id).toEqual(expectedIdOne); + expect(component.ciphers[0].edit).toEqual(true); + expect(component.ciphers[1].id).toEqual(expectedIdTwo); + expect(component.ciphers[1].edit).toEqual(true); + }); +}); diff --git a/apps/web/src/app/reports/pages/unsecured-websites-report.component.ts b/apps/web/src/app/reports/pages/unsecured-websites-report.component.ts index 42ddb3b1d5d..c1d91cf0bf5 100644 --- a/apps/web/src/app/reports/pages/unsecured-websites-report.component.ts +++ b/apps/web/src/app/reports/pages/unsecured-websites-report.component.ts @@ -1,7 +1,7 @@ import { Component, OnInit } from "@angular/core"; import { ModalService } from "@bitwarden/angular/services/modal.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums/cipher-type"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -14,13 +14,15 @@ import { CipherReportComponent } from "./cipher-report.component"; templateUrl: "unsecured-websites-report.component.html", }) export class UnsecuredWebsitesReportComponent extends CipherReportComponent implements OnInit { + disabled = true; + constructor( protected cipherService: CipherService, + protected organizationService: OrganizationService, modalService: ModalService, - messagingService: MessagingService, passwordRepromptService: PasswordRepromptService ) { - super(modalService, messagingService, true, passwordRepromptService); + super(modalService, passwordRepromptService, organizationService); } async ngOnInit() { @@ -35,7 +37,9 @@ export class UnsecuredWebsitesReportComponent extends CipherReportComponent impl } return c.login.uris.some((u) => u.uri != null && u.uri.indexOf("http://") === 0); }); - this.ciphers = unsecuredCiphers; + this.ciphers = unsecuredCiphers.filter( + (c) => (!this.organization && c.edit) || (this.organization && !c.edit) + ); } protected getAllCiphers(): Promise { diff --git a/apps/web/src/app/reports/pages/weak-passwords-report.component.html b/apps/web/src/app/reports/pages/weak-passwords-report.component.html index 4dbe20192d4..044483a5de8 100644 --- a/apps/web/src/app/reports/pages/weak-passwords-report.component.html +++ b/apps/web/src/app/reports/pages/weak-passwords-report.component.html @@ -64,6 +64,16 @@


{{ c.subTitle }} + + + + {{ passwordStrengthMap.get(c.id)[0] | i18n }} diff --git a/apps/web/src/app/reports/pages/weak-passwords-report.component.spec.ts b/apps/web/src/app/reports/pages/weak-passwords-report.component.spec.ts new file mode 100644 index 00000000000..9d74412dc27 --- /dev/null +++ b/apps/web/src/app/reports/pages/weak-passwords-report.component.spec.ts @@ -0,0 +1,82 @@ +// eslint-disable-next-line no-restricted-imports +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { mock, MockProxy } from "jest-mock-extended"; + +import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; +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 { PasswordRepromptService } from "@bitwarden/vault"; + +import { cipherData } from "./reports-ciphers.mock"; +import { WeakPasswordsReportComponent } from "./weak-passwords-report.component"; + +describe("WeakPasswordsReportComponent", () => { + let component: WeakPasswordsReportComponent; + let fixture: ComponentFixture; + let passwordStrengthService: MockProxy; + + beforeEach(() => { + passwordStrengthService = mock(); + TestBed.configureTestingModule({ + declarations: [WeakPasswordsReportComponent, I18nPipe], + providers: [ + { + provide: CipherService, + useValue: mock(), + }, + { + provide: PasswordStrengthServiceAbstraction, + useValue: passwordStrengthService, + }, + { + provide: OrganizationService, + useValue: mock(), + }, + { + provide: ModalService, + useValue: mock(), + }, + { + provide: PasswordRepromptService, + useValue: mock(), + }, + { + provide: I18nService, + useValue: mock(), + }, + ], + schemas: [], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(WeakPasswordsReportComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should initialize component", () => { + expect(component).toBeTruthy(); + }); + + it('should get only ciphers with weak passwords that the user has "Can Edit" access to', async () => { + const expectedIdOne: any = "cbea34a8-bde4-46ad-9d19-b05001228ab2"; + const expectedIdTwo = "cbea34a8-bde4-46ad-9d19-b05001228cd3"; + + jest.spyOn(passwordStrengthService, "getPasswordStrength").mockReturnValue({ + password: "123", + score: 0, + } as any); + jest.spyOn(component as any, "getAllCiphers").mockReturnValue(Promise.resolve(cipherData)); + await component.setCiphers(); + + expect(component.ciphers.length).toEqual(2); + expect(component.ciphers[0].id).toEqual(expectedIdOne); + expect(component.ciphers[0].edit).toEqual(true); + expect(component.ciphers[1].id).toEqual(expectedIdTwo); + expect(component.ciphers[1].edit).toEqual(true); + }); +}); diff --git a/apps/web/src/app/reports/pages/weak-passwords-report.component.ts b/apps/web/src/app/reports/pages/weak-passwords-report.component.ts index b5bad708afd..6ae7b15baca 100644 --- a/apps/web/src/app/reports/pages/weak-passwords-report.component.ts +++ b/apps/web/src/app/reports/pages/weak-passwords-report.component.ts @@ -1,7 +1,8 @@ import { Component, OnInit } from "@angular/core"; import { ModalService } from "@bitwarden/angular/services/modal.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +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"; import { CipherType } from "@bitwarden/common/vault/enums/cipher-type"; @@ -17,17 +18,19 @@ import { CipherReportComponent } from "./cipher-report.component"; }) export class WeakPasswordsReportComponent extends CipherReportComponent implements OnInit { passwordStrengthMap = new Map(); + disabled = true; private passwordStrengthCache = new Map(); + weakPasswordCiphers: CipherView[] = []; constructor( protected cipherService: CipherService, protected passwordStrengthService: PasswordStrengthServiceAbstraction, + protected organizationService: OrganizationService, modalService: ModalService, - messagingService: MessagingService, passwordRepromptService: PasswordRepromptService ) { - super(modalService, messagingService, true, passwordRepromptService); + super(modalService, passwordRepromptService, organizationService); } async ngOnInit() { @@ -36,33 +39,32 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen async setCiphers() { const allCiphers = await this.getAllCiphers(); - const weakPasswordCiphers: CipherView[] = []; - const isUserNameNotEmpty = (c: CipherView): boolean => { - return c.login.username != null && c.login.username.trim() !== ""; - }; - const getCacheKey = (c: CipherView): string => { - return c.login.password + "_____" + (isUserNameNotEmpty(c) ? c.login.username : ""); - }; + this.findWeakPasswords(allCiphers); + } - allCiphers.forEach((c) => { + protected findWeakPasswords(ciphers: any[]): void { + ciphers.forEach((ciph) => { + const { type, login, isDeleted, edit, viewPassword, id } = ciph; if ( - c.type !== CipherType.Login || - c.login.password == null || - c.login.password === "" || - c.isDeleted + type !== CipherType.Login || + login.password == null || + login.password === "" || + isDeleted || + (!this.organization && !edit) || + !viewPassword ) { return; } - const hasUserName = isUserNameNotEmpty(c); - const cacheKey = getCacheKey(c); + const hasUserName = this.isUserNameNotEmpty(ciph); + const cacheKey = this.getCacheKey(ciph); if (!this.passwordStrengthCache.has(cacheKey)) { let userInput: string[] = []; if (hasUserName) { - const atPosition = c.login.username.indexOf("@"); + const atPosition = login.username.indexOf("@"); if (atPosition > -1) { userInput = userInput .concat( - c.login.username + login.username .substr(0, atPosition) .trim() .toLowerCase() @@ -70,15 +72,15 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen ) .filter((i) => i.length >= 3); } else { - userInput = c.login.username + userInput = login.username .trim() .toLowerCase() .split(/[^A-Za-z0-9]/) - .filter((i) => i.length >= 3); + .filter((i: any) => i.length >= 3); } } const result = this.passwordStrengthService.getPasswordStrength( - c.login.password, + login.password, null, userInput.length > 0 ? userInput : null ); @@ -86,17 +88,17 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen } const score = this.passwordStrengthCache.get(cacheKey); if (score != null && score <= 2) { - this.passwordStrengthMap.set(c.id, this.scoreKey(score)); - weakPasswordCiphers.push(c); + this.passwordStrengthMap.set(id, this.scoreKey(score)); + this.weakPasswordCiphers.push(ciph); } }); - weakPasswordCiphers.sort((a, b) => { + this.weakPasswordCiphers.sort((a, b) => { return ( - this.passwordStrengthCache.get(getCacheKey(a)) - - this.passwordStrengthCache.get(getCacheKey(b)) + this.passwordStrengthCache.get(this.getCacheKey(a)) - + this.passwordStrengthCache.get(this.getCacheKey(b)) ); }); - this.ciphers = weakPasswordCiphers; + this.ciphers = [...this.weakPasswordCiphers]; } protected getAllCiphers(): Promise { @@ -108,6 +110,14 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen return true; } + private isUserNameNotEmpty(c: CipherView): boolean { + return !Utils.isNullOrWhitespace(c.login.username); + } + + private getCacheKey(c: CipherView): string { + return c.login.password + "_____" + (this.isUserNameNotEmpty(c) ? c.login.username : ""); + } + private scoreKey(score: number): [string, BadgeTypes] { switch (score) { case 4: diff --git a/apps/web/src/app/reports/reports.module.ts b/apps/web/src/app/reports/reports.module.ts index f4150f2382d..500865cfb1c 100644 --- a/apps/web/src/app/reports/reports.module.ts +++ b/apps/web/src/app/reports/reports.module.ts @@ -2,6 +2,8 @@ import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; import { SharedModule } from "../shared"; +import { OrganizationBadgeModule } from "../vault/individual-vault/organization-badge/organization-badge.module"; +import { PipesModule } from "../vault/individual-vault/pipes/pipes.module"; import { BreachReportComponent } from "./pages/breach-report.component"; import { ExposedPasswordsReportComponent } from "./pages/exposed-passwords-report.component"; @@ -15,7 +17,14 @@ import { ReportsRoutingModule } from "./reports-routing.module"; import { ReportsSharedModule } from "./shared"; @NgModule({ - imports: [CommonModule, SharedModule, ReportsSharedModule, ReportsRoutingModule], + imports: [ + CommonModule, + SharedModule, + ReportsSharedModule, + ReportsRoutingModule, + OrganizationBadgeModule, + PipesModule, + ], declarations: [ BreachReportComponent, ExposedPasswordsReportComponent, @@ -25,7 +34,6 @@ import { ReportsSharedModule } from "./shared"; ReusedPasswordsReportComponent, UnsecuredWebsitesReportComponent, WeakPasswordsReportComponent, - WeakPasswordsReportComponent, ], }) export class ReportsModule {} diff --git a/apps/web/src/app/shared/loose-components.module.ts b/apps/web/src/app/shared/loose-components.module.ts index 767d9cadc35..c96e1227c8c 100644 --- a/apps/web/src/app/shared/loose-components.module.ts +++ b/apps/web/src/app/shared/loose-components.module.ts @@ -81,6 +81,8 @@ import { AddEditComponent } from "../vault/individual-vault/add-edit.component"; import { AttachmentsComponent } from "../vault/individual-vault/attachments.component"; import { CollectionsComponent } from "../vault/individual-vault/collections.component"; import { FolderAddEditComponent } from "../vault/individual-vault/folder-add-edit.component"; +import { OrganizationBadgeModule } from "../vault/individual-vault/organization-badge/organization-badge.module"; +import { PipesModule } from "../vault/individual-vault/pipes/pipes.module"; import { ShareComponent } from "../vault/individual-vault/share.component"; import { AddEditComponent as OrgAddEditComponent } from "../vault/org-vault/add-edit.component"; import { AttachmentsComponent as OrgAttachmentsComponent } from "../vault/org-vault/attachments.component"; @@ -102,6 +104,8 @@ import { SharedModule } from "./shared.module"; DynamicAvatarComponent, EnvironmentSelectorModule, AccountFingerprintComponent, + OrganizationBadgeModule, + PipesModule, PasswordCalloutComponent, ], declarations: [