1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-18 17:23:37 +00:00

PM-15070 Star critical apps (#12109)

Ability to star a record when flagged as critical. This is still behind a feature flag
This commit is contained in:
Vijay Oommen
2025-01-16 08:47:36 -06:00
committed by GitHub
parent 51717cab07
commit ad8694b641
12 changed files with 523 additions and 48 deletions

View File

@@ -30,6 +30,10 @@ export type ApplicationHealthReportDetail = {
atRiskMemberDetails: MemberDetailsFlat[]; atRiskMemberDetails: MemberDetailsFlat[];
}; };
export type ApplicationHealthReportDetailWithCriticalFlag = ApplicationHealthReportDetail & {
isMarkedAsCritical: boolean;
};
/** /**
* 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

@@ -0,0 +1,79 @@
import { mock } from "jest-mock-extended";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationId } from "@bitwarden/common/types/guid";
import { CriticalAppsApiService } from "./critical-apps-api.service";
import {
PasswordHealthReportApplicationId,
PasswordHealthReportApplicationsRequest,
PasswordHealthReportApplicationsResponse,
} from "./critical-apps.service";
describe("CriticalAppsApiService", () => {
let service: CriticalAppsApiService;
const apiService = mock<ApiService>();
beforeEach(() => {
service = new CriticalAppsApiService(apiService);
});
it("should be created", () => {
expect(service).toBeTruthy();
});
it("should call apiService.send with correct parameters for SaveCriticalApps", (done) => {
const requests: PasswordHealthReportApplicationsRequest[] = [
{ organizationId: "org1" as OrganizationId, url: "test one" },
{ organizationId: "org1" as OrganizationId, url: "test two" },
];
const response: PasswordHealthReportApplicationsResponse[] = [
{
id: "1" as PasswordHealthReportApplicationId,
organizationId: "org1" as OrganizationId,
uri: "test one",
},
{
id: "2" as PasswordHealthReportApplicationId,
organizationId: "org1" as OrganizationId,
uri: "test two",
},
];
apiService.send.mockReturnValue(Promise.resolve(response));
service.saveCriticalApps(requests).subscribe((result) => {
expect(result).toEqual(response);
expect(apiService.send).toHaveBeenCalledWith(
"POST",
"/reports/password-health-report-applications/",
requests,
true,
true,
);
done();
});
});
it("should call apiService.send with correct parameters for GetCriticalApps", (done) => {
const orgId: OrganizationId = "org1" as OrganizationId;
const response: PasswordHealthReportApplicationsResponse[] = [
{ id: "1" as PasswordHealthReportApplicationId, organizationId: orgId, uri: "test one" },
{ id: "2" as PasswordHealthReportApplicationId, organizationId: orgId, uri: "test two" },
];
apiService.send.mockReturnValue(Promise.resolve(response));
service.getCriticalApps(orgId).subscribe((result) => {
expect(result).toEqual(response);
expect(apiService.send).toHaveBeenCalledWith(
"GET",
`/reports/password-health-report-applications/${orgId.toString()}`,
null,
true,
true,
);
done();
});
});
});

View File

@@ -0,0 +1,39 @@
import { from, Observable } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationId } from "@bitwarden/common/types/guid";
import {
PasswordHealthReportApplicationsRequest,
PasswordHealthReportApplicationsResponse,
} from "./critical-apps.service";
export class CriticalAppsApiService {
constructor(private apiService: ApiService) {}
saveCriticalApps(
requests: PasswordHealthReportApplicationsRequest[],
): Observable<PasswordHealthReportApplicationsResponse[]> {
const dbResponse = this.apiService.send(
"POST",
"/reports/password-health-report-applications/",
requests,
true,
true,
);
return from(dbResponse as Promise<PasswordHealthReportApplicationsResponse[]>);
}
getCriticalApps(orgId: OrganizationId): Observable<PasswordHealthReportApplicationsResponse[]> {
const dbResponse = this.apiService.send(
"GET",
`/reports/password-health-report-applications/${orgId.toString()}`,
null,
true,
true,
);
return from(dbResponse as Promise<PasswordHealthReportApplicationsResponse[]>);
}
}

View File

@@ -0,0 +1,142 @@
import { randomUUID } from "crypto";
import { fakeAsync, flush } from "@angular/core/testing";
import { mock } from "jest-mock-extended";
import { of } from "rxjs";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { CsprngArray } from "@bitwarden/common/types/csprng";
import { OrganizationId } from "@bitwarden/common/types/guid";
import { OrgKey } from "@bitwarden/common/types/key";
import { KeyService } from "@bitwarden/key-management";
import { CriticalAppsApiService } from "./critical-apps-api.service";
import {
CriticalAppsService,
PasswordHealthReportApplicationId,
PasswordHealthReportApplicationsRequest,
PasswordHealthReportApplicationsResponse,
} from "./critical-apps.service";
describe("CriticalAppsService", () => {
let service: CriticalAppsService;
const keyService = mock<KeyService>();
const encryptService = mock<EncryptService>();
const criticalAppsApiService = mock<CriticalAppsApiService>({
saveCriticalApps: jest.fn(),
getCriticalApps: jest.fn(),
});
beforeEach(() => {
service = new CriticalAppsService(keyService, encryptService, criticalAppsApiService);
// reset mocks
jest.resetAllMocks();
});
it("should be created", () => {
expect(service).toBeTruthy();
});
it("should set critical apps", async () => {
// arrange
const criticalApps = ["https://example.com", "https://example.org"];
const request = [
{ organizationId: "org1", url: "encryptedUrlName" },
{ organizationId: "org1", url: "encryptedUrlName" },
] as PasswordHealthReportApplicationsRequest[];
const response = [
{ id: "id1", organizationId: "org1", uri: "https://example.com" },
{ id: "id2", organizationId: "org1", uri: "https://example.org" },
] as PasswordHealthReportApplicationsResponse[];
encryptService.encrypt.mockResolvedValue(new EncString("encryptedUrlName"));
criticalAppsApiService.saveCriticalApps.mockReturnValue(of(response));
// act
await service.setCriticalApps("org1", criticalApps);
// expectations
expect(keyService.getOrgKey).toHaveBeenCalledWith("org1");
expect(encryptService.encrypt).toHaveBeenCalledTimes(2);
expect(criticalAppsApiService.saveCriticalApps).toHaveBeenCalledWith(request);
});
it("should exclude records that already exist", async () => {
// arrange
// one record already exists
service.setAppsInListForOrg([
{
id: randomUUID() as PasswordHealthReportApplicationId,
organizationId: "org1" as OrganizationId,
uri: "https://example.com",
},
]);
// two records are selected - one already in the database
const selectedUrls = ["https://example.com", "https://example.org"];
// expect only one record to be sent to the server
const request = [
{ organizationId: "org1", url: "encryptedUrlName" },
] as PasswordHealthReportApplicationsRequest[];
// mocked response
const response = [
{ id: "id1", organizationId: "org1", uri: "test" },
] as PasswordHealthReportApplicationsResponse[];
encryptService.encrypt.mockResolvedValue(new EncString("encryptedUrlName"));
criticalAppsApiService.saveCriticalApps.mockReturnValue(of(response));
// act
await service.setCriticalApps("org1", selectedUrls);
// expectations
expect(keyService.getOrgKey).toHaveBeenCalledWith("org1");
expect(encryptService.encrypt).toHaveBeenCalledTimes(1);
expect(criticalAppsApiService.saveCriticalApps).toHaveBeenCalledWith(request);
});
it("should get critical apps", fakeAsync(() => {
const orgId = "org1" as OrganizationId;
const response = [
{ id: "id1", organizationId: "org1", uri: "https://example.com" },
{ id: "id2", organizationId: "org1", uri: "https://example.org" },
] as PasswordHealthReportApplicationsResponse[];
encryptService.decryptToUtf8.mockResolvedValue("https://example.com");
criticalAppsApiService.getCriticalApps.mockReturnValue(of(response));
const mockRandomBytes = new Uint8Array(64) as CsprngArray;
const mockOrgKey = new SymmetricCryptoKey(mockRandomBytes) as OrgKey;
keyService.getOrgKey.mockResolvedValue(mockOrgKey);
service.setOrganizationId(orgId as OrganizationId);
flush();
expect(keyService.getOrgKey).toHaveBeenCalledWith(orgId.toString());
expect(encryptService.decryptToUtf8).toHaveBeenCalledTimes(2);
expect(criticalAppsApiService.getCriticalApps).toHaveBeenCalledWith(orgId);
}));
it("should get by org id", () => {
const orgId = "org1" as OrganizationId;
const response = [
{ id: "id1", organizationId: "org1", uri: "https://example.com" },
{ id: "id2", organizationId: "org1", uri: "https://example.org" },
{ id: "id3", organizationId: "org2", uri: "https://example.org" },
{ id: "id4", organizationId: "org2", uri: "https://example.org" },
] as PasswordHealthReportApplicationsResponse[];
service.setAppsInListForOrg(response);
service.getAppsListForOrg(orgId as OrganizationId).subscribe((res) => {
expect(res).toHaveLength(2);
});
});
});

View File

@@ -0,0 +1,159 @@
import {
BehaviorSubject,
first,
firstValueFrom,
forkJoin,
from,
map,
Observable,
of,
Subject,
switchMap,
takeUntil,
zip,
} from "rxjs";
import { Opaque } from "type-fest";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { OrganizationId } from "@bitwarden/common/types/guid";
import { OrgKey } from "@bitwarden/common/types/key";
import { KeyService } from "@bitwarden/key-management";
import { CriticalAppsApiService } from "./critical-apps-api.service";
/* Retrieves and decrypts critical apps for a given organization
* Encrypts and saves data for a given organization
*/
export class CriticalAppsService {
private orgId = new BehaviorSubject<OrganizationId | null>(null);
private criticalAppsList = new BehaviorSubject<PasswordHealthReportApplicationsResponse[]>([]);
private teardown = new Subject<void>();
private fetchOrg$ = this.orgId
.pipe(
switchMap((orgId) => this.retrieveCriticalApps(orgId)),
takeUntil(this.teardown),
)
.subscribe((apps) => this.criticalAppsList.next(apps));
constructor(
private keyService: KeyService,
private encryptService: EncryptService,
private criticalAppsApiService: CriticalAppsApiService,
) {}
// Get a list of critical apps for a given organization
getAppsListForOrg(orgId: string): Observable<PasswordHealthReportApplicationsResponse[]> {
return this.criticalAppsList
.asObservable()
.pipe(map((apps) => apps.filter((app) => app.organizationId === orgId)));
}
// Reset the critical apps list
setAppsInListForOrg(apps: PasswordHealthReportApplicationsResponse[]) {
this.criticalAppsList.next(apps);
}
// Save the selected critical apps for a given organization
async setCriticalApps(orgId: string, selectedUrls: string[]) {
const key = await this.keyService.getOrgKey(orgId);
// only save records that are not already in the database
const newEntries = await this.filterNewEntries(orgId as OrganizationId, selectedUrls);
const criticalAppsRequests = await this.encryptNewEntries(
orgId as OrganizationId,
key,
newEntries,
);
const dbResponse = await firstValueFrom(
this.criticalAppsApiService.saveCriticalApps(criticalAppsRequests),
);
// add the new entries to the criticalAppsList
const updatedList = [...this.criticalAppsList.value];
for (const responseItem of dbResponse) {
const decryptedUrl = await this.encryptService.decryptToUtf8(
new EncString(responseItem.uri),
key,
);
if (!updatedList.some((f) => f.uri === decryptedUrl)) {
updatedList.push({
id: responseItem.id,
organizationId: responseItem.organizationId,
uri: decryptedUrl,
} as PasswordHealthReportApplicationsResponse);
}
}
this.criticalAppsList.next(updatedList);
}
// Get the critical apps for a given organization
setOrganizationId(orgId: OrganizationId) {
this.orgId.next(orgId);
}
private retrieveCriticalApps(
orgId: OrganizationId | null,
): Observable<PasswordHealthReportApplicationsResponse[]> {
if (orgId === null) {
return of([]);
}
const result$ = zip(
this.criticalAppsApiService.getCriticalApps(orgId),
from(this.keyService.getOrgKey(orgId)),
).pipe(
switchMap(([response, key]) => {
const results = response.map(async (r: PasswordHealthReportApplicationsResponse) => {
const encrypted = new EncString(r.uri);
const uri = await this.encryptService.decryptToUtf8(encrypted, key);
return { id: r.id, organizationId: r.organizationId, uri: uri };
});
return forkJoin(results);
}),
first(),
);
return result$ as Observable<PasswordHealthReportApplicationsResponse[]>;
}
private async filterNewEntries(orgId: OrganizationId, selectedUrls: string[]): Promise<string[]> {
return await firstValueFrom(this.criticalAppsList).then((criticalApps) => {
const criticalAppsUri = criticalApps
.filter((f) => f.organizationId === orgId)
.map((f) => f.uri);
return selectedUrls.filter((url) => !criticalAppsUri.includes(url));
});
}
private async encryptNewEntries(
orgId: OrganizationId,
key: OrgKey,
newEntries: string[],
): Promise<PasswordHealthReportApplicationsRequest[]> {
const criticalAppsPromises = newEntries.map(async (url) => {
const encryptedUrlName = await this.encryptService.encrypt(url, key);
return {
organizationId: orgId,
url: encryptedUrlName?.encryptedString?.toString() ?? "",
} as PasswordHealthReportApplicationsRequest;
});
return await Promise.all(criticalAppsPromises);
}
}
export interface PasswordHealthReportApplicationsRequest {
organizationId: OrganizationId;
url: string;
}
export interface PasswordHealthReportApplicationsResponse {
id: PasswordHealthReportApplicationId;
organizationId: OrganizationId;
uri: string;
}
export type PasswordHealthReportApplicationId = Opaque<string, "PasswordHealthReportApplicationId">;

