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:
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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<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.
|
||||
* 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) {
|
||||
|
||||
@@ -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<ApplicationHealthReportDetailWithCriticalFlag>();
|
||||
protected dataSource =
|
||||
new TableDataSource<ApplicationHealthReportDetailWithCriticalFlagAndCipher>();
|
||||
protected selectedUrls: Set<string> = new Set<string>();
|
||||
protected searchControl = new FormControl("", { nonNullable: true });
|
||||
protected loading = true;
|
||||
@@ -74,7 +76,7 @@ export class AllApplicationsComponent implements OnInit {
|
||||
isLoading$: Observable<boolean> = 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]) => {
|
||||
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 }) => {
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<i class="bwi bwi-star-f" *ngIf="row.isMarkedAsCritical"></i>
|
||||
</td>
|
||||
<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
|
||||
class="tw-cursor-pointer"
|
||||
|
||||
@@ -2,7 +2,7 @@ import { CommonModule } from "@angular/common";
|
||||
import { Component, Input } from "@angular/core";
|
||||
|
||||
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 { SharedModule } from "@bitwarden/web-vault/app/shared";
|
||||
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",
|
||||
})
|
||||
export class AppTableRowScrollableComponent {
|
||||
@Input() dataSource!: TableDataSource<ApplicationHealthReportDetailWithCriticalFlag>;
|
||||
@Input() dataSource!: TableDataSource<ApplicationHealthReportDetailWithCriticalFlagAndCipher>;
|
||||
@Input() showRowMenuForCriticalApps: boolean = false;
|
||||
@Input() showRowCheckBox: boolean = false;
|
||||
@Input() selectedUrls: Set<string> = new Set<string>();
|
||||
|
||||
@@ -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<ApplicationHealthReportDetailWithCriticalFlag>();
|
||||
protected dataSource =
|
||||
new TableDataSource<ApplicationHealthReportDetailWithCriticalFlagAndCipher>();
|
||||
protected selectedIds: Set<number> = new Set<number>();
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user