1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-19 17:53:39 +00:00

[PM-21713] Include CipherId and find Ciphers in Risk Insights report (#14823)

This commit is contained in:
Vijay Oommen
2025-06-04 14:33:46 -05:00
committed by GitHub
parent a17cc0b265
commit 0032d1457f
7 changed files with 80 additions and 17 deletions

View File

@@ -32,13 +32,18 @@ export type ApplicationHealthReportDetail = {
atRiskMemberCount: number; atRiskMemberCount: number;
memberDetails: MemberDetailsFlat[]; memberDetails: MemberDetailsFlat[];
atRiskMemberDetails: MemberDetailsFlat[]; atRiskMemberDetails: MemberDetailsFlat[];
cipher: CipherView; cipherIds: string[];
}; };
export type ApplicationHealthReportDetailWithCriticalFlag = ApplicationHealthReportDetail & { export type ApplicationHealthReportDetailWithCriticalFlag = ApplicationHealthReportDetail & {
isMarkedAsCritical: boolean; isMarkedAsCritical: boolean;
}; };
export type ApplicationHealthReportDetailWithCriticalFlagAndCipher =
ApplicationHealthReportDetailWithCriticalFlag & {
ciphers: CipherView[];
};
/** /**
* Breaks the cipher health info out by uri and passes * Breaks the cipher health info out by uri and passes
* along the password health and member info * along the password health and member info

View File

@@ -141,6 +141,11 @@ export class CriticalAppsService {
const uri = await this.encryptService.decryptString(encrypted, key); const uri = await this.encryptService.decryptString(encrypted, key);
return { id: r.id, organizationId: r.organizationId, uri: uri }; return { id: r.id, organizationId: r.organizationId, uri: uri };
}); });
if (results.length === 0) {
return of([]); // emits an empty array immediately
}
return forkJoin(results); return forkJoin(results);
}), }),
first(), first(),

View File

@@ -20,6 +20,7 @@ import {
MemberDetailsFlat, MemberDetailsFlat,
WeakPasswordDetail, WeakPasswordDetail,
WeakPasswordScore, WeakPasswordScore,
ApplicationHealthReportDetailWithCriticalFlagAndCipher,
} from "../models/password-health"; } from "../models/password-health";
import { MemberCipherDetailsApiService } from "./member-cipher-details-api.service"; import { MemberCipherDetailsApiService } from "./member-cipher-details-api.service";
@@ -164,6 +165,22 @@ export class RiskInsightsReportService {
}; };
} }
async identifyCiphers(
data: ApplicationHealthReportDetail[],
organizationId: string,
): Promise<ApplicationHealthReportDetailWithCriticalFlagAndCipher[]> {
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. * Associates the members with the ciphers they have access to. Calculates the password health.
* Finds the trimmed uris. * Finds the trimmed uris.
@@ -358,7 +375,9 @@ export class RiskInsightsReportService {
atRiskPasswordCount: existingUriDetail ? existingUriDetail.atRiskPasswordCount : 0, atRiskPasswordCount: existingUriDetail ? existingUriDetail.atRiskPasswordCount : 0,
atRiskCipherIds: existingUriDetail ? existingUriDetail.atRiskCipherIds : [], atRiskCipherIds: existingUriDetail ? existingUriDetail.atRiskCipherIds : [],
atRiskMemberCount: existingUriDetail ? existingUriDetail.atRiskMemberDetails.length : 0, atRiskMemberCount: existingUriDetail ? existingUriDetail.atRiskMemberDetails.length : 0,
cipher: newUriDetail.cipher, cipherIds: existingUriDetail
? existingUriDetail.cipherIds.concat(newUriDetail.cipherId)
: [newUriDetail.cipherId],
} as ApplicationHealthReportDetail; } as ApplicationHealthReportDetail;
if (isAtRisk) { if (isAtRisk) {

View File

@@ -2,7 +2,7 @@ import { Component, DestroyRef, inject, OnInit } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { FormControl } from "@angular/forms"; import { FormControl } from "@angular/forms";
import { ActivatedRoute } from "@angular/router"; 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 { import {
CriticalAppsService, CriticalAppsService,
@@ -12,6 +12,7 @@ import {
import { import {
ApplicationHealthReportDetail, ApplicationHealthReportDetail,
ApplicationHealthReportDetailWithCriticalFlag, ApplicationHealthReportDetailWithCriticalFlag,
ApplicationHealthReportDetailWithCriticalFlagAndCipher,
ApplicationHealthReportSummary, ApplicationHealthReportSummary,
} from "@bitwarden/bit-common/dirt/reports/risk-insights/models/password-health"; } from "@bitwarden/bit-common/dirt/reports/risk-insights/models/password-health";
import { import {
@@ -56,7 +57,8 @@ import { ApplicationsLoadingComponent } from "./risk-insights-loading.component"
], ],
}) })
export class AllApplicationsComponent implements OnInit { export class AllApplicationsComponent implements OnInit {
protected dataSource = new TableDataSource<ApplicationHealthReportDetailWithCriticalFlag>(); protected dataSource =
new TableDataSource<ApplicationHealthReportDetailWithCriticalFlagAndCipher>();
protected selectedUrls: Set<string> = new Set<string>(); protected selectedUrls: Set<string> = new Set<string>();
protected searchControl = new FormControl("", { nonNullable: true }); protected searchControl = new FormControl("", { nonNullable: true });
protected loading = true; protected loading = true;
@@ -74,7 +76,7 @@ export class AllApplicationsComponent implements OnInit {
isLoading$: Observable<boolean> = of(false); isLoading$: Observable<boolean> = of(false);
async ngOnInit() { 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$)); const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
if (organizationId) { if (organizationId) {
@@ -89,14 +91,32 @@ export class AllApplicationsComponent implements OnInit {
]) ])
.pipe( .pipe(
takeUntilDestroyed(this.destroyRef), takeUntilDestroyed(this.destroyRef),
skipWhile(([_, __, organization]) => !organization),
map(([applications, criticalApps, organization]) => { map(([applications, criticalApps, organization]) => {
const criticalUrls = criticalApps.map((ca) => ca.uri); if (applications && applications.length === 0 && criticalApps && criticalApps) {
const data = applications?.map((app) => ({ const criticalUrls = criticalApps.map((ca) => ca.uri);
...app, const data = applications?.map((app) => ({
isMarkedAsCritical: criticalUrls.includes(app.applicationName), ...app,
})) as ApplicationHealthReportDetailWithCriticalFlag[]; isMarkedAsCritical: criticalUrls.includes(app.applicationName),
return { data, organization }; })) 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 }) => { .subscribe(({ data, organization }) => {

View File

@@ -32,7 +32,7 @@
<i class="bwi bwi-star-f" *ngIf="row.isMarkedAsCritical"></i> <i class="bwi bwi-star-f" *ngIf="row.isMarkedAsCritical"></i>
</td> </td>
<td bitCell> <td bitCell>
<app-vault-icon [cipher]="row.cipher"></app-vault-icon> <app-vault-icon *ngIf="row.ciphers.length > 0" [cipher]="row.ciphers[0]"></app-vault-icon>
</td> </td>
<td <td
class="tw-cursor-pointer" class="tw-cursor-pointer"

View File

@@ -2,7 +2,7 @@ import { CommonModule } from "@angular/common";
import { Component, Input } from "@angular/core"; import { Component, Input } from "@angular/core";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
import { ApplicationHealthReportDetailWithCriticalFlag } from "@bitwarden/bit-common/dirt/reports/risk-insights/models/password-health"; import { ApplicationHealthReportDetailWithCriticalFlagAndCipher } from "@bitwarden/bit-common/dirt/reports/risk-insights/models/password-health";
import { MenuModule, TableDataSource, TableModule } from "@bitwarden/components"; import { MenuModule, TableDataSource, TableModule } from "@bitwarden/components";
import { SharedModule } from "@bitwarden/web-vault/app/shared"; import { SharedModule } from "@bitwarden/web-vault/app/shared";
import { PipesModule } from "@bitwarden/web-vault/app/vault/individual-vault/pipes/pipes.module"; import { PipesModule } from "@bitwarden/web-vault/app/vault/individual-vault/pipes/pipes.module";
@@ -13,7 +13,7 @@ import { PipesModule } from "@bitwarden/web-vault/app/vault/individual-vault/pip
templateUrl: "./app-table-row-scrollable.component.html", templateUrl: "./app-table-row-scrollable.component.html",
}) })
export class AppTableRowScrollableComponent { export class AppTableRowScrollableComponent {
@Input() dataSource!: TableDataSource<ApplicationHealthReportDetailWithCriticalFlag>; @Input() dataSource!: TableDataSource<ApplicationHealthReportDetailWithCriticalFlagAndCipher>;
@Input() showRowMenuForCriticalApps: boolean = false; @Input() showRowMenuForCriticalApps: boolean = false;
@Input() showRowCheckBox: boolean = false; @Input() showRowCheckBox: boolean = false;
@Input() selectedUrls: Set<string> = new Set<string>(); @Input() selectedUrls: Set<string> = new Set<string>();

View File

@@ -4,7 +4,7 @@ import { Component, DestroyRef, inject, OnInit } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { FormControl } from "@angular/forms"; import { FormControl } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router"; import { ActivatedRoute, Router } from "@angular/router";
import { combineLatest, debounceTime, map } from "rxjs"; import { combineLatest, debounceTime, map, switchMap } from "rxjs";
import { import {
CriticalAppsService, CriticalAppsService,
@@ -13,6 +13,7 @@ import {
} from "@bitwarden/bit-common/dirt/reports/risk-insights"; } from "@bitwarden/bit-common/dirt/reports/risk-insights";
import { import {
ApplicationHealthReportDetailWithCriticalFlag, ApplicationHealthReportDetailWithCriticalFlag,
ApplicationHealthReportDetailWithCriticalFlagAndCipher,
ApplicationHealthReportSummary, ApplicationHealthReportSummary,
} from "@bitwarden/bit-common/dirt/reports/risk-insights/models/password-health"; } from "@bitwarden/bit-common/dirt/reports/risk-insights/models/password-health";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
@@ -53,7 +54,8 @@ import { RiskInsightsTabType } from "./risk-insights.component";
providers: [DefaultAdminTaskService], providers: [DefaultAdminTaskService],
}) })
export class CriticalApplicationsComponent implements OnInit { export class CriticalApplicationsComponent implements OnInit {
protected dataSource = new TableDataSource<ApplicationHealthReportDetailWithCriticalFlag>(); protected dataSource =
new TableDataSource<ApplicationHealthReportDetailWithCriticalFlagAndCipher>();
protected selectedIds: Set<number> = new Set<number>(); protected selectedIds: Set<number> = new Set<number>();
protected searchControl = new FormControl("", { nonNullable: true }); protected searchControl = new FormControl("", { nonNullable: true });
private destroyRef = inject(DestroyRef); private destroyRef = inject(DestroyRef);
@@ -68,7 +70,9 @@ export class CriticalApplicationsComponent implements OnInit {
this.isNotificationsFeatureEnabled = await this.configService.getFeatureFlag( this.isNotificationsFeatureEnabled = await this.configService.getFeatureFlag(
FeatureFlag.EnableRiskInsightsNotifications, FeatureFlag.EnableRiskInsightsNotifications,
); );
this.organizationId = this.activatedRoute.snapshot.paramMap.get("organizationId") ?? ""; this.organizationId = this.activatedRoute.snapshot.paramMap.get("organizationId") ?? "";
combineLatest([ combineLatest([
this.dataService.applications$, this.dataService.applications$,
this.criticalAppsService.getAppsListForOrg(this.organizationId), this.criticalAppsService.getAppsListForOrg(this.organizationId),
@@ -83,6 +87,16 @@ export class CriticalApplicationsComponent implements OnInit {
})) as ApplicationHealthReportDetailWithCriticalFlag[]; })) as ApplicationHealthReportDetailWithCriticalFlag[];
return data?.filter((app) => app.isMarkedAsCritical); 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) => { .subscribe((applications) => {
if (applications) { if (applications) {