1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-04 02:33:33 +00:00

Add orchestration updates for fetching applications as well as migrating data.

This commit is contained in:
Leslie Tilton
2025-10-09 16:01:57 -05:00
parent 6e56fd2e87
commit 127f4cc017
15 changed files with 585 additions and 351 deletions

View File

@@ -1,8 +1,14 @@
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { OrganizationReportId } from "@bitwarden/common/types/guid";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { MemberCipherDetailsResponse } from "../models";
import {
AtRiskApplicationDetail,
AtRiskMemberDetail,
MemberCipherDetailsResponse,
} from "../models";
import {
ApplicationHealthReportDetail,
MemberDetails,
OrganizationReportSummary,
RiskInsightsData,
@@ -61,6 +67,7 @@ export function getUniqueMembers(orgMembers: MemberDetails[]): MemberDetails[] {
*/
export function createNewReportData(): RiskInsightsData {
return {
id: "" as OrganizationReportId,
creationDate: new Date(),
reportData: [],
summaryData: createNewSummaryData(),
@@ -86,3 +93,60 @@ export function createNewSummaryData(): OrganizationReportSummary {
newApplications: [],
};
}
export function getAtRiskApplicationList(
cipherHealthReportDetails: ApplicationHealthReportDetail[],
): AtRiskApplicationDetail[] {
const applicationPasswordRiskMap = new Map<string, number>();
cipherHealthReportDetails
.filter((app) => app.atRiskPasswordCount > 0)
.forEach((app) => {
const atRiskPasswordCount = applicationPasswordRiskMap.get(app.applicationName) ?? 0;
applicationPasswordRiskMap.set(
app.applicationName,
atRiskPasswordCount + app.atRiskPasswordCount,
);
});
return Array.from(applicationPasswordRiskMap.entries()).map(
([applicationName, atRiskPasswordCount]) => ({
applicationName,
atRiskPasswordCount,
}),
);
}
/**
* Generates a list of members with at-risk passwords along with the number of at-risk passwords.
*/
export function getAtRiskMemberList(
cipherHealthReportDetails: ApplicationHealthReportDetail[],
): AtRiskMemberDetail[] {
const memberRiskMap = new Map<string, number>();
cipherHealthReportDetails.forEach((app) => {
app.atRiskMemberDetails.forEach((member) => {
const currentCount = memberRiskMap.get(member.email) ?? 0;
memberRiskMap.set(member.email, currentCount + 1);
});
});
return Array.from(memberRiskMap.entries()).map(([email, atRiskPasswordCount]) => ({
email,
atRiskPasswordCount,
}));
}
/**
* Builds a map of passwords to the number of times they are used across ciphers
*
* @param ciphers List of ciphers to check for password reuse
* @returns A map where the key is the password and the value is the number of times it is used
*/
export function buildPasswordUseMap(ciphers: CipherView[]): Map<string, number> {
const passwordUseMap = new Map<string, number>();
ciphers.forEach((cipher) => {
const password = cipher.login.password!;
passwordUseMap.set(password, (passwordUseMap.get(password) || 0) + 1);
});
return passwordUseMap;
}

View File

@@ -0,0 +1,44 @@
import { MemberDetails } from "./report-models";
// -------------------- Drawer and UI Models --------------------
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum DrawerType {
None = 0,
AppAtRiskMembers = 1,
OrgAtRiskMembers = 2,
OrgAtRiskApps = 3,
}
export type DrawerDetails = {
open: boolean;
invokerId: string;
activeDrawerType: DrawerType;
atRiskMemberDetails?: AtRiskMemberDetail[];
appAtRiskMembers?: AppAtRiskMembersDialogParams | null;
atRiskAppDetails?: AtRiskApplicationDetail[] | null;
};
export type AppAtRiskMembersDialogParams = {
members: MemberDetails[];
applicationName: string;
};
/**
* Member email with the number of at risk passwords
* At risk member detail that contains the email
* and the count of at risk ciphers
*/
export type AtRiskMemberDetail = {
email: string;
atRiskPasswordCount: number;
};
/*
* A list of applications and the count of
* at risk passwords for each application
*/
export type AtRiskApplicationDetail = {
applicationName: string;
atRiskPasswordCount: number;
};

View File

@@ -3,3 +3,4 @@ export * from "./password-health";
export * from "./report-data-service.types";
export * from "./report-encryption.types";
export * from "./report-models";
export * from "./drawer-models.types";

View File

@@ -81,10 +81,12 @@ export const mockApplicationData: OrganizationReportApplication[] = [
{
applicationName: "application1.com",
isCritical: true,
reviewedDate: new Date(),
},
{
applicationName: "application2.com",
isCritical: false,
reviewedDate: null,
},
];

View File

@@ -1,44 +1,12 @@
import { Opaque } from "type-fest";
import { OrganizationReportId } from "@bitwarden/common/types/guid";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { BadgeVariant } from "@bitwarden/components";
import { ExposedPasswordDetail, WeakPasswordDetail } from "./password-health";
// -------------------- Drawer and UI Models --------------------
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum DrawerType {
None = 0,
AppAtRiskMembers = 1,
OrgAtRiskMembers = 2,
OrgAtRiskApps = 3,
}
export type DrawerDetails = {
open: boolean;
invokerId: string;
activeDrawerType: DrawerType;
atRiskMemberDetails?: AtRiskMemberDetail[];
appAtRiskMembers?: AppAtRiskMembersDialogParams | null;
atRiskAppDetails?: AtRiskApplicationDetail[] | null;
};
export type AppAtRiskMembersDialogParams = {
members: MemberDetails[];
applicationName: string;
};
// -------------------- Member Models --------------------
/**
* Member email with the number of at risk passwords
* At risk member detail that contains the email
* and the count of at risk ciphers
*/
export type AtRiskMemberDetail = {
email: string;
atRiskPasswordCount: number;
};
/**
* Flattened member details that associates an
@@ -71,18 +39,6 @@ export type CipherHealthReport = {
cipher: CipherView;
};
/**
* Breaks the cipher health info out by uri and passes
* along the password health and member info
*/
export type CipherApplicationView = {
cipherId: string;
cipher: CipherView;
cipherMembers: MemberDetails[];
application: string;
healthData: PasswordHealthData;
};
// -------------------- Application Health Report Models --------------------
/**
* All applications report summary. The total members,
@@ -91,21 +47,16 @@ export type CipherApplicationView = {
*/
export type OrganizationReportSummary = {
totalMemberCount: number;
totalCriticalMemberCount: number;
totalAtRiskMemberCount: number;
totalCriticalAtRiskMemberCount: number;
totalApplicationCount: number;
totalCriticalApplicationCount: number;
totalAtRiskMemberCount: number;
totalAtRiskApplicationCount: number;
totalCriticalApplicationCount: number;
totalCriticalMemberCount: number;
totalCriticalAtRiskMemberCount: number;
totalCriticalAtRiskApplicationCount: number;
newApplications: string[];
};
export type CriticalSummaryDetails = {
totalCriticalMembersCount: number;
totalCriticalApplicationsCount: number;
};
/**
* An entry for an organization application and if it is
* marked as critical
@@ -113,6 +64,7 @@ export type CriticalSummaryDetails = {
export type OrganizationReportApplication = {
applicationName: string;
isCritical: boolean;
reviewedDate: Date | null;
};
/**
@@ -131,15 +83,6 @@ export type ApplicationHealthReportDetail = {
cipherIds: string[];
};
/*
* A list of applications and the count of
* at risk passwords for each application
*/
export type AtRiskApplicationDetail = {
applicationName: string;
atRiskPasswordCount: number;
};
// -------------------- Password Health Report Models --------------------
export type PasswordHealthReportApplicationId = Opaque<string, "PasswordHealthReportApplicationId">;
@@ -152,6 +95,7 @@ export type ReportResult = CipherView & {
};
export interface RiskInsightsData {
id: OrganizationReportId;
creationDate: Date;
reportData: ApplicationHealthReportDetail[];
summaryData: OrganizationReportSummary;
@@ -163,3 +107,13 @@ export interface ReportState {
error: string | null;
data: RiskInsightsData | null;
}
// TODO Make Versioned models for structure changes
// export type VersionedRiskInsightsData = RiskInsightsDataV1 | RiskInsightsDataV2;
// export interface RiskInsightsDataV1 {
// version: 1;
// creationDate: Date;
// reportData: ApplicationHealthReportDetail[];
// summaryData: OrganizationReportSummary;
// applicationData: OrganizationReportApplication[];
// }

View File

@@ -228,11 +228,12 @@ describe("RiskInsightsApiService", () => {
it("updateRiskInsightsApplicationData$ should call apiService.send with correct parameters and return an Observable", async () => {
const reportId = "report123" as OrganizationReportId;
const mockApplication = mockApplicationData[0];
// TODO Update to be encrypted test
const mockApplication = makeEncString("application-data");
mockApiService.send.mockResolvedValueOnce(undefined);
const result = await firstValueFrom(
service.updateRiskInsightsApplicationData$(mockApplication, orgId, reportId),
service.updateRiskInsightsApplicationData$(mockApplication.encryptedString, orgId, reportId),
);
expect(mockApiService.send).toHaveBeenCalledWith(
"PATCH",

View File

@@ -4,7 +4,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { OrganizationId, OrganizationReportId } from "@bitwarden/common/types/guid";
import { EncryptedDataWithKey, OrganizationReportApplication } from "../../models";
import { EncryptedDataWithKey } from "../../models";
import {
GetRiskInsightsApplicationDataResponse,
GetRiskInsightsReportResponse,
@@ -102,7 +102,7 @@ export class RiskInsightsApiService {
}
updateRiskInsightsApplicationData$(
applicationData: OrganizationReportApplication,
applicationData: string,
orgId: OrganizationId,
reportId: OrganizationReportId,
): Observable<void> {

View File

@@ -139,10 +139,11 @@ export class CriticalAppsService {
return;
}
await this.criticalAppsApiService.dropCriticalApp({
organizationId: app.organizationId,
passwordHealthReportApplicationIds: [app.id],
});
// TODO Uncomment when done testing that the migration is working
// await this.criticalAppsApiService.dropCriticalApp({
// organizationId: app.organizationId,
// passwordHealthReportApplicationIds: [app.id],
// });
this.criticalAppsListSubject$.next(
this.criticalAppsListSubject$.value.filter((f) => f.uri !== selectedUrl),

View File

@@ -3,7 +3,9 @@ import { of, throwError } from "rxjs";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
import { OrganizationId, OrganizationReportId, UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { LogService } from "@bitwarden/logging";
import { createNewSummaryData } from "../../helpers";
import { ReportState } from "../../models";
@@ -12,8 +14,12 @@ import {
mockEnrichedReportData,
mockSummaryData,
} from "../../models/mocks/mock-data";
import { MemberCipherDetailsApiService } from "../api/member-cipher-details-api.service";
import { RiskInsightsApiService } from "../api/risk-insights-api.service";
import { CriticalAppsService } from "./critical-apps.service";
import { PasswordHealthService } from "./password-health.service";
import { RiskInsightsEncryptionService } from "./risk-insights-encryption.service";
import { RiskInsightsOrchestratorService } from "./risk-insights-orchestrator.service";
import { RiskInsightsReportService } from "./risk-insights-report.service";
@@ -24,6 +30,7 @@ describe("RiskInsightsOrchestratorService", () => {
const mockOrgId = "org-789" as OrganizationId;
const mockOrgName = "Test Org";
const mockUserId = "user-101" as UserId;
const mockReportId = "report-1" as OrganizationReportId;
// Mock services
const mockAccountService = mock<AccountService>({
@@ -33,14 +40,26 @@ describe("RiskInsightsOrchestratorService", () => {
criticalAppsList$: of([]),
});
const mockOrganizationService = mock<OrganizationService>();
const mockCipherService = mock<CipherService>();
const mockMemberCipherDetailsApiService = mock<MemberCipherDetailsApiService>();
const mockPasswordHealthService = mock<PasswordHealthService>();
const mockReportApiService = mock<RiskInsightsApiService>();
const mockReportService = mock<RiskInsightsReportService>();
const mockRiskInsightsEncryptionService = mock<RiskInsightsEncryptionService>();
const mockLogService = mock<LogService>();
beforeEach(() => {
service = new RiskInsightsOrchestratorService(
mockAccountService,
mockCipherService,
mockCriticalAppsService,
mockMemberCipherDetailsApiService,
mockOrganizationService,
mockPasswordHealthService,
mockReportApiService,
mockReportService,
mockRiskInsightsEncryptionService,
mockLogService,
);
});
@@ -53,6 +72,7 @@ describe("RiskInsightsOrchestratorService", () => {
loading: false,
error: null,
data: {
id: mockReportId,
reportData: [],
summaryData: createNewSummaryData(),
applicationData: [],
@@ -106,14 +126,14 @@ describe("RiskInsightsOrchestratorService", () => {
});
describe("generateReport", () => {
it("should call reportService.generateApplicationsReport$ and saveRiskInsightsReport$ and emit ReportState", (done) => {
it("should call reportService.generateApplicationsReport and saveRiskInsightsReport$ and emit ReportState", (done) => {
const privateOrganizationDetailsSubject = service["_organizationDetailsSubject"];
const privateUserIdSubject = service["_userIdSubject"];
// Arrange
mockReportService.generateApplicationsReport$.mockReturnValueOnce(of(mockEnrichedReportData));
mockReportService.generateApplicationsSummary.mockReturnValueOnce(mockSummaryData);
mockReportService.generateOrganizationApplications.mockReturnValueOnce(mockApplicationData);
mockReportService.generateApplicationsReport.mockReturnValueOnce(mockEnrichedReportData);
mockReportService.getApplicationsSummary.mockReturnValueOnce(mockSummaryData);
mockReportService.getOrganizationApplications.mockReturnValueOnce(mockApplicationData);
mockReportService.saveRiskInsightsReport$.mockReturnValueOnce(of(null));
privateOrganizationDetailsSubject.next({
organizationId: mockOrgId,
@@ -127,7 +147,7 @@ describe("RiskInsightsOrchestratorService", () => {
// Assert
service.rawReportData$.subscribe((state) => {
if (!state.loading) {
expect(mockReportService.generateApplicationsReport$).toHaveBeenCalledWith(mockOrgId);
expect(mockReportService.generateApplicationsReport).toHaveBeenCalledWith(mockOrgId);
expect(mockReportService.saveRiskInsightsReport$).toHaveBeenCalledWith(
mockEnrichedReportData,
mockSummaryData,
@@ -142,35 +162,13 @@ describe("RiskInsightsOrchestratorService", () => {
});
});
it("should emit error ReportState when generateApplicationsReport$ throws", (done) => {
const privateOrganizationDetailsSubject = service["_organizationDetailsSubject"];
const privateUserIdSubject = service["_userIdSubject"];
mockReportService.generateApplicationsReport$.mockReturnValueOnce(
throwError(() => new Error("Generate error")),
);
privateOrganizationDetailsSubject.next({
organizationId: mockOrgId,
organizationName: mockOrgName,
});
privateUserIdSubject.next(mockUserId);
service.generateReport();
service.rawReportData$.subscribe((state) => {
if (!state.loading) {
expect(state.error).toBe("Failed to generate or save report");
expect(state.data).toBeNull();
done();
}
});
});
it("should emit error ReportState when saveRiskInsightsReport$ throws", (done) => {
const privateOrganizationDetailsSubject = service["_organizationDetailsSubject"];
const privateUserIdSubject = service["_userIdSubject"];
mockReportService.generateApplicationsReport$.mockReturnValueOnce(of(mockEnrichedReportData));
mockReportService.generateApplicationsSummary.mockReturnValueOnce(mockSummaryData);
mockReportService.generateOrganizationApplications.mockReturnValueOnce(mockApplicationData);
mockReportService.generateApplicationsReport.mockReturnValueOnce(mockEnrichedReportData);
mockReportService.getApplicationsSummary.mockReturnValueOnce(mockSummaryData);
mockReportService.getOrganizationApplications.mockReturnValueOnce(mockApplicationData);
mockReportService.saveRiskInsightsReport$.mockReturnValueOnce(
throwError(() => new Error("Save error")),
);

View File

@@ -1,6 +1,7 @@
import {
BehaviorSubject,
combineLatest,
forkJoin,
from,
merge,
Observable,
@@ -30,13 +31,26 @@ import {
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
import { OrganizationId, OrganizationReportId, UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { LogService } from "@bitwarden/logging";
import { buildPasswordUseMap, flattenMemberDetails, getTrimmedCipherUris } from "../../helpers";
import { ApplicationHealthReportDetailEnriched } from "../../models";
import { RiskInsightsEnrichedData } from "../../models/report-data-service.types";
import { ReportState } from "../../models/report-models";
import {
CipherHealthReport,
MemberDetails,
OrganizationReportApplication,
ReportState,
} from "../../models/report-models";
import { MemberCipherDetailsApiService } from "../api/member-cipher-details-api.service";
import { RiskInsightsApiService } from "../api/risk-insights-api.service";
import { CriticalAppsService } from "./critical-apps.service";
import { PasswordHealthService } from "./password-health.service";
import { RiskInsightsEncryptionService } from "./risk-insights-encryption.service";
import { RiskInsightsReportService } from "./risk-insights-report.service";
export class RiskInsightsOrchestratorService {
@@ -54,6 +68,10 @@ export class RiskInsightsOrchestratorService {
} | null>(null);
organizationDetails$ = this._organizationDetailsSubject.asObservable();
// ------------------------- Raw data -------------------------
private _ciphersSubject = new BehaviorSubject<CipherView[] | null>(null);
private _ciphers$ = this._ciphersSubject.asObservable();
// ------------------------- Report Variables ----------------
private _rawReportDataSubject = new BehaviorSubject<ReportState>({
loading: true,
@@ -78,19 +96,28 @@ export class RiskInsightsOrchestratorService {
constructor(
private accountService: AccountService,
private cipherService: CipherService,
private criticalAppsService: CriticalAppsService,
private memberCipherDetailsApiService: MemberCipherDetailsApiService,
private organizationService: OrganizationService,
private passwordHealthService: PasswordHealthService,
private reportApiService: RiskInsightsApiService,
private reportService: RiskInsightsReportService,
private riskInsightsEncryptionService: RiskInsightsEncryptionService,
private logService: LogService,
) {
this.logService.debug("[RiskInsightsOrchestratorService] Setting up");
this._setupCriticalApplicationContext();
this._setupCriticalApplicationReport();
this._setupEnrichedReportData();
this._setupInitializationPipeline();
this._setupMigrationAndCleanup();
this._setupReportState();
this._setupUserId();
}
destroy(): void {
this.logService.debug("[RiskInsightsOrchestratorService] Destroying");
if (this._reportStateSubscription) {
this._reportStateSubscription.unsubscribe();
}
@@ -98,19 +125,11 @@ export class RiskInsightsOrchestratorService {
this._destroy$.complete();
}
/**
* Initializes the service context for a specific organization
*
* @param organizationId The ID of the organization to initialize context for
*/
initializeForOrganization(organizationId: OrganizationId) {
this._initializeOrganizationTriggerSubject.next(organizationId);
}
/**
* Fetches the latest report for the current organization and user
*/
fetchReport(): void {
this.logService.debug("[RiskInsightsOrchestratorService] Fetch report triggered");
this._fetchReportTriggerSubject.next();
}
@@ -118,16 +137,125 @@ export class RiskInsightsOrchestratorService {
* Generates a new report for the current organization and user
*/
generateReport(): void {
this.logService.debug("[RiskInsightsOrchestratorService] Create new report triggered");
this._generateReportTriggerSubject.next(true);
}
/**
* Initializes the service context for a specific organization
*
* @param organizationId The ID of the organization to initialize context for
*/
initializeForOrganization(organizationId: OrganizationId) {
this.logService.debug("[RiskInsightsOrchestratorService] Initializing for org", organizationId);
this._initializeOrganizationTriggerSubject.next(organizationId);
}
setCriticalApplications$(criticalApplications: string[]): Observable<ReportState> {
return this.rawReportData$.pipe(
take(1),
withLatestFrom(this.organizationDetails$, this._userId$),
map(([reportState, organizationDetails, userId]) => {
if (!organizationDetails) {
this.logService.warning(
"[RiskInsightsOrchestratorService] No organization details available when setting critical applications.",
);
return {
reportState,
organizationDetails: null,
updatedState: reportState,
}; // Return current state if no org details
}
// Handle the case where there is no report data
if (!reportState?.data) {
this.logService.warning(
"[RiskInsightsOrchestratorService] Attempted to set critical applications with no report data.",
);
return {
reportState,
organizationDetails,
updatedState: reportState,
};
}
// Create a set for quick lookup of the new critical apps
const newCriticalAppNamesSet = new Set(criticalApplications);
const existingApplicationData = reportState.data.applicationData || [];
const updatedApplicationData = this._mergeApplicationData(
existingApplicationData,
newCriticalAppNamesSet,
);
const updatedState: ReportState = {
...reportState,
data: {
...reportState.data,
applicationData: updatedApplicationData,
},
};
this.logService.debug(
"[RiskInsightsOrchestratorService] Updated applications data",
updatedState,
);
return { reportState, organizationDetails, updatedState, userId };
}),
switchMap(({ reportState, organizationDetails, updatedState, userId }) => {
return from(
this.riskInsightsEncryptionService.encryptRiskInsightsReport(
{
organizationId: organizationDetails!.organizationId,
userId,
},
{
reportData: reportState.data.reportData,
summaryData: reportState.data.summaryData,
applicationData: updatedState.data.applicationData,
},
),
).pipe(
map((encryptedData) => ({
reportState,
organizationDetails,
updatedState,
encryptedData,
})),
);
}),
switchMap(({ reportState, organizationDetails, updatedState, encryptedData }) => {
// Chain the save operation using switchMap
return this.reportApiService
.updateRiskInsightsApplicationData$(
encryptedData.encryptedApplicationData.encryptedString,
organizationDetails.organizationId,
reportState.data.id,
)
.pipe(
// Map the result of the save operation to the updated state
map(() => updatedState),
// Use tap to push the updated state to the subject
tap((finalState) => this._rawReportDataSubject.next(finalState)),
// Handle errors from the save operation
catchError((error: unknown) => {
this.logService.error("Failed to save updated applicationData", error);
return of({ ...reportState, error: "Failed to save application data" });
}),
);
}),
);
}
private _fetchReport$(organizationId: OrganizationId, userId: UserId): Observable<ReportState> {
return this.reportService.getRiskInsightsReport$(organizationId, userId).pipe(
tap(() => this.logService.debug("[RiskInsightsOrchestratorService] Fetching report")),
map(
({ reportData, summaryData, applicationData, creationDate }): ReportState => ({
({ id, reportData, summaryData, applicationData, creationDate }): ReportState => ({
loading: false,
error: null,
data: {
id,
reportData,
summaryData,
applicationData,
@@ -140,16 +268,30 @@ export class RiskInsightsOrchestratorService {
);
}
private _generateApplicationsReport$(
private _generateNewApplicationsReport$(
organizationId: OrganizationId,
userId: UserId,
): Observable<ReportState> {
// Generate the report
return this.reportService.generateApplicationsReport$(organizationId).pipe(
map((enrichedReport) => ({
report: enrichedReport,
summary: this.reportService.generateApplicationsSummary(enrichedReport),
applications: this.reportService.generateOrganizationApplications(enrichedReport),
const memberCiphers$ = from(
this.memberCipherDetailsApiService.getMemberCipherDetails(organizationId),
).pipe(map((memberCiphers) => flattenMemberDetails(memberCiphers)));
return forkJoin([this._ciphers$, memberCiphers$]).pipe(
tap(() => this.logService.debug("[RiskInsightsOrchestratorService] Generating new report")),
switchMap(([ciphers, memberCiphers]) => this._getCipherHealth(ciphers, memberCiphers)),
map((cipherHealthReports) =>
this.reportService.generateApplicationsReport(cipherHealthReports),
),
withLatestFrom(this.rawReportData$),
map(([report, previousReport]) => ({
report: report,
summary: this.reportService.getApplicationsSummary(report),
applications: this.reportService.getOrganizationApplications(
report,
previousReport.data.applicationData,
),
})),
switchMap(({ report, summary, applications }) =>
// Save the report after enrichment
@@ -172,6 +314,7 @@ export class RiskInsightsOrchestratorService {
loading: false,
error: null,
data: {
id: "" as OrganizationReportId,
reportData: report,
summaryData: summary,
applicationData: applications,
@@ -186,6 +329,106 @@ export class RiskInsightsOrchestratorService {
);
}
/**
* Associates the members with the ciphers they have access to. Calculates the password health.
* Finds the trimmed uris.
* @param ciphers Org ciphers
* @param memberDetails Org members
* @returns Cipher password health data with trimmed uris and associated members
*/
private _getCipherHealth(
ciphers: CipherView[],
memberDetails: MemberDetails[],
): Observable<CipherHealthReport[]> {
const validCiphers = ciphers.filter((cipher) =>
this.passwordHealthService.isValidCipher(cipher),
);
const passwordUseMap = buildPasswordUseMap(validCiphers);
// Check for exposed passwords and map to cipher health report
return this.passwordHealthService.auditPasswordLeaks$(validCiphers).pipe(
map((exposedDetails) => {
return validCiphers.map((cipher) => {
const exposedPasswordDetail = exposedDetails.find((x) => x?.cipherId === cipher.id);
const cipherMembers = memberDetails.filter((x) => x.cipherId === cipher.id);
const applications = getTrimmedCipherUris(cipher);
const weakPasswordDetail = this.passwordHealthService.findWeakPasswordDetails(cipher);
const reusedPasswordCount = passwordUseMap.get(cipher.login.password!) ?? 0;
return {
cipher,
cipherMembers,
applications,
healthData: {
weakPasswordDetail,
reusedPasswordCount,
exposedPasswordDetail,
},
};
});
}),
);
}
private _runMigrationAndCleanup$(): Observable<OrganizationReportApplication[]> {
// Start with rawReportData$ to ensure it has a value
return this.rawReportData$.pipe(
// Ensure rawReportData has a data payload
filter((reportState) => !!reportState.data),
take(1), // Use the first valid report state
// Now switch to the migration logic
switchMap((rawReportData) =>
this.criticalAppsService.criticalAppsList$.pipe(
take(1),
withLatestFrom(this.organizationDetails$),
switchMap(([savedCriticalApps, organizationDetails]) => {
// Check if there are any critical apps to migrate.
if (!savedCriticalApps || savedCriticalApps.length === 0) {
this.logService.debug(
"[RiskInsightsOrchestratorService] No critical apps to migrate.",
);
return of([]);
}
// Map the saved critical apps to the new format
const migratedApps = savedCriticalApps.map(
(app): OrganizationReportApplication => ({
applicationName: app.uri,
isCritical: true,
reviewedDate: null,
}),
);
// Use the setCriticalApplications$ function to update and save the report
return this.setCriticalApplications$(
migratedApps.map((app) => app.applicationName),
).pipe(
// After setCriticalApplications$ completes, trigger the deletion.
switchMap(() => {
const deleteObservables = savedCriticalApps.map(
(app) => of(null),
// this.criticalAppsService.dropCriticalApp(
// organizationDetails!.organizationId,
// app.id,
// ),
);
return forkJoin(deleteObservables).pipe(
// After all deletes complete, map to the migrated apps.
map(() => {
this.logService.debug(
"[RiskInsightsOrchestratorService] Migrated and deleted critical applications.",
);
return migratedApps;
}),
);
}),
);
}),
),
),
);
}
// Setup the pipeline to load critical applications when organization or user changes
private _setupCriticalApplicationContext() {
this.organizationDetails$
@@ -194,6 +437,10 @@ export class RiskInsightsOrchestratorService {
withLatestFrom(this._userId$),
filter(([_, userId]) => !!userId),
tap(([orgDetails, userId]) => {
this.logService.debug(
"[RiskInsightsOrchestratorService] Loading critical applications for org",
orgDetails!.organizationId,
);
this.criticalAppsService.loadOrganizationContext(orgDetails!.organizationId, userId!);
}),
takeUntil(this._destroy$),
@@ -206,16 +453,22 @@ export class RiskInsightsOrchestratorService {
const criticalReportResultsPipeline$ = this.enrichedReportData$.pipe(
filter((state) => !!state),
map((enrichedReports) => {
this.logService.debug(
"[RiskInsightsOrchestratorService] Generating critical applications report from",
enrichedReports,
);
const criticalApplications = enrichedReports!.reportData.filter(
(app) => app.isMarkedAsCritical,
);
const summary = this.reportService.generateApplicationsSummary(criticalApplications);
// Generate a new summary based on just the critical applications
const summary = this.reportService.getApplicationsSummary(criticalApplications);
return {
...enrichedReports,
summaryData: summary,
reportData: criticalApplications,
};
}),
shareReplay({ bufferSize: 1, refCount: true }),
);
this.criticalReportResults$ = criticalReportResultsPipeline$;
@@ -229,27 +482,29 @@ export class RiskInsightsOrchestratorService {
// Setup the enriched report data pipeline
const enrichmentSubscription = combineLatest([
this.rawReportData$.pipe(filter((data) => !!data)),
this.organizationDetails$.pipe(filter((details) => !!details)),
this.criticalAppsService.criticalAppsList$.pipe(filter((list) => !!list)),
this._ciphers$.pipe(filter((data) => !!data)),
]).pipe(
switchMap(([rawReportData, orgDetails, criticalApps]) => {
const criticalApplicationNames = new Set(criticalApps.map((ca) => ca.uri));
const rawReports = rawReportData.data?.reportData || [];
return from(
this.reportService.getApplicationCipherMap(rawReports, orgDetails!.organizationId),
).pipe(
map((cipherMap) => {
return rawReports.map((app) => ({
...app,
ciphers: cipherMap.get(app.applicationName) || [],
isMarkedAsCritical: criticalApplicationNames.has(app.applicationName),
})) as ApplicationHealthReportDetailEnriched[];
}),
map((enrichedReportData) => ({ ...rawReportData.data, reportData: enrichedReportData })),
catchError(() => {
return of(null);
}),
switchMap(([rawReportData, ciphers]) => {
this.logService.debug(
"[RiskInsightsOrchestratorService] Enriching report data with ciphers and critical app status",
);
const criticalApps = rawReportData?.data?.applicationData.filter((app) => app.isCritical);
const criticalApplicationNames = new Set(criticalApps.map((ca) => ca.applicationName));
const rawReports = rawReportData.data?.reportData || [];
const cipherMap = this.reportService.getApplicationCipherMap(ciphers, rawReports);
const enrichedReports: ApplicationHealthReportDetailEnriched[] = rawReports.map((app) => ({
...app,
ciphers: cipherMap.get(app.applicationName) || [],
isMarkedAsCritical: criticalApplicationNames.has(app.applicationName),
}));
const enrichedData: RiskInsightsEnrichedData = {
...rawReportData.data,
reportData: enrichedReports,
};
return of(enrichedData);
}),
shareReplay({ bufferSize: 1, refCount: true }),
);
@@ -271,11 +526,50 @@ export class RiskInsightsOrchestratorService {
map((org) => ({ organizationId: orgId, organizationName: org.name })),
),
),
tap(async (orgDetails) => {
this.logService.debug("[RiskInsightsOrchestratorService] Fetching organization ciphers");
const ciphers = await this.cipherService.getAllFromApiForOrganization(
orgDetails.organizationId,
);
this._ciphersSubject.next(ciphers);
}),
takeUntil(this._destroy$),
)
.subscribe((orgDetails) => this._organizationDetailsSubject.next(orgDetails));
}
private _setupMigrationAndCleanup() {
this.criticalAppsService.criticalAppsList$
.pipe(
filter((criticalApps) => criticalApps.length > 0),
tap(() => {
this.logService.debug(
"[RiskInsightsOrchestratorService] Detected legacy critical apps, running migration and cleanup.",
);
}),
switchMap(() =>
this._runMigrationAndCleanup$().pipe(
tap((migratedApps) => {
if (migratedApps.length > 0) {
this.logService.debug(
"[RiskInsightsOrchestratorService] Migration and cleanup completed.",
migratedApps,
);
}
}),
catchError((error: unknown) => {
this.logService.error(
"[RiskInsightsOrchestratorService] Migration and cleanup failed.",
error,
);
return of([]);
}),
),
),
)
.subscribe();
}
// Setup the report state management pipeline
private _setupReportState() {
// Dependencies needed for report state
@@ -285,14 +579,9 @@ export class RiskInsightsOrchestratorService {
]).pipe(shareReplay({ bufferSize: 1, refCount: true }));
// A stream for the initial report fetch (triggered by critical apps loading)
const initialReportLoad$ = combineLatest([
this.criticalAppsService.criticalAppsList$,
reportDependencies$,
]).pipe(
const initialReportLoad$ = reportDependencies$.pipe(
take(1), // Fetch only once on initial data load
exhaustMap(([_, [orgDetails, userId]]) =>
this._fetchReport$(orgDetails!.organizationId, userId!),
),
exhaustMap(([orgDetails, userId]) => this._fetchReport$(orgDetails!.organizationId, userId!)),
);
// A stream for manually triggered fetches
@@ -309,7 +598,7 @@ export class RiskInsightsOrchestratorService {
filter((isRunning) => isRunning),
withLatestFrom(reportDependencies$),
exhaustMap(([_, [orgDetails, userId]]) =>
this._generateApplicationsReport$(orgDetails!.organizationId, userId),
this._generateNewApplicationsReport$(orgDetails!.organizationId, userId),
),
);
@@ -332,6 +621,7 @@ export class RiskInsightsOrchestratorService {
this._reportStateSubscription = mergedReportState$
.pipe(takeUntil(this._destroy$))
.subscribe((state) => {
this.logService.debug("[RiskInsightsOrchestratorService] Updating report state", state);
this._rawReportDataSubject.next(state);
});
}
@@ -343,4 +633,19 @@ export class RiskInsightsOrchestratorService {
this._userIdSubject.next(userId);
});
}
private _mergeApplicationData(
existingApps: OrganizationReportApplication[],
newCriticalAppNamesSet: Set<string>,
): OrganizationReportApplication[] {
// First, iterate through the existing apps and update their isCritical flag
const updatedApps = existingApps.map((app) => {
return {
...app,
isCritical: newCriticalAppNamesSet.has(app.applicationName) ?? app.isCritical,
};
});
return updatedApps;
}
}

View File

@@ -79,9 +79,6 @@ describe("RiskInsightsReportService", () => {
});
service = new RiskInsightsReportService(
cipherService,
memberCipherDetailsService,
mockPasswordHealthService,
mockRiskInsightsApiService,
mockRiskInsightsEncryptionService,
);
@@ -93,23 +90,21 @@ describe("RiskInsightsReportService", () => {
};
});
it("should group and aggregate application health reports correctly", (done) => {
it("should group and aggregate application health reports correctly", () => {
// Mock the service methods
cipherService.getAllFromApiForOrganization.mockResolvedValue(mockCipherViews);
memberCipherDetailsService.getMemberCipherDetails.mockResolvedValue(mockMemberDetails);
service.generateApplicationsReport$("orgId" as any).subscribe((result) => {
expect(Array.isArray(result)).toBe(true);
const result = service.generateApplicationsReport("orgId" as any);
expect(Array.isArray(result)).toBe(true);
// Should group by application name (trimmedUris)
const appCom = result.find((r) => r.applicationName === "app.com");
const otherCom = result.find((r) => r.applicationName === "other.com");
expect(appCom).toBeTruthy();
expect(appCom?.passwordCount).toBe(2);
expect(otherCom).toBeTruthy();
expect(otherCom?.passwordCount).toBe(1);
done();
});
// Should group by application name (trimmedUris)
const appCom = result.find((r) => r.applicationName === "app.com");
const otherCom = result.find((r) => r.applicationName === "other.com");
expect(appCom).toBeTruthy();
expect(appCom?.passwordCount).toBe(2);
expect(otherCom).toBeTruthy();
expect(otherCom?.passwordCount).toBe(1);
});
describe("saveRiskInsightsReport$", () => {

View File

@@ -1,25 +1,9 @@
import {
catchError,
EMPTY,
forkJoin,
from,
map,
Observable,
of,
switchMap,
throwError,
} from "rxjs";
import { catchError, EMPTY, from, map, Observable, of, switchMap, throwError } from "rxjs";
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { OrganizationId, OrganizationReportId, UserId } from "@bitwarden/common/types/guid";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import {
createNewReportData,
flattenMemberDetails,
getTrimmedCipherUris,
getUniqueMembers,
} from "../../helpers/risk-insights-data-mappers";
import { createNewReportData, getUniqueMembers } from "../../helpers/risk-insights-data-mappers";
import {
isSaveRiskInsightsReportResponse,
SaveRiskInsightsReportResponse,
@@ -27,109 +11,34 @@ import {
import {
ApplicationHealthReportDetail,
OrganizationReportSummary,
AtRiskApplicationDetail,
AtRiskMemberDetail,
CipherHealthReport,
MemberDetails,
PasswordHealthData,
OrganizationReportApplication,
RiskInsightsData,
} from "../../models/report-models";
import { MemberCipherDetailsApiService } from "../api/member-cipher-details-api.service";
import { RiskInsightsApiService } from "../api/risk-insights-api.service";
import { PasswordHealthService } from "./password-health.service";
import { RiskInsightsEncryptionService } from "./risk-insights-encryption.service";
export class RiskInsightsReportService {
// [FIXME] CipherData
// Cipher data
// private _ciphersSubject = new BehaviorSubject<CipherView[] | null>(null);
// _ciphers$ = this._ciphersSubject.asObservable();
constructor(
private cipherService: CipherService,
private memberCipherDetailsApiService: MemberCipherDetailsApiService,
private passwordHealthService: PasswordHealthService,
private riskInsightsApiService: RiskInsightsApiService,
private riskInsightsEncryptionService: RiskInsightsEncryptionService,
) {}
// [FIXME] CipherData
// async loadCiphersForOrganization(organizationId: OrganizationId): Promise<void> {
// await this.cipherService.getAllFromApiForOrganization(organizationId).then((ciphers) => {
// this._ciphersSubject.next(ciphers);
// });
// }
/**
* Report data for the aggregation of uris to like uris and getting password/member counts,
* members, and at risk statuses.
*
* @param organizationId Id of the organization
* @param ciphers The list of ciphers to analyze
* @param memberCiphers The list of member cipher details to associate members to ciphers
* @returns The all applications health report data
*/
generateApplicationsReport$(
organizationId: OrganizationId,
): Observable<ApplicationHealthReportDetail[]> {
const allCiphers$ = from(this.cipherService.getAllFromApiForOrganization(organizationId));
const memberCiphers$ = from(
this.memberCipherDetailsApiService.getMemberCipherDetails(organizationId),
).pipe(map((memberCiphers) => flattenMemberDetails(memberCiphers)));
generateApplicationsReport(ciphers: CipherHealthReport[]): ApplicationHealthReportDetail[] {
const groupedByApplication = this._groupCiphersByApplication(ciphers);
return forkJoin([allCiphers$, memberCiphers$]).pipe(
switchMap(([ciphers, memberCiphers]) => this._getCipherDetails(ciphers, memberCiphers)),
map((cipherApplications) => {
const groupedByApplication = this._groupCiphersByApplication(cipherApplications);
return Array.from(groupedByApplication.entries()).map(([application, ciphers]) =>
this._getApplicationHealthReport(application, ciphers),
);
}),
);
}
/**
* Generates a list of members with at-risk passwords along with the number of at-risk passwords.
*/
generateAtRiskMemberList(
cipherHealthReportDetails: ApplicationHealthReportDetail[],
): AtRiskMemberDetail[] {
const memberRiskMap = new Map<string, number>();
cipherHealthReportDetails.forEach((app) => {
app.atRiskMemberDetails.forEach((member) => {
const currentCount = memberRiskMap.get(member.email) ?? 0;
memberRiskMap.set(member.email, currentCount + 1);
});
});
return Array.from(memberRiskMap.entries()).map(([email, atRiskPasswordCount]) => ({
email,
atRiskPasswordCount,
}));
}
generateAtRiskApplicationList(
cipherHealthReportDetails: ApplicationHealthReportDetail[],
): AtRiskApplicationDetail[] {
const applicationPasswordRiskMap = new Map<string, number>();
cipherHealthReportDetails
.filter((app) => app.atRiskPasswordCount > 0)
.forEach((app) => {
const atRiskPasswordCount = applicationPasswordRiskMap.get(app.applicationName) ?? 0;
applicationPasswordRiskMap.set(
app.applicationName,
atRiskPasswordCount + app.atRiskPasswordCount,
);
});
return Array.from(applicationPasswordRiskMap.entries()).map(
([applicationName, atRiskPasswordCount]) => ({
applicationName,
atRiskPasswordCount,
}),
return Array.from(groupedByApplication.entries()).map(([application, ciphers]) =>
this._getApplicationHealthReport(application, ciphers),
);
}
@@ -139,7 +48,7 @@ export class RiskInsightsReportService {
* @param reports The previously calculated application health report data
* @returns A summary object containing report totals
*/
generateApplicationsSummary(reports: ApplicationHealthReportDetail[]): OrganizationReportSummary {
getApplicationsSummary(reports: ApplicationHealthReportDetail[]): OrganizationReportSummary {
const totalMembers = reports.flatMap((x) => x.memberDetails);
const uniqueMembers = getUniqueMembers(totalMembers);
@@ -182,13 +91,32 @@ export class RiskInsightsReportService {
* @param reports
* @returns A list of applications with a critical marking flag
*/
generateOrganizationApplications(
getOrganizationApplications(
reports: ApplicationHealthReportDetail[],
previousApplications: OrganizationReportApplication[] = [],
): OrganizationReportApplication[] {
return reports.map((report) => ({
applicationName: report.applicationName,
isCritical: false,
}));
if (previousApplications.length > 0) {
// Preserve existing critical application markings and dates
return reports.map((report) => {
const existingApp = previousApplications.find(
(app) => app.applicationName === report.applicationName,
);
return {
applicationName: report.applicationName,
isCritical: existingApp ? existingApp.isCritical : false,
reviewedDate: existingApp ? existingApp.reviewedDate : null,
};
});
}
// No previous applications, return all as non-critical with current date
return reports.map(
(report): OrganizationReportApplication => ({
applicationName: report.applicationName,
isCritical: false,
reviewedDate: null,
}),
);
}
/**
@@ -236,6 +164,7 @@ export class RiskInsightsReportService {
),
).pipe(
map((decryptedData) => ({
id: response.id as OrganizationReportId,
reportData: decryptedData.reportData,
summaryData: decryptedData.summaryData,
applicationData: decryptedData.applicationData,
@@ -319,28 +248,12 @@ export class RiskInsightsReportService {
);
}
private _buildPasswordUseMap(ciphers: CipherView[]): Map<string, number> {
const passwordUseMap = new Map<string, number>();
ciphers.forEach((cipher) => {
const password = cipher.login.password!;
passwordUseMap.set(password, (passwordUseMap.get(password) || 0) + 1);
});
return passwordUseMap;
}
private _groupCiphersByApplication(
cipherHealthData: CipherHealthReport[],
): Map<string, CipherHealthReport[]> {
const applicationMap = new Map<string, CipherHealthReport[]>();
cipherHealthData.forEach((cipher: CipherHealthReport) => {
// Warning: Currently does not show ciphers with NO Application
// if (cipher.applications.length === 0) {
// const existingApplication = applicationMap.get("None") || [];
// existingApplication.push(cipher);
// applicationMap.set("None", existingApplication);
// }
cipher.applications.forEach((application) => {
const existingApplication = applicationMap.get(application) || [];
existingApplication.push(cipher);
@@ -357,19 +270,13 @@ export class RiskInsightsReportService {
* @param organizationId
* @returns
*/
async getApplicationCipherMap(
getApplicationCipherMap(
ciphers: CipherView[],
applications: ApplicationHealthReportDetail[],
organizationId: OrganizationId,
): Promise<Map<string, CipherView[]>> {
// [FIXME] CipherData
// This call is made multiple times. We can optimize this
// by loading the ciphers once via a load method to avoid multiple API calls
// for the same organization
const allCiphers = await this.cipherService.getAllFromApiForOrganization(organizationId);
): Map<string, CipherView[]> {
const cipherMap = new Map<string, CipherView[]>();
applications.forEach((app) => {
const filteredCiphers = allCiphers.filter((c) => app.cipherIds.includes(c.id));
const filteredCiphers = ciphers.filter((c) => app.cipherIds.includes(c.id));
cipherMap.set(app.applicationName, filteredCiphers);
});
return cipherMap;
@@ -465,42 +372,4 @@ export class RiskInsightsReportService {
healthData.reusedPasswordCount > 1
);
}
/**
* Associates the members with the ciphers they have access to. Calculates the password health.
* Finds the trimmed uris.
* @param ciphers Org ciphers
* @param memberDetails Org members
* @returns Cipher password health data with trimmed uris and associated members
*/
private _getCipherDetails(
ciphers: CipherView[],
memberDetails: MemberDetails[],
): Observable<CipherHealthReport[]> {
const validCiphers = ciphers.filter((cipher) =>
this.passwordHealthService.isValidCipher(cipher),
);
// Build password use map
const passwordUseMap = this._buildPasswordUseMap(validCiphers);
return this.passwordHealthService.auditPasswordLeaks$(validCiphers).pipe(
map((exposedDetails) => {
return validCiphers.map((cipher) => {
const exposedPassword = exposedDetails.find((x) => x?.cipherId === cipher.id);
const cipherMembers = memberDetails.filter((x) => x.cipherId === cipher.id);
const result = {
cipher: cipher,
cipherMembers,
healthData: {
weakPasswordDetail: this.passwordHealthService.findWeakPasswordDetails(cipher),
exposedPasswordDetail: exposedPassword,
reusedPasswordCount: passwordUseMap.get(cipher.login.password!) ?? 0,
},
applications: getTrimmedCipherUris(cipher),
} as CipherHealthReport;
return result;
});
}),
);
}
}

View File

@@ -3,8 +3,8 @@ import { catchError, distinctUntilChanged, exhaustMap, map } from "rxjs/operator
import { OrganizationId } from "@bitwarden/common/types/guid";
import { RiskInsightsEnrichedData } from "../../models/report-data-service.types";
import { DrawerType, DrawerDetails, ReportState } from "../../models/report-models";
import { getAtRiskApplicationList, getAtRiskMemberList } from "../../helpers";
import { ReportState, DrawerDetails, DrawerType, RiskInsightsEnrichedData } from "../../models";
import { CriticalAppsService } from "../domain/critical-apps.service";
import { RiskInsightsOrchestratorService } from "../domain/risk-insights-orchestrator.service";
import { RiskInsightsReportService } from "../domain/risk-insights-report.service";
@@ -113,9 +113,7 @@ export class RiskInsightsDataService {
return;
}
const atRiskMemberDetails = this.reportService.generateAtRiskMemberList(
reportResults.reportData,
);
const atRiskMemberDetails = getAtRiskMemberList(reportResults.reportData);
this.drawerDetailsSubject.next({
open: true,
@@ -170,9 +168,7 @@ export class RiskInsightsDataService {
if (!reportResults) {
return;
}
const atRiskAppDetails = this.reportService.generateAtRiskApplicationList(
reportResults.reportData,
);
const atRiskAppDetails = getAtRiskApplicationList(reportResults.reportData);
this.drawerDetailsSubject.next({
open: true,
@@ -187,6 +183,7 @@ export class RiskInsightsDataService {
// ------------------------------ Critical application methods --------------
saveCriticalApplications(selectedUrls: string[]) {
this.orchestrator.setCriticalApplications$(selectedUrls);
return this.organizationDetails$.pipe(
exhaustMap((organizationDetails) => {
if (!organizationDetails?.organizationId) {

View File

@@ -23,6 +23,7 @@ import { EncryptService } from "@bitwarden/common/key-management/crypto/abstract
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength/password-strength.service.abstraction";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { KeyService } from "@bitwarden/key-management";
import { LogService } from "@bitwarden/logging";
import { AccessIntelligenceRoutingModule } from "./access-intelligence-routing.module";
import { RiskInsightsComponent } from "./risk-insights.component";
@@ -48,21 +49,21 @@ import { RiskInsightsComponent } from "./risk-insights.component";
safeProvider({
provide: RiskInsightsReportService,
useClass: RiskInsightsReportService,
deps: [
CipherService,
MemberCipherDetailsApiService,
PasswordHealthService,
RiskInsightsApiService,
RiskInsightsEncryptionService,
],
deps: [RiskInsightsApiService, RiskInsightsEncryptionService],
}),
safeProvider({
provide: RiskInsightsOrchestratorService,
deps: [
AccountServiceAbstraction,
CipherService,
CriticalAppsService,
MemberCipherDetailsApiService,
OrganizationService,
PasswordHealthService,
RiskInsightsApiService,
RiskInsightsReportService,
RiskInsightsEncryptionService,
LogService,
],
}),
safeProvider({

View File

@@ -6,8 +6,10 @@ import { EMPTY } from "rxjs";
import { map, tap } from "rxjs/operators";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { RiskInsightsDataService } from "@bitwarden/bit-common/dirt/reports/risk-insights";
import { DrawerType } from "@bitwarden/bit-common/dirt/reports/risk-insights/models/report-models";
import {
DrawerType,
RiskInsightsDataService,
} from "@bitwarden/bit-common/dirt/reports/risk-insights";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { OrganizationId } from "@bitwarden/common/types/guid";