View File

@@ -1,4 +1,6 @@
export * from "./member-cipher-details-api.service"; export * from "./member-cipher-details-api.service";
export * from "./password-health.service"; export * from "./password-health.service";
export * from "./critical-apps.service";
export * from "./critical-apps-api.service";
export * from "./risk-insights-report.service"; export * from "./risk-insights-report.service";
export * from "./risk-insights-data.service"; export * from "./risk-insights-data.service";

View File

@@ -1,14 +1,19 @@
import { NgModule } from "@angular/core"; import { NgModule } from "@angular/core";
import { safeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
import { CriticalAppsService } from "@bitwarden/bit-common/tools/reports/risk-insights";
import { import {
CriticalAppsApiService,
MemberCipherDetailsApiService, MemberCipherDetailsApiService,
RiskInsightsDataService, RiskInsightsDataService,
RiskInsightsReportService, RiskInsightsReportService,
} from "@bitwarden/bit-common/tools/reports/risk-insights/services"; } from "@bitwarden/bit-common/tools/reports/risk-insights/services";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength/password-strength.service.abstraction"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength/password-strength.service.abstraction";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { KeyService } from "@bitwarden/key-management";
import { AccessIntelligenceRoutingModule } from "./access-intelligence-routing.module"; import { AccessIntelligenceRoutingModule } from "./access-intelligence-routing.module";
import { RiskInsightsComponent } from "./risk-insights.component"; import { RiskInsightsComponent } from "./risk-insights.component";
@@ -33,6 +38,16 @@ import { RiskInsightsComponent } from "./risk-insights.component";
provide: RiskInsightsDataService, provide: RiskInsightsDataService,
deps: [RiskInsightsReportService], deps: [RiskInsightsReportService],
}, },
safeProvider({
provide: CriticalAppsService,
useClass: CriticalAppsService,
deps: [KeyService, EncryptService, CriticalAppsApiService],
}),
safeProvider({
provide: CriticalAppsApiService,
useClass: CriticalAppsApiService,
deps: [ApiService],
}),
], ],
}) })
export class AccessIntelligenceModule {} export class AccessIntelligenceModule {}

