1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-03-02 03:41:09 +01:00

AC-1333 vault report org ciphers (#5998)

* updated report components to only show can edit ciphers, added badges, spec files
---------
Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>
This commit is contained in:
Jason Ng 2023-11-17 11:58:37 -05:00 committed by GitHub
parent 3952af058c
commit a141890b09
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 690 additions and 102 deletions

View File

@ -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() {

View File

@ -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() {

View File

@ -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() {

View File

@ -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() {

View File

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

View File

@ -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<Organization[]>;
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;

View File

@ -49,6 +49,16 @@
<br />
<small>{{ c.subTitle }}</small>
</td>
<td>
<app-org-badge
*ngIf="!organization"
[disabled]="disabled"
[organizationId]="c.organizationId"
[organizationName]="c.organizationId | orgNameFromId : (organizations$ | async)"
appStopProp
>
</app-org-badge>
</td>
<td class="text-right">
<span bitBadge badgeType="warning">
{{ "exposedXTimes" | i18n : (exposedPasswordMap.get(c.id) | number) }}

View File

@ -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<ExposedPasswordsReportComponent>;
let auditService: MockProxy<AuditService>;
beforeEach(() => {
auditService = mock<AuditService>();
TestBed.configureTestingModule({
declarations: [ExposedPasswordsReportComponent, I18nPipe],
providers: [
{
provide: CipherService,
useValue: mock<CipherService>(),
},
{
provide: AuditService,
useValue: auditService,
},
{
provide: OrganizationService,
useValue: mock<OrganizationService>(),
},
{
provide: ModalService,
useValue: mock<ModalService>(),
},
{
provide: PasswordRepromptService,
useValue: mock<PasswordRepromptService>(),
},
{
provide: I18nService,
useValue: mock<I18nService>(),
},
],
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<any>(1234));
jest.spyOn(component as any, "getAllCiphers").mockReturnValue(Promise.resolve<any>(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);
});
});

View File

@ -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<string, number>();
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<void>[] = [];
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<CipherView[]> {

View File

@ -59,6 +59,16 @@
<br />
<small>{{ c.subTitle }}</small>
</td>
<td>
<app-org-badge
*ngIf="!organization"
[disabled]="disabled"
[organizationId]="c.organizationId"
[organizationName]="c.organizationId | orgNameFromId : (organizations$ | async)"
appStopProp
>
</app-org-badge>
</td>
<td class="text-right">
<a
bitBadge

View File

@ -0,0 +1,84 @@
// 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 { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { PasswordRepromptService } from "@bitwarden/vault";
import { InactiveTwoFactorReportComponent } from "./inactive-two-factor-report.component";
import { cipherData } from "./reports-ciphers.mock";
describe("InactiveTwoFactorReportComponent", () => {
let component: InactiveTwoFactorReportComponent;
let fixture: ComponentFixture<InactiveTwoFactorReportComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [InactiveTwoFactorReportComponent, I18nPipe],
providers: [
{
provide: CipherService,
useValue: mock<CipherService>(),
},
{
provide: OrganizationService,
useValue: mock<OrganizationService>(),
},
{
provide: ModalService,
useValue: mock<ModalService>(),
},
{
provide: LogService,
useValue: mock<LogService>(),
},
{
provide: PasswordRepromptService,
useValue: mock<PasswordRepromptService>(),
},
{
provide: I18nService,
useValue: mock<I18nService>(),
},
],
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<any>(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);
});
});

View File

@ -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<string, string>();
cipherDocs = new Map<string, string>();
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<void>[] = [];
const docs = new Map<string, string>();
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;
}
}

View File

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

View File

@ -64,6 +64,16 @@
<br />
<small>{{ c.subTitle }}</small>
</td>
<td>
<app-org-badge
*ngIf="!organization"
[disabled]="disabled"
[organizationId]="c.organizationId"
[organizationName]="c.organizationId | orgNameFromId : (organizations$ | async)"
appStopProp
>
</app-org-badge>
</td>
<td class="text-right">
<span bitBadge badgeType="warning">
{{ "reusedXTimes" | i18n : passwordUseMap.get(c.login.password) }}

View File

@ -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<ReusedPasswordsReportComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ReusedPasswordsReportComponent, I18nPipe],
providers: [
{
provide: CipherService,
useValue: mock<CipherService>(),
},
{
provide: OrganizationService,
useValue: mock<OrganizationService>(),
},
{
provide: ModalService,
useValue: mock<ModalService>(),
},
{
provide: PasswordRepromptService,
useValue: mock<PasswordRepromptService>(),
},
{
provide: I18nService,
useValue: mock<I18nService>(),
},
],
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<any>(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);
});
});

View File

@ -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<string, number>;
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<string, number>();
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(

View File

@ -59,6 +59,16 @@
<br />
<small>{{ c.subTitle }}</small>
</td>
<td>
<app-org-badge
*ngIf="!organization"
[disabled]="disabled"
[organizationId]="c.organizationId"
[organizationName]="c.organizationId | orgNameFromId : (organizations$ | async)"
appStopProp
>
</app-org-badge>
</td>
</tr>
</tbody>
</table>

View File

@ -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<UnsecuredWebsitesReportComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [UnsecuredWebsitesReportComponent, I18nPipe],
providers: [
{
provide: CipherService,
useValue: mock<CipherService>(),
},
{
provide: OrganizationService,
useValue: mock<OrganizationService>(),
},
{
provide: ModalService,
useValue: mock<ModalService>(),
},
{
provide: PasswordRepromptService,
useValue: mock<PasswordRepromptService>(),
},
{
provide: I18nService,
useValue: mock<I18nService>(),
},
],
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<any>(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);
});
});

View File

@ -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<CipherView[]> {

View File

@ -64,6 +64,16 @@
<br />
<small>{{ c.subTitle }}</small>
</td>
<td>
<app-org-badge
*ngIf="!organization"
[disabled]="disabled"
[organizationId]="c.organizationId"
[organizationName]="c.organizationId | orgNameFromId : (organizations$ | async)"
appStopProp
>
</app-org-badge>
</td>
<td class="text-right">
<span bitBadge [badgeType]="passwordStrengthMap.get(c.id)[1]">
{{ passwordStrengthMap.get(c.id)[0] | i18n }}

View File

@ -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<WeakPasswordsReportComponent>;
let passwordStrengthService: MockProxy<PasswordStrengthServiceAbstraction>;
beforeEach(() => {
passwordStrengthService = mock<PasswordStrengthServiceAbstraction>();
TestBed.configureTestingModule({
declarations: [WeakPasswordsReportComponent, I18nPipe],
providers: [
{
provide: CipherService,
useValue: mock<CipherService>(),
},
{
provide: PasswordStrengthServiceAbstraction,
useValue: passwordStrengthService,
},
{
provide: OrganizationService,
useValue: mock<OrganizationService>(),
},
{
provide: ModalService,
useValue: mock<ModalService>(),
},
{
provide: PasswordRepromptService,
useValue: mock<PasswordRepromptService>(),
},
{
provide: I18nService,
useValue: mock<I18nService>(),
},
],
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<any>(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);
});
});

View File

@ -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<string, [string, BadgeTypes]>();
disabled = true;
private passwordStrengthCache = new Map<string, number>();
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<CipherView[]> {
@ -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:

View File

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

View File

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