diff --git a/apps/web/src/app/tools/risk-insights/all-applications.component.html b/apps/web/src/app/tools/risk-insights/all-applications.component.html
index 4ed31adea78..48eb7e2bac6 100644
--- a/apps/web/src/app/tools/risk-insights/all-applications.component.html
+++ b/apps/web/src/app/tools/risk-insights/all-applications.component.html
@@ -58,7 +58,7 @@
buttonType="secondary"
bitButton
*ngIf="isCritialAppsFeatureEnabled"
- [disabled]="!selectedIds.size"
+ [disabled]="!selectedUrls.size"
[loading]="markingAsCritical"
(click)="markAppsAsCritical()"
>
@@ -83,8 +83,8 @@
diff --git a/apps/web/src/app/tools/risk-insights/all-applications.component.ts b/apps/web/src/app/tools/risk-insights/all-applications.component.ts
index 5d76403f46b..cebf479e834 100644
--- a/apps/web/src/app/tools/risk-insights/all-applications.component.ts
+++ b/apps/web/src/app/tools/risk-insights/all-applications.component.ts
@@ -2,14 +2,22 @@ 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 { debounceTime, firstValueFrom, map } from "rxjs";
+import { debounceTime, firstValueFrom, map, switchMap } from "rxjs";
+// eslint-disable-next-line no-restricted-imports
+import {
+ CriticalAppsService,
+ PasswordHealthReportApplicationsRequest,
+ PasswordHealthReportApplicationsResponse,
+} from "@bitwarden/bit-common/tools/reports/risk-insights";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
+import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
+import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@@ -20,6 +28,7 @@ import {
TableDataSource,
ToastService,
} from "@bitwarden/components";
+import { KeyService } from "@bitwarden/key-management";
import { CardComponent } from "@bitwarden/tools-card";
import { HeaderModule } from "../../layouts/header/header.module";
@@ -36,7 +45,7 @@ import { applicationTableMockData } from "./application-table.mock";
})
export class AllApplicationsComponent implements OnInit {
protected dataSource = new TableDataSource();
- protected selectedIds: Set = new Set();
+ protected selectedUrls: Set = new Set();
protected searchControl = new FormControl("", { nonNullable: true });
private destroyRef = inject(DestroyRef);
protected loading = false;
@@ -44,6 +53,7 @@ export class AllApplicationsComponent implements OnInit {
noItemsIcon = Icons.Security;
protected markingAsCritical = false;
isCritialAppsFeatureEnabled = false;
+ private flaggedCriticalApps: PasswordHealthReportApplicationsResponse[] = [];
// MOCK DATA
protected mockData = applicationTableMockData;
@@ -59,8 +69,23 @@ export class AllApplicationsComponent implements OnInit {
map(async (params) => {
const organizationId = params.get("organizationId");
this.organization = await firstValueFrom(this.organizationService.get$(organizationId));
+ return params;
// TODO: use organizationId to fetch data
}),
+ switchMap(async (params) => {
+ const organizationId = (await params).get("organizationId");
+ const result = await this.criticalAppsService.getCriticalApps(organizationId);
+ const key = await this.keyService.getOrgKey(this.organization.id);
+ const flaggedCriticalAppsPromise = result.map(async (r) => {
+ const decryptedUrl = await this.encryptService.decryptToUtf8(new EncString(r.uri), key);
+ return {
+ id: r.id,
+ organizationId: r.organizationId,
+ uri: decryptedUrl,
+ } as PasswordHealthReportApplicationsResponse;
+ });
+ this.flaggedCriticalApps = await Promise.all(flaggedCriticalAppsPromise);
+ }),
)
.subscribe();
@@ -78,6 +103,9 @@ export class AllApplicationsComponent implements OnInit {
protected toastService: ToastService,
protected organizationService: OrganizationService,
protected configService: ConfigService,
+ protected criticalAppsService: CriticalAppsService,
+ private keyService: KeyService,
+ private encryptService: EncryptService,
) {
this.dataSource.data = applicationTableMockData;
this.searchControl.valueChanges
@@ -95,32 +123,61 @@ export class AllApplicationsComponent implements OnInit {
};
markAppsAsCritical = async () => {
- // TODO: Send to API once implemented
this.markingAsCritical = true;
- return new Promise((resolve) => {
- setTimeout(() => {
- this.selectedIds.clear();
+ const key = await this.keyService.getOrgKey(this.organization.id);
+
+ // only save records that are not already in the database
+ const newEntries = Array.from(this.selectedUrls).filter((url) => {
+ return !this.flaggedCriticalApps.some((r) => r.uri === url);
+ });
+
+ const criticalAppsPromises = newEntries.map(async (url) => {
+ const encryptedUrlName = await this.encryptService.encrypt(url, key);
+ return {
+ organizationId: this.organization.id,
+ url: encryptedUrlName.encryptedString.toString(),
+ } as PasswordHealthReportApplicationsRequest;
+ });
+
+ const criticalApps = await Promise.all(criticalAppsPromises);
+
+ await this.criticalAppsService
+ .setCriticalApps(criticalApps)
+ .then((result) => {
+ // append to flaggedCriticalApps
+ result
+ .filter((r) => !this.flaggedCriticalApps.some((f) => f.uri === r.uri))
+ .forEach(async (r) => {
+ const decryptedUrl = await this.encryptService.decryptToUtf8(new EncString(r.uri), key);
+ this.flaggedCriticalApps.push({
+ id: r.id,
+ organizationId: r.organizationId,
+ uri: decryptedUrl,
+ } as PasswordHealthReportApplicationsResponse);
+ });
+
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("appsMarkedAsCritical"),
});
- resolve(true);
+ })
+ .finally(() => {
+ this.selectedUrls.clear();
this.markingAsCritical = false;
- }, 1000);
- });
+ });
};
trackByFunction(_: number, item: CipherView) {
return item.id;
}
- onCheckboxChange(id: number, event: Event) {
+ onCheckboxChange(urlName: string, event: Event) {
const isChecked = (event.target as HTMLInputElement).checked;
if (isChecked) {
- this.selectedIds.add(id);
+ this.selectedUrls.add(urlName);
} else {
- this.selectedIds.delete(id);
+ this.selectedUrls.delete(urlName);
}
}
}
diff --git a/apps/web/src/app/tools/risk-insights/application-table.mock.ts b/apps/web/src/app/tools/risk-insights/application-table.mock.ts
index 4df363ab2c7..4dffa60b562 100644
--- a/apps/web/src/app/tools/risk-insights/application-table.mock.ts
+++ b/apps/web/src/app/tools/risk-insights/application-table.mock.ts
@@ -6,6 +6,7 @@ export const applicationTableMockData = [
totalPasswords: 10,
atRiskMembers: 2,
totalMembers: 5,
+ isMarkedAsCritical: false,
},
{
id: 2,
@@ -14,6 +15,7 @@ export const applicationTableMockData = [
totalPasswords: 8,
atRiskMembers: 1,
totalMembers: 3,
+ isMarkedAsCritical: false,
},
{
id: 3,
@@ -22,6 +24,7 @@ export const applicationTableMockData = [
totalPasswords: 6,
atRiskMembers: 0,
totalMembers: 2,
+ isMarkedAsCritical: false,
},
{
id: 4,
@@ -30,6 +33,7 @@ export const applicationTableMockData = [
totalPasswords: 4,
atRiskMembers: 0,
totalMembers: 1,
+ isMarkedAsCritical: false,
},
{
id: 5,
@@ -38,6 +42,7 @@ export const applicationTableMockData = [
totalPasswords: 2,
atRiskMembers: 0,
totalMembers: 0,
+ isMarkedAsCritical: false,
},
{
id: 6,
@@ -46,5 +51,6 @@ export const applicationTableMockData = [
totalPasswords: 1,
atRiskMembers: 0,
totalMembers: 0,
+ isMarkedAsCritical: false,
},
];
diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps.service.spec.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps.service.spec.ts
new file mode 100644
index 00000000000..2e9a9c33e7f
--- /dev/null
+++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps.service.spec.ts
@@ -0,0 +1,68 @@
+import { TestBed } from "@angular/core/testing";
+import { mock } from "jest-mock-extended";
+
+import { ApiService } from "@bitwarden/common/abstractions/api.service";
+
+import {
+ CriticalAppsService,
+ PasswordHealthReportApplicationsRequest,
+ PasswordHealthReportApplicationsResponse,
+} from "./critical-apps.service";
+
+describe("CriticaAppsService", () => {
+ let service: CriticalAppsService;
+ const apiService = mock();
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [CriticalAppsService, { provide: ApiService, useValue: apiService }],
+ });
+ service = TestBed.inject(CriticalAppsService);
+ });
+
+ it("should be created", () => {
+ expect(service).toBeTruthy();
+ });
+
+ it("should set critical apps", async () => {
+ const criticalApps = [
+ { organizationId: "org1", url: "https://example.com" },
+ { organizationId: "org2", url: "https://example.org" },
+ ] as PasswordHealthReportApplicationsRequest[];
+
+ const response = [
+ { id: "id1", organizationId: "org1", uri: "https://example.com" },
+ { id: "id2", organizationId: "org2", uri: "https://example.org" },
+ ] as PasswordHealthReportApplicationsResponse[];
+
+ apiService.send.mockResolvedValue(response);
+ await service.setCriticalApps(criticalApps);
+
+ expect(apiService.send).toHaveBeenCalledWith(
+ "POST",
+ "/reports/password-health-report-applications/",
+ criticalApps,
+ true,
+ true,
+ );
+ });
+
+ it("should get critical apps", async () => {
+ const orgId = "org1";
+ const response = [
+ { id: "id1", organizationId: "org1", uri: "https://example.com" },
+ { id: "id2", organizationId: "org2", uri: "https://example.org" },
+ ] as PasswordHealthReportApplicationsResponse[];
+
+ apiService.send.mockResolvedValue(response);
+ await service.getCriticalApps(orgId);
+
+ expect(apiService.send).toHaveBeenCalledWith(
+ "GET",
+ `/reports/password-health-report-applications/${orgId}`,
+ null,
+ true,
+ true,
+ );
+ });
+});
diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps.service.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps.service.ts
new file mode 100644
index 00000000000..f1fb8dd3a09
--- /dev/null
+++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps.service.ts
@@ -0,0 +1,60 @@
+import { Injectable } from "@angular/core";
+
+import { ApiService } from "@bitwarden/common/abstractions/api.service";
+import { Guid } from "@bitwarden/common/types/guid";
+
+@Injectable({
+ providedIn: "root",
+})
+export class CriticalAppsService {
+ constructor(private apiService: ApiService) {}
+
+ async setCriticalApps(
+ criticalApps: PasswordHealthReportApplicationsRequest[],
+ ): Promise {
+ const response = await this.apiService.send(
+ "POST",
+ "/reports/password-health-report-applications/",
+ criticalApps,
+ true,
+ true,
+ );
+
+ return response.map((r: { id: any; organizationId: any; uri: any }) => {
+ return {
+ id: r.id,
+ organizationId: r.organizationId,
+ uri: r.uri,
+ };
+ });
+ }
+
+ async getCriticalApps(orgId: string): Promise {
+ const response = await this.apiService.send(
+ "GET",
+ `/reports/password-health-report-applications/${orgId}`,
+ null,
+ true,
+ true,
+ );
+
+ return response.map((r: { id: any; organizationId: any; uri: any }) => {
+ return {
+ id: r.id,
+ organizationId: r.organizationId,
+ uri: r.uri,
+ };
+ });
+ }
+}
+
+export interface PasswordHealthReportApplicationsRequest {
+ organizationId: Guid;
+ url: string;
+}
+
+export interface PasswordHealthReportApplicationsResponse {
+ id: Guid;
+ organizationId: Guid;
+ uri: string;
+}
diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/index.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/index.ts
index c7bace84e5b..aa34a8b6f43 100644
--- a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/index.ts
+++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/index.ts
@@ -1,2 +1,3 @@
export * from "./member-cipher-details-api.service";
export * from "./password-health.service";
+export * from "./critical-apps.service";
|