View File

@@ -55,7 +55,7 @@
buttonType="secondary" buttonType="secondary"
bitButton bitButton
*ngIf="isCriticalAppsFeatureEnabled" *ngIf="isCriticalAppsFeatureEnabled"
[disabled]="!selectedIds.size" [disabled]="!selectedUrls.size"
[loading]="markingAsCritical" [loading]="markingAsCritical"
(click)="markAppsAsCritical()" (click)="markAppsAsCritical()"
> >
@@ -80,9 +80,11 @@
<input <input
bitCheckbox bitCheckbox
type="checkbox" type="checkbox"
[checked]="selectedIds.has(r.id)" *ngIf="!r.isMarkedAsCritical"
(change)="onCheckboxChange(r.id, $event)" [checked]="selectedUrls.has(r.applicationName)"
(change)="onCheckboxChange(r.applicationName, $event)"
/> />
<i class="bwi bwi-star-f" *ngIf="r.isMarkedAsCritical"></i>
</td> </td>
<td class="tw-cursor-pointer" (click)="showAppAtRiskMembers(r.applicationName)" bitCell> <td class="tw-cursor-pointer" (click)="showAppAtRiskMembers(r.applicationName)" bitCell>
<span>{{ r.applicationName }}</span> <span>{{ r.applicationName }}</span>

View File

@@ -1,15 +1,17 @@
import { Component, DestroyRef, OnDestroy, OnInit, inject } from "@angular/core"; 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 { debounceTime, map, Observable, of, Subscription } from "rxjs"; import { combineLatest, debounceTime, map, Observable, of, skipWhile } from "rxjs";
import { import {
CriticalAppsService,
RiskInsightsDataService, RiskInsightsDataService,
RiskInsightsReportService, RiskInsightsReportService,
} from "@bitwarden/bit-common/tools/reports/risk-insights"; } from "@bitwarden/bit-common/tools/reports/risk-insights";
import { import {
ApplicationHealthReportDetail, ApplicationHealthReportDetail,
ApplicationHealthReportDetailWithCriticalFlag,
ApplicationHealthReportSummary, ApplicationHealthReportSummary,
} from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health"; } from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
@@ -50,16 +52,15 @@ import { ApplicationsLoadingComponent } from "./risk-insights-loading.component"
SharedModule, SharedModule,
], ],
}) })
export class AllApplicationsComponent implements OnInit, OnDestroy { export class AllApplicationsComponent implements OnInit {
protected dataSource = new TableDataSource<ApplicationHealthReportDetail>(); protected dataSource = new TableDataSource<ApplicationHealthReportDetailWithCriticalFlag>();
protected selectedIds: Set<number> = new Set<number>(); 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;
protected organization = {} as Organization; protected organization = {} as Organization;
noItemsIcon = Icons.Security; noItemsIcon = Icons.Security;
protected markingAsCritical = false; protected markingAsCritical = false;
protected applicationSummary = {} as ApplicationHealthReportSummary; protected applicationSummary = {} as ApplicationHealthReportSummary;
private subscription = new Subscription();
destroyRef = inject(DestroyRef); destroyRef = inject(DestroyRef);
isLoading$: Observable<boolean> = of(false); isLoading$: Observable<boolean> = of(false);
@@ -70,28 +71,33 @@ export class AllApplicationsComponent implements OnInit, OnDestroy {
FeatureFlag.CriticalApps, FeatureFlag.CriticalApps,
); );
const organizationId = this.activatedRoute.snapshot.paramMap.get("organizationId"); const organizationId = this.activatedRoute.snapshot.paramMap.get("organizationId") ?? "";
combineLatest([
this.dataService.applications$,
this.criticalAppsService.getAppsListForOrg(organizationId),
this.organizationService.get$(organizationId),
])
.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 };
}),
)
.subscribe(({ data, organization }) => {
this.dataSource.data = data ?? [];
this.applicationSummary = this.reportService.generateApplicationsSummary(data ?? []);
if (organization) {
this.organization = organization;
}
});
if (organizationId) { this.isLoading$ = this.dataService.isLoading$;
this.organization = await this.organizationService.get(organizationId);
this.subscription = this.dataService.applications$
.pipe(
map((applications) => {
if (applications) {
this.dataSource.data = applications;
this.applicationSummary =
this.reportService.generateApplicationsSummary(applications);
}
}),
takeUntilDestroyed(this.destroyRef),
)
.subscribe();
this.isLoading$ = this.dataService.isLoading$;
}
}
ngOnDestroy(): void {
this.subscription?.unsubscribe();
} }
constructor( constructor(
@@ -103,6 +109,7 @@ export class AllApplicationsComponent implements OnInit, OnDestroy {
protected dataService: RiskInsightsDataService, protected dataService: RiskInsightsDataService,
protected organizationService: OrganizationService, protected organizationService: OrganizationService,
protected reportService: RiskInsightsReportService, protected reportService: RiskInsightsReportService,
protected criticalAppsService: CriticalAppsService,
protected dialogService: DialogService, protected dialogService: DialogService,
) { ) {
this.searchControl.valueChanges this.searchControl.valueChanges
@@ -119,21 +126,28 @@ export class AllApplicationsComponent implements OnInit, OnDestroy {
}); });
}; };
isMarkedAsCriticalItem(applicationName: string) {
return this.selectedUrls.has(applicationName);
}
markAppsAsCritical = async () => { markAppsAsCritical = async () => {
// TODO: Send to API once implemented
this.markingAsCritical = true; this.markingAsCritical = true;
return new Promise((resolve) => {
setTimeout(() => { try {
this.selectedIds.clear(); await this.criticalAppsService.setCriticalApps(
this.toastService.showToast({ this.organization.id,
variant: "success", Array.from(this.selectedUrls),
title: "", );
message: this.i18nService.t("appsMarkedAsCritical"),
}); this.toastService.showToast({
resolve(true); variant: "success",
this.markingAsCritical = false; title: "",
}, 1000); message: this.i18nService.t("appsMarkedAsCritical"),
}); });
} finally {
this.selectedUrls.clear();
this.markingAsCritical = false;
}
}; };
trackByFunction(_: number, item: ApplicationHealthReportDetail) { trackByFunction(_: number, item: ApplicationHealthReportDetail) {
@@ -161,12 +175,14 @@ export class AllApplicationsComponent implements OnInit, OnDestroy {
}); });
}; };
onCheckboxChange(id: number, event: Event) { onCheckboxChange(applicationName: string, event: Event) {
const isChecked = (event.target as HTMLInputElement).checked; const isChecked = (event.target as HTMLInputElement).checked;
if (isChecked) { if (isChecked) {
this.selectedIds.add(id); this.selectedUrls.add(applicationName);
} else { } else {
this.selectedIds.delete(id); this.selectedUrls.delete(applicationName);
} }
} }
getSelectedUrls = () => Array.from(this.selectedUrls);
} }

View File

@@ -6,6 +6,7 @@ export const applicationTableMockData = [
totalPasswords: 10, totalPasswords: 10,
atRiskMembers: 2, atRiskMembers: 2,
totalMembers: 5, totalMembers: 5,
isMarkedAsCritical: false,
}, },
{ {
id: 2, id: 2,
@@ -14,6 +15,7 @@ export const applicationTableMockData = [
totalPasswords: 8, totalPasswords: 8,
atRiskMembers: 1, atRiskMembers: 1,
totalMembers: 3, totalMembers: 3,
isMarkedAsCritical: false,
}, },
{ {
id: 3, id: 3,
@@ -22,6 +24,7 @@ export const applicationTableMockData = [
totalPasswords: 6, totalPasswords: 6,
atRiskMembers: 0, atRiskMembers: 0,
totalMembers: 2, totalMembers: 2,
isMarkedAsCritical: false,
}, },
{ {
id: 4, id: 4,
@@ -30,6 +33,7 @@ export const applicationTableMockData = [
totalPasswords: 4, totalPasswords: 4,
atRiskMembers: 0, atRiskMembers: 0,
totalMembers: 1, totalMembers: 1,
isMarkedAsCritical: false,
}, },
{ {
id: 5, id: 5,
@@ -38,6 +42,7 @@ export const applicationTableMockData = [
totalPasswords: 2, totalPasswords: 2,
atRiskMembers: 0, atRiskMembers: 0,
totalMembers: 0, totalMembers: 0,
isMarkedAsCritical: false,
}, },
{ {
id: 6, id: 6,
@@ -46,5 +51,6 @@ export const applicationTableMockData = [
totalPasswords: 1, totalPasswords: 1,
atRiskMembers: 0, atRiskMembers: 0,
totalMembers: 0, totalMembers: 0,
isMarkedAsCritical: false,
}, },
]; ];

View File

@@ -40,7 +40,7 @@
<bit-tab *ngIf="isCriticalAppsFeatureEnabled"> <bit-tab *ngIf="isCriticalAppsFeatureEnabled">
<ng-template bitTabLabel> <ng-template bitTabLabel>
<i class="bwi bwi-star"></i> <i class="bwi bwi-star"></i>
{{ "criticalApplicationsWithCount" | i18n: criticalAppsCount }} {{ "criticalApplicationsWithCount" | i18n: (criticalApps$ | async)?.length ?? 0 }}
</ng-template> </ng-template>
<tools-critical-applications></tools-critical-applications> <tools-critical-applications></tools-critical-applications>
</bit-tab> </bit-tab>

View File

@@ -6,11 +6,17 @@ import { Observable, EMPTY } from "rxjs";
import { map, switchMap } from "rxjs/operators"; import { map, switchMap } from "rxjs/operators";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
import { RiskInsightsDataService } from "@bitwarden/bit-common/tools/reports/risk-insights"; import {
RiskInsightsDataService,
CriticalAppsService,
PasswordHealthReportApplicationsResponse,
} from "@bitwarden/bit-common/tools/reports/risk-insights";
import { ApplicationHealthReportDetail } from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health"; import { ApplicationHealthReportDetail } from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health";
// eslint-disable-next-line no-restricted-imports -- used for dependency injection
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { devFlagEnabled } from "@bitwarden/common/platform/misc/flags"; import { devFlagEnabled } from "@bitwarden/common/platform/misc/flags";
import { OrganizationId } from "@bitwarden/common/types/guid";
import { AsyncActionsModule, ButtonModule, TabsModule } from "@bitwarden/components"; import { AsyncActionsModule, ButtonModule, TabsModule } from "@bitwarden/components";
import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module"; import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module";
@@ -51,6 +57,7 @@ export class RiskInsightsComponent implements OnInit {
dataLastUpdated: Date = new Date(); dataLastUpdated: Date = new Date();
isCriticalAppsFeatureEnabled: boolean = false; isCriticalAppsFeatureEnabled: boolean = false;
criticalApps$: Observable<PasswordHealthReportApplicationsResponse[]> = new Observable();
showDebugTabs: boolean = false; showDebugTabs: boolean = false;
appsCount: number = 0; appsCount: number = 0;
@@ -69,10 +76,13 @@ export class RiskInsightsComponent implements OnInit {
private router: Router, private router: Router,
private configService: ConfigService, private configService: ConfigService,
private dataService: RiskInsightsDataService, private dataService: RiskInsightsDataService,
private criticalAppsService: CriticalAppsService,
) { ) {
this.route.queryParams.pipe(takeUntilDestroyed()).subscribe(({ tabIndex }) => { this.route.queryParams.pipe(takeUntilDestroyed()).subscribe(({ tabIndex }) => {
this.tabIndex = !isNaN(Number(tabIndex)) ? Number(tabIndex) : RiskInsightsTabType.AllApps; this.tabIndex = !isNaN(Number(tabIndex)) ? Number(tabIndex) : RiskInsightsTabType.AllApps;
}); });
const orgId = this.route.snapshot.paramMap.get("organizationId") ?? "";
this.criticalApps$ = this.criticalAppsService.getAppsListForOrg(orgId);
} }
async ngOnInit() { async ngOnInit() {
@@ -104,6 +114,7 @@ export class RiskInsightsComponent implements OnInit {
if (applications) { if (applications) {
this.appsCount = applications.length; this.appsCount = applications.length;
} }
this.criticalAppsService.setOrganizationId(this.organizationId as OrganizationId);
}, },
}); });
} }