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;
|
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
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 }) => {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>();
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user