mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +00:00
[PM-25611][PM-25612] Update components to use persistance code (#16655)
* Add password trigger logic to report service. Also updated api to use classes that properly handle encstring with placeholders for upcoming usage * Fix merged test case conflict * Fix type errors and test cases. Make create data functions for report and summary * Update Risk Insights Report Data Type * Update encryption usage and test cases. Moved mock data * Remove unused variable * Move all-application constructor * Update all applications and risk insights to look at fetched logic * Fix name of variable. Fetch last report run * Cleanup all and critical application tabs drawer dependencies * Rename components from tool to dirt. Hook up all applications to use reportResult summary * Critical application cleanup. Trigger refetch of report for enriching when critical applications change * Fix type errors * Rename loader from tools to dirt. Cleanup * Add activity tab updates using data service * Use safeProviders in access intelligence * Fix refresh button not appearing. Change "refresh" to "run report" * Remove multiple async calls for isRunningReport * Fix report button not showing * Add no report ran message * Fix password change on critical applications
This commit is contained in:
@@ -9,7 +9,7 @@ import {
|
||||
import {
|
||||
ApplicationHealthReportDetail,
|
||||
OrganizationReportSummary,
|
||||
RiskInsightsReportData,
|
||||
RiskInsightsData,
|
||||
} from "../models/report-models";
|
||||
import { MemberCipherDetailsResponse } from "../response/member-cipher-details.response";
|
||||
|
||||
@@ -154,10 +154,12 @@ export function getApplicationReportDetail(
|
||||
*
|
||||
* @returns An empty report
|
||||
*/
|
||||
export function createNewReportData(): RiskInsightsReportData {
|
||||
export function createNewReportData(): RiskInsightsData {
|
||||
return {
|
||||
data: [],
|
||||
summary: createNewSummaryData(),
|
||||
creationDate: new Date(),
|
||||
reportData: [],
|
||||
summaryData: createNewSummaryData(),
|
||||
applicationData: [],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -37,8 +37,10 @@ export interface PasswordHealthReportApplicationsRequest {
|
||||
export interface SaveRiskInsightsReportRequest {
|
||||
data: {
|
||||
organizationId: OrganizationId;
|
||||
date: string;
|
||||
creationDate: string;
|
||||
reportData: string;
|
||||
summaryData: string;
|
||||
applicationData: string;
|
||||
contentEncryptionKey: string;
|
||||
};
|
||||
}
|
||||
@@ -58,9 +60,10 @@ export function isSaveRiskInsightsReportResponse(obj: any): obj is SaveRiskInsig
|
||||
export class GetRiskInsightsReportResponse extends BaseResponse {
|
||||
id: string;
|
||||
organizationId: OrganizationId;
|
||||
// TODO Update to use creationDate from server
|
||||
date: string;
|
||||
creationDate: Date;
|
||||
reportData: EncString;
|
||||
summaryData: EncString;
|
||||
applicationData: EncString;
|
||||
contentEncryptionKey: EncString;
|
||||
|
||||
constructor(response: any) {
|
||||
@@ -68,8 +71,10 @@ export class GetRiskInsightsReportResponse extends BaseResponse {
|
||||
|
||||
this.id = this.getResponseProperty("organizationId");
|
||||
this.organizationId = this.getResponseProperty("organizationId");
|
||||
this.date = this.getResponseProperty("date");
|
||||
this.creationDate = new Date(this.getResponseProperty("creationDate"));
|
||||
this.reportData = new EncString(this.getResponseProperty("reportData"));
|
||||
this.summaryData = new EncString(this.getResponseProperty("summaryData"));
|
||||
this.applicationData = new EncString(this.getResponseProperty("applicationData"));
|
||||
this.contentEncryptionKey = new EncString(this.getResponseProperty("contentEncryptionKey"));
|
||||
}
|
||||
}
|
||||
@@ -77,7 +82,7 @@ export class GetRiskInsightsReportResponse extends BaseResponse {
|
||||
export class GetRiskInsightsSummaryResponse extends BaseResponse {
|
||||
id: string;
|
||||
organizationId: OrganizationId;
|
||||
encryptedData: EncString; // Decrypted as OrganizationReportSummary
|
||||
encryptedSummary: EncString; // Decrypted as OrganizationReportSummary
|
||||
contentEncryptionKey: EncString;
|
||||
|
||||
constructor(response: any) {
|
||||
@@ -85,7 +90,7 @@ export class GetRiskInsightsSummaryResponse extends BaseResponse {
|
||||
// TODO Handle taking array of summary data and converting to array
|
||||
this.id = this.getResponseProperty("id");
|
||||
this.organizationId = this.getResponseProperty("organizationId");
|
||||
this.encryptedData = this.getResponseProperty("encryptedData");
|
||||
this.encryptedSummary = this.getResponseProperty("encryptedData");
|
||||
this.contentEncryptionKey = this.getResponseProperty("contentEncryptionKey");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export * from "./api-models.types";
|
||||
export * from "./password-health";
|
||||
export * from "./report-data-service.types";
|
||||
export * from "./report-encryption.types";
|
||||
export * from "./report-models";
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
import { MemberCipherDetailsResponse } from "../response/member-cipher-details.response";
|
||||
|
||||
import { ApplicationHealthReportDetailEnriched } from "./report-data-service.types";
|
||||
import {
|
||||
ApplicationHealthReportDetail,
|
||||
OrganizationReportApplication,
|
||||
OrganizationReportSummary,
|
||||
} from "./report-models";
|
||||
|
||||
const mockApplication1: ApplicationHealthReportDetail = {
|
||||
applicationName: "application1.com",
|
||||
passwordCount: 2,
|
||||
atRiskPasswordCount: 1,
|
||||
atRiskCipherIds: ["cipher-1"],
|
||||
memberCount: 2,
|
||||
atRiskMemberCount: 1,
|
||||
memberDetails: [
|
||||
{
|
||||
userGuid: "user-id-1",
|
||||
userName: "tom",
|
||||
email: "tom@application1.com",
|
||||
cipherId: "cipher-1",
|
||||
},
|
||||
],
|
||||
atRiskMemberDetails: [
|
||||
{
|
||||
userGuid: "user-id-2",
|
||||
userName: "tom",
|
||||
email: "tom2@application1.com",
|
||||
cipherId: "cipher-2",
|
||||
},
|
||||
],
|
||||
cipherIds: ["cipher-1", "cipher-2"],
|
||||
};
|
||||
|
||||
const mockApplication2: ApplicationHealthReportDetail = {
|
||||
applicationName: "site2.application1.com",
|
||||
passwordCount: 0,
|
||||
atRiskPasswordCount: 0,
|
||||
atRiskCipherIds: [],
|
||||
memberCount: 0,
|
||||
atRiskMemberCount: 0,
|
||||
memberDetails: [],
|
||||
atRiskMemberDetails: [],
|
||||
cipherIds: [],
|
||||
};
|
||||
const mockApplication3: ApplicationHealthReportDetail = {
|
||||
applicationName: "application2.com",
|
||||
passwordCount: 0,
|
||||
atRiskPasswordCount: 0,
|
||||
atRiskCipherIds: [],
|
||||
memberCount: 0,
|
||||
atRiskMemberCount: 0,
|
||||
memberDetails: [],
|
||||
atRiskMemberDetails: [],
|
||||
cipherIds: [],
|
||||
};
|
||||
|
||||
export const mockReportData: ApplicationHealthReportDetail[] = [
|
||||
mockApplication1,
|
||||
mockApplication2,
|
||||
mockApplication3,
|
||||
];
|
||||
|
||||
export const mockSummaryData: OrganizationReportSummary = {
|
||||
totalMemberCount: 5,
|
||||
totalAtRiskMemberCount: 2,
|
||||
totalApplicationCount: 3,
|
||||
totalAtRiskApplicationCount: 1,
|
||||
totalCriticalMemberCount: 1,
|
||||
totalCriticalAtRiskMemberCount: 1,
|
||||
totalCriticalApplicationCount: 1,
|
||||
totalCriticalAtRiskApplicationCount: 1,
|
||||
newApplications: [],
|
||||
};
|
||||
export const mockApplicationData: OrganizationReportApplication[] = [
|
||||
{
|
||||
applicationName: "application1.com",
|
||||
isCritical: true,
|
||||
},
|
||||
{
|
||||
applicationName: "application2.com",
|
||||
isCritical: false,
|
||||
},
|
||||
];
|
||||
|
||||
export const mockEnrichedReportData: ApplicationHealthReportDetailEnriched[] = [
|
||||
{ ...mockApplication1, isMarkedAsCritical: true, ciphers: [] },
|
||||
{ ...mockApplication2, isMarkedAsCritical: false, ciphers: [] },
|
||||
];
|
||||
|
||||
export const mockCipherViews: CipherView[] = [
|
||||
mock<CipherView>({
|
||||
id: "cipher-1",
|
||||
type: CipherType.Login,
|
||||
login: { password: "pass1", username: "user1", uris: [{ uri: "https://app.com/login" }] },
|
||||
isDeleted: false,
|
||||
viewPassword: true,
|
||||
}),
|
||||
mock<CipherView>({
|
||||
id: "cipher-2",
|
||||
type: CipherType.Login,
|
||||
login: { password: "pass2", username: "user2", uris: [{ uri: "app.com/home" }] },
|
||||
isDeleted: false,
|
||||
viewPassword: true,
|
||||
}),
|
||||
mock<CipherView>({
|
||||
id: "cipher-3",
|
||||
type: CipherType.Login,
|
||||
login: { password: "pass3", username: "user3", uris: [{ uri: "https://other.com" }] },
|
||||
isDeleted: false,
|
||||
viewPassword: true,
|
||||
}),
|
||||
];
|
||||
|
||||
export const mockMemberDetails = [
|
||||
mock<MemberCipherDetailsResponse>({
|
||||
cipherIds: ["cipher-1"],
|
||||
userGuid: "user1",
|
||||
userName: "User 1",
|
||||
email: "user1@app.com",
|
||||
}),
|
||||
mock<MemberCipherDetailsResponse>({
|
||||
cipherIds: ["cipher-2"],
|
||||
userGuid: "user2",
|
||||
userName: "User 2",
|
||||
email: "user2@app.com",
|
||||
}),
|
||||
mock<MemberCipherDetailsResponse>({
|
||||
cipherIds: ["cipher-3"],
|
||||
userGuid: "user3",
|
||||
userName: "User 3",
|
||||
email: "user3@other.com",
|
||||
}),
|
||||
];
|
||||
@@ -1,7 +1,5 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { BadgeVariant } from "@bitwarden/components";
|
||||
|
||||
@@ -33,16 +31,6 @@ export type ExposedPasswordDetail = {
|
||||
exposedXTimes: number;
|
||||
} | null;
|
||||
|
||||
/*
|
||||
* After data is encrypted, it is returned with the
|
||||
* encryption key used to encrypt the data.
|
||||
*/
|
||||
export interface EncryptedDataWithKey {
|
||||
organizationId: OrganizationId;
|
||||
encryptedData: EncString;
|
||||
contentEncryptionKey: EncString;
|
||||
}
|
||||
|
||||
export type LEGACY_MemberDetailsFlat = {
|
||||
userGuid: string;
|
||||
userName: string;
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
import {
|
||||
ApplicationHealthReportDetail,
|
||||
OrganizationReportApplication,
|
||||
OrganizationReportSummary,
|
||||
} from "./report-models";
|
||||
|
||||
export type ApplicationHealthReportDetailEnriched = ApplicationHealthReportDetail & {
|
||||
isMarkedAsCritical: boolean;
|
||||
ciphers: CipherView[];
|
||||
};
|
||||
export interface RiskInsightsEnrichedData {
|
||||
reportData: ApplicationHealthReportDetailEnriched[];
|
||||
summaryData: OrganizationReportSummary;
|
||||
applicationData: OrganizationReportApplication[];
|
||||
creationDate: Date;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import {
|
||||
ApplicationHealthReportDetail,
|
||||
OrganizationReportApplication,
|
||||
OrganizationReportSummary,
|
||||
} from "./report-models";
|
||||
|
||||
/*
|
||||
* After data is encrypted, it is returned with the
|
||||
* encryption key used to encrypt the data.
|
||||
*/
|
||||
export interface EncryptedDataWithKey {
|
||||
organizationId: OrganizationId;
|
||||
encryptedReportData: EncString;
|
||||
encryptedSummaryData: EncString;
|
||||
encryptedApplicationData: EncString;
|
||||
contentEncryptionKey: EncString;
|
||||
}
|
||||
|
||||
export interface DecryptedReportData {
|
||||
reportData: ApplicationHealthReportDetail[];
|
||||
summaryData: OrganizationReportSummary;
|
||||
applicationData: OrganizationReportApplication[];
|
||||
}
|
||||
|
||||
export interface EncryptedReportData {
|
||||
encryptedReportData: EncString;
|
||||
encryptedSummaryData: EncString;
|
||||
encryptedApplicationData: EncString;
|
||||
}
|
||||
@@ -131,11 +131,6 @@ export type ApplicationHealthReportDetail = {
|
||||
cipherIds: string[];
|
||||
};
|
||||
|
||||
export type ApplicationHealthReportDetailEnriched = ApplicationHealthReportDetail & {
|
||||
isMarkedAsCritical: boolean;
|
||||
ciphers: CipherView[];
|
||||
};
|
||||
|
||||
/*
|
||||
* A list of applications and the count of
|
||||
* at risk passwords for each application
|
||||
@@ -148,12 +143,6 @@ export type AtRiskApplicationDetail = {
|
||||
// -------------------- Password Health Report Models --------------------
|
||||
export type PasswordHealthReportApplicationId = Opaque<string, "PasswordHealthReportApplicationId">;
|
||||
|
||||
// -------------------- Risk Insights Report Models --------------------
|
||||
export interface RiskInsightsReportData {
|
||||
data: ApplicationHealthReportDetailEnriched[];
|
||||
summary: OrganizationReportSummary;
|
||||
}
|
||||
|
||||
export type ReportScore = { label: string; badgeVariant: BadgeVariant; sortOrder: number };
|
||||
|
||||
export type ReportResult = CipherView & {
|
||||
@@ -162,8 +151,9 @@ export type ReportResult = CipherView & {
|
||||
scoreKey: number;
|
||||
};
|
||||
|
||||
export type ReportDetailsAndSummary = {
|
||||
data: ApplicationHealthReportDetailEnriched[];
|
||||
summary: OrganizationReportSummary;
|
||||
dateCreated: Date;
|
||||
};
|
||||
export interface RiskInsightsData {
|
||||
creationDate: Date;
|
||||
reportData: ApplicationHealthReportDetail[];
|
||||
summaryData: OrganizationReportSummary;
|
||||
applicationData: OrganizationReportApplication[];
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher } from "../models";
|
||||
import { ApplicationHealthReportDetailEnriched } from "../models";
|
||||
import { OrganizationReportSummary } from "../models/report-models";
|
||||
|
||||
import { RiskInsightsDataService } from "./risk-insights-data.service";
|
||||
|
||||
export class AllActivitiesService {
|
||||
/// This class is used to manage the summary of all applications
|
||||
/// and critical applications.
|
||||
@@ -20,12 +22,10 @@ export class AllActivitiesService {
|
||||
totalCriticalAtRiskApplicationCount: 0,
|
||||
newApplications: [],
|
||||
});
|
||||
|
||||
reportSummary$ = this.reportSummarySubject$.asObservable();
|
||||
|
||||
private allApplicationsDetailsSubject$: BehaviorSubject<
|
||||
LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher[]
|
||||
> = new BehaviorSubject<LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher[]>([]);
|
||||
private allApplicationsDetailsSubject$: BehaviorSubject<ApplicationHealthReportDetailEnriched[]> =
|
||||
new BehaviorSubject<ApplicationHealthReportDetailEnriched[]>([]);
|
||||
allApplicationsDetails$ = this.allApplicationsDetailsSubject$.asObservable();
|
||||
|
||||
private atRiskPasswordsCountSubject$ = new BehaviorSubject<number>(0);
|
||||
@@ -35,6 +35,22 @@ export class AllActivitiesService {
|
||||
passwordChangeProgressMetricHasProgressBar$ =
|
||||
this.passwordChangeProgressMetricHasProgressBarSubject$.asObservable();
|
||||
|
||||
constructor(private dataService: RiskInsightsDataService) {
|
||||
// All application summary changes
|
||||
this.dataService.reportResults$.subscribe((report) => {
|
||||
if (report) {
|
||||
this.setAllAppsReportSummary(report.summaryData);
|
||||
this.setAllAppsReportDetails(report.reportData);
|
||||
}
|
||||
});
|
||||
// Critical application summary changes
|
||||
this.dataService.criticalReportResults$.subscribe((report) => {
|
||||
if (report) {
|
||||
this.setCriticalAppsReportSummary(report.summaryData);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setCriticalAppsReportSummary(summary: OrganizationReportSummary) {
|
||||
this.reportSummarySubject$.next({
|
||||
...this.reportSummarySubject$.getValue(),
|
||||
@@ -55,9 +71,7 @@ export class AllActivitiesService {
|
||||
});
|
||||
}
|
||||
|
||||
setAllAppsReportDetails(
|
||||
applications: LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher[],
|
||||
) {
|
||||
setAllAppsReportDetails(applications: ApplicationHealthReportDetailEnriched[]) {
|
||||
const totalAtRiskPasswords = applications.reduce(
|
||||
(sum, app) => sum + app.atRiskPasswordCount,
|
||||
0,
|
||||
|
||||
@@ -82,9 +82,10 @@ describe("CriticalAppsService", () => {
|
||||
});
|
||||
|
||||
it("should exclude records that already exist", async () => {
|
||||
const privateCriticalAppsSubject = service["criticalAppsListSubject$"];
|
||||
// arrange
|
||||
// one record already exists
|
||||
service.setAppsInListForOrg([
|
||||
privateCriticalAppsSubject.next([
|
||||
{
|
||||
id: randomUUID() as PasswordHealthReportApplicationId,
|
||||
organizationId: SomeOrganization,
|
||||
@@ -145,6 +146,7 @@ describe("CriticalAppsService", () => {
|
||||
|
||||
it("should get by org id", () => {
|
||||
const orgId = "some organization" as OrganizationId;
|
||||
const privateCriticalAppsSubject = service["criticalAppsListSubject$"];
|
||||
const response = [
|
||||
{ id: "id1", organizationId: "some organization", uri: "https://example.com" },
|
||||
{ id: "id2", organizationId: "some organization", uri: "https://example.org" },
|
||||
@@ -155,13 +157,14 @@ describe("CriticalAppsService", () => {
|
||||
const orgKey$ = new BehaviorSubject(OrgRecords);
|
||||
keyService.orgKeys$.mockReturnValue(orgKey$);
|
||||
service.loadOrganizationContext(SomeOrganization, SomeUser);
|
||||
service.setAppsInListForOrg(response);
|
||||
privateCriticalAppsSubject.next(response);
|
||||
service.getAppsListForOrg(orgId as OrganizationId).subscribe((res) => {
|
||||
expect(res).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
it("should drop a critical app", async () => {
|
||||
const privateCriticalAppsSubject = service["criticalAppsListSubject$"];
|
||||
// arrange
|
||||
const selectedUrl = "https://example.com";
|
||||
|
||||
@@ -175,7 +178,7 @@ describe("CriticalAppsService", () => {
|
||||
|
||||
service.loadOrganizationContext(SomeOrganization, SomeUser);
|
||||
|
||||
service.setAppsInListForOrg(initialList);
|
||||
privateCriticalAppsSubject.next(initialList);
|
||||
|
||||
// act
|
||||
await service.dropCriticalApp(SomeOrganization, selectedUrl);
|
||||
@@ -193,6 +196,7 @@ describe("CriticalAppsService", () => {
|
||||
});
|
||||
|
||||
it("should not drop a critical app if it does not exist", async () => {
|
||||
const privateCriticalAppsSubject = service["criticalAppsListSubject$"];
|
||||
// arrange
|
||||
const selectedUrl = "https://nonexistent.com";
|
||||
|
||||
@@ -206,7 +210,7 @@ describe("CriticalAppsService", () => {
|
||||
|
||||
service.loadOrganizationContext(SomeOrganization, SomeUser);
|
||||
|
||||
service.setAppsInListForOrg(initialList);
|
||||
privateCriticalAppsSubject.next(initialList);
|
||||
|
||||
// act
|
||||
await service.dropCriticalApp(SomeOrganization, selectedUrl);
|
||||
|
||||
@@ -83,11 +83,6 @@ export class CriticalAppsService {
|
||||
.pipe(map((apps) => apps.filter((app) => app.organizationId === orgId)));
|
||||
}
|
||||
|
||||
// Reset the critical apps list
|
||||
setAppsInListForOrg(apps: PasswordHealthReportApplicationsResponse[]) {
|
||||
this.criticalAppsListSubject$.next(apps);
|
||||
}
|
||||
|
||||
// Save the selected critical apps for a given organization
|
||||
async setCriticalApps(orgId: OrganizationId, selectedUrls: string[]) {
|
||||
if (orgId != this.organizationId.value) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ErrorResponse } from "@bitwarden/common/models/response/error.response"
|
||||
import { makeEncString } from "@bitwarden/common/spec";
|
||||
import { OrganizationId, OrganizationReportId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { EncryptedDataWithKey } from "../models";
|
||||
import {
|
||||
GetRiskInsightsApplicationDataResponse,
|
||||
GetRiskInsightsReportResponse,
|
||||
@@ -14,7 +15,7 @@ import {
|
||||
SaveRiskInsightsReportRequest,
|
||||
SaveRiskInsightsReportResponse,
|
||||
} from "../models/api-models.types";
|
||||
import { EncryptedDataWithKey } from "../models/password-health";
|
||||
import { mockApplicationData, mockReportData, mockSummaryData } from "../models/mock-data";
|
||||
|
||||
import { RiskInsightsApiService } from "./risk-insights-api.service";
|
||||
|
||||
@@ -26,17 +27,21 @@ describe("RiskInsightsApiService", () => {
|
||||
const orgId = "org1" as OrganizationId;
|
||||
const mockReportId = "report-1";
|
||||
const mockKey = "encryption-key-1";
|
||||
const mockData = "encrypted-data";
|
||||
|
||||
const reportData = makeEncString("test").encryptedString?.toString() ?? "";
|
||||
const reportKey = makeEncString("test-key").encryptedString?.toString() ?? "";
|
||||
const mockReportKey = makeEncString("test-key");
|
||||
|
||||
const saveRiskInsightsReportRequest: SaveRiskInsightsReportRequest = {
|
||||
const mockReportEnc = makeEncString(JSON.stringify(mockReportData));
|
||||
const mockSummaryEnc = makeEncString(JSON.stringify(mockSummaryData));
|
||||
const mockApplicationsEnc = makeEncString(JSON.stringify(mockApplicationData));
|
||||
|
||||
const mockSaveRiskInsightsReportRequest: SaveRiskInsightsReportRequest = {
|
||||
data: {
|
||||
organizationId: orgId,
|
||||
date: new Date().toISOString(),
|
||||
reportData: reportData,
|
||||
contentEncryptionKey: reportKey,
|
||||
creationDate: new Date().toISOString(),
|
||||
reportData: mockReportEnc.decryptedValue ?? "",
|
||||
summaryData: mockReportEnc.decryptedValue ?? "",
|
||||
applicationData: mockReportEnc.decryptedValue ?? "",
|
||||
contentEncryptionKey: mockReportKey.decryptedValue ?? "",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -53,7 +58,9 @@ describe("RiskInsightsApiService", () => {
|
||||
id: mockId,
|
||||
organizationId: orgId,
|
||||
date: new Date().toISOString(),
|
||||
reportData: mockData,
|
||||
reportData: mockReportEnc,
|
||||
summaryData: mockSummaryEnc,
|
||||
applicationData: mockApplicationsEnc,
|
||||
contentEncryptionKey: mockKey,
|
||||
};
|
||||
|
||||
@@ -96,17 +103,17 @@ describe("RiskInsightsApiService", () => {
|
||||
});
|
||||
|
||||
it("saveRiskInsightsReport$ should call apiService.send with correct parameters", async () => {
|
||||
mockApiService.send.mockReturnValue(Promise.resolve(saveRiskInsightsReportRequest));
|
||||
mockApiService.send.mockReturnValue(Promise.resolve(mockSaveRiskInsightsReportRequest));
|
||||
|
||||
const result = await firstValueFrom(
|
||||
service.saveRiskInsightsReport$(saveRiskInsightsReportRequest, orgId),
|
||||
service.saveRiskInsightsReport$(mockSaveRiskInsightsReportRequest, orgId),
|
||||
);
|
||||
|
||||
expect(result).toEqual(new SaveRiskInsightsReportResponse(saveRiskInsightsReportRequest));
|
||||
expect(result).toEqual(new SaveRiskInsightsReportResponse(mockSaveRiskInsightsReportRequest));
|
||||
expect(mockApiService.send).toHaveBeenCalledWith(
|
||||
"POST",
|
||||
`/reports/organizations/${orgId.toString()}`,
|
||||
saveRiskInsightsReportRequest.data,
|
||||
mockSaveRiskInsightsReportRequest.data,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
@@ -117,13 +124,13 @@ describe("RiskInsightsApiService", () => {
|
||||
mockApiService.send.mockReturnValue(Promise.reject(error));
|
||||
|
||||
await expect(
|
||||
firstValueFrom(service.saveRiskInsightsReport$(saveRiskInsightsReportRequest, orgId)),
|
||||
firstValueFrom(service.saveRiskInsightsReport$(mockSaveRiskInsightsReportRequest, orgId)),
|
||||
).rejects.toEqual(error);
|
||||
|
||||
expect(mockApiService.send).toHaveBeenCalledWith(
|
||||
"POST",
|
||||
`/reports/organizations/${orgId.toString()}`,
|
||||
saveRiskInsightsReportRequest.data,
|
||||
mockSaveRiskInsightsReportRequest.data,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
@@ -134,13 +141,13 @@ describe("RiskInsightsApiService", () => {
|
||||
mockApiService.send.mockReturnValue(Promise.reject(error));
|
||||
|
||||
await expect(
|
||||
firstValueFrom(service.saveRiskInsightsReport$(saveRiskInsightsReportRequest, orgId)),
|
||||
firstValueFrom(service.saveRiskInsightsReport$(mockSaveRiskInsightsReportRequest, orgId)),
|
||||
).rejects.toEqual(error);
|
||||
|
||||
expect(mockApiService.send).toHaveBeenCalledWith(
|
||||
"POST",
|
||||
`/reports/organizations/${orgId.toString()}`,
|
||||
saveRiskInsightsReportRequest.data,
|
||||
mockSaveRiskInsightsReportRequest.data,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
@@ -153,7 +160,7 @@ describe("RiskInsightsApiService", () => {
|
||||
{
|
||||
reportId: mockReportId,
|
||||
organizationId: orgId,
|
||||
encryptedData: mockData,
|
||||
encryptedData: mockReportData,
|
||||
contentEncryptionKey: mockKey,
|
||||
},
|
||||
];
|
||||
@@ -175,8 +182,10 @@ describe("RiskInsightsApiService", () => {
|
||||
it("updateRiskInsightsSummary$ should call apiService.send with correct parameters and return an Observable", async () => {
|
||||
const data: EncryptedDataWithKey = {
|
||||
organizationId: orgId,
|
||||
encryptedData: new EncString(mockData),
|
||||
contentEncryptionKey: new EncString(mockKey),
|
||||
encryptedReportData: new EncString(JSON.stringify(mockReportData)),
|
||||
encryptedSummaryData: new EncString(JSON.stringify(mockSummaryData)),
|
||||
encryptedApplicationData: new EncString(JSON.stringify(mockApplicationData)),
|
||||
};
|
||||
|
||||
const reportId = "report123" as OrganizationReportId;
|
||||
@@ -199,7 +208,9 @@ describe("RiskInsightsApiService", () => {
|
||||
const reportId = "report123" as OrganizationReportId;
|
||||
const mockResponse: EncryptedDataWithKey | null = {
|
||||
organizationId: orgId,
|
||||
encryptedData: new EncString(mockData),
|
||||
encryptedReportData: new EncString(JSON.stringify(mockReportData)),
|
||||
encryptedSummaryData: new EncString(JSON.stringify(mockSummaryData)),
|
||||
encryptedApplicationData: new EncString(JSON.stringify(mockApplicationData)),
|
||||
contentEncryptionKey: new EncString(mockKey),
|
||||
};
|
||||
|
||||
@@ -217,21 +228,17 @@ describe("RiskInsightsApiService", () => {
|
||||
});
|
||||
|
||||
it("updateRiskInsightsApplicationData$ should call apiService.send with correct parameters and return an Observable", async () => {
|
||||
const applicationData: EncryptedDataWithKey = {
|
||||
organizationId: orgId,
|
||||
encryptedData: new EncString(mockData),
|
||||
contentEncryptionKey: new EncString(mockKey),
|
||||
};
|
||||
const reportId = "report123" as OrganizationReportId;
|
||||
const mockApplication = mockApplicationData[0];
|
||||
|
||||
mockApiService.send.mockResolvedValueOnce(undefined);
|
||||
const result = await firstValueFrom(
|
||||
service.updateRiskInsightsApplicationData$(applicationData, orgId, reportId),
|
||||
service.updateRiskInsightsApplicationData$(mockApplication, orgId, reportId),
|
||||
);
|
||||
expect(mockApiService.send).toHaveBeenCalledWith(
|
||||
"PATCH",
|
||||
`/reports/organizations/${orgId.toString()}/data/application/${reportId.toString()}`,
|
||||
applicationData,
|
||||
mockApplication,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { OrganizationId, OrganizationReportId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { EncryptedDataWithKey, OrganizationReportApplication } from "../models";
|
||||
import {
|
||||
GetRiskInsightsApplicationDataResponse,
|
||||
GetRiskInsightsReportResponse,
|
||||
@@ -11,7 +12,6 @@ import {
|
||||
SaveRiskInsightsReportRequest,
|
||||
SaveRiskInsightsReportResponse,
|
||||
} from "../models/api-models.types";
|
||||
import { EncryptedDataWithKey } from "../models/password-health";
|
||||
|
||||
export class RiskInsightsApiService {
|
||||
constructor(private apiService: ApiService) {}
|
||||
@@ -102,7 +102,7 @@ export class RiskInsightsApiService {
|
||||
}
|
||||
|
||||
updateRiskInsightsApplicationData$(
|
||||
applicationData: EncryptedDataWithKey,
|
||||
applicationData: OrganizationReportApplication,
|
||||
orgId: OrganizationId,
|
||||
reportId: OrganizationReportId,
|
||||
): Observable<void> {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { BehaviorSubject, EMPTY, firstValueFrom, Observable, of } from "rxjs";
|
||||
import { BehaviorSubject, EMPTY, firstValueFrom, Observable, of, throwError } from "rxjs";
|
||||
import {
|
||||
catchError,
|
||||
distinctUntilChanged,
|
||||
exhaustMap,
|
||||
filter,
|
||||
finalize,
|
||||
map,
|
||||
shareReplay,
|
||||
switchMap,
|
||||
tap,
|
||||
withLatestFrom,
|
||||
@@ -18,19 +20,13 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import {
|
||||
AppAtRiskMembersDialogParams,
|
||||
AtRiskApplicationDetail,
|
||||
AtRiskMemberDetail,
|
||||
DrawerType,
|
||||
DrawerDetails,
|
||||
ApplicationHealthReportDetail,
|
||||
ApplicationHealthReportDetailEnriched,
|
||||
ReportDetailsAndSummary,
|
||||
} from "../models/report-models";
|
||||
import { ApplicationHealthReportDetailEnriched } from "../models";
|
||||
import { RiskInsightsEnrichedData } from "../models/report-data-service.types";
|
||||
import { DrawerType, DrawerDetails, ApplicationHealthReportDetail } from "../models/report-models";
|
||||
|
||||
import { CriticalAppsService } from "./critical-apps.service";
|
||||
import { RiskInsightsReportService } from "./risk-insights-report.service";
|
||||
|
||||
export class RiskInsightsDataService {
|
||||
// -------------------------- Context state --------------------------
|
||||
// Current user viewing risk insights
|
||||
@@ -45,16 +41,17 @@ export class RiskInsightsDataService {
|
||||
organizationDetails$ = this.organizationDetailsSubject.asObservable();
|
||||
|
||||
// -------------------------- Data ------------------------------------
|
||||
private applicationsSubject = new BehaviorSubject<ApplicationHealthReportDetail[] | null>(null);
|
||||
applications$ = this.applicationsSubject.asObservable();
|
||||
// TODO: Remove. Will use report results
|
||||
private LEGACY_applicationsSubject = new BehaviorSubject<ApplicationHealthReportDetail[] | null>(
|
||||
null,
|
||||
);
|
||||
LEGACY_applications$ = this.LEGACY_applicationsSubject.asObservable();
|
||||
|
||||
private dataLastUpdatedSubject = new BehaviorSubject<Date | null>(null);
|
||||
dataLastUpdated$ = this.dataLastUpdatedSubject.asObservable();
|
||||
|
||||
criticalApps$ = this.criticalAppsService.criticalAppsList$;
|
||||
// TODO: Remove. Will use date from report results
|
||||
private LEGACY_dataLastUpdatedSubject = new BehaviorSubject<Date | null>(null);
|
||||
dataLastUpdated$ = this.LEGACY_dataLastUpdatedSubject.asObservable();
|
||||
|
||||
// --------------------------- UI State ------------------------------------
|
||||
|
||||
private isLoadingSubject = new BehaviorSubject<boolean>(false);
|
||||
isLoading$ = this.isLoadingSubject.asObservable();
|
||||
|
||||
@@ -78,21 +75,52 @@ export class RiskInsightsDataService {
|
||||
|
||||
// ------------------------- Report Variables ----------------
|
||||
// The last run report details
|
||||
private reportResultsSubject = new BehaviorSubject<ReportDetailsAndSummary | null>(null);
|
||||
private reportResultsSubject = new BehaviorSubject<RiskInsightsEnrichedData | null>(null);
|
||||
reportResults$ = this.reportResultsSubject.asObservable();
|
||||
// Is a report being generated
|
||||
private isRunningReportSubject = new BehaviorSubject<boolean>(false);
|
||||
isRunningReport$ = this.isRunningReportSubject.asObservable();
|
||||
// The error from report generation if there was an error
|
||||
|
||||
// --------------------------- Critical Application data ---------------------
|
||||
criticalReportResults$: Observable<RiskInsightsEnrichedData | null> = of(null);
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private criticalAppsService: CriticalAppsService,
|
||||
private organizationService: OrganizationService,
|
||||
private reportService: RiskInsightsReportService,
|
||||
) {}
|
||||
) {
|
||||
// Reload report if critical applications change
|
||||
// This also handles the original report load
|
||||
this.criticalAppsService.criticalAppsList$
|
||||
.pipe(withLatestFrom(this.organizationDetails$, this.userId$))
|
||||
.subscribe({
|
||||
next: ([_criticalApps, organizationDetails, userId]) => {
|
||||
if (organizationDetails?.organizationId && userId) {
|
||||
this.fetchLastReport(organizationDetails?.organizationId, userId);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Setup critical application data and summary generation for live critical application usage
|
||||
this.criticalReportResults$ = this.reportResults$.pipe(
|
||||
filter((report) => !!report),
|
||||
map((r) => {
|
||||
const criticalApplications = r.reportData.filter(
|
||||
(application) => application.isMarkedAsCritical,
|
||||
);
|
||||
const summary = this.reportService.generateApplicationsSummary(criticalApplications);
|
||||
|
||||
return {
|
||||
...r,
|
||||
summaryData: summary,
|
||||
reportData: criticalApplications,
|
||||
};
|
||||
}),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
}
|
||||
|
||||
// [FIXME] PM-25612 - Call Initialization in RiskInsightsComponent instead of child components
|
||||
async initializeForOrganization(organizationId: OrganizationId) {
|
||||
// Fetch current user
|
||||
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
@@ -115,9 +143,6 @@ export class RiskInsightsDataService {
|
||||
// Load critical applications for organization
|
||||
await this.criticalAppsService.loadOrganizationContext(organizationId, userId);
|
||||
|
||||
// Load existing report
|
||||
this.fetchLastReport(organizationId, userId);
|
||||
|
||||
// Setup new report generation
|
||||
this._runApplicationsReport().subscribe({
|
||||
next: (result) => {
|
||||
@@ -133,7 +158,7 @@ export class RiskInsightsDataService {
|
||||
* Fetches the applications report and updates the applicationsSubject.
|
||||
* @param organizationId The ID of the organization.
|
||||
*/
|
||||
fetchApplicationsReport(organizationId: OrganizationId, isRefresh?: boolean): void {
|
||||
LEGACY_fetchApplicationsReport(organizationId: OrganizationId, isRefresh?: boolean): void {
|
||||
if (isRefresh) {
|
||||
this.isRefreshingSubject.next(true);
|
||||
} else {
|
||||
@@ -145,24 +170,20 @@ export class RiskInsightsDataService {
|
||||
finalize(() => {
|
||||
this.isLoadingSubject.next(false);
|
||||
this.isRefreshingSubject.next(false);
|
||||
this.dataLastUpdatedSubject.next(new Date());
|
||||
this.LEGACY_dataLastUpdatedSubject.next(new Date());
|
||||
}),
|
||||
)
|
||||
.subscribe({
|
||||
next: (reports: ApplicationHealthReportDetail[]) => {
|
||||
this.applicationsSubject.next(reports);
|
||||
this.LEGACY_applicationsSubject.next(reports);
|
||||
this.errorSubject.next(null);
|
||||
},
|
||||
error: () => {
|
||||
this.applicationsSubject.next([]);
|
||||
this.LEGACY_applicationsSubject.next([]);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
refreshApplicationsReport(organizationId: OrganizationId): void {
|
||||
this.fetchApplicationsReport(organizationId, true);
|
||||
}
|
||||
|
||||
// ------------------------------- Enrichment methods -------------------------------
|
||||
/**
|
||||
* Takes the basic application health report details and enriches them to include
|
||||
@@ -174,8 +195,10 @@ export class RiskInsightsDataService {
|
||||
enrichReportData$(
|
||||
applications: ApplicationHealthReportDetail[],
|
||||
): Observable<ApplicationHealthReportDetailEnriched[]> {
|
||||
// TODO Compare applications on report to updated critical applications
|
||||
// TODO Compare applications on report to any new applications
|
||||
return of(applications).pipe(
|
||||
withLatestFrom(this.organizationDetails$, this.criticalApps$),
|
||||
withLatestFrom(this.organizationDetails$, this.criticalAppsService.criticalAppsList$),
|
||||
switchMap(async ([apps, orgDetails, criticalApps]) => {
|
||||
if (!orgDetails) {
|
||||
return [];
|
||||
@@ -200,19 +223,11 @@ export class RiskInsightsDataService {
|
||||
);
|
||||
}
|
||||
|
||||
// ------------------------------- Drawer management methods -------------------------------
|
||||
// ------------------------- Drawer functions -----------------------------
|
||||
|
||||
isActiveDrawerType$ = (drawerType: DrawerType): Observable<boolean> => {
|
||||
return this.drawerDetails$.pipe(map((details) => details.activeDrawerType === drawerType));
|
||||
};
|
||||
isActiveDrawerType = (drawerType: DrawerType): boolean => {
|
||||
return this.drawerDetailsSubject.value.activeDrawerType === drawerType;
|
||||
};
|
||||
|
||||
isDrawerOpenForInvoker$ = (applicationName: string) => {
|
||||
return this.drawerDetails$.pipe(map((details) => details.invokerId === applicationName));
|
||||
};
|
||||
isDrawerOpenForInvoker = (applicationName: string): boolean => {
|
||||
return this.drawerDetailsSubject.value.invokerId === applicationName;
|
||||
};
|
||||
@@ -228,10 +243,7 @@ export class RiskInsightsDataService {
|
||||
});
|
||||
};
|
||||
|
||||
setDrawerForOrgAtRiskMembers = (
|
||||
atRiskMemberDetails: AtRiskMemberDetail[],
|
||||
invokerId: string = "",
|
||||
): void => {
|
||||
setDrawerForOrgAtRiskMembers = async (invokerId: string = ""): Promise<void> => {
|
||||
const { open, activeDrawerType, invokerId: currentInvokerId } = this.drawerDetailsSubject.value;
|
||||
const shouldClose =
|
||||
open && activeDrawerType === DrawerType.OrgAtRiskMembers && currentInvokerId === invokerId;
|
||||
@@ -239,6 +251,15 @@ export class RiskInsightsDataService {
|
||||
if (shouldClose) {
|
||||
this.closeDrawer();
|
||||
} else {
|
||||
const reportResults = await firstValueFrom(this.reportResults$);
|
||||
if (!reportResults) {
|
||||
return;
|
||||
}
|
||||
|
||||
const atRiskMemberDetails = this.reportService.generateAtRiskMemberList(
|
||||
reportResults.reportData,
|
||||
);
|
||||
|
||||
this.drawerDetailsSubject.next({
|
||||
open: true,
|
||||
invokerId,
|
||||
@@ -250,10 +271,7 @@ export class RiskInsightsDataService {
|
||||
}
|
||||
};
|
||||
|
||||
setDrawerForAppAtRiskMembers = (
|
||||
atRiskMembersDialogParams: AppAtRiskMembersDialogParams,
|
||||
invokerId: string = "",
|
||||
): void => {
|
||||
setDrawerForAppAtRiskMembers = async (invokerId: string = ""): Promise<void> => {
|
||||
const { open, activeDrawerType, invokerId: currentInvokerId } = this.drawerDetailsSubject.value;
|
||||
const shouldClose =
|
||||
open && activeDrawerType === DrawerType.AppAtRiskMembers && currentInvokerId === invokerId;
|
||||
@@ -261,21 +279,29 @@ export class RiskInsightsDataService {
|
||||
if (shouldClose) {
|
||||
this.closeDrawer();
|
||||
} else {
|
||||
const reportResults = await firstValueFrom(this.reportResults$);
|
||||
if (!reportResults) {
|
||||
return;
|
||||
}
|
||||
|
||||
const atRiskMembers = {
|
||||
members:
|
||||
reportResults.reportData.find((app) => app.applicationName === invokerId)
|
||||
?.atRiskMemberDetails ?? [],
|
||||
applicationName: invokerId,
|
||||
};
|
||||
this.drawerDetailsSubject.next({
|
||||
open: true,
|
||||
invokerId,
|
||||
activeDrawerType: DrawerType.AppAtRiskMembers,
|
||||
atRiskMemberDetails: [],
|
||||
appAtRiskMembers: atRiskMembersDialogParams,
|
||||
appAtRiskMembers: atRiskMembers,
|
||||
atRiskAppDetails: null,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
setDrawerForOrgAtRiskApps = (
|
||||
atRiskApps: AtRiskApplicationDetail[],
|
||||
invokerId: string = "",
|
||||
): void => {
|
||||
setDrawerForOrgAtRiskApps = async (invokerId: string = ""): Promise<void> => {
|
||||
const { open, activeDrawerType, invokerId: currentInvokerId } = this.drawerDetailsSubject.value;
|
||||
const shouldClose =
|
||||
open && activeDrawerType === DrawerType.OrgAtRiskApps && currentInvokerId === invokerId;
|
||||
@@ -283,13 +309,21 @@ export class RiskInsightsDataService {
|
||||
if (shouldClose) {
|
||||
this.closeDrawer();
|
||||
} else {
|
||||
const reportResults = await firstValueFrom(this.reportResults$);
|
||||
if (!reportResults) {
|
||||
return;
|
||||
}
|
||||
const atRiskAppDetails = this.reportService.generateAtRiskApplicationList(
|
||||
reportResults.reportData,
|
||||
);
|
||||
|
||||
this.drawerDetailsSubject.next({
|
||||
open: true,
|
||||
invokerId,
|
||||
activeDrawerType: DrawerType.OrgAtRiskApps,
|
||||
atRiskMemberDetails: [],
|
||||
appAtRiskMembers: null,
|
||||
atRiskAppDetails: atRiskApps,
|
||||
atRiskAppDetails,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -311,23 +345,31 @@ export class RiskInsightsDataService {
|
||||
.getRiskInsightsReport$(organizationId, userId)
|
||||
.pipe(
|
||||
switchMap((report) => {
|
||||
return this.enrichReportData$(report.data).pipe(
|
||||
// Take fetched report data and merge with critical applications
|
||||
return this.enrichReportData$(report.reportData).pipe(
|
||||
map((enrichedReport) => ({
|
||||
data: enrichedReport,
|
||||
summary: report.summary,
|
||||
report: enrichedReport,
|
||||
summary: report.summaryData,
|
||||
applications: report.applicationData,
|
||||
creationDate: report.creationDate,
|
||||
})),
|
||||
);
|
||||
}),
|
||||
catchError((error: unknown) => {
|
||||
// console.error("An error occurred when fetching the last report", error);
|
||||
return EMPTY;
|
||||
}),
|
||||
finalize(() => {
|
||||
this.isLoadingSubject.next(false);
|
||||
}),
|
||||
)
|
||||
.subscribe({
|
||||
next: ({ data, summary }) => {
|
||||
next: ({ report, summary, applications, creationDate }) => {
|
||||
this.reportResultsSubject.next({
|
||||
data,
|
||||
summary,
|
||||
dateCreated: new Date(),
|
||||
reportData: report,
|
||||
summaryData: summary,
|
||||
applicationData: applications,
|
||||
creationDate: creationDate,
|
||||
});
|
||||
this.errorSubject.next(null);
|
||||
this.isLoadingSubject.next(false);
|
||||
@@ -343,6 +385,7 @@ export class RiskInsightsDataService {
|
||||
private _runApplicationsReport() {
|
||||
return this.isRunningReport$.pipe(
|
||||
distinctUntilChanged(),
|
||||
// Only run this report if the flag for running is true
|
||||
filter((isRunning) => isRunning),
|
||||
withLatestFrom(this.organizationDetails$, this.userId$),
|
||||
exhaustMap(([_, organizationDetails, userId]) => {
|
||||
@@ -353,22 +396,30 @@ export class RiskInsightsDataService {
|
||||
|
||||
// Generate the report
|
||||
return this.reportService.generateApplicationsReport$(organizationId).pipe(
|
||||
map((data) => ({
|
||||
data,
|
||||
summary: this.reportService.generateApplicationsSummary(data),
|
||||
map((report) => ({
|
||||
report,
|
||||
summary: this.reportService.generateApplicationsSummary(report),
|
||||
applications: this.reportService.generateOrganizationApplications(report),
|
||||
})),
|
||||
switchMap(({ data, summary }) =>
|
||||
this.enrichReportData$(data).pipe(
|
||||
map((enrichedData) => ({ data: enrichedData, summary })),
|
||||
// Enrich report with critical markings
|
||||
switchMap(({ report, summary, applications }) =>
|
||||
this.enrichReportData$(report).pipe(
|
||||
map((enrichedReport) => ({ report: enrichedReport, summary, applications })),
|
||||
),
|
||||
),
|
||||
tap(({ data, summary }) => {
|
||||
this.reportResultsSubject.next({ data, summary, dateCreated: new Date() });
|
||||
// Load the updated data into the UI
|
||||
tap(({ report, summary, applications }) => {
|
||||
this.reportResultsSubject.next({
|
||||
reportData: report,
|
||||
summaryData: summary,
|
||||
applicationData: applications,
|
||||
creationDate: new Date(),
|
||||
});
|
||||
this.errorSubject.next(null);
|
||||
}),
|
||||
switchMap(({ data, summary }) => {
|
||||
// Just returns ID
|
||||
return this.reportService.saveRiskInsightsReport$(data, summary, {
|
||||
switchMap(({ report, summary, applications }) => {
|
||||
// Save the generated data
|
||||
return this.reportService.saveRiskInsightsReport$(report, summary, applications, {
|
||||
organizationId,
|
||||
userId,
|
||||
});
|
||||
@@ -377,4 +428,42 @@ export class RiskInsightsDataService {
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// ------------------------------ Critical application methods --------------
|
||||
|
||||
saveCriticalApplications(selectedUrls: string[]) {
|
||||
return this.organizationDetails$.pipe(
|
||||
exhaustMap((organizationDetails) => {
|
||||
if (!organizationDetails?.organizationId) {
|
||||
return EMPTY;
|
||||
}
|
||||
return this.criticalAppsService.setCriticalApps(
|
||||
organizationDetails?.organizationId,
|
||||
selectedUrls,
|
||||
);
|
||||
}),
|
||||
catchError((error: unknown) => {
|
||||
this.errorSubject.next("Failed to save critical applications");
|
||||
return throwError(() => error);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
removeCriticalApplication(hostname: string) {
|
||||
return this.organizationDetails$.pipe(
|
||||
exhaustMap((organizationDetails) => {
|
||||
if (!organizationDetails?.organizationId) {
|
||||
return EMPTY;
|
||||
}
|
||||
return this.criticalAppsService.dropCriticalApp(
|
||||
organizationDetails?.organizationId,
|
||||
hostname,
|
||||
);
|
||||
}),
|
||||
catchError((error: unknown) => {
|
||||
this.errorSubject.next("Failed to remove critical application");
|
||||
return throwError(() => error);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,9 @@ import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { EncryptedReportData, DecryptedReportData } from "../models";
|
||||
import { mockApplicationData, mockReportData, mockSummaryData } from "../models/mock-data";
|
||||
|
||||
import { RiskInsightsEncryptionService } from "./risk-insights-encryption.service";
|
||||
|
||||
describe("RiskInsightsEncryptionService", () => {
|
||||
@@ -31,6 +34,10 @@ describe("RiskInsightsEncryptionService", () => {
|
||||
};
|
||||
const orgKey$ = new BehaviorSubject(OrgRecords);
|
||||
|
||||
let mockDecryptedData: DecryptedReportData;
|
||||
let mockEncryptedData: EncryptedReportData;
|
||||
let mockKey: EncString;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new RiskInsightsEncryptionService(
|
||||
mockKeyService,
|
||||
@@ -47,6 +54,18 @@ describe("RiskInsightsEncryptionService", () => {
|
||||
mockEncryptService.unwrapSymmetricKey.mockResolvedValue(contentEncryptionKey);
|
||||
mockEncryptService.decryptString.mockResolvedValue(JSON.stringify(testData));
|
||||
mockKeyService.orgKeys$.mockReturnValue(orgKey$);
|
||||
|
||||
mockKey = new EncString("wrapped-key");
|
||||
mockEncryptedData = {
|
||||
encryptedReportData: new EncString(JSON.stringify(mockReportData)),
|
||||
encryptedSummaryData: new EncString(JSON.stringify(mockSummaryData)),
|
||||
encryptedApplicationData: new EncString(JSON.stringify(mockApplicationData)),
|
||||
};
|
||||
mockDecryptedData = {
|
||||
reportData: mockReportData,
|
||||
summaryData: mockSummaryData,
|
||||
applicationData: mockApplicationData,
|
||||
};
|
||||
});
|
||||
|
||||
describe("encryptRiskInsightsReport", () => {
|
||||
@@ -55,22 +74,40 @@ describe("RiskInsightsEncryptionService", () => {
|
||||
mockKeyService.orgKeys$.mockReturnValue(orgKey$);
|
||||
|
||||
// Act: call the method under test
|
||||
const result = await service.encryptRiskInsightsReport(orgId, userId, testData);
|
||||
const result = await service.encryptRiskInsightsReport(
|
||||
{ organizationId: orgId, userId },
|
||||
mockDecryptedData,
|
||||
);
|
||||
|
||||
// Assert: ensure that the methods were called with the expected parameters
|
||||
expect(mockKeyService.orgKeys$).toHaveBeenCalledWith(userId);
|
||||
expect(mockKeyGenerationService.createKey).toHaveBeenCalledWith(512);
|
||||
|
||||
// Assert all variables were encrypted
|
||||
expect(mockEncryptService.encryptString).toHaveBeenCalledWith(
|
||||
JSON.stringify(testData),
|
||||
JSON.stringify(mockDecryptedData.reportData),
|
||||
contentEncryptionKey,
|
||||
);
|
||||
expect(mockEncryptService.encryptString).toHaveBeenCalledWith(
|
||||
JSON.stringify(mockDecryptedData.summaryData),
|
||||
contentEncryptionKey,
|
||||
);
|
||||
expect(mockEncryptService.encryptString).toHaveBeenCalledWith(
|
||||
JSON.stringify(mockDecryptedData.applicationData),
|
||||
contentEncryptionKey,
|
||||
);
|
||||
|
||||
expect(mockEncryptService.wrapSymmetricKey).toHaveBeenCalledWith(
|
||||
contentEncryptionKey,
|
||||
orgKey,
|
||||
);
|
||||
|
||||
// Mocked encrypt returns ENCRYPTED_TEXT
|
||||
expect(result).toEqual({
|
||||
organizationId: orgId,
|
||||
encryptedData: new EncString(ENCRYPTED_TEXT),
|
||||
encryptedReportData: new EncString(ENCRYPTED_TEXT),
|
||||
encryptedSummaryData: new EncString(ENCRYPTED_TEXT),
|
||||
encryptedApplicationData: new EncString(ENCRYPTED_TEXT),
|
||||
contentEncryptionKey: new EncString(ENCRYPTED_KEY),
|
||||
});
|
||||
});
|
||||
@@ -82,9 +119,9 @@ describe("RiskInsightsEncryptionService", () => {
|
||||
mockEncryptService.wrapSymmetricKey.mockResolvedValue(new EncString(ENCRYPTED_KEY));
|
||||
|
||||
// Act & Assert: call the method under test and expect rejection
|
||||
await expect(service.encryptRiskInsightsReport(orgId, userId, testData)).rejects.toThrow(
|
||||
"Encryption failed, encrypted strings are null",
|
||||
);
|
||||
await expect(
|
||||
service.encryptRiskInsightsReport({ organizationId: orgId, userId }, mockDecryptedData),
|
||||
).rejects.toThrow("Encryption failed, encrypted strings are null");
|
||||
});
|
||||
|
||||
it("should throw an error when encrypted key is null or empty", async () => {
|
||||
@@ -94,18 +131,18 @@ describe("RiskInsightsEncryptionService", () => {
|
||||
mockEncryptService.wrapSymmetricKey.mockResolvedValue(new EncString(""));
|
||||
|
||||
// Act & Assert: call the method under test and expect rejection
|
||||
await expect(service.encryptRiskInsightsReport(orgId, userId, testData)).rejects.toThrow(
|
||||
"Encryption failed, encrypted strings are null",
|
||||
);
|
||||
await expect(
|
||||
service.encryptRiskInsightsReport({ organizationId: orgId, userId }, mockDecryptedData),
|
||||
).rejects.toThrow("Encryption failed, encrypted strings are null");
|
||||
});
|
||||
|
||||
it("should throw if org key is not found", async () => {
|
||||
// when we cannot get an organization key, we should throw an error
|
||||
mockKeyService.orgKeys$.mockReturnValue(new BehaviorSubject({}));
|
||||
|
||||
await expect(service.encryptRiskInsightsReport(orgId, userId, testData)).rejects.toThrow(
|
||||
"Organization key not found",
|
||||
);
|
||||
await expect(
|
||||
service.encryptRiskInsightsReport({ organizationId: orgId, userId }, mockDecryptedData),
|
||||
).rejects.toThrow("Organization key not found");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -120,23 +157,21 @@ describe("RiskInsightsEncryptionService", () => {
|
||||
// actual decryption does not happen here,
|
||||
// we just want to ensure the method calls are correct
|
||||
const result = await service.decryptRiskInsightsReport(
|
||||
orgId,
|
||||
userId,
|
||||
new EncString("encrypted-data"),
|
||||
new EncString("wrapped-key"),
|
||||
(data) => data as typeof testData,
|
||||
{ organizationId: orgId, userId },
|
||||
mockEncryptedData,
|
||||
mockKey,
|
||||
);
|
||||
|
||||
expect(mockKeyService.orgKeys$).toHaveBeenCalledWith(userId);
|
||||
expect(mockEncryptService.unwrapSymmetricKey).toHaveBeenCalledWith(
|
||||
new EncString("wrapped-key"),
|
||||
orgKey,
|
||||
);
|
||||
expect(mockEncryptService.decryptString).toHaveBeenCalledWith(
|
||||
new EncString("encrypted-data"),
|
||||
contentEncryptionKey,
|
||||
);
|
||||
expect(result).toEqual(testData);
|
||||
expect(mockEncryptService.unwrapSymmetricKey).toHaveBeenCalledWith(mockKey, orgKey);
|
||||
expect(mockEncryptService.decryptString).toHaveBeenCalledTimes(3);
|
||||
|
||||
// Mock decrypt returns JSON.stringify(testData)
|
||||
expect(result).toEqual({
|
||||
reportData: testData,
|
||||
summaryData: testData,
|
||||
applicationData: testData,
|
||||
});
|
||||
});
|
||||
|
||||
it("should invoke data type validation method during decryption", async () => {
|
||||
@@ -144,77 +179,47 @@ describe("RiskInsightsEncryptionService", () => {
|
||||
mockKeyService.orgKeys$.mockReturnValue(orgKey$);
|
||||
mockEncryptService.unwrapSymmetricKey.mockResolvedValue(contentEncryptionKey);
|
||||
mockEncryptService.decryptString.mockResolvedValue(JSON.stringify(testData));
|
||||
const mockParseFn = jest.fn((data) => data as typeof testData);
|
||||
|
||||
// act: call the decrypt method - with any params
|
||||
// actual decryption does not happen here,
|
||||
// we just want to ensure the method calls are correct
|
||||
const result = await service.decryptRiskInsightsReport(
|
||||
orgId,
|
||||
userId,
|
||||
new EncString("encrypted-data"),
|
||||
new EncString("wrapped-key"),
|
||||
mockParseFn,
|
||||
{ organizationId: orgId, userId },
|
||||
mockEncryptedData,
|
||||
mockKey,
|
||||
);
|
||||
|
||||
expect(mockParseFn).toHaveBeenCalledWith(JSON.parse(JSON.stringify(testData)));
|
||||
expect(result).toEqual(testData);
|
||||
expect(result).toEqual({
|
||||
reportData: testData,
|
||||
summaryData: testData,
|
||||
applicationData: testData,
|
||||
});
|
||||
});
|
||||
|
||||
it("should return null if org key is not found", async () => {
|
||||
mockKeyService.orgKeys$.mockReturnValue(new BehaviorSubject({}));
|
||||
await expect(
|
||||
service.decryptRiskInsightsReport(
|
||||
{ organizationId: orgId, userId },
|
||||
|
||||
const result = await service.decryptRiskInsightsReport(
|
||||
orgId,
|
||||
userId,
|
||||
new EncString("encrypted-data"),
|
||||
new EncString("wrapped-key"),
|
||||
(data) => data as typeof testData,
|
||||
);
|
||||
|
||||
expect(result).toBeNull();
|
||||
mockEncryptedData,
|
||||
mockKey,
|
||||
),
|
||||
).rejects.toEqual(Error("Organization key not found"));
|
||||
});
|
||||
|
||||
it("should return null if decrypt throws", async () => {
|
||||
mockKeyService.orgKeys$.mockReturnValue(orgKey$);
|
||||
mockEncryptService.unwrapSymmetricKey.mockRejectedValue(new Error("fail"));
|
||||
|
||||
const result = await service.decryptRiskInsightsReport(
|
||||
orgId,
|
||||
userId,
|
||||
new EncString("encrypted-data"),
|
||||
new EncString("wrapped-key"),
|
||||
(data) => data as typeof testData,
|
||||
);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
await expect(
|
||||
service.decryptRiskInsightsReport(
|
||||
{ organizationId: orgId, userId },
|
||||
|
||||
it("should return null if decrypt throws", async () => {
|
||||
mockKeyService.orgKeys$.mockReturnValue(orgKey$);
|
||||
mockEncryptService.unwrapSymmetricKey.mockRejectedValue(new Error("fail"));
|
||||
|
||||
const result = await service.decryptRiskInsightsReport(
|
||||
orgId,
|
||||
userId,
|
||||
new EncString("encrypted-data"),
|
||||
new EncString("wrapped-key"),
|
||||
(data) => data as typeof testData,
|
||||
);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should return null if decrypt throws", async () => {
|
||||
mockKeyService.orgKeys$.mockReturnValue(orgKey$);
|
||||
mockEncryptService.unwrapSymmetricKey.mockRejectedValue(new Error("fail"));
|
||||
|
||||
const result = await service.decryptRiskInsightsReport(
|
||||
orgId,
|
||||
userId,
|
||||
new EncString("encrypted-data"),
|
||||
new EncString("wrapped-key"),
|
||||
(data) => data as typeof testData,
|
||||
);
|
||||
expect(result).toBeNull();
|
||||
mockEncryptedData,
|
||||
mockKey,
|
||||
),
|
||||
).rejects.toEqual(Error("fail"));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { KeyGenerationService } from "@bitwarden/common/key-management/crypto";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { EncryptedDataWithKey } from "../models/password-health";
|
||||
import { DecryptedReportData, EncryptedReportData, EncryptedDataWithKey } from "../models";
|
||||
|
||||
export class RiskInsightsEncryptionService {
|
||||
constructor(
|
||||
@@ -16,11 +16,15 @@ export class RiskInsightsEncryptionService {
|
||||
private keyGeneratorService: KeyGenerationService,
|
||||
) {}
|
||||
|
||||
async encryptRiskInsightsReport<T>(
|
||||
organizationId: OrganizationId,
|
||||
userId: UserId,
|
||||
data: T,
|
||||
async encryptRiskInsightsReport(
|
||||
context: {
|
||||
organizationId: OrganizationId;
|
||||
userId: UserId;
|
||||
},
|
||||
data: DecryptedReportData,
|
||||
wrappedKey?: EncString,
|
||||
): Promise<EncryptedDataWithKey> {
|
||||
const { userId, organizationId } = context;
|
||||
const orgKey = await firstValueFrom(
|
||||
this.keyService
|
||||
.orgKeys$(userId)
|
||||
@@ -35,10 +39,28 @@ export class RiskInsightsEncryptionService {
|
||||
throw new Error("Organization key not found");
|
||||
}
|
||||
|
||||
const contentEncryptionKey = await this.keyGeneratorService.createKey(512);
|
||||
let contentEncryptionKey: SymmetricCryptoKey;
|
||||
if (!wrappedKey) {
|
||||
// Generate a new key
|
||||
contentEncryptionKey = await this.keyGeneratorService.createKey(512);
|
||||
} else {
|
||||
// Unwrap the existing key
|
||||
contentEncryptionKey = await this.encryptService.unwrapSymmetricKey(wrappedKey, orgKey);
|
||||
}
|
||||
|
||||
const dataEncrypted = await this.encryptService.encryptString(
|
||||
JSON.stringify(data),
|
||||
const { reportData, summaryData, applicationData } = data;
|
||||
|
||||
// Encrypt the data
|
||||
const encryptedReportData = await this.encryptService.encryptString(
|
||||
JSON.stringify(reportData),
|
||||
contentEncryptionKey,
|
||||
);
|
||||
const encryptedSummaryData = await this.encryptService.encryptString(
|
||||
JSON.stringify(summaryData),
|
||||
contentEncryptionKey,
|
||||
);
|
||||
const encryptedApplicationData = await this.encryptService.encryptString(
|
||||
JSON.stringify(applicationData),
|
||||
contentEncryptionKey,
|
||||
);
|
||||
|
||||
@@ -47,59 +69,87 @@ export class RiskInsightsEncryptionService {
|
||||
orgKey,
|
||||
);
|
||||
|
||||
if (!dataEncrypted.encryptedString || !wrappedEncryptionKey.encryptedString) {
|
||||
if (
|
||||
!encryptedReportData.encryptedString ||
|
||||
!encryptedSummaryData.encryptedString ||
|
||||
!encryptedApplicationData.encryptedString ||
|
||||
!wrappedEncryptionKey.encryptedString
|
||||
) {
|
||||
throw new Error("Encryption failed, encrypted strings are null");
|
||||
}
|
||||
|
||||
const encryptedData = dataEncrypted;
|
||||
const contentEncryptionKeyString = wrappedEncryptionKey;
|
||||
|
||||
const encryptedDataPacket: EncryptedDataWithKey = {
|
||||
organizationId,
|
||||
encryptedData,
|
||||
contentEncryptionKey: contentEncryptionKeyString,
|
||||
encryptedReportData: encryptedReportData,
|
||||
encryptedSummaryData: encryptedSummaryData,
|
||||
encryptedApplicationData: encryptedApplicationData,
|
||||
contentEncryptionKey: wrappedEncryptionKey,
|
||||
};
|
||||
|
||||
return encryptedDataPacket;
|
||||
}
|
||||
|
||||
async decryptRiskInsightsReport<T>(
|
||||
organizationId: OrganizationId,
|
||||
userId: UserId,
|
||||
encryptedData: EncString,
|
||||
async decryptRiskInsightsReport(
|
||||
context: {
|
||||
organizationId: OrganizationId;
|
||||
userId: UserId;
|
||||
},
|
||||
encryptedData: EncryptedReportData,
|
||||
wrappedKey: EncString,
|
||||
parser: (data: Jsonify<T>) => T,
|
||||
): Promise<T | null> {
|
||||
try {
|
||||
const orgKey = await firstValueFrom(
|
||||
this.keyService
|
||||
.orgKeys$(userId)
|
||||
.pipe(
|
||||
map((organizationKeysById) =>
|
||||
organizationKeysById ? organizationKeysById[organizationId] : null,
|
||||
),
|
||||
): Promise<DecryptedReportData> {
|
||||
const { userId, organizationId } = context;
|
||||
const orgKey = await firstValueFrom(
|
||||
this.keyService
|
||||
.orgKeys$(userId)
|
||||
.pipe(
|
||||
map((organizationKeysById) =>
|
||||
organizationKeysById ? organizationKeysById[organizationId] : null,
|
||||
),
|
||||
);
|
||||
),
|
||||
);
|
||||
|
||||
if (!orgKey) {
|
||||
throw new Error("Organization key not found");
|
||||
}
|
||||
|
||||
const unwrappedEncryptionKey = await this.encryptService.unwrapSymmetricKey(
|
||||
wrappedKey,
|
||||
orgKey,
|
||||
);
|
||||
|
||||
const dataUnencrypted = await this.encryptService.decryptString(
|
||||
encryptedData,
|
||||
unwrappedEncryptionKey,
|
||||
);
|
||||
|
||||
const dataUnencryptedJson = parser(JSON.parse(dataUnencrypted));
|
||||
|
||||
return dataUnencryptedJson as T;
|
||||
} catch {
|
||||
return null;
|
||||
if (!orgKey) {
|
||||
throw new Error("Organization key not found");
|
||||
}
|
||||
|
||||
const unwrappedEncryptionKey = await this.encryptService.unwrapSymmetricKey(wrappedKey, orgKey);
|
||||
if (!unwrappedEncryptionKey) {
|
||||
throw Error("Encryption key not found");
|
||||
}
|
||||
|
||||
const { encryptedReportData, encryptedSummaryData, encryptedApplicationData } = encryptedData;
|
||||
if (!encryptedReportData || !encryptedSummaryData || !encryptedApplicationData) {
|
||||
throw new Error("Missing data");
|
||||
}
|
||||
|
||||
// Decrypt the data
|
||||
const decryptedReportData = await this.encryptService.decryptString(
|
||||
encryptedReportData,
|
||||
unwrappedEncryptionKey,
|
||||
);
|
||||
const decryptedSummaryData = await this.encryptService.decryptString(
|
||||
encryptedSummaryData,
|
||||
unwrappedEncryptionKey,
|
||||
);
|
||||
const decryptedApplicationData = await this.encryptService.decryptString(
|
||||
encryptedApplicationData,
|
||||
unwrappedEncryptionKey,
|
||||
);
|
||||
|
||||
if (!decryptedReportData || !decryptedSummaryData || !decryptedApplicationData) {
|
||||
throw new Error("Decryption failed, decrypted strings are null");
|
||||
}
|
||||
|
||||
const decryptedReportDataJson = JSON.parse(decryptedReportData);
|
||||
const decryptedSummaryDataJson = JSON.parse(decryptedSummaryData);
|
||||
const decryptedApplicationDataJson = JSON.parse(decryptedApplicationData);
|
||||
|
||||
const decryptedFullReport = {
|
||||
reportData: decryptedReportDataJson,
|
||||
summaryData: decryptedSummaryDataJson,
|
||||
applicationData: decryptedApplicationDataJson,
|
||||
};
|
||||
|
||||
return decryptedFullReport;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,23 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { firstValueFrom, of } from "rxjs";
|
||||
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { makeEncString } from "@bitwarden/common/spec";
|
||||
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
import { createNewSummaryData } from "../helpers";
|
||||
import { DecryptedReportData, EncryptedDataWithKey } from "../models";
|
||||
import {
|
||||
GetRiskInsightsReportResponse,
|
||||
SaveRiskInsightsReportResponse,
|
||||
} from "../models/api-models.types";
|
||||
import { EncryptedDataWithKey } from "../models/password-health";
|
||||
import {
|
||||
ApplicationHealthReportDetail,
|
||||
OrganizationReportSummary,
|
||||
RiskInsightsReportData,
|
||||
} from "../models/report-models";
|
||||
import { MemberCipherDetailsResponse } from "../response/member-cipher-details.response";
|
||||
mockApplicationData,
|
||||
mockCipherViews,
|
||||
mockMemberDetails,
|
||||
mockReportData,
|
||||
mockSummaryData,
|
||||
} from "../models/mock-data";
|
||||
|
||||
import { mockCiphers } from "./ciphers.mock";
|
||||
import { MemberCipherDetailsApiService } from "./member-cipher-details-api.service";
|
||||
@@ -45,17 +43,13 @@ describe("RiskInsightsReportService", () => {
|
||||
// Non changing mock data
|
||||
const mockOrganizationId = "orgId" as OrganizationId;
|
||||
const mockUserId = "userId" as UserId;
|
||||
const ENCRYPTED_TEXT = "This data has been encrypted";
|
||||
const ENCRYPTED_KEY = "Re-encrypted Cipher Key";
|
||||
const mockEncryptedText = new EncString(ENCRYPTED_TEXT);
|
||||
const mockEncryptedKey = new EncString(ENCRYPTED_KEY);
|
||||
const mockEncryptedKey = makeEncString("test-key");
|
||||
|
||||
// Changing mock data
|
||||
let mockCipherViews: CipherView[];
|
||||
let mockMemberDetails: MemberCipherDetailsResponse[];
|
||||
let mockReport: ApplicationHealthReportDetail[];
|
||||
let mockSummary: OrganizationReportSummary;
|
||||
let mockEncryptedReport: EncryptedDataWithKey;
|
||||
let mockDecryptedData: DecryptedReportData;
|
||||
const mockReportEnc = makeEncString(JSON.stringify(mockReportData));
|
||||
const mockSummaryEnc = makeEncString(JSON.stringify(mockSummaryData));
|
||||
const mockApplicationsEnc = makeEncString(JSON.stringify(mockApplicationData));
|
||||
|
||||
beforeEach(() => {
|
||||
cipherService.getAllFromApiForOrganization.mockResolvedValue(mockCiphers);
|
||||
@@ -87,75 +81,15 @@ describe("RiskInsightsReportService", () => {
|
||||
service = new RiskInsightsReportService(
|
||||
cipherService,
|
||||
memberCipherDetailsService,
|
||||
mockPasswordHealthService,
|
||||
mockRiskInsightsApiService,
|
||||
mockRiskInsightsEncryptionService,
|
||||
mockPasswordHealthService,
|
||||
);
|
||||
|
||||
// Reset mock ciphers before each test
|
||||
mockCipherViews = [
|
||||
mock<CipherView>({
|
||||
id: "cipher-1",
|
||||
type: CipherType.Login,
|
||||
login: { password: "pass1", username: "user1", uris: [{ uri: "https://app.com/login" }] },
|
||||
isDeleted: false,
|
||||
viewPassword: true,
|
||||
}),
|
||||
mock<CipherView>({
|
||||
id: "cipher-2",
|
||||
type: CipherType.Login,
|
||||
login: { password: "pass2", username: "user2", uris: [{ uri: "app.com/home" }] },
|
||||
isDeleted: false,
|
||||
viewPassword: true,
|
||||
}),
|
||||
mock<CipherView>({
|
||||
id: "cipher-3",
|
||||
type: CipherType.Login,
|
||||
login: { password: "pass3", username: "user3", uris: [{ uri: "https://other.com" }] },
|
||||
isDeleted: false,
|
||||
viewPassword: true,
|
||||
}),
|
||||
];
|
||||
mockMemberDetails = [
|
||||
mock<MemberCipherDetailsResponse>({
|
||||
cipherIds: ["cipher-1"],
|
||||
userGuid: "user1",
|
||||
userName: "User 1",
|
||||
email: "user1@app.com",
|
||||
}),
|
||||
mock<MemberCipherDetailsResponse>({
|
||||
cipherIds: ["cipher-2"],
|
||||
userGuid: "user2",
|
||||
userName: "User 2",
|
||||
email: "user2@app.com",
|
||||
}),
|
||||
mock<MemberCipherDetailsResponse>({
|
||||
cipherIds: ["cipher-3"],
|
||||
userGuid: "user3",
|
||||
userName: "User 3",
|
||||
email: "user3@other.com",
|
||||
}),
|
||||
];
|
||||
|
||||
mockReport = [
|
||||
{
|
||||
applicationName: "app1",
|
||||
passwordCount: 0,
|
||||
atRiskPasswordCount: 0,
|
||||
atRiskCipherIds: [],
|
||||
memberCount: 0,
|
||||
atRiskMemberCount: 0,
|
||||
memberDetails: [],
|
||||
atRiskMemberDetails: [],
|
||||
cipherIds: [],
|
||||
},
|
||||
];
|
||||
mockSummary = createNewSummaryData();
|
||||
|
||||
mockEncryptedReport = {
|
||||
organizationId: mockOrganizationId,
|
||||
encryptedData: mockEncryptedText,
|
||||
contentEncryptionKey: mockEncryptedKey,
|
||||
mockDecryptedData = {
|
||||
reportData: mockReportData,
|
||||
summaryData: mockSummaryData,
|
||||
applicationData: mockApplicationData,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -284,15 +218,22 @@ describe("RiskInsightsReportService", () => {
|
||||
|
||||
describe("saveRiskInsightsReport$", () => {
|
||||
it("should not update subjects if save response does not have id", (done) => {
|
||||
const mockEncryptedOutput: EncryptedDataWithKey = {
|
||||
organizationId: mockOrganizationId,
|
||||
encryptedReportData: mockReportEnc,
|
||||
encryptedSummaryData: mockSummaryEnc,
|
||||
encryptedApplicationData: mockApplicationsEnc,
|
||||
contentEncryptionKey: mockEncryptedKey,
|
||||
};
|
||||
mockRiskInsightsEncryptionService.encryptRiskInsightsReport.mockResolvedValue(
|
||||
mockEncryptedReport,
|
||||
mockEncryptedOutput,
|
||||
);
|
||||
|
||||
const saveResponse = new SaveRiskInsightsReportResponse({ id: "" }); // Simulating no ID in response
|
||||
mockRiskInsightsApiService.saveRiskInsightsReport$.mockReturnValue(of(saveResponse));
|
||||
|
||||
service
|
||||
.saveRiskInsightsReport$(mockReport, mockSummary, {
|
||||
.saveRiskInsightsReport$(mockReportData, mockSummaryData, mockApplicationData, {
|
||||
organizationId: mockOrganizationId,
|
||||
userId: mockUserId,
|
||||
})
|
||||
@@ -321,17 +262,19 @@ describe("RiskInsightsReportService", () => {
|
||||
it("should call with the correct organizationId", async () => {
|
||||
// we need to ensure that the api is invoked with the specified organizationId
|
||||
// here it doesn't matter what the Api returns
|
||||
const apiResponse = {
|
||||
const apiResponse = new GetRiskInsightsReportResponse({
|
||||
id: "reportId",
|
||||
date: new Date().toISOString(),
|
||||
date: new Date(),
|
||||
organizationId: mockOrganizationId,
|
||||
reportData: mockEncryptedReport.encryptedData,
|
||||
contentEncryptionKey: mockEncryptedReport.contentEncryptionKey,
|
||||
} as GetRiskInsightsReportResponse;
|
||||
reportData: mockReportEnc.encryptedString,
|
||||
summaryData: mockSummaryEnc.encryptedString,
|
||||
applicationData: mockApplicationsEnc.encryptedString,
|
||||
contentEncryptionKey: mockEncryptedKey.encryptedString,
|
||||
});
|
||||
|
||||
const decryptedResponse: RiskInsightsReportData = {
|
||||
data: [],
|
||||
summary: {
|
||||
const decryptedResponse: DecryptedReportData = {
|
||||
reportData: [],
|
||||
summaryData: {
|
||||
totalMemberCount: 1,
|
||||
totalAtRiskMemberCount: 1,
|
||||
totalApplicationCount: 1,
|
||||
@@ -342,9 +285,9 @@ describe("RiskInsightsReportService", () => {
|
||||
totalCriticalAtRiskApplicationCount: 1,
|
||||
newApplications: [],
|
||||
},
|
||||
applicationData: [],
|
||||
};
|
||||
|
||||
const organizationId = "orgId" as OrganizationId;
|
||||
const userId = "userId" as UserId;
|
||||
|
||||
// Mock api returned encrypted data
|
||||
@@ -355,17 +298,15 @@ describe("RiskInsightsReportService", () => {
|
||||
Promise.resolve(decryptedResponse),
|
||||
);
|
||||
|
||||
await firstValueFrom(service.getRiskInsightsReport$(organizationId, userId));
|
||||
await firstValueFrom(service.getRiskInsightsReport$(mockOrganizationId, userId));
|
||||
|
||||
expect(mockRiskInsightsApiService.getRiskInsightsReport$).toHaveBeenCalledWith(
|
||||
organizationId,
|
||||
mockOrganizationId,
|
||||
);
|
||||
expect(mockRiskInsightsEncryptionService.decryptRiskInsightsReport).toHaveBeenCalledWith(
|
||||
organizationId,
|
||||
userId,
|
||||
expect.anything(), // encryptedData
|
||||
expect.anything(), // wrappedKey
|
||||
expect.any(Function), // parser
|
||||
{ organizationId: mockOrganizationId, userId },
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -375,32 +316,29 @@ describe("RiskInsightsReportService", () => {
|
||||
const organizationId = "orgId" as OrganizationId;
|
||||
const userId = "userId" as UserId;
|
||||
|
||||
const mockResponse = {
|
||||
const mockResponse = new GetRiskInsightsReportResponse({
|
||||
id: "reportId",
|
||||
date: new Date().toISOString(),
|
||||
creationDate: new Date(),
|
||||
organizationId: organizationId as OrganizationId,
|
||||
reportData: mockEncryptedReport.encryptedData,
|
||||
contentEncryptionKey: mockEncryptedReport.contentEncryptionKey,
|
||||
} as GetRiskInsightsReportResponse;
|
||||
reportData: mockReportEnc.encryptedString,
|
||||
summaryData: mockSummaryEnc.encryptedString,
|
||||
applicationData: mockApplicationsEnc.encryptedString,
|
||||
contentEncryptionKey: mockEncryptedKey.encryptedString,
|
||||
});
|
||||
|
||||
const decryptedReport = {
|
||||
data: [{ foo: "bar" }],
|
||||
};
|
||||
mockRiskInsightsApiService.getRiskInsightsReport$.mockReturnValue(of(mockResponse));
|
||||
mockRiskInsightsEncryptionService.decryptRiskInsightsReport.mockResolvedValue(
|
||||
decryptedReport,
|
||||
mockDecryptedData,
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(service.getRiskInsightsReport$(organizationId, userId));
|
||||
|
||||
expect(mockRiskInsightsEncryptionService.decryptRiskInsightsReport).toHaveBeenCalledWith(
|
||||
organizationId,
|
||||
userId,
|
||||
{ organizationId: mockOrganizationId, userId },
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(result).toEqual(decryptedReport);
|
||||
expect(result).toEqual({ ...mockDecryptedData, creationDate: mockResponse.creationDate });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
BehaviorSubject,
|
||||
catchError,
|
||||
concatMap,
|
||||
EMPTY,
|
||||
first,
|
||||
firstValueFrom,
|
||||
forkJoin,
|
||||
@@ -19,7 +20,6 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
import {
|
||||
createNewReportData,
|
||||
createNewSummaryData,
|
||||
flattenMemberDetails,
|
||||
getApplicationReportDetail,
|
||||
getFlattenedCipherDetails,
|
||||
@@ -45,7 +45,8 @@ import {
|
||||
CipherHealthReport,
|
||||
MemberDetails,
|
||||
PasswordHealthData,
|
||||
RiskInsightsReportData,
|
||||
OrganizationReportApplication,
|
||||
RiskInsightsData,
|
||||
} from "../models/report-models";
|
||||
|
||||
import { MemberCipherDetailsApiService } from "./member-cipher-details-api.service";
|
||||
@@ -54,14 +55,6 @@ import { RiskInsightsApiService } from "./risk-insights-api.service";
|
||||
import { RiskInsightsEncryptionService } from "./risk-insights-encryption.service";
|
||||
|
||||
export class RiskInsightsReportService {
|
||||
private riskInsightsReportSubject = new BehaviorSubject<ApplicationHealthReportDetail[]>([]);
|
||||
riskInsightsReport$ = this.riskInsightsReportSubject.asObservable();
|
||||
|
||||
private riskInsightsSummarySubject = new BehaviorSubject<OrganizationReportSummary>(
|
||||
createNewSummaryData(),
|
||||
);
|
||||
riskInsightsSummary$ = this.riskInsightsSummarySubject.asObservable();
|
||||
|
||||
// [FIXME] CipherData
|
||||
// Cipher data
|
||||
// private _ciphersSubject = new BehaviorSubject<CipherView[] | null>(null);
|
||||
@@ -70,9 +63,9 @@ export class RiskInsightsReportService {
|
||||
constructor(
|
||||
private cipherService: CipherService,
|
||||
private memberCipherDetailsApiService: MemberCipherDetailsApiService,
|
||||
private passwordHealthService: PasswordHealthService,
|
||||
private riskInsightsApiService: RiskInsightsApiService,
|
||||
private riskInsightsEncryptionService: RiskInsightsEncryptionService,
|
||||
private passwordHealthService: PasswordHealthService,
|
||||
) {}
|
||||
|
||||
// [FIXME] CipherData
|
||||
@@ -152,6 +145,7 @@ export class RiskInsightsReportService {
|
||||
/**
|
||||
* Report data for the aggregation of uris to like uris and getting password/member counts,
|
||||
* members, and at risk statuses.
|
||||
*
|
||||
* @param organizationId Id of the organization
|
||||
* @returns The all applications health report data
|
||||
*/
|
||||
@@ -235,17 +229,32 @@ export class RiskInsightsReportService {
|
||||
// TODO: totalCriticalMemberCount, totalCriticalAtRiskMemberCount, totalCriticalApplicationCount, totalCriticalAtRiskApplicationCount, and newApplications will be handled with future logic implementation
|
||||
return {
|
||||
totalMemberCount: uniqueMembers.length,
|
||||
totalCriticalMemberCount: 0,
|
||||
totalAtRiskMemberCount: uniqueAtRiskMembers.length,
|
||||
totalCriticalAtRiskMemberCount: 0,
|
||||
totalApplicationCount: reports.length,
|
||||
totalCriticalApplicationCount: 0,
|
||||
totalAtRiskApplicationCount: reports.filter((app) => app.atRiskPasswordCount > 0).length,
|
||||
totalCriticalMemberCount: 0,
|
||||
totalCriticalAtRiskMemberCount: 0,
|
||||
totalCriticalApplicationCount: 0,
|
||||
totalCriticalAtRiskApplicationCount: 0,
|
||||
newApplications: [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a snapshot of applications and related data associated to this report
|
||||
*
|
||||
* @param reports
|
||||
* @returns A list of applications with a critical marking flag
|
||||
*/
|
||||
generateOrganizationApplications(
|
||||
reports: ApplicationHealthReportDetail[],
|
||||
): OrganizationReportApplication[] {
|
||||
return reports.map((report) => ({
|
||||
applicationName: report.applicationName,
|
||||
isCritical: false,
|
||||
}));
|
||||
}
|
||||
|
||||
async identifyCiphers(
|
||||
data: ApplicationHealthReportDetail[],
|
||||
organizationId: OrganizationId,
|
||||
@@ -272,12 +281,12 @@ export class RiskInsightsReportService {
|
||||
getRiskInsightsReport$(
|
||||
organizationId: OrganizationId,
|
||||
userId: UserId,
|
||||
): Observable<RiskInsightsReportData> {
|
||||
): Observable<RiskInsightsData> {
|
||||
return this.riskInsightsApiService.getRiskInsightsReport$(organizationId).pipe(
|
||||
switchMap((response): Observable<RiskInsightsReportData> => {
|
||||
switchMap((response) => {
|
||||
if (!response) {
|
||||
// Return an empty report and summary if response is falsy
|
||||
return of<RiskInsightsReportData>(createNewReportData());
|
||||
return of<RiskInsightsData>(createNewReportData());
|
||||
}
|
||||
if (!response.contentEncryptionKey || response.contentEncryptionKey.data == "") {
|
||||
return throwError(() => new Error("Report key not found"));
|
||||
@@ -285,15 +294,43 @@ export class RiskInsightsReportService {
|
||||
if (!response.reportData) {
|
||||
return throwError(() => new Error("Report data not found"));
|
||||
}
|
||||
if (!response.summaryData) {
|
||||
return throwError(() => new Error("Summary data not found"));
|
||||
}
|
||||
if (!response.applicationData) {
|
||||
return throwError(() => new Error("Application data not found"));
|
||||
}
|
||||
|
||||
return from(
|
||||
this.riskInsightsEncryptionService.decryptRiskInsightsReport<RiskInsightsReportData>(
|
||||
organizationId,
|
||||
userId,
|
||||
response.reportData,
|
||||
this.riskInsightsEncryptionService.decryptRiskInsightsReport(
|
||||
{
|
||||
organizationId,
|
||||
userId,
|
||||
},
|
||||
{
|
||||
encryptedReportData: response.reportData,
|
||||
encryptedSummaryData: response.summaryData,
|
||||
encryptedApplicationData: response.applicationData,
|
||||
},
|
||||
response.contentEncryptionKey,
|
||||
(data) => data as RiskInsightsReportData,
|
||||
),
|
||||
).pipe(map((decryptedReport) => decryptedReport ?? createNewReportData()));
|
||||
).pipe(
|
||||
map((decryptedData) => ({
|
||||
reportData: decryptedData.reportData,
|
||||
summaryData: decryptedData.summaryData,
|
||||
applicationData: decryptedData.applicationData,
|
||||
creationDate: response.creationDate,
|
||||
})),
|
||||
catchError((error: unknown) => {
|
||||
// TODO Handle errors appropriately
|
||||
// console.error("An error occurred when decrypting report", error);
|
||||
return EMPTY;
|
||||
}),
|
||||
);
|
||||
}),
|
||||
catchError((error: unknown) => {
|
||||
// console.error("An error occurred when fetching the last report", error);
|
||||
return EMPTY;
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -308,6 +345,7 @@ export class RiskInsightsReportService {
|
||||
saveRiskInsightsReport$(
|
||||
report: ApplicationHealthReportDetail[],
|
||||
summary: OrganizationReportSummary,
|
||||
applications: OrganizationReportApplication[],
|
||||
encryptionParameters: {
|
||||
organizationId: OrganizationId;
|
||||
userId: UserId;
|
||||
@@ -315,28 +353,43 @@ export class RiskInsightsReportService {
|
||||
): Observable<SaveRiskInsightsReportResponse> {
|
||||
return from(
|
||||
this.riskInsightsEncryptionService.encryptRiskInsightsReport(
|
||||
encryptionParameters.organizationId,
|
||||
encryptionParameters.userId,
|
||||
{
|
||||
data: report,
|
||||
summary: summary,
|
||||
organizationId: encryptionParameters.organizationId,
|
||||
userId: encryptionParameters.userId,
|
||||
},
|
||||
{
|
||||
reportData: report,
|
||||
summaryData: summary,
|
||||
applicationData: applications,
|
||||
},
|
||||
),
|
||||
).pipe(
|
||||
map(({ encryptedData, contentEncryptionKey }) => ({
|
||||
data: {
|
||||
organizationId: encryptionParameters.organizationId,
|
||||
date: new Date().toISOString(),
|
||||
reportData: encryptedData.toSdk(),
|
||||
contentEncryptionKey: contentEncryptionKey.toSdk(),
|
||||
},
|
||||
})),
|
||||
map(
|
||||
({
|
||||
encryptedReportData,
|
||||
encryptedSummaryData,
|
||||
encryptedApplicationData,
|
||||
contentEncryptionKey,
|
||||
}) => ({
|
||||
data: {
|
||||
organizationId: encryptionParameters.organizationId,
|
||||
creationDate: new Date().toISOString(),
|
||||
reportData: encryptedReportData.toSdk(),
|
||||
summaryData: encryptedSummaryData.toSdk(),
|
||||
applicationData: encryptedApplicationData.toSdk(),
|
||||
contentEncryptionKey: contentEncryptionKey.toSdk(),
|
||||
},
|
||||
}),
|
||||
),
|
||||
switchMap((encryptedReport) =>
|
||||
this.riskInsightsApiService.saveRiskInsightsReport$(
|
||||
encryptedReport,
|
||||
encryptionParameters.organizationId,
|
||||
),
|
||||
),
|
||||
catchError((error: unknown) => {
|
||||
return EMPTY;
|
||||
}),
|
||||
map((response) => {
|
||||
if (!isSaveRiskInsightsReportResponse(response)) {
|
||||
throw new Error("Invalid response from API");
|
||||
@@ -457,6 +510,13 @@ export class RiskInsightsReportService {
|
||||
const applicationMap = new Map<string, CipherHealthReport[]>();
|
||||
|
||||
cipherHealthData.forEach((cipher: CipherHealthReport) => {
|
||||
// Warning: Currently does not show ciphers with NO Application
|
||||
// if (cipher.applications.length === 0) {
|
||||
// const existingApplication = applicationMap.get("None") || [];
|
||||
// existingApplication.push(cipher);
|
||||
// applicationMap.set("None", existingApplication);
|
||||
// }
|
||||
|
||||
cipher.applications.forEach((application) => {
|
||||
const existingApplication = applicationMap.get(application) || [];
|
||||
existingApplication.push(cipher);
|
||||
|
||||
@@ -29,28 +29,32 @@ import { RiskInsightsComponent } from "./risk-insights.component";
|
||||
@NgModule({
|
||||
imports: [RiskInsightsComponent, AccessIntelligenceRoutingModule],
|
||||
providers: [
|
||||
{
|
||||
safeProvider({
|
||||
provide: MemberCipherDetailsApiService,
|
||||
useClass: MemberCipherDetailsApiService,
|
||||
deps: [ApiService],
|
||||
},
|
||||
{
|
||||
}),
|
||||
safeProvider({
|
||||
provide: PasswordHealthService,
|
||||
useClass: PasswordHealthService,
|
||||
deps: [PasswordStrengthServiceAbstraction, AuditService],
|
||||
},
|
||||
{
|
||||
}),
|
||||
safeProvider({
|
||||
provide: RiskInsightsApiService,
|
||||
useClass: RiskInsightsApiService,
|
||||
deps: [ApiService],
|
||||
},
|
||||
{
|
||||
}),
|
||||
safeProvider({
|
||||
provide: RiskInsightsReportService,
|
||||
useClass: RiskInsightsReportService,
|
||||
deps: [
|
||||
CipherService,
|
||||
MemberCipherDetailsApiService,
|
||||
PasswordHealthService,
|
||||
RiskInsightsApiService,
|
||||
RiskInsightsEncryptionService,
|
||||
PasswordHealthService,
|
||||
],
|
||||
},
|
||||
}),
|
||||
safeProvider({
|
||||
provide: RiskInsightsDataService,
|
||||
deps: [
|
||||
@@ -78,7 +82,7 @@ import { RiskInsightsComponent } from "./risk-insights.component";
|
||||
safeProvider({
|
||||
provide: AllActivitiesService,
|
||||
useClass: AllActivitiesService,
|
||||
deps: [],
|
||||
deps: [RiskInsightsDataService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SecurityTasksApiService,
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
@if (isLoading$ | async) {
|
||||
<div *ngIf="isLoading$ | async">
|
||||
<tools-risk-insights-loading></tools-risk-insights-loading>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!(isLoading$ | async)) {
|
||||
@if (dataService.isLoading$ | async) {
|
||||
<dirt-risk-insights-loading></dirt-risk-insights-loading>
|
||||
} @else {
|
||||
<ul
|
||||
class="tw-inline-grid tw-grid-cols-3 tw-gap-6 tw-m-0 tw-p-0 tw-w-full tw-auto-cols-auto tw-list-none"
|
||||
>
|
||||
|
||||
@@ -20,7 +20,7 @@ import { ApplicationsLoadingComponent } from "./risk-insights-loading.component"
|
||||
import { RiskInsightsTabType } from "./risk-insights.component";
|
||||
|
||||
@Component({
|
||||
selector: "tools-all-activity",
|
||||
selector: "dirt-all-activity",
|
||||
imports: [
|
||||
ApplicationsLoadingComponent,
|
||||
SharedModule,
|
||||
@@ -30,7 +30,6 @@ import { RiskInsightsTabType } from "./risk-insights.component";
|
||||
templateUrl: "./all-activity.component.html",
|
||||
})
|
||||
export class AllActivityComponent implements OnInit {
|
||||
protected isLoading$ = this.dataService.isLoading$;
|
||||
organization: Organization | null = null;
|
||||
totalCriticalAppsAtRiskMemberCount = 0;
|
||||
totalCriticalAppsCount = 0;
|
||||
|
||||
@@ -1,96 +1,102 @@
|
||||
<div *ngIf="isLoading$ | async">
|
||||
<tools-risk-insights-loading></tools-risk-insights-loading>
|
||||
</div>
|
||||
<div class="tw-mt-4" *ngIf="!(isLoading$ | async) && !dataSource.data.length">
|
||||
<bit-no-items [icon]="noItemsIcon" class="tw-text-main">
|
||||
<ng-container slot="title">
|
||||
<h2 class="tw-font-semibold tw-mt-4">
|
||||
{{ "noAppsInOrgTitle" | i18n: organization?.name }}
|
||||
</h2>
|
||||
</ng-container>
|
||||
<ng-container slot="description">
|
||||
<div class="tw-flex tw-flex-col tw-mb-2">
|
||||
<span class="tw-text-muted">
|
||||
{{ "noAppsInOrgDescription" | i18n }}
|
||||
</span>
|
||||
<a class="tw-text-primary-600" routerLink="/login">{{ "learnMore" | i18n }}</a>
|
||||
@if (dataService.isLoading$ | async) {
|
||||
<dirt-risk-insights-loading></dirt-risk-insights-loading>
|
||||
} @else {
|
||||
@let drawerDetails = dataService.drawerDetails$ | async;
|
||||
@if (!dataSource.data.length) {
|
||||
<div class="tw-mt-4">
|
||||
<bit-no-items [icon]="noItemsIcon" class="tw-text-main">
|
||||
<ng-container slot="title">
|
||||
<h2 class="tw-font-semibold tw-mt-4">
|
||||
{{
|
||||
"noAppsInOrgTitle"
|
||||
| i18n: (dataService.organizationDetails$ | async)?.organizationName || ""
|
||||
}}
|
||||
</h2>
|
||||
</ng-container>
|
||||
<ng-container slot="description">
|
||||
<div class="tw-flex tw-flex-col tw-mb-2">
|
||||
<span class="tw-text-muted">
|
||||
{{ "noAppsInOrgDescription" | i18n }}
|
||||
</span>
|
||||
<a class="tw-text-primary-600" routerLink="/login">{{ "learnMore" | i18n }}</a>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container slot="button">
|
||||
<button (click)="goToCreateNewLoginItem()" bitButton buttonType="primary" type="button">
|
||||
{{ "createNewLoginItem" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-no-items>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="tw-mt-4 tw-flex tw-flex-col">
|
||||
<h2 class="tw-mb-6" bitTypography="h2">{{ "allApplications" | i18n }}</h2>
|
||||
<div class="tw-flex tw-gap-6">
|
||||
<button
|
||||
type="button"
|
||||
class="tw-flex-1"
|
||||
tabindex="0"
|
||||
(click)="dataService.setDrawerForOrgAtRiskMembers('allAppsOrgAtRiskMembers')"
|
||||
>
|
||||
<dirt-card
|
||||
#allAppsOrgAtRiskMembers
|
||||
class="tw-w-full"
|
||||
[ngClass]="{
|
||||
'tw-bg-primary-100': drawerDetails.invokerId === 'allAppsOrgAtRiskMembers',
|
||||
}"
|
||||
[title]="'atRiskMembers' | i18n"
|
||||
[value]="applicationSummary.totalAtRiskMemberCount"
|
||||
[maxValue]="applicationSummary.totalMemberCount"
|
||||
>
|
||||
</dirt-card>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="tw-flex-1"
|
||||
tabindex="0"
|
||||
(click)="dataService.setDrawerForOrgAtRiskApps('allAppsOrgAtRiskApplications')"
|
||||
>
|
||||
<dirt-card
|
||||
#allAppsOrgAtRiskApplications
|
||||
class="tw-w-full"
|
||||
[ngClass]="{
|
||||
'tw-bg-primary-100': drawerDetails.invokerId === 'allAppsOrgAtRiskApplications',
|
||||
}"
|
||||
[title]="'atRiskApplications' | i18n"
|
||||
[value]="applicationSummary.totalAtRiskApplicationCount"
|
||||
[maxValue]="applicationSummary.totalApplicationCount"
|
||||
>
|
||||
</dirt-card>
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container slot="button">
|
||||
<button (click)="goToCreateNewLoginItem()" bitButton buttonType="primary" type="button">
|
||||
{{ "createNewLoginItem" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-no-items>
|
||||
</div>
|
||||
<div class="tw-mt-4 tw-flex tw-flex-col" *ngIf="!(isLoading$ | async) && dataSource.data.length">
|
||||
<h2 class="tw-mb-6" bitTypography="h2">{{ "allApplications" | i18n }}</h2>
|
||||
@if (dataService.drawerDetails$ | async; as drawerDetails) {
|
||||
<div class="tw-flex tw-gap-6">
|
||||
<button
|
||||
type="button"
|
||||
class="tw-flex-1"
|
||||
tabindex="0"
|
||||
(click)="showOrgAtRiskMembers('allAppsOrgAtRiskMembers')"
|
||||
>
|
||||
<dirt-card
|
||||
#allAppsOrgAtRiskMembers
|
||||
class="tw-w-full"
|
||||
[ngClass]="{
|
||||
'tw-bg-primary-100': drawerDetails.invokerId === 'allAppsOrgAtRiskMembers',
|
||||
}"
|
||||
[title]="'atRiskMembers' | i18n"
|
||||
[value]="applicationSummary.totalAtRiskMemberCount"
|
||||
[maxValue]="applicationSummary.totalMemberCount"
|
||||
<div class="tw-flex tw-mt-8 tw-mb-4 tw-gap-4">
|
||||
<bit-search
|
||||
[placeholder]="'searchApps' | i18n"
|
||||
class="tw-grow"
|
||||
[formControl]="searchControl"
|
||||
></bit-search>
|
||||
<button
|
||||
type="button"
|
||||
[buttonType]="'primary'"
|
||||
bitButton
|
||||
[disabled]="!selectedUrls.size"
|
||||
[loading]="markingAsCritical"
|
||||
(click)="markAppsAsCritical()"
|
||||
>
|
||||
</dirt-card>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="tw-flex-1"
|
||||
tabindex="0"
|
||||
(click)="showOrgAtRiskApps('allAppsOrgAtRiskApplications')"
|
||||
>
|
||||
<dirt-card
|
||||
#allAppsOrgAtRiskApplications
|
||||
class="tw-w-full"
|
||||
[ngClass]="{
|
||||
'tw-bg-primary-100': drawerDetails.invokerId === 'allAppsOrgAtRiskApplications',
|
||||
}"
|
||||
[title]="'atRiskApplications' | i18n"
|
||||
[value]="applicationSummary.totalAtRiskApplicationCount"
|
||||
[maxValue]="applicationSummary.totalApplicationCount"
|
||||
>
|
||||
</dirt-card>
|
||||
</button>
|
||||
</div>
|
||||
<div class="tw-flex tw-mt-8 tw-mb-4 tw-gap-4">
|
||||
<bit-search
|
||||
[placeholder]="'searchApps' | i18n"
|
||||
class="tw-grow"
|
||||
[formControl]="searchControl"
|
||||
></bit-search>
|
||||
<button
|
||||
type="button"
|
||||
[buttonType]="'primary'"
|
||||
bitButton
|
||||
[disabled]="!selectedUrls.size"
|
||||
[loading]="markingAsCritical"
|
||||
(click)="markAppsAsCritical()"
|
||||
>
|
||||
<i class="bwi tw-mr-2" [ngClass]="selectedUrls.size ? 'bwi-star-f' : 'bwi-star'"></i>
|
||||
{{ "markAppAsCritical" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
<i class="bwi tw-mr-2" [ngClass]="selectedUrls.size ? 'bwi-star-f' : 'bwi-star'"></i>
|
||||
{{ "markAppAsCritical" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<app-table-row-scrollable
|
||||
[dataSource]="dataSource"
|
||||
[showRowCheckBox]="true"
|
||||
[showRowMenuForCriticalApps]="false"
|
||||
[selectedUrls]="selectedUrls"
|
||||
[openApplication]="drawerDetails.invokerId || ''"
|
||||
[checkboxChange]="onCheckboxChange"
|
||||
[showAppAtRiskMembers]="showAppAtRiskMembers"
|
||||
></app-table-row-scrollable>
|
||||
<app-table-row-scrollable
|
||||
[dataSource]="dataSource"
|
||||
[showRowCheckBox]="true"
|
||||
[showRowMenuForCriticalApps]="false"
|
||||
[selectedUrls]="selectedUrls"
|
||||
[openApplication]="drawerDetails.invokerId || ''"
|
||||
[checkboxChange]="onCheckboxChange"
|
||||
[showAppAtRiskMembers]="showAppAtRiskMembers"
|
||||
></app-table-row-scrollable>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -2,33 +2,17 @@ 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, switchMap } from "rxjs";
|
||||
import { debounceTime } from "rxjs";
|
||||
|
||||
import { Security } from "@bitwarden/assets/svg";
|
||||
import {
|
||||
AllActivitiesService,
|
||||
CriticalAppsService,
|
||||
ApplicationHealthReportDetailEnriched,
|
||||
RiskInsightsDataService,
|
||||
RiskInsightsReportService,
|
||||
} from "@bitwarden/bit-common/dirt/reports/risk-insights";
|
||||
import { createNewSummaryData } from "@bitwarden/bit-common/dirt/reports/risk-insights/helpers";
|
||||
import {
|
||||
LEGACY_ApplicationHealthReportDetailWithCriticalFlag,
|
||||
LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher,
|
||||
} from "@bitwarden/bit-common/dirt/reports/risk-insights/models/password-health";
|
||||
import { OrganizationReportSummary } from "@bitwarden/bit-common/dirt/reports/risk-insights/models/report-models";
|
||||
import { RiskInsightsEncryptionService } from "@bitwarden/bit-common/dirt/reports/risk-insights/services/risk-insights-encryption.service";
|
||||
import {
|
||||
getOrganizationById,
|
||||
OrganizationService,
|
||||
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import {
|
||||
IconButtonModule,
|
||||
NoItemsModule,
|
||||
@@ -45,7 +29,7 @@ import { AppTableRowScrollableComponent } from "./app-table-row-scrollable.compo
|
||||
import { ApplicationsLoadingComponent } from "./risk-insights-loading.component";
|
||||
|
||||
@Component({
|
||||
selector: "tools-all-applications",
|
||||
selector: "dirt-all-applications",
|
||||
templateUrl: "./all-applications.component.html",
|
||||
imports: [
|
||||
ApplicationsLoadingComponent,
|
||||
@@ -60,97 +44,44 @@ import { ApplicationsLoadingComponent } from "./risk-insights-loading.component"
|
||||
],
|
||||
})
|
||||
export class AllApplicationsComponent implements OnInit {
|
||||
protected dataSource =
|
||||
new TableDataSource<LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher>();
|
||||
protected dataSource = new TableDataSource<ApplicationHealthReportDetailEnriched>();
|
||||
protected selectedUrls: Set<string> = new Set<string>();
|
||||
protected searchControl = new FormControl("", { nonNullable: true });
|
||||
protected loading = true;
|
||||
protected organization = new Organization();
|
||||
noItemsIcon = Security;
|
||||
protected markingAsCritical = false;
|
||||
protected applicationSummary: OrganizationReportSummary = createNewSummaryData();
|
||||
|
||||
destroyRef = inject(DestroyRef);
|
||||
isLoading$: Observable<boolean> = of(false);
|
||||
|
||||
async ngOnInit() {
|
||||
const organizationId = this.activatedRoute.snapshot.paramMap.get("organizationId");
|
||||
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
|
||||
if (organizationId) {
|
||||
const organization$ = this.organizationService
|
||||
.organizations$(userId)
|
||||
.pipe(getOrganizationById(organizationId));
|
||||
|
||||
combineLatest([
|
||||
this.dataService.applications$,
|
||||
this.criticalAppsService.getAppsListForOrg(organizationId as OrganizationId),
|
||||
organization$,
|
||||
])
|
||||
.pipe(
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
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 LEGACY_ApplicationHealthReportDetailWithCriticalFlag[];
|
||||
return { data, organization };
|
||||
}
|
||||
|
||||
return { data: applications, organization };
|
||||
}),
|
||||
switchMap(async ({ data, organization }) => {
|
||||
if (data && organization) {
|
||||
const dataWithCiphers = await this.reportService.identifyCiphers(
|
||||
data,
|
||||
organization.id as OrganizationId,
|
||||
);
|
||||
|
||||
return {
|
||||
data: dataWithCiphers,
|
||||
organization,
|
||||
};
|
||||
}
|
||||
|
||||
return { data: [], organization };
|
||||
}),
|
||||
)
|
||||
.subscribe(({ data, organization }) => {
|
||||
if (data) {
|
||||
this.dataSource.data = data;
|
||||
this.applicationSummary = this.reportService.generateApplicationsSummary(data);
|
||||
this.allActivitiesService.setAllAppsReportSummary(this.applicationSummary);
|
||||
}
|
||||
if (organization) {
|
||||
this.organization = organization;
|
||||
}
|
||||
});
|
||||
|
||||
this.isLoading$ = this.dataService.isLoading$;
|
||||
}
|
||||
}
|
||||
|
||||
constructor(
|
||||
protected cipherService: CipherService,
|
||||
protected i18nService: I18nService,
|
||||
protected activatedRoute: ActivatedRoute,
|
||||
protected toastService: ToastService,
|
||||
protected configService: ConfigService,
|
||||
protected dataService: RiskInsightsDataService,
|
||||
protected organizationService: OrganizationService,
|
||||
protected reportService: RiskInsightsReportService,
|
||||
private accountService: AccountService,
|
||||
protected criticalAppsService: CriticalAppsService,
|
||||
protected riskInsightsEncryptionService: RiskInsightsEncryptionService,
|
||||
protected allActivitiesService: AllActivitiesService,
|
||||
// protected allActivitiesService: AllActivitiesService,
|
||||
) {
|
||||
this.searchControl.valueChanges
|
||||
.pipe(debounceTime(200), takeUntilDestroyed())
|
||||
.subscribe((v) => (this.dataSource.filter = v));
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.dataService.reportResults$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
|
||||
next: (report) => {
|
||||
this.applicationSummary = report?.summaryData ?? createNewSummaryData();
|
||||
this.dataSource.data = report?.reportData ?? [];
|
||||
},
|
||||
error: () => {
|
||||
this.dataSource.data = [];
|
||||
},
|
||||
});
|
||||
|
||||
// TODO
|
||||
// this.applicationSummary = this.reportService.generateApplicationsSummary(data);
|
||||
// this.allActivitiesService.setAllAppsReportSummary(this.applicationSummary);
|
||||
}
|
||||
|
||||
goToCreateNewLoginItem = async () => {
|
||||
// TODO: implement
|
||||
this.toastService.showToast({
|
||||
@@ -167,41 +98,31 @@ export class AllApplicationsComponent implements OnInit {
|
||||
markAppsAsCritical = async () => {
|
||||
this.markingAsCritical = true;
|
||||
|
||||
try {
|
||||
await this.criticalAppsService.setCriticalApps(
|
||||
this.organization.id as OrganizationId,
|
||||
Array.from(this.selectedUrls),
|
||||
);
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: "",
|
||||
message: this.i18nService.t("applicationsMarkedAsCriticalSuccess"),
|
||||
this.dataService
|
||||
.saveCriticalApplications(Array.from(this.selectedUrls))
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: "",
|
||||
message: this.i18nService.t("applicationsMarkedAsCriticalSuccess"),
|
||||
});
|
||||
this.selectedUrls.clear();
|
||||
this.markingAsCritical = false;
|
||||
},
|
||||
error: () => {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: "",
|
||||
message: this.i18nService.t("applicationsMarkedAsCriticalFail"),
|
||||
});
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
this.selectedUrls.clear();
|
||||
this.markingAsCritical = false;
|
||||
}
|
||||
};
|
||||
|
||||
showAppAtRiskMembers = async (applicationName: string) => {
|
||||
const info = {
|
||||
members:
|
||||
this.dataSource.data.find((app) => app.applicationName === applicationName)
|
||||
?.atRiskMemberDetails ?? [],
|
||||
applicationName,
|
||||
};
|
||||
this.dataService.setDrawerForAppAtRiskMembers(info, applicationName);
|
||||
};
|
||||
|
||||
showOrgAtRiskMembers = async (invokerId: string) => {
|
||||
const dialogData = this.reportService.generateAtRiskMemberList(this.dataSource.data);
|
||||
this.dataService.setDrawerForOrgAtRiskMembers(dialogData, invokerId);
|
||||
};
|
||||
|
||||
showOrgAtRiskApps = async (invokerId: string) => {
|
||||
const data = this.reportService.generateAtRiskApplicationList(this.dataSource.data);
|
||||
this.dataService.setDrawerForOrgAtRiskApps(data, invokerId);
|
||||
await this.dataService.setDrawerForAppAtRiskMembers(applicationName);
|
||||
};
|
||||
|
||||
onCheckboxChange = (applicationName: string, event: Event) => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { CommonModule } from "@angular/common";
|
||||
import { Component, Input } from "@angular/core";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher } from "@bitwarden/bit-common/dirt/reports/risk-insights/models/password-health";
|
||||
import { ApplicationHealthReportDetailEnriched } from "@bitwarden/bit-common/dirt/reports/risk-insights";
|
||||
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";
|
||||
@@ -14,7 +14,7 @@ import { PipesModule } from "@bitwarden/web-vault/app/vault/individual-vault/pip
|
||||
})
|
||||
export class AppTableRowScrollableComponent {
|
||||
@Input()
|
||||
dataSource!: TableDataSource<LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher>;
|
||||
dataSource!: TableDataSource<ApplicationHealthReportDetailEnriched>;
|
||||
@Input() showRowMenuForCriticalApps: boolean = false;
|
||||
@Input() showRowCheckBox: boolean = false;
|
||||
@Input() selectedUrls: Set<string> = new Set<string>();
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
type="button"
|
||||
class="tw-flex-1"
|
||||
tabindex="0"
|
||||
(click)="showOrgAtRiskMembers('criticalAppsAtRiskMembers')"
|
||||
(click)="dataService.setDrawerForOrgAtRiskMembers('criticalAppsAtRiskMembers')"
|
||||
>
|
||||
<dirt-card
|
||||
#criticalAppsAtRiskMembers
|
||||
@@ -67,7 +67,7 @@
|
||||
type="button"
|
||||
class="tw-flex-1"
|
||||
tabindex="0"
|
||||
(click)="showOrgAtRiskApps('criticalAppsAtRiskApplications')"
|
||||
(click)="dataService.setDrawerForOrgAtRiskApps('criticalAppsAtRiskApplications')"
|
||||
>
|
||||
<dirt-card
|
||||
#criticalAppsAtRiskApplications
|
||||
@@ -96,7 +96,7 @@
|
||||
[showRowMenuForCriticalApps]="true"
|
||||
[openApplication]="drawerDetails.invokerId || ''"
|
||||
[showAppAtRiskMembers]="showAppAtRiskMembers"
|
||||
[unmarkAsCritical]="unmarkAsCritical"
|
||||
[unmarkAsCritical]="removeCriticalApplication"
|
||||
></app-table-row-scrollable>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -4,23 +4,15 @@ 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, firstValueFrom, map, switchMap } from "rxjs";
|
||||
import { debounceTime, EMPTY, map, switchMap } from "rxjs";
|
||||
|
||||
import { Security } from "@bitwarden/assets/svg";
|
||||
import {
|
||||
AllActivitiesService,
|
||||
CriticalAppsService,
|
||||
ApplicationHealthReportDetailEnriched,
|
||||
RiskInsightsDataService,
|
||||
RiskInsightsReportService,
|
||||
} from "@bitwarden/bit-common/dirt/reports/risk-insights";
|
||||
import {
|
||||
LEGACY_ApplicationHealthReportDetailWithCriticalFlag,
|
||||
LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher,
|
||||
} from "@bitwarden/bit-common/dirt/reports/risk-insights/models/password-health";
|
||||
import { createNewSummaryData } from "@bitwarden/bit-common/dirt/reports/risk-insights/helpers";
|
||||
import { OrganizationReportSummary } from "@bitwarden/bit-common/dirt/reports/risk-insights/models/report-models";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CipherId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { SecurityTaskType } from "@bitwarden/common/vault/tasks";
|
||||
@@ -37,7 +29,7 @@ import { AppTableRowScrollableComponent } from "./app-table-row-scrollable.compo
|
||||
import { RiskInsightsTabType } from "./risk-insights.component";
|
||||
|
||||
@Component({
|
||||
selector: "tools-critical-applications",
|
||||
selector: "dirt-critical-applications",
|
||||
templateUrl: "./critical-applications.component.html",
|
||||
imports: [
|
||||
CardComponent,
|
||||
@@ -51,60 +43,57 @@ import { RiskInsightsTabType } from "./risk-insights.component";
|
||||
providers: [DefaultAdminTaskService],
|
||||
})
|
||||
export class CriticalApplicationsComponent implements OnInit {
|
||||
protected dataSource =
|
||||
new TableDataSource<LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher>();
|
||||
protected selectedIds: Set<number> = new Set<number>();
|
||||
protected searchControl = new FormControl("", { nonNullable: true });
|
||||
private destroyRef = inject(DestroyRef);
|
||||
protected loading = false;
|
||||
protected enableRequestPasswordChange = false;
|
||||
protected organizationId: OrganizationId;
|
||||
protected applicationSummary = {} as OrganizationReportSummary;
|
||||
noItemsIcon = Security;
|
||||
enableRequestPasswordChange = false;
|
||||
|
||||
protected dataSource = new TableDataSource<ApplicationHealthReportDetailEnriched>();
|
||||
protected applicationSummary = {} as OrganizationReportSummary;
|
||||
|
||||
protected selectedIds: Set<number> = new Set<number>();
|
||||
protected searchControl = new FormControl("", { nonNullable: true });
|
||||
|
||||
constructor(
|
||||
protected activatedRoute: ActivatedRoute,
|
||||
protected router: Router,
|
||||
protected toastService: ToastService,
|
||||
protected dataService: RiskInsightsDataService,
|
||||
protected i18nService: I18nService,
|
||||
private adminTaskService: DefaultAdminTaskService,
|
||||
) {
|
||||
this.searchControl.valueChanges
|
||||
.pipe(debounceTime(200), takeUntilDestroyed())
|
||||
.subscribe((v) => (this.dataSource.filter = v));
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.organizationId = this.activatedRoute.snapshot.paramMap.get(
|
||||
"organizationId",
|
||||
) as OrganizationId;
|
||||
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
this.criticalAppsService.loadOrganizationContext(this.organizationId as OrganizationId, userId);
|
||||
|
||||
if (this.organizationId) {
|
||||
combineLatest([
|
||||
this.dataService.applications$,
|
||||
this.criticalAppsService.getAppsListForOrg(this.organizationId as OrganizationId),
|
||||
])
|
||||
.pipe(
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
map(([applications, criticalApps]) => {
|
||||
const criticalUrls = criticalApps.map((ca) => ca.uri);
|
||||
const data = applications?.map((app) => ({
|
||||
...app,
|
||||
isMarkedAsCritical: criticalUrls.includes(app.applicationName),
|
||||
})) as LEGACY_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) {
|
||||
this.dataSource.data = applications;
|
||||
this.applicationSummary = this.reportService.generateApplicationsSummary(applications);
|
||||
this.enableRequestPasswordChange = this.applicationSummary.totalAtRiskMemberCount > 0;
|
||||
this.allActivitiesService.setCriticalAppsReportSummary(this.applicationSummary);
|
||||
this.allActivitiesService.setAllAppsReportDetails(applications);
|
||||
this.dataService.criticalReportResults$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
|
||||
next: (criticalReport) => {
|
||||
this.dataSource.data = criticalReport?.reportData ?? [];
|
||||
this.applicationSummary = criticalReport?.summaryData ?? createNewSummaryData();
|
||||
this.enableRequestPasswordChange = criticalReport?.summaryData?.totalAtRiskMemberCount > 0;
|
||||
},
|
||||
error: () => {
|
||||
this.dataSource.data = [];
|
||||
this.applicationSummary = createNewSummaryData();
|
||||
this.enableRequestPasswordChange = false;
|
||||
},
|
||||
});
|
||||
this.activatedRoute.paramMap
|
||||
.pipe(
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
map((params) => params.get("organizationId")),
|
||||
switchMap(async (orgId) => {
|
||||
if (orgId) {
|
||||
this.organizationId = orgId as OrganizationId;
|
||||
} else {
|
||||
return EMPTY;
|
||||
}
|
||||
});
|
||||
}
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
goToAllAppsTab = async () => {
|
||||
@@ -117,26 +106,25 @@ export class CriticalApplicationsComponent implements OnInit {
|
||||
);
|
||||
};
|
||||
|
||||
unmarkAsCritical = async (hostname: string) => {
|
||||
try {
|
||||
await this.criticalAppsService.dropCriticalApp(
|
||||
this.organizationId as OrganizationId,
|
||||
hostname,
|
||||
);
|
||||
} catch {
|
||||
this.toastService.showToast({
|
||||
message: this.i18nService.t("unexpectedError"),
|
||||
variant: "error",
|
||||
title: this.i18nService.t("error"),
|
||||
removeCriticalApplication = async (hostname: string) => {
|
||||
this.dataService
|
||||
.removeCriticalApplication(hostname)
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.toastService.showToast({
|
||||
message: this.i18nService.t("criticalApplicationUnmarkedSuccessfully"),
|
||||
variant: "success",
|
||||
});
|
||||
},
|
||||
error: () => {
|
||||
this.toastService.showToast({
|
||||
message: this.i18nService.t("unexpectedError"),
|
||||
variant: "error",
|
||||
title: this.i18nService.t("error"),
|
||||
});
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.toastService.showToast({
|
||||
message: this.i18nService.t("criticalApplicationUnmarkedSuccessfully"),
|
||||
variant: "success",
|
||||
});
|
||||
this.dataSource.data = this.dataSource.data.filter((app) => app.applicationName !== hostname);
|
||||
};
|
||||
|
||||
async requestPasswordChange() {
|
||||
@@ -167,42 +155,7 @@ export class CriticalApplicationsComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
constructor(
|
||||
protected activatedRoute: ActivatedRoute,
|
||||
protected router: Router,
|
||||
protected toastService: ToastService,
|
||||
protected dataService: RiskInsightsDataService,
|
||||
protected criticalAppsService: CriticalAppsService,
|
||||
protected reportService: RiskInsightsReportService,
|
||||
protected i18nService: I18nService,
|
||||
private configService: ConfigService,
|
||||
private adminTaskService: DefaultAdminTaskService,
|
||||
private accountService: AccountService,
|
||||
private allActivitiesService: AllActivitiesService,
|
||||
) {
|
||||
this.searchControl.valueChanges
|
||||
.pipe(debounceTime(200), takeUntilDestroyed())
|
||||
.subscribe((v) => (this.dataSource.filter = v));
|
||||
}
|
||||
|
||||
showAppAtRiskMembers = async (applicationName: string) => {
|
||||
const data = {
|
||||
members:
|
||||
this.dataSource.data.find((app) => app.applicationName === applicationName)
|
||||
?.atRiskMemberDetails ?? [],
|
||||
applicationName,
|
||||
};
|
||||
this.dataService.setDrawerForAppAtRiskMembers(data, applicationName);
|
||||
};
|
||||
|
||||
showOrgAtRiskMembers = async (invokerId: string) => {
|
||||
const data = this.reportService.generateAtRiskMemberList(this.dataSource.data);
|
||||
this.dataService.setDrawerForOrgAtRiskMembers(data, invokerId);
|
||||
};
|
||||
|
||||
showOrgAtRiskApps = async (invokerId: string) => {
|
||||
const data = this.reportService.generateAtRiskApplicationList(this.dataSource.data);
|
||||
this.dataService.setDrawerForOrgAtRiskApps(data, invokerId);
|
||||
await this.dataService.setDrawerForAppAtRiskMembers(applicationName);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Component } from "@angular/core";
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
|
||||
@Component({
|
||||
selector: "tools-risk-insights-loading",
|
||||
selector: "dirt-risk-insights-loading",
|
||||
imports: [CommonModule, JslibModule],
|
||||
templateUrl: "./risk-insights-loading.component.html",
|
||||
})
|
||||
|
||||
@@ -4,19 +4,23 @@
|
||||
{{ "reviewAtRiskPasswords" | i18n }}
|
||||
</div>
|
||||
<div
|
||||
*ngIf="dataLastUpdated$ | async"
|
||||
class="tw-bg-primary-100 tw-rounded-lg tw-w-full tw-px-8 tw-py-4 tw-my-4 tw-flex tw-items-center"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-exclamation-triangle bwi-lg tw-text-[1.2rem] tw-text-muted"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-mx-4">{{
|
||||
"dataLastUpdated" | i18n: (dataLastUpdated$ | async | date: "MMMM d, y 'at' h:mm a")
|
||||
}}</span>
|
||||
<span class="tw-flex tw-justify-center tw-w-16">
|
||||
@if (dataLastUpdated) {
|
||||
<span class="tw-mx-4">{{
|
||||
"dataLastUpdated" | i18n: (dataLastUpdated | date: "MMMM d, y 'at' h:mm a")
|
||||
}}</span>
|
||||
} @else {
|
||||
<span class="tw-mx-4">{{ "noReportRan" | i18n }}</span>
|
||||
}
|
||||
@let isRunningReport = dataService.isRunningReport$ | async;
|
||||
<span class="tw-flex tw-justify-center">
|
||||
<button
|
||||
*ngIf="!(isRefreshing$ | async)"
|
||||
*ngIf="!isRunningReport"
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="secondary"
|
||||
@@ -24,11 +28,11 @@
|
||||
tabindex="0"
|
||||
[bitAction]="refreshData.bind(this)"
|
||||
>
|
||||
{{ "refresh" | i18n }}
|
||||
{{ "riskInsightsRunReport" | i18n }}
|
||||
</button>
|
||||
<span>
|
||||
<i
|
||||
*ngIf="isRefreshing$ | async"
|
||||
*ngIf="isRunningReport"
|
||||
class="bwi bwi-spinner bwi-spin tw-text-muted tw-text-[1.2rem]"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
@@ -38,18 +42,21 @@
|
||||
<bit-tab-group [(selectedIndex)]="tabIndex" (selectedIndexChange)="onTabChange($event)">
|
||||
@if (isRiskInsightsActivityTabFeatureEnabled) {
|
||||
<bit-tab label="{{ 'activity' | i18n }}">
|
||||
<tools-all-activity></tools-all-activity>
|
||||
<dirt-all-activity></dirt-all-activity>
|
||||
</bit-tab>
|
||||
}
|
||||
<bit-tab label="{{ 'allApplicationsWithCount' | i18n: appsCount }}">
|
||||
<tools-all-applications></tools-all-applications>
|
||||
<dirt-all-applications></dirt-all-applications>
|
||||
</bit-tab>
|
||||
<bit-tab>
|
||||
<ng-template bitTabLabel>
|
||||
<i class="bwi bwi-star"></i>
|
||||
{{ "criticalApplicationsWithCount" | i18n: (criticalApps$ | async)?.length ?? 0 }}
|
||||
{{
|
||||
"criticalApplicationsWithCount"
|
||||
| i18n: (dataService.criticalReportResults$ | async)?.reportData?.length ?? 0
|
||||
}}
|
||||
</ng-template>
|
||||
<tools-critical-applications></tools-critical-applications>
|
||||
<dirt-critical-applications></dirt-critical-applications>
|
||||
</bit-tab>
|
||||
</bit-tab-group>
|
||||
|
||||
@@ -69,7 +76,9 @@
|
||||
}}</span>
|
||||
<ng-container *ngIf="drawerDetails.atRiskMemberDetails.length > 0">
|
||||
<div class="tw-flex tw-justify-between tw-mt-2 tw-text-muted">
|
||||
<div bitTypography="body2" class="tw-text-sm tw-font-bold">{{ "email" | i18n }}</div>
|
||||
<div bitTypography="body2" class="tw-text-sm tw-font-bold">
|
||||
{{ "email" | i18n }}
|
||||
</div>
|
||||
<div bitTypography="body2" class="tw-text-sm tw-font-bold">
|
||||
{{ "atRiskPasswords" | i18n }}
|
||||
</div>
|
||||
|
||||
@@ -2,21 +2,12 @@ import { CommonModule } from "@angular/common";
|
||||
import { Component, DestroyRef, OnInit, inject } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { EMPTY, firstValueFrom, Observable } from "rxjs";
|
||||
import { EMPTY } from "rxjs";
|
||||
import { map, switchMap } from "rxjs/operators";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import {
|
||||
CriticalAppsService,
|
||||
RiskInsightsDataService,
|
||||
} from "@bitwarden/bit-common/dirt/reports/risk-insights";
|
||||
import { PasswordHealthReportApplicationsResponse } from "@bitwarden/bit-common/dirt/reports/risk-insights/models/api-models.types";
|
||||
import {
|
||||
ApplicationHealthReportDetail,
|
||||
DrawerType,
|
||||
} from "@bitwarden/bit-common/dirt/reports/risk-insights/models/report-models";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { RiskInsightsDataService } from "@bitwarden/bit-common/dirt/reports/risk-insights";
|
||||
import { DrawerType } from "@bitwarden/bit-common/dirt/reports/risk-insights/models/report-models";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
@@ -67,19 +58,13 @@ export class RiskInsightsComponent implements OnInit {
|
||||
tabIndex: RiskInsightsTabType = RiskInsightsTabType.AllApps;
|
||||
isRiskInsightsActivityTabFeatureEnabled: boolean = false;
|
||||
|
||||
dataLastUpdated: Date = new Date();
|
||||
|
||||
criticalApps$: Observable<PasswordHealthReportApplicationsResponse[]> = new Observable();
|
||||
|
||||
appsCount: number = 0;
|
||||
criticalAppsCount: number = 0;
|
||||
notifiedMembersCount: number = 0;
|
||||
// Leaving this commented because it's not used but seems important
|
||||
// notifiedMembersCount: number = 0;
|
||||
|
||||
private organizationId: OrganizationId = "" as OrganizationId;
|
||||
|
||||
isLoading$: Observable<boolean> = new Observable<boolean>();
|
||||
isRefreshing$: Observable<boolean> = new Observable<boolean>();
|
||||
dataLastUpdated$: Observable<Date | null> = new Observable<Date | null>();
|
||||
dataLastUpdated: Date | null = null;
|
||||
refetching: boolean = false;
|
||||
|
||||
constructor(
|
||||
@@ -87,8 +72,6 @@ export class RiskInsightsComponent implements OnInit {
|
||||
private router: Router,
|
||||
private configService: ConfigService,
|
||||
protected dataService: RiskInsightsDataService,
|
||||
private criticalAppsService: CriticalAppsService,
|
||||
private accountService: AccountService,
|
||||
) {
|
||||
this.route.queryParams.pipe(takeUntilDestroyed()).subscribe(({ tabIndex }) => {
|
||||
this.tabIndex = !isNaN(Number(tabIndex)) ? Number(tabIndex) : RiskInsightsTabType.AllApps;
|
||||
@@ -104,39 +87,29 @@ export class RiskInsightsComponent implements OnInit {
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
|
||||
this.route.paramMap
|
||||
.pipe(
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
map((params) => params.get("organizationId")),
|
||||
switchMap((orgId) => {
|
||||
switchMap(async (orgId) => {
|
||||
if (orgId) {
|
||||
// Initialize Data Service
|
||||
await this.dataService.initializeForOrganization(orgId as OrganizationId);
|
||||
|
||||
this.organizationId = orgId as OrganizationId;
|
||||
this.dataService.fetchApplicationsReport(this.organizationId);
|
||||
this.isLoading$ = this.dataService.isLoading$;
|
||||
this.isRefreshing$ = this.dataService.isRefreshing$;
|
||||
this.dataLastUpdated$ = this.dataService.dataLastUpdated$;
|
||||
return this.dataService.applications$;
|
||||
} else {
|
||||
return EMPTY;
|
||||
}
|
||||
}),
|
||||
)
|
||||
.subscribe({
|
||||
next: (applications: ApplicationHealthReportDetail[] | null) => {
|
||||
if (applications) {
|
||||
this.appsCount = applications.length;
|
||||
}
|
||||
.subscribe();
|
||||
|
||||
this.criticalAppsService.loadOrganizationContext(
|
||||
this.organizationId as OrganizationId,
|
||||
userId,
|
||||
);
|
||||
this.criticalApps$ = this.criticalAppsService.getAppsListForOrg(
|
||||
this.organizationId as OrganizationId,
|
||||
);
|
||||
},
|
||||
// Subscribe to report result details
|
||||
this.dataService.reportResults$
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((report) => {
|
||||
this.appsCount = report?.reportData.length ?? 0;
|
||||
this.dataLastUpdated = report?.creationDate ?? null;
|
||||
});
|
||||
|
||||
// Subscribe to drawer state changes
|
||||
@@ -156,7 +129,7 @@ export class RiskInsightsComponent implements OnInit {
|
||||
*/
|
||||
refreshData(): void {
|
||||
if (this.organizationId) {
|
||||
this.dataService.refreshApplicationsReport(this.organizationId);
|
||||
this.dataService.triggerReport();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user