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