From 1539e8308ab50d2f8a4753908ceaf9adbac168df Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 28 Oct 2025 21:17:32 +0000 Subject: [PATCH] feat(dirt): add saveApplicationReviewStatus$ to orchestrator Implement method to save application review status and critical flags. Updates all applications where reviewedDate === null to set current date, and marks selected applications as critical. - Add saveApplicationReviewStatus$() method - Add _updateReviewStatusAndCriticalFlags() helper - Uses existing encryption and API update patterns - Single API call for both review status and critical flags - Follows same pattern as saveCriticalApplications$() Related to PM-27284 --- .../risk-insights-orchestrator.service.ts | 145 ++++++++++++++++++ 1 file changed, 145 insertions(+) diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/domain/risk-insights-orchestrator.service.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/domain/risk-insights-orchestrator.service.ts index 90a64ceffa5..7feec4a29d4 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/domain/risk-insights-orchestrator.service.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/domain/risk-insights-orchestrator.service.ts @@ -350,6 +350,118 @@ export class RiskInsightsOrchestratorService { ); } + /** + * Saves review status for new applications and optionally marks selected ones as critical. + * This method: + * 1. Sets reviewedDate to current date for all applications where reviewedDate === null + * 2. Sets isCritical = true for applications in the selectedCriticalApps array + * + * @param selectedCriticalApps Array of application names to mark as critical (can be empty) + * @returns Observable of updated ReportState + */ + saveApplicationReviewStatus$(selectedCriticalApps: string[]): Observable { + this.logService.info( + "[RiskInsightsOrchestratorService] Saving application review status", + { + criticalAppsCount: selectedCriticalApps.length, + }, + ); + + return this.rawReportData$.pipe( + take(1), + filter((data) => !data.loading && data.data != null), + withLatestFrom( + this.organizationDetails$.pipe(filter((org) => !!org && !!org.organizationId)), + this._userId$.pipe(filter((userId) => !!userId)), + ), + map(([reportState, organizationDetails, userId]) => { + const existingApplicationData = reportState?.data?.applicationData || []; + const updatedApplicationData = this._updateReviewStatusAndCriticalFlags( + existingApplicationData, + selectedCriticalApps, + ); + + const updatedState = { + ...reportState, + data: { + ...reportState.data, + applicationData: updatedApplicationData, + }, + } as ReportState; + + this.logService.debug( + "[RiskInsightsOrchestratorService] Updated review status", + { + totalApps: updatedApplicationData.length, + reviewedApps: updatedApplicationData.filter((app) => app.reviewedDate !== null).length, + criticalApps: updatedApplicationData.filter((app) => app.isCritical).length, + }, + ); + + return { reportState, organizationDetails, updatedState, userId }; + }), + switchMap(({ reportState, organizationDetails, updatedState, userId }) => { + return from( + this.riskInsightsEncryptionService.encryptRiskInsightsReport( + { + organizationId: organizationDetails!.organizationId, + userId: userId!, + }, + { + reportData: reportState?.data?.reportData ?? [], + summaryData: reportState?.data?.summaryData ?? createNewSummaryData(), + applicationData: updatedState?.data?.applicationData ?? [], + }, + reportState?.data?.contentEncryptionKey, + ), + ).pipe( + map((encryptedData) => ({ + reportState, + organizationDetails, + updatedState, + encryptedData, + })), + ); + }), + switchMap(({ reportState, organizationDetails, updatedState, encryptedData }) => { + this.logService.debug( + `[RiskInsightsOrchestratorService] Persisting review status - report id: ${reportState?.data?.id}`, + ); + + if (!reportState?.data?.id || !organizationDetails?.organizationId) { + this.logService.warning( + "[RiskInsightsOrchestratorService] Cannot save review status - missing report id or org id", + ); + return of({ ...reportState }); + } + + return this.reportApiService + .updateRiskInsightsApplicationData$( + reportState.data.id, + organizationDetails.organizationId, + { + data: { + applicationData: encryptedData.encryptedApplicationData.toSdk(), + }, + }, + ) + .pipe( + map(() => updatedState), + tap((finalState) => { + this._markUnmarkUpdatesSubject.next(finalState); + }), + catchError((error: unknown) => { + this.logService.error( + "[RiskInsightsOrchestratorService] Failed to save review status", + error, + ); + return of({ ...reportState, error: "Failed to save application review status" }); + }), + ); + }), + ); + } + private _fetchReport$(organizationId: OrganizationId, userId: UserId): Observable { return this.reportService.getRiskInsightsReport$(organizationId, userId).pipe( tap(() => this.logService.debug("[RiskInsightsOrchestratorService] Fetching report")), @@ -501,6 +613,39 @@ export class RiskInsightsOrchestratorService { return updatedApps; } + /** + * Updates review status and critical flags for applications. + * Sets reviewedDate for all apps with null reviewedDate. + * Sets isCritical flag for apps in the criticalApplications array. + * + * @param existingApplications Current application data + * @param criticalApplications Array of application names to mark as critical + * @returns Updated application data with review dates and critical flags + */ + private _updateReviewStatusAndCriticalFlags( + existingApplications: OrganizationReportApplication[], + criticalApplications: string[], + ): OrganizationReportApplication[] { + const criticalSet = new Set(criticalApplications); + const currentDate = new Date(); + + return existingApplications.map((app) => { + const shouldMarkCritical = criticalSet.has(app.applicationName); + const needsReviewDate = app.reviewedDate === null; + + // Only create new object if changes are needed + if (needsReviewDate || shouldMarkCritical) { + return { + ...app, + reviewedDate: needsReviewDate ? currentDate : app.reviewedDate, + isCritical: shouldMarkCritical || app.isCritical, + }; + } + + return app; + }); + } + // Toggles the isCritical flag on applications via criticalApplicationName private _removeCriticalApplication( applicationData: OrganizationReportApplication[],