From 16506525e75c40cbdc90a119e8451503576162c0 Mon Sep 17 00:00:00 2001 From: voommen-livefront Date: Mon, 14 Jul 2025 15:00:57 -0500 Subject: [PATCH] PM-20132 do not filter out members --- .../risk-insights/models/password-health.ts | 15 +++++++ .../services/risk-insights-data.service.ts | 10 ++++- .../risk-insights-report.service.spec.ts | 29 +++++++----- .../services/risk-insights-report.service.ts | 44 +++++++++++++++---- .../all-applications.component.ts | 17 ++++--- .../critical-applications.component.ts | 17 ++++--- 6 files changed, 100 insertions(+), 32 deletions(-) diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/password-health.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/password-health.ts index 62eb0122dca..07108d13c89 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/password-health.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/password-health.ts @@ -167,4 +167,19 @@ export enum DrawerType { OrgAtRiskApps = 3, } +export interface CipherHealthWithMemberDetails { + ciphers: CipherHealthReportDetail[]; + members: MemberDetailsFlat[]; +} + +export interface CipherHealthReportUriDetailWithMemberDetails { + ciphers: CipherHealthReportUriDetail[]; + members: MemberDetailsFlat[]; +} + +export interface HealthReportUriDetailWithMemberDetails { + healthReport: ApplicationHealthReportDetail[]; + members: MemberDetailsFlat[]; +} + export type PasswordHealthReportApplicationId = Opaque; diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts index 386c6fd6865..f39e3cc1a60 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts @@ -7,6 +7,8 @@ import { AtRiskApplicationDetail, AtRiskMemberDetail, DrawerType, + HealthReportUriDetailWithMemberDetails, + MemberDetailsFlat, } from "../models/password-health"; import { RiskInsightsReportService } from "./risk-insights-report.service"; @@ -27,6 +29,9 @@ export class RiskInsightsDataService { private dataLastUpdatedSubject = new BehaviorSubject(null); dataLastUpdated$ = this.dataLastUpdatedSubject.asObservable(); + private memberDetailsSubject = new BehaviorSubject([]); + memberDetails$ = this.memberDetailsSubject.asObservable(); + openDrawer = false; drawerInvokerId: string = ""; activeDrawerType: DrawerType = DrawerType.None; @@ -56,8 +61,9 @@ export class RiskInsightsDataService { }), ) .subscribe({ - next: (reports: ApplicationHealthReportDetail[]) => { - this.applicationsSubject.next(reports); + next: (reports: HealthReportUriDetailWithMemberDetails) => { + this.applicationsSubject.next(reports.healthReport); + this.memberDetailsSubject.next(reports.members); this.errorSubject.next(null); }, error: () => { diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.spec.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.spec.ts index 3aa624f1e59..a64d59e2138 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.spec.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.spec.ts @@ -43,9 +43,11 @@ describe("RiskInsightsReportService", () => { it("should generate the raw data report correctly", async () => { const result = await firstValueFrom(service.generateRawDataReport$("orgId")); - expect(result).toHaveLength(6); + expect(result.ciphers).toHaveLength(6); - let testCaseResults = result.filter((x) => x.id === "cbea34a8-bde4-46ad-9d19-b05001228ab1"); + let testCaseResults = result.ciphers.filter( + (x) => x.id === "cbea34a8-bde4-46ad-9d19-b05001228ab1", + ); expect(testCaseResults).toHaveLength(1); let testCase = testCaseResults[0]; expect(testCase).toBeTruthy(); @@ -55,7 +57,7 @@ describe("RiskInsightsReportService", () => { expect(testCase.exposedPasswordDetail).toBeTruthy(); expect(testCase.reusedPasswordCount).toEqual(2); - testCaseResults = result.filter((x) => x.id === "cbea34a8-bde4-46ad-9d19-b05001227tt1"); + testCaseResults = result.ciphers.filter((x) => x.id === "cbea34a8-bde4-46ad-9d19-b05001227tt1"); expect(testCaseResults).toHaveLength(1); testCase = testCaseResults[0]; expect(testCase).toBeTruthy(); @@ -69,14 +71,16 @@ describe("RiskInsightsReportService", () => { it("should generate the raw data + uri report correctly", async () => { const result = await firstValueFrom(service.generateRawDataUriReport$("orgId")); - expect(result).toHaveLength(11); + expect(result.ciphers).toHaveLength(11); // Two ciphers that have google.com as their uri. There should be 2 results - const googleResults = result.filter((x) => x.trimmedUri === "google.com"); + const googleResults = result.ciphers.filter((x) => x.trimmedUri === "google.com"); expect(googleResults).toHaveLength(2); // There is an invalid uri and it should not be trimmed - const invalidUriResults = result.filter((x) => x.trimmedUri === "this_is-not|a-valid-uri123@+"); + const invalidUriResults = result.ciphers.filter( + (x) => x.trimmedUri === "this_is-not|a-valid-uri123@+", + ); expect(invalidUriResults).toHaveLength(1); // Verify the details for one of the googles matches the password health info @@ -92,13 +96,13 @@ describe("RiskInsightsReportService", () => { it("should generate applications health report data correctly", async () => { const result = await firstValueFrom(service.generateApplicationsReport$("orgId")); - expect(result).toHaveLength(8); + expect(result.healthReport).toHaveLength(8); // Two ciphers have google.com associated with them. The first cipher // has 2 members and the second has 4. However, the 2 members in the first // cipher are also associated with the second. The total amount of members // should be 4 not 6 - const googleTestResults = result.filter((x) => x.applicationName === "google.com"); + const googleTestResults = result.healthReport.filter((x) => x.applicationName === "google.com"); expect(googleTestResults).toHaveLength(1); const googleTest = googleTestResults[0]; expect(googleTest.memberCount).toEqual(4); @@ -111,7 +115,9 @@ describe("RiskInsightsReportService", () => { expect(googleTest.atRiskPasswordCount).toEqual(2); // There are 2 ciphers associated with 101domain.com - const domain101TestResults = result.filter((x) => x.applicationName === "101domain.com"); + const domain101TestResults = result.healthReport.filter( + (x) => x.applicationName === "101domain.com", + ); expect(domain101TestResults).toHaveLength(1); const domain101Test = domain101TestResults[0]; expect(domain101Test.passwordCount).toEqual(2); @@ -132,7 +138,10 @@ describe("RiskInsightsReportService", () => { it("should generate applications summary data correctly", async () => { const reportResult = await firstValueFrom(service.generateApplicationsReport$("orgId")); - const reportSummary = service.generateApplicationsSummary(reportResult); + const reportSummary = service.generateApplicationsSummary( + reportResult.healthReport, + reportResult.members, + ); expect(reportSummary.totalMemberCount).toEqual(7); expect(reportSummary.totalAtRiskMemberCount).toEqual(6); diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.ts index 182e8aa6882..44081e2b87e 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.ts @@ -21,6 +21,9 @@ import { WeakPasswordDetail, WeakPasswordScore, ApplicationHealthReportDetailWithCriticalFlagAndCipher, + CipherHealthWithMemberDetails, + CipherHealthReportUriDetailWithMemberDetails, + HealthReportUriDetailWithMemberDetails, } from "../models/password-health"; import { MemberCipherDetailsApiService } from "./member-cipher-details-api.service"; @@ -40,7 +43,7 @@ export class RiskInsightsReportService { * @param organizationId * @returns Cipher health report data with members and trimmed uris */ - generateRawDataReport$(organizationId: string): Observable { + generateRawDataReport$(organizationId: string): Observable { const allCiphers$ = from(this.cipherService.getAllFromApiForOrganization(organizationId)); const memberCiphers$ = from( this.memberCipherDetailsApiService.getMemberCipherDetails(organizationId), @@ -55,7 +58,14 @@ export class RiskInsightsReportService { ); return [allCiphers, details] as const; }), - concatMap(([ciphers, flattenedDetails]) => this.getCipherDetails(ciphers, flattenedDetails)), + concatMap(async ([ciphers, flattenedDetails]) => { + const cipherDetails = await this.getCipherDetails(ciphers, flattenedDetails); + const memberDetails = this.getUniqueMembers(flattenedDetails); + return { + ciphers: cipherDetails, + members: memberDetails, + }; + }), first(), ); @@ -68,10 +78,18 @@ export class RiskInsightsReportService { * @param organizationId Id of the organization * @returns Cipher health report data flattened to the uris */ - generateRawDataUriReport$(organizationId: string): Observable { + generateRawDataUriReport$( + organizationId: string, + ): Observable { const cipherHealthDetails$ = this.generateRawDataReport$(organizationId); const results$ = cipherHealthDetails$.pipe( - map((healthDetails) => this.getCipherUriDetails(healthDetails)), + map((healthDetails) => { + const cipherHealthUriDetail = this.getCipherUriDetails(healthDetails.ciphers); + return { + ciphers: cipherHealthUriDetail, + members: healthDetails.members, + }; + }), first(), ); @@ -84,10 +102,18 @@ export class RiskInsightsReportService { * @param organizationId Id of the organization * @returns The all applications health report data */ - generateApplicationsReport$(organizationId: string): Observable { + generateApplicationsReport$( + organizationId: string, + ): Observable { const cipherHealthUriReport$ = this.generateRawDataUriReport$(organizationId); const results$ = cipherHealthUriReport$.pipe( - map((uriDetails) => this.getApplicationHealthReport(uriDetails)), + map((uriDetails) => { + const appHealthReportDetail = this.getApplicationHealthReport(uriDetails.ciphers); + return { + healthReport: appHealthReportDetail, + members: uriDetails.members, + }; + }), first(), ); @@ -150,9 +176,11 @@ export class RiskInsightsReportService { */ generateApplicationsSummary( reports: ApplicationHealthReportDetail[], + memberDetails: MemberDetailsFlat[], ): ApplicationHealthReportSummary { - const totalMembers = reports.flatMap((x) => x.memberDetails); - const uniqueMembers = this.getUniqueMembers(totalMembers); + // const totalMembers = reports.flatMap((x) => x.memberDetails); + // const uniqueMembers = this.getUniqueMembers(totalMembers); + const uniqueMembers = this.getUniqueMembers(memberDetails); const atRiskMembers = reports.flatMap((x) => x.atRiskMemberDetails); const uniqueAtRiskMembers = this.getUniqueMembers(atRiskMembers); diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications.component.ts index ee08ec6661e..5179709108a 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications.component.ts @@ -86,12 +86,13 @@ export class AllApplicationsComponent implements OnInit { combineLatest([ this.dataService.applications$, + this.dataService.memberDetails$, this.criticalAppsService.getAppsListForOrg(organizationId), organization$, ]) .pipe( takeUntilDestroyed(this.destroyRef), - map(([applications, criticalApps, organization]) => { + map(([applications, memberDetails, criticalApps, organization]) => { if (applications && applications.length === 0 && criticalApps && criticalApps) { const criticalUrls = criticalApps.map((ca) => ca.uri); const data = applications?.map((app) => ({ @@ -101,9 +102,9 @@ export class AllApplicationsComponent implements OnInit { return { data, organization }; } - return { data: applications, organization }; + return { data: applications, organization, memberDetails }; }), - switchMap(async ({ data, organization }) => { + switchMap(async ({ data, organization, memberDetails }) => { if (data && organization) { const dataWithCiphers = await this.reportService.identifyCiphers( data, @@ -113,16 +114,20 @@ export class AllApplicationsComponent implements OnInit { return { data: dataWithCiphers, organization, + memberDetails, }; } - return { data: [], organization }; + return { data: [], organization, memberDetails: [] }; }), ) - .subscribe(({ data, organization }) => { + .subscribe(({ data, organization, memberDetails }) => { if (data) { this.dataSource.data = data; - this.applicationSummary = this.reportService.generateApplicationsSummary(data); + this.applicationSummary = this.reportService.generateApplicationsSummary( + data, + memberDetails, + ); } if (organization) { this.organization = organization; diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications.component.ts index fcca568da6e..75d7cc6c94a 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications.component.ts @@ -75,33 +75,38 @@ export class CriticalApplicationsComponent implements OnInit { combineLatest([ this.dataService.applications$, + this.dataService.memberDetails$, this.criticalAppsService.getAppsListForOrg(this.organizationId), ]) .pipe( takeUntilDestroyed(this.destroyRef), - map(([applications, criticalApps]) => { + map(([applications, memberDetails, criticalApps]) => { const criticalUrls = criticalApps.map((ca) => ca.uri); const data = applications?.map((app) => ({ ...app, isMarkedAsCritical: criticalUrls.includes(app.applicationName), })) as ApplicationHealthReportDetailWithCriticalFlag[]; - return data?.filter((app) => app.isMarkedAsCritical); + const appsMarkedAsCritical = data?.filter((app) => app.isMarkedAsCritical); + return { data: appsMarkedAsCritical, memberDetails }; }), - switchMap(async (data) => { + switchMap(async ({ data, memberDetails }) => { if (data) { const dataWithCiphers = await this.reportService.identifyCiphers( data, this.organizationId, ); - return dataWithCiphers; + return { applications: dataWithCiphers, memberDetails }; } return null; }), ) - .subscribe((applications) => { + .subscribe(({ applications, memberDetails }) => { if (applications) { this.dataSource.data = applications; - this.applicationSummary = this.reportService.generateApplicationsSummary(applications); + this.applicationSummary = this.reportService.generateApplicationsSummary( + applications, + memberDetails, + ); this.enableRequestPasswordChange = this.applicationSummary.totalAtRiskMemberCount > 0; } });