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 acb4a116b8f..62eb0122dca 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 @@ -32,13 +32,18 @@ export type ApplicationHealthReportDetail = { atRiskMemberCount: number; memberDetails: MemberDetailsFlat[]; atRiskMemberDetails: MemberDetailsFlat[]; - cipher: CipherView; + cipherIds: string[]; }; export type ApplicationHealthReportDetailWithCriticalFlag = ApplicationHealthReportDetail & { isMarkedAsCritical: boolean; }; +export type ApplicationHealthReportDetailWithCriticalFlagAndCipher = + ApplicationHealthReportDetailWithCriticalFlag & { + ciphers: CipherView[]; + }; + /** * Breaks the cipher health info out by uri and passes * along the password health and member info diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/critical-apps.service.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/critical-apps.service.ts index b879ef94705..6ad1cb71051 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/critical-apps.service.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/critical-apps.service.ts @@ -141,6 +141,11 @@ export class CriticalAppsService { const uri = await this.encryptService.decryptString(encrypted, key); return { id: r.id, organizationId: r.organizationId, uri: uri }; }); + + if (results.length === 0) { + return of([]); // emits an empty array immediately + } + return forkJoin(results); }), first(), 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 afd246e1836..182e8aa6882 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 @@ -20,6 +20,7 @@ import { MemberDetailsFlat, WeakPasswordDetail, WeakPasswordScore, + ApplicationHealthReportDetailWithCriticalFlagAndCipher, } from "../models/password-health"; import { MemberCipherDetailsApiService } from "./member-cipher-details-api.service"; @@ -164,6 +165,22 @@ export class RiskInsightsReportService { }; } + async identifyCiphers( + data: ApplicationHealthReportDetail[], + organizationId: string, + ): Promise { + const cipherViews = await this.cipherService.getAllFromApiForOrganization(organizationId); + + const dataWithCiphers = data.map( + (app, index) => + ({ + ...app, + ciphers: cipherViews.filter((c) => app.cipherIds.some((a) => a === c.id)), + }) as ApplicationHealthReportDetailWithCriticalFlagAndCipher, + ); + return dataWithCiphers; + } + /** * Associates the members with the ciphers they have access to. Calculates the password health. * Finds the trimmed uris. @@ -358,7 +375,9 @@ export class RiskInsightsReportService { atRiskPasswordCount: existingUriDetail ? existingUriDetail.atRiskPasswordCount : 0, atRiskCipherIds: existingUriDetail ? existingUriDetail.atRiskCipherIds : [], atRiskMemberCount: existingUriDetail ? existingUriDetail.atRiskMemberDetails.length : 0, - cipher: newUriDetail.cipher, + cipherIds: existingUriDetail + ? existingUriDetail.cipherIds.concat(newUriDetail.cipherId) + : [newUriDetail.cipherId], } as ApplicationHealthReportDetail; if (isAtRisk) { 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 b5f5773727f..ee08ec6661e 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 @@ -2,7 +2,7 @@ import { Component, DestroyRef, inject, OnInit } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormControl } from "@angular/forms"; import { ActivatedRoute } from "@angular/router"; -import { combineLatest, debounceTime, firstValueFrom, map, Observable, of, skipWhile } from "rxjs"; +import { combineLatest, debounceTime, firstValueFrom, map, Observable, of, switchMap } from "rxjs"; import { CriticalAppsService, @@ -12,6 +12,7 @@ import { import { ApplicationHealthReportDetail, ApplicationHealthReportDetailWithCriticalFlag, + ApplicationHealthReportDetailWithCriticalFlagAndCipher, ApplicationHealthReportSummary, } from "@bitwarden/bit-common/dirt/reports/risk-insights/models/password-health"; import { @@ -56,7 +57,8 @@ import { ApplicationsLoadingComponent } from "./risk-insights-loading.component" ], }) export class AllApplicationsComponent implements OnInit { - protected dataSource = new TableDataSource(); + protected dataSource = + new TableDataSource(); protected selectedUrls: Set = new Set(); protected searchControl = new FormControl("", { nonNullable: true }); protected loading = true; @@ -74,7 +76,7 @@ export class AllApplicationsComponent implements OnInit { isLoading$: Observable = of(false); async ngOnInit() { - const organizationId = this.activatedRoute.snapshot.paramMap.get("organizationId") ?? ""; + const organizationId = this.activatedRoute.snapshot.paramMap.get("organizationId"); const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); if (organizationId) { @@ -89,14 +91,32 @@ export class AllApplicationsComponent implements OnInit { ]) .pipe( takeUntilDestroyed(this.destroyRef), - skipWhile(([_, __, organization]) => !organization), map(([applications, criticalApps, organization]) => { - const criticalUrls = criticalApps.map((ca) => ca.uri); - const data = applications?.map((app) => ({ - ...app, - isMarkedAsCritical: criticalUrls.includes(app.applicationName), - })) as ApplicationHealthReportDetailWithCriticalFlag[]; - return { data, organization }; + if (applications && applications.length === 0 && criticalApps && criticalApps) { + const criticalUrls = criticalApps.map((ca) => ca.uri); + const data = applications?.map((app) => ({ + ...app, + isMarkedAsCritical: criticalUrls.includes(app.applicationName), + })) as ApplicationHealthReportDetailWithCriticalFlag[]; + return { data, organization }; + } + + return { data: applications, organization }; + }), + switchMap(async ({ data, organization }) => { + if (data && organization) { + const dataWithCiphers = await this.reportService.identifyCiphers( + data, + organization.id, + ); + + return { + data: dataWithCiphers, + organization, + }; + } + + return { data: [], organization }; }), ) .subscribe(({ data, organization }) => { diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/app-table-row-scrollable.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/app-table-row-scrollable.component.html index 383780b450c..97c5b187ef9 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/app-table-row-scrollable.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/app-table-row-scrollable.component.html @@ -32,7 +32,7 @@ - + ; + @Input() dataSource!: TableDataSource; @Input() showRowMenuForCriticalApps: boolean = false; @Input() showRowCheckBox: boolean = false; @Input() selectedUrls: Set = new Set(); 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 183869c55fa..765d979bbe6 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 @@ -4,7 +4,7 @@ import { Component, DestroyRef, inject, OnInit } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormControl } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; -import { combineLatest, debounceTime, map } from "rxjs"; +import { combineLatest, debounceTime, map, switchMap } from "rxjs"; import { CriticalAppsService, @@ -13,6 +13,7 @@ import { } from "@bitwarden/bit-common/dirt/reports/risk-insights"; import { ApplicationHealthReportDetailWithCriticalFlag, + ApplicationHealthReportDetailWithCriticalFlagAndCipher, ApplicationHealthReportSummary, } from "@bitwarden/bit-common/dirt/reports/risk-insights/models/password-health"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; @@ -53,7 +54,8 @@ import { RiskInsightsTabType } from "./risk-insights.component"; providers: [DefaultAdminTaskService], }) export class CriticalApplicationsComponent implements OnInit { - protected dataSource = new TableDataSource(); + protected dataSource = + new TableDataSource(); protected selectedIds: Set = new Set(); protected searchControl = new FormControl("", { nonNullable: true }); private destroyRef = inject(DestroyRef); @@ -68,7 +70,9 @@ export class CriticalApplicationsComponent implements OnInit { this.isNotificationsFeatureEnabled = await this.configService.getFeatureFlag( FeatureFlag.EnableRiskInsightsNotifications, ); + this.organizationId = this.activatedRoute.snapshot.paramMap.get("organizationId") ?? ""; + combineLatest([ this.dataService.applications$, this.criticalAppsService.getAppsListForOrg(this.organizationId), @@ -83,6 +87,16 @@ export class CriticalApplicationsComponent implements OnInit { })) as ApplicationHealthReportDetailWithCriticalFlag[]; return data?.filter((app) => app.isMarkedAsCritical); }), + switchMap(async (data) => { + if (data) { + const dataWithCiphers = await this.reportService.identifyCiphers( + data, + this.organizationId, + ); + return dataWithCiphers; + } + return null; + }), ) .subscribe((applications) => { if (applications) {