From 336f916c763ece58908cec9eb2bdae6679cbdd3a Mon Sep 17 00:00:00 2001 From: jaasen-livefront Date: Thu, 14 Nov 2024 17:36:38 -0800 Subject: [PATCH] final data aggregation for risk insights --- .../all-applications.component.html | 12 +- .../all-applications.component.ts | 32 ++- .../risk-insights.component.html | 17 -- .../risk-insights.component.ts | 6 - .../risk-insights/services/ciphers.mock.ts | 53 +++-- .../member-cipher-details-api.service.spec.ts | 6 +- .../services/password-health.service.spec.ts | 161 ++++++------- .../services/password-health.service.ts | 224 +++++++++++------- 8 files changed, 271 insertions(+), 240 deletions(-) diff --git a/apps/web/src/app/tools/access-intelligence/all-applications.component.html b/apps/web/src/app/tools/access-intelligence/all-applications.component.html index 4ed31adea78..730da309aad 100644 --- a/apps/web/src/app/tools/access-intelligence/all-applications.component.html +++ b/apps/web/src/app/tools/access-intelligence/all-applications.component.html @@ -6,7 +6,7 @@ > {{ "loading" | i18n }} -
+

@@ -34,15 +34,15 @@

@@ -88,7 +88,7 @@ /> - {{ r.name }} + {{ r.application }} diff --git a/apps/web/src/app/tools/access-intelligence/all-applications.component.ts b/apps/web/src/app/tools/access-intelligence/all-applications.component.ts index 5d76403f46b..5cfa348ca3b 100644 --- a/apps/web/src/app/tools/access-intelligence/all-applications.component.ts +++ b/apps/web/src/app/tools/access-intelligence/all-applications.component.ts @@ -4,6 +4,12 @@ import { FormControl } from "@angular/forms"; import { ActivatedRoute } from "@angular/router"; import { debounceTime, firstValueFrom, map } from "rxjs"; +// eslint-disable-next-line no-restricted-imports +import { + PasswordHealthService, + MemberCipherDetailsApiService, + ApplicationHealthReport, +} from "@bitwarden/bit-common/tools/reports/risk-insights"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; @@ -26,31 +32,30 @@ import { HeaderModule } from "../../layouts/header/header.module"; import { SharedModule } from "../../shared"; import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module"; -import { applicationTableMockData } from "./application-table.mock"; - @Component({ standalone: true, selector: "tools-all-applications", templateUrl: "./all-applications.component.html", imports: [HeaderModule, CardComponent, SearchModule, PipesModule, NoItemsModule, SharedModule], + providers: [MemberCipherDetailsApiService], }) export class AllApplicationsComponent implements OnInit { protected dataSource = new TableDataSource(); protected selectedIds: Set = new Set(); protected searchControl = new FormControl("", { nonNullable: true }); private destroyRef = inject(DestroyRef); - protected loading = false; + protected loading = true; protected organization: Organization; noItemsIcon = Icons.Security; protected markingAsCritical = false; + protected applicationHealthReport: ApplicationHealthReport; isCritialAppsFeatureEnabled = false; - - // MOCK DATA - protected mockData = applicationTableMockData; - protected mockAtRiskMembersCount = 0; - protected mockAtRiskAppsCount = 0; - protected mockTotalMembersCount = 0; - protected mockTotalAppsCount = 0; + passwordHealthService = new PasswordHealthService( + this.passwordStrengthService, + this.auditService, + this.cipherService, + this.memberCipherDetailsApiService, + ); async ngOnInit() { this.activatedRoute.paramMap @@ -59,7 +64,10 @@ export class AllApplicationsComponent implements OnInit { map(async (params) => { const organizationId = params.get("organizationId"); this.organization = await firstValueFrom(this.organizationService.get$(organizationId)); - // TODO: use organizationId to fetch data + this.applicationHealthReport = + await this.passwordHealthService.generateReportDetails(organizationId); + this.dataSource.data = this.applicationHealthReport.details; + this.loading = false; }), ) .subscribe(); @@ -78,8 +86,8 @@ export class AllApplicationsComponent implements OnInit { protected toastService: ToastService, protected organizationService: OrganizationService, protected configService: ConfigService, + protected memberCipherDetailsApiService: MemberCipherDetailsApiService, ) { - this.dataSource.data = applicationTableMockData; this.searchControl.valueChanges .pipe(debounceTime(200), takeUntilDestroyed()) .subscribe((v) => (this.dataSource.filter = v)); diff --git a/apps/web/src/app/tools/access-intelligence/risk-insights.component.html b/apps/web/src/app/tools/access-intelligence/risk-insights.component.html index 067207160d4..66cdb41aaf8 100644 --- a/apps/web/src/app/tools/access-intelligence/risk-insights.component.html +++ b/apps/web/src/app/tools/access-intelligence/risk-insights.component.html @@ -29,21 +29,4 @@ - - - - - - - - - - diff --git a/apps/web/src/app/tools/access-intelligence/risk-insights.component.ts b/apps/web/src/app/tools/access-intelligence/risk-insights.component.ts index 1c6a36b4454..b76f0064d36 100644 --- a/apps/web/src/app/tools/access-intelligence/risk-insights.component.ts +++ b/apps/web/src/app/tools/access-intelligence/risk-insights.component.ts @@ -13,9 +13,6 @@ import { HeaderModule } from "../../layouts/header/header.module"; import { AllApplicationsComponent } from "./all-applications.component"; import { CriticalApplicationsComponent } from "./critical-applications.component"; import { NotifiedMembersTableComponent } from "./notified-members-table.component"; -import { PasswordHealthMembersURIComponent } from "./password-health-members-uri.component"; -import { PasswordHealthMembersComponent } from "./password-health-members.component"; -import { PasswordHealthComponent } from "./password-health.component"; export enum RiskInsightsTabType { AllApps = 0, @@ -34,9 +31,6 @@ export enum RiskInsightsTabType { CriticalApplicationsComponent, JslibModule, HeaderModule, - PasswordHealthComponent, - PasswordHealthMembersComponent, - PasswordHealthMembersURIComponent, NotifiedMembersTableComponent, TabsModule, ], diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/ciphers.mock.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/ciphers.mock.ts index e7693e46a32..7538d7c92b9 100644 --- a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/ciphers.mock.ts +++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/ciphers.mock.ts @@ -1,10 +1,18 @@ +import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view"; + +const createLoginUriView = (uri: string): LoginUriView => { + const view = new LoginUriView(); + view.uri = uri; + return view; +}; + export const mockCiphers: any[] = [ { initializerKey: 1, id: "cbea34a8-bde4-46ad-9d19-b05001228ab1", organizationId: null, folderId: null, - name: "Cannot Be Edited", + name: "Weak Password Cipher", notes: null, isDeleted: false, type: 1, @@ -14,10 +22,11 @@ export const mockCiphers: any[] = [ password: "123", hasUris: true, uris: [ - { uri: "www.google.com" }, - { uri: "accounts.google.com" }, - { uri: "https://www.google.com" }, - { uri: "https://www.google.com/login" }, + createLoginUriView("101domain.com"), + createLoginUriView("www.google.com"), + createLoginUriView("accounts.google.com"), + createLoginUriView("https://www.google.com"), + createLoginUriView("https://www.google.com/login"), ], }, edit: false, @@ -31,23 +40,18 @@ export const mockCiphers: any[] = [ }, { initializerKey: 1, - id: "cbea34a8-bde4-46ad-9d19-b05001228ab2", + id: "cbea34a8-bde4-46ad-9d19-b05001228cd3", organizationId: null, folderId: null, - name: "Can Be Edited id ending 2", + name: "Strong Password Cipher", notes: null, - isDeleted: false, type: 1, favorite: false, organizationUseTotp: false, login: { - password: "123", + password: "Password!123", hasUris: true, - uris: [ - { - uri: "http://nothing.com", - }, - ], + uris: [createLoginUriView("http://example.com")], }, edit: true, viewPassword: true, @@ -60,22 +64,18 @@ export const mockCiphers: any[] = [ }, { initializerKey: 1, - id: "cbea34a8-bde4-46ad-9d19-b05001228cd3", + id: "cbea34a8-bde4-46ad-9d19-b05001228ab2", organizationId: null, folderId: null, - name: "Can Be Edited id ending 3", + name: "Strong password Cipher", notes: null, type: 1, favorite: false, organizationUseTotp: false, login: { - password: "123", hasUris: true, - uris: [ - { - uri: "http://example.com", - }, - ], + password: "Password!1234", + uris: [createLoginUriView("101domain.com")], }, edit: true, viewPassword: true, @@ -91,14 +91,15 @@ export const mockCiphers: any[] = [ id: "cbea34a8-bde4-46ad-9d19-b05001228xy4", organizationId: null, folderId: null, - name: "Can Be Edited id ending 4", + name: "Weak password Cipher", notes: null, type: 1, favorite: false, organizationUseTotp: false, login: { hasUris: true, - uris: [{ uri: "101domain.com" }], + password: "Password!123", + uris: [createLoginUriView("example.com")], }, edit: true, viewPassword: true, @@ -114,14 +115,14 @@ export const mockCiphers: any[] = [ id: "cbea34a8-bde4-46ad-9d19-b05001227nm5", organizationId: null, folderId: null, - name: "Can Be Edited id ending 5", + name: "No password Cipher", notes: null, type: 1, favorite: false, organizationUseTotp: false, login: { hasUris: true, - uris: [{ uri: "123formbuilder.com" }], + uris: [createLoginUriView("123formbuilder.com")], }, edit: true, viewPassword: true, diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/member-cipher-details-api.service.spec.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/member-cipher-details-api.service.spec.ts index 872a4cdff55..6ea49551239 100644 --- a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/member-cipher-details-api.service.spec.ts +++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/member-cipher-details-api.service.spec.ts @@ -63,11 +63,7 @@ export const mockMemberCipherDetails: any = [ userName: "Chris Finch", email: "chris.finch@wernhamhogg.uk", usesKeyConnector: true, - cipherIds: [ - "cbea34a8-bde4-46ad-9d19-b05001228ab2", - "cbea34a8-bde4-46ad-9d19-b05001228cd3", - "cbea34a8-bde4-46ad-9d19-b05001228xy4", - ], + cipherIds: ["cbea34a8-bde4-46ad-9d19-b05001227nm5"], }, ]; diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/password-health.service.spec.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/password-health.service.spec.ts index c0f77abeb79..b3b36072fd9 100644 --- a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/password-health.service.spec.ts +++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/password-health.service.spec.ts @@ -3,7 +3,9 @@ import { TestBed } from "@angular/core/testing"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; 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 { LoginView } from "@bitwarden/common/vault/models/view/login.view"; import { mockCiphers } from "./ciphers.mock"; import { MemberCipherDetailsApiService } from "./member-cipher-details-api.service"; @@ -12,8 +14,6 @@ import { PasswordHealthService } from "./password-health.service"; describe("PasswordHealthService", () => { let service: PasswordHealthService; - let cipherService: CipherService; - let memberCipherDetailsApiService: MemberCipherDetailsApiService; beforeEach(() => { TestBed.configureTestingModule({ @@ -46,104 +46,101 @@ describe("PasswordHealthService", () => { getMemberCipherDetails: jest.fn().mockResolvedValue(mockMemberCipherDetails), }, }, - { provide: "organizationId", useValue: "org1" }, ], }); service = TestBed.inject(PasswordHealthService); - cipherService = TestBed.inject(CipherService); - memberCipherDetailsApiService = TestBed.inject(MemberCipherDetailsApiService); }); - it("should be created", () => { - expect(service).toBeTruthy(); + it("should build the application health report correctly", async () => { + // Execute the method + const result = await service.generateReportDetails("orgId"); + + const expected = [ + { + application: "101domain.com", + atRiskPasswords: 1, + totalPasswords: 2, + atRiskMembers: 2, + totalMembers: 4, + }, + { + application: "123formbuilder.com", + atRiskPasswords: 0, + totalPasswords: 1, + atRiskMembers: 0, + totalMembers: 5, + }, + { + application: "example.com", + atRiskPasswords: 1, + totalPasswords: 2, + atRiskMembers: 5, + totalMembers: 5, + }, + { + application: "google.com", + atRiskPasswords: 1, + totalPasswords: 1, + atRiskMembers: 2, + totalMembers: 2, + }, + ]; + + // Sort for comparison + const sortFn = (a: any, b: any) => a.application.localeCompare(b.application); + + expect(result.details.sort(sortFn)).toEqual(expected.sort(sortFn)); + expect(result.totalAtRiskMembers).toBe(5); + expect(result.totalMembers).toBe(6); + expect(result.totalAtRiskApps).toBe(3); + expect(result.totalApps).toBe(4); }); - it("should initialize properties", () => { - expect(service.reportCiphers).toEqual([]); - expect(service.reportCipherIds).toEqual([]); - expect(service.passwordStrengthMap.size).toBe(0); - expect(service.passwordUseMap.size).toBe(0); - expect(service.exposedPasswordMap.size).toBe(0); - expect(service.totalMembersMap.size).toBe(0); - }); + describe("isWeakPassword", () => { + it("should return true for a weak password", () => { + const cipher = new CipherView(); + cipher.type = CipherType.Login; + cipher.login = { password: "123", username: "user" } as LoginView; + cipher.viewPassword = true; - describe("generateReport", () => { - beforeEach(async () => { - await service.generateReport(); + expect(service.isWeakPassword(cipher)).toBe(true); }); - it("should fetch all ciphers for the organization", () => { - expect(cipherService.getAllFromApiForOrganization).toHaveBeenCalledWith("org1"); - }); + it("should return false for a strong password", () => { + const cipher = new CipherView(); + cipher.type = CipherType.Login; + cipher.login = { password: "StrongPass!123", username: "user" } as LoginView; + cipher.viewPassword = true; - it("should fetch member cipher details", () => { - expect(memberCipherDetailsApiService.getMemberCipherDetails).toHaveBeenCalledWith("org1"); - }); - - it("should populate reportCiphers with ciphers that have issues", () => { - expect(service.reportCiphers.length).toBeGreaterThan(0); - }); - - it("should detect weak passwords", () => { - expect(service.passwordStrengthMap.size).toBeGreaterThan(0); - expect(service.passwordStrengthMap.get("cbea34a8-bde4-46ad-9d19-b05001228ab1")).toEqual([ - "veryWeak", - "danger", - ]); - expect(service.passwordStrengthMap.get("cbea34a8-bde4-46ad-9d19-b05001228ab2")).toEqual([ - "veryWeak", - "danger", - ]); - expect(service.passwordStrengthMap.get("cbea34a8-bde4-46ad-9d19-b05001228cd3")).toEqual([ - "veryWeak", - "danger", - ]); - }); - - it("should detect reused passwords", () => { - expect(service.passwordUseMap.get("123")).toBe(3); - }); - - it("should detect exposed passwords", () => { - expect(service.exposedPasswordMap.size).toBeGreaterThan(0); - expect(service.exposedPasswordMap.get("cbea34a8-bde4-46ad-9d19-b05001228ab1")).toBe(100); - }); - - it("should calculate total members per cipher", () => { - expect(service.totalMembersMap.size).toBeGreaterThan(0); - expect(service.totalMembersMap.get("cbea34a8-bde4-46ad-9d19-b05001228ab1")).toBe(2); - expect(service.totalMembersMap.get("cbea34a8-bde4-46ad-9d19-b05001228ab2")).toBe(4); - expect(service.totalMembersMap.get("cbea34a8-bde4-46ad-9d19-b05001228cd3")).toBe(5); - expect(service.totalMembersMap.get("cbea34a8-bde4-46ad-9d19-b05001227nm5")).toBe(4); - expect(service.totalMembersMap.get("cbea34a8-bde4-46ad-9d19-b05001227nm7")).toBe(1); - expect(service.totalMembersMap.get("cbea34a8-bde4-46ad-9d19-b05001228xy4")).toBe(6); + expect(service.isWeakPassword(cipher)).toBe(false); }); }); - describe("findWeakPassword", () => { - it("should add weak passwords to passwordStrengthMap", () => { - const weakCipher = mockCiphers.find((c) => c.login?.password === "123") as CipherView; - service.findWeakPassword(weakCipher); - expect(service.passwordStrengthMap.get(weakCipher.id)).toEqual(["veryWeak", "danger"]); - }); - }); + describe("isReusedPassword", () => { + it("should return false for a new password", () => { + const cipher = new CipherView(); + cipher.type = CipherType.Login; + cipher.login = { password: "uniquePassword", username: "user" } as LoginView; + cipher.viewPassword = true; - describe("findReusedPassword", () => { - it("should detect password reuse", () => { - mockCiphers.forEach((cipher) => { - service.findReusedPassword(cipher as CipherView); - }); - const reuseCounts = Array.from(service.passwordUseMap.values()).filter((count) => count > 1); - expect(reuseCounts.length).toBeGreaterThan(0); + expect(service.isReusedPassword(cipher)).toBe(false); }); - }); - describe("findExposedPassword", () => { - it("should add exposed passwords to exposedPasswordMap", async () => { - const exposedCipher = mockCiphers.find((c) => c.login?.password === "123") as CipherView; - await service.findExposedPassword(exposedCipher); - expect(service.exposedPasswordMap.get(exposedCipher.id)).toBe(100); + it("should return true for a reused password", () => { + const cipher1 = new CipherView(); + cipher1.type = CipherType.Login; + cipher1.login = { password: "reusedPassword", username: "user" } as LoginView; + cipher1.viewPassword = true; + + const cipher2 = new CipherView(); + cipher2.type = CipherType.Login; + cipher2.login = { password: "reusedPassword", username: "user" } as LoginView; + cipher2.viewPassword = true; + + service.isReusedPassword(cipher1); // Adds 'reusedPassword' to usedPasswords + + expect(service.isReusedPassword(cipher2)).toBe(true); }); }); }); diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/password-health.service.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/password-health.service.ts index 4070b23d29e..1161415ea58 100644 --- a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/password-health.service.ts +++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/password-health.service.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from "@angular/core"; +import { Injectable } from "@angular/core"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -6,57 +6,157 @@ import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/pass 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 { BadgeVariant } from "@bitwarden/components"; +import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view"; import { MemberCipherDetailsApiService } from "./member-cipher-details-api.service"; +export interface ApplicationHealthReport { + details: ApplicationHealthReportDetail[]; + totalAtRiskMembers: number; + totalMembers: number; + totalAtRiskApps: number; + totalApps: number; +} + +interface ApplicationHealthReportDetail { + application: string; + atRiskPasswords: number; + totalPasswords: number; + atRiskMembers: number; + totalMembers: number; +} + @Injectable() export class PasswordHealthService { reportCiphers: CipherView[] = []; reportCipherIds: string[] = []; - passwordStrengthMap = new Map(); + usedPasswords: string[] = []; - passwordUseMap = new Map(); - - exposedPasswordMap = new Map(); - - totalMembersMap = new Map(); + applicationHealthReport: ApplicationHealthReportDetail[] = []; constructor( private passwordStrengthService: PasswordStrengthServiceAbstraction, private auditService: AuditService, private cipherService: CipherService, private memberCipherDetailsApiService: MemberCipherDetailsApiService, - @Inject("organizationId") private organizationId: string, ) {} - async generateReport() { - const allCiphers = await this.cipherService.getAllFromApiForOrganization(this.organizationId); - allCiphers.forEach(async (cipher) => { - this.findWeakPassword(cipher); - this.findReusedPassword(cipher); - await this.findExposedPassword(cipher); - }); + async generateReportDetails(organizationId: string): Promise { + // Helper function to normalize hostnames to TLDs + const hostnameToTLD = (uriView: LoginUriView): string => { + const match = uriView.hostname.match(/([^.]+\.[^.]+)$/); + return match ? match[1] : uriView.hostname; + }; - const memberCipherDetails = await this.memberCipherDetailsApiService.getMemberCipherDetails( - this.organizationId, - ); + const members = await this.memberCipherDetailsApiService.getMemberCipherDetails(organizationId); - memberCipherDetails.forEach((user) => { - user.cipherIds.forEach((cipherId: string) => { - if (this.totalMembersMap.has(cipherId)) { - this.totalMembersMap.set(cipherId, (this.totalMembersMap.get(cipherId) || 0) + 1); - } else { - this.totalMembersMap.set(cipherId, 1); + const ciphers = await this.cipherService.getAllFromApiForOrganization(organizationId); + + // Map to store application data + const appDataMap = new Map(); + + // Map cipher IDs to ciphers + const cipherMap = new Map(ciphers.map((cipher) => [cipher.id, cipher])); + + // Set to store at-risk member IDs + const totalAtRiskMembers = new Set(); + + // Set to store at-risk app IDs + const totalAtRiskApps = new Set(); + + // Set to store at-risk cipher IDs + const atRiskCipherIds = new Set(); + + // Determine at-risk ciphers + for (const cipher of ciphers) { + const isWeak = this.isWeakPassword(cipher); + const isReused = this.isReusedPassword(cipher); + const isExposed = await this.isExposedPassword(cipher); + if (isWeak || isReused || isExposed) { + atRiskCipherIds.add(cipher.id); + } + } + + // Group ciphers by application + for (const cipher of ciphers) { + const applications = new Set(cipher.login.uris.map(hostnameToTLD)); + + for (const app of applications) { + if (!appDataMap.has(app)) { + appDataMap.set(app, { + application: app, + atRiskPasswords: 0, + totalPasswords: 0, + atRiskMembers: 0, + totalMembers: 0, + }); } - }); - }); + + const appData = appDataMap.get(app)!; + appData.totalPasswords += 1; + + if (atRiskCipherIds.has(cipher.id)) { + appData.atRiskPasswords += 1; + } + } + } + + // Associate members with applications + for (const member of members) { + const memberApps = new Set(); + const atRiskApps = new Set(); + + for (const cipherId of member.cipherIds) { + const cipher = cipherMap.get(cipherId); + if (!cipher) { + continue; + } + + const applications = new Set(cipher.login.uris.map(hostnameToTLD)); + + for (const app of applications) { + memberApps.add(app); + if (atRiskCipherIds.has(cipherId)) { + atRiskApps.add(app); + } + } + } + + for (const app of memberApps) { + const appData = appDataMap.get(app); + if (appData) { + appData.totalMembers += 1; + } + } + + for (const app of atRiskApps) { + const appData = appDataMap.get(app); + if (appData) { + if (!totalAtRiskMembers.has(member.userName)) { + totalAtRiskMembers.add(member.userName); + } + if (!totalAtRiskApps.has(app)) { + totalAtRiskApps.add(app); + } + appData.atRiskMembers += 1; + } + } + } + + // Convert map to array + return { + totalAtRiskMembers: totalAtRiskMembers.size, + totalMembers: members.length, + totalAtRiskApps: totalAtRiskApps.size, + totalApps: appDataMap.size, + details: Array.from(appDataMap.values()), + }; } - async findExposedPassword(cipher: CipherView) { - const { type, login, isDeleted, viewPassword, id } = cipher; + async isExposedPassword(cipher: CipherView) { + const { type, login, isDeleted, viewPassword } = cipher; if ( type !== CipherType.Login || login.password == null || @@ -68,13 +168,10 @@ export class PasswordHealthService { } const exposedCount = await this.auditService.passwordLeaked(login.password); - if (exposedCount > 0) { - this.exposedPasswordMap.set(id, exposedCount); - this.checkForExistingCipher(cipher); - } + return exposedCount > 0; } - findReusedPassword(cipher: CipherView) { + isReusedPassword(cipher: CipherView) { const { type, login, isDeleted, viewPassword } = cipher; if ( type !== CipherType.Login || @@ -86,16 +183,15 @@ export class PasswordHealthService { return; } - if (this.passwordUseMap.has(login.password)) { - this.passwordUseMap.set(login.password, (this.passwordUseMap.get(login.password) || 0) + 1); - } else { - this.passwordUseMap.set(login.password, 1); + if (this.usedPasswords.includes(login.password)) { + return true; } - this.checkForExistingCipher(cipher); + this.usedPasswords.push(login.password); + return false; } - findWeakPassword(cipher: CipherView): void { + isWeakPassword(cipher: CipherView) { const { type, login, isDeleted, viewPassword } = cipher; if ( type !== CipherType.Login || @@ -107,7 +203,7 @@ export class PasswordHealthService { return; } - const hasUserName = this.isUserNameNotEmpty(cipher); + const hasUserName = !Utils.isNullOrWhitespace(cipher.login.username); let userInput: string[] = []; if (hasUserName) { const atPosition = login.username.indexOf("@"); @@ -135,50 +231,6 @@ export class PasswordHealthService { userInput.length > 0 ? userInput : null, ); - if (score != null && score <= 2) { - this.passwordStrengthMap.set(cipher.id, this.scoreKey(score)); - this.checkForExistingCipher(cipher); - } - } - - private isUserNameNotEmpty(c: CipherView): boolean { - return !Utils.isNullOrWhitespace(c.login.username); - } - - private scoreKey(score: number): [string, BadgeVariant] { - switch (score) { - case 4: - return ["strong", "success"]; - case 3: - return ["good", "primary"]; - case 2: - return ["weak", "warning"]; - default: - return ["veryWeak", "danger"]; - } - } - - checkForExistingCipher(ciph: CipherView) { - if (!this.reportCipherIds.includes(ciph.id)) { - this.reportCipherIds.push(ciph.id); - this.reportCiphers.push(ciph); - } - } - - groupCiphersByLoginUri(): CipherView[] { - const cipherViews: CipherView[] = []; - const cipherUris: string[] = []; - const ciphers = this.reportCiphers; - - ciphers.forEach((ciph) => { - const uris = ciph.login?.uris ?? []; - uris.map((u: { uri: string }) => { - const uri = Utils.getHostname(u.uri).replace("www.", ""); - cipherUris.push(uri); - cipherViews.push({ ...ciph, hostURI: uri } as CipherView & { hostURI: string }); - }); - }); - - return cipherViews; + return score != null && score <= 2; } }