From 8960acbe0b77b58b78b9d8a10cd826e93d694a7c Mon Sep 17 00:00:00 2001 From: voommen-livefront Date: Tue, 24 Jun 2025 14:47:30 -0500 Subject: [PATCH] PM-20578 retrieve report from DB --- .../services/report-decipher.service.ts | 99 +++++++++++++++++++ .../services/risk-insights-data.service.ts | 67 +++++++------ .../risk-insights-report.service.spec.ts | 12 --- .../services/risk-insights-report.service.ts | 91 +---------------- .../access-intelligence.module.ts | 17 +++- .../all-applications.component.ts | 55 ++++++----- 6 files changed, 178 insertions(+), 163 deletions(-) create mode 100644 bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/report-decipher.service.ts diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/report-decipher.service.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/report-decipher.service.ts new file mode 100644 index 00000000000..dc1358d36ba --- /dev/null +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/report-decipher.service.ts @@ -0,0 +1,99 @@ +// FIXME: Update this file to be type safe +// @ts-strict-ignore +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; +import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; +import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; +import { OrganizationId } from "@bitwarden/common/types/guid"; +import { KeyService } from "@bitwarden/key-management"; + +import { + ApplicationHealthReportDetail, + ApplicationHealthReportSummary, + RiskInsightsReport, + GetRiskInsightsReportResponse, +} from "../models/password-health"; + +export class ReportDecipherService { + constructor( + private keyService: KeyService, + private encryptService: EncryptService, + private keyGeneratorService: KeyGenerationService, + ) {} + + async generateEncryptedRiskInsightsReport( + organizationId: OrganizationId, + details: ApplicationHealthReportDetail[], + summary: ApplicationHealthReportSummary, + ): Promise { + const orgKey = await this.keyService.getOrgKey(organizationId as string); + if (orgKey === null) { + throw new Error("Organization key not found"); + } + + const reportWithSummary = { details, summary }; + + const reportContentEncryptionKey = await this.keyGeneratorService.createKey(512); + + const reportEncrypted = await this.encryptService.encryptString( + JSON.stringify(reportWithSummary), + reportContentEncryptionKey, + ); + + const wrappedReportContentEncryptionKey = await this.encryptService.wrapSymmetricKey( + reportContentEncryptionKey, + orgKey, + ); + + const reportDataWithWrappedKey = { + data: reportEncrypted.encryptedString, + key: wrappedReportContentEncryptionKey.encryptedString, + }; + + const riskInsightReport = { + organizationId: organizationId, + date: new Date().toISOString(), + reportData: JSON.stringify(reportDataWithWrappedKey), + totalMembers: 0, + totalAtRiskMembers: 0, + totalApplications: 0, + totalAtRiskApplications: 0, + totalCriticalApplications: 0, + }; + + return riskInsightReport; + } + + async decryptRiskInsightsReport( + organizationId: OrganizationId, + riskInsightsReportResponse: GetRiskInsightsReportResponse, + ): Promise<[ApplicationHealthReportDetail[], ApplicationHealthReportSummary]> { + try { + const orgKey = await this.keyService.getOrgKey(organizationId as string); + if (orgKey === null) { + throw new Error("Organization key not found"); + } + + const reportDataInJson = JSON.parse(riskInsightsReportResponse.reportData); + const reportEncrypted = reportDataInJson.data; + const wrappedReportContentEncryptionKey = reportDataInJson.key; + + const unwrappedReportContentEncryptionKey = await this.encryptService.unwrapSymmetricKey( + new EncString(wrappedReportContentEncryptionKey), + orgKey, + ); + + const reportUnencrypted = await this.encryptService.decryptString( + new EncString(reportEncrypted), + unwrappedReportContentEncryptionKey, + ); + + const reportWithSummary = JSON.parse(reportUnencrypted); + const reportJson = reportWithSummary.details; + const reportSummary = reportWithSummary.summary; + + return [reportJson, reportSummary]; + } catch { + return [null, null]; + } + } +} diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts index 8c22a336709..497e08d48d1 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts @@ -1,6 +1,5 @@ -import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; -import { BehaviorSubject } from "rxjs"; -import { switchMap } from "rxjs/operators"; +import { BehaviorSubject, firstValueFrom } from "rxjs"; +import { map, switchMap } from "rxjs/operators"; import { OrganizationId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -15,6 +14,7 @@ import { DrawerType, } from "../models/password-health"; +import { ReportDecipherService } from "./report-decipher.service"; import { RiskInsightsApiService } from "./risk-insights-api.service"; import { RiskInsightsReportService } from "./risk-insights-report.service"; export class RiskInsightsDataService { @@ -52,6 +52,7 @@ export class RiskInsightsDataService { private reportService: RiskInsightsReportService, private riskInsightsApiService: RiskInsightsApiService, private cipherService: CipherService, + private reportDecipherService: ReportDecipherService, ) {} /** @@ -59,38 +60,46 @@ export class RiskInsightsDataService { * @param organizationId The ID of the organization. */ fetchApplicationsReport(organizationId: string, isRefresh?: boolean): void { - this.reportService - .generateApplicationsReport$(organizationId) - .pipe(takeUntilDestroyed()) - .subscribe({ - next: (reports: ApplicationHealthReportDetail[]) => { - this.applicationsSubject.next(reports); - this.errorSubject.next(null); - this.appsSummarySubject.next(this.reportService.generateApplicationsSummary(reports)); - this.dataLastUpdatedSubject.next(new Date()); - }, - error: () => { - this.applicationsSubject.next([]); - }, - }); + this.reportService.generateApplicationsReport$(organizationId).subscribe({ + next: (reports: ApplicationHealthReportDetail[]) => { + this.applicationsSubject.next(reports); + this.errorSubject.next(null); + this.appsSummarySubject.next(this.reportService.generateApplicationsSummary(reports)); + this.dataLastUpdatedSubject.next(new Date()); + }, + error: () => { + this.applicationsSubject.next([]); + }, + }); } - fetchApplicationsReportFromCache(organizationId: string) { + fetchApplicationsReportFromCache(organizationId: string, isRefresh: boolean = false) { return this.riskInsightsApiService .getRiskInsightsReport(organizationId as OrganizationId) .pipe( + map((reportFromArchive) => { + if (isRefresh) { + // we force a refresh if isRefresh is true + // ignore all data from the server + return null; + } + return reportFromArchive; + }), switchMap(async (reportFromArchive) => { if (!reportFromArchive || !reportFromArchive?.date) { - this.fetchApplicationsReport(organizationId); + const report = await firstValueFrom( + this.reportService.generateApplicationsReport$(organizationId), + ); + const summary = this.reportService.generateApplicationsSummary(report); return { - report: [], - summary: null, + report, + summary, fromArchive: false, lastUpdated: new Date(), }; } else { - const [report, summary] = await this.reportService.decryptRiskInsightsReport( + const [report, summary] = await this.reportDecipherService.decryptRiskInsightsReport( organizationId as OrganizationId, reportFromArchive, ); @@ -106,14 +115,9 @@ export class RiskInsightsDataService { ) .subscribe({ next: ({ report, summary, fromArchive, lastUpdated }) => { - // in this block, only set the applicationsSubject and appsSummarySubject if the report is from archive - // the fetchApplicationsReport will set them if the report is not from archive - if (fromArchive) { - this.applicationsSubject.next(report); - this.errorSubject.next(null); - this.appsSummarySubject.next(summary); - } - + this.applicationsSubject.next(report); + this.errorSubject.next(null); + this.appsSummarySubject.next(summary); this.isReportFromArchiveSubject.next(fromArchive); this.dataLastUpdatedSubject.next(lastUpdated); }, @@ -150,7 +154,8 @@ export class RiskInsightsDataService { } refreshApplicationsReport(organizationId: string): void { - this.fetchApplicationsReport(organizationId, true); + this.isLoadingData(true); + this.fetchApplicationsReportFromCache(organizationId, true); } isActiveDrawerType = (drawerType: DrawerType): boolean => { diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.spec.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.spec.ts index b38262cde3e..3aa624f1e59 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.spec.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.spec.ts @@ -3,14 +3,10 @@ import { firstValueFrom } from "rxjs"; import { ZXCVBNResult } from "zxcvbn"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; -import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; -import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { KeyService } from "@bitwarden/key-management"; import { mockCiphers } from "./ciphers.mock"; -import { CriticalAppsService } from "./critical-apps.service"; import { MemberCipherDetailsApiService } from "./member-cipher-details-api.service"; import { mockMemberCipherDetails } from "./member-cipher-details-api.service.spec"; import { RiskInsightsReportService } from "./risk-insights-report.service"; @@ -21,10 +17,6 @@ describe("RiskInsightsReportService", () => { const auditService = mock(); const cipherService = mock(); const memberCipherDetailsService = mock(); - const keyService = mock(); - const encryptService = mock(); - const criticalAppsService = mock(); - const keyGenerationService = mock(); beforeEach(() => { pwdStrengthService.getPasswordStrength.mockImplementation((password: string) => { @@ -45,10 +37,6 @@ describe("RiskInsightsReportService", () => { auditService, cipherService, memberCipherDetailsService, - keyService, - encryptService, - criticalAppsService, - keyGenerationService, ); }); diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.ts index abe3e733be2..6ca3a7f97dd 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.ts @@ -3,16 +3,11 @@ import { concatMap, first, from, map, Observable, zip } from "rxjs"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; -import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; -import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; -import { OrganizationId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { KeyService } from "@bitwarden/key-management"; import { ApplicationHealthReportDetail, @@ -25,12 +20,9 @@ import { MemberDetailsFlat, WeakPasswordDetail, WeakPasswordScore, - RiskInsightsReport, - GetRiskInsightsReportResponse, ApplicationHealthReportDetailWithCriticalFlagAndCipher, } from "../models/password-health"; -import { CriticalAppsService } from "./critical-apps.service"; import { MemberCipherDetailsApiService } from "./member-cipher-details-api.service"; export class RiskInsightsReportService { @@ -39,10 +31,6 @@ export class RiskInsightsReportService { private auditService: AuditService, private cipherService: CipherService, private memberCipherDetailsApiService: MemberCipherDetailsApiService, - private keyService: KeyService, - private encryptService: EncryptService, - private criticalAppsService: CriticalAppsService, - private keyGeneratorService: KeyGenerationService, ) {} /** @@ -177,89 +165,12 @@ export class RiskInsightsReportService { }; } - async generateEncryptedRiskInsightsReport( - organizationId: OrganizationId, - details: ApplicationHealthReportDetail[], - summary: ApplicationHealthReportSummary, - ): Promise { - const orgKey = await this.keyService.getOrgKey(organizationId as string); - if (orgKey === null) { - throw new Error("Organization key not found"); - } - - const reportWithSummary = { details, summary }; - - const reportContentEncryptionKey = await this.keyGeneratorService.createKey(512); - - const reportEncrypted = await this.encryptService.encryptString( - JSON.stringify(reportWithSummary), - reportContentEncryptionKey, - ); - - const wrappedReportContentEncryptionKey = await this.encryptService.wrapSymmetricKey( - reportContentEncryptionKey, - orgKey, - ); - - const reportDataWithWrappedKey = { - data: reportEncrypted.encryptedString, - key: wrappedReportContentEncryptionKey.encryptedString, - }; - - const riskInsightReport = { - organizationId: organizationId, - date: new Date().toISOString(), - reportData: JSON.stringify(reportDataWithWrappedKey), - totalMembers: 0, - totalAtRiskMembers: 0, - totalApplications: 0, - totalAtRiskApplications: 0, - totalCriticalApplications: 0, - }; - - return riskInsightReport; - } - - async decryptRiskInsightsReport( - organizationId: OrganizationId, - riskInsightsReportResponse: GetRiskInsightsReportResponse, - ): Promise<[ApplicationHealthReportDetail[], ApplicationHealthReportSummary]> { - try { - const orgKey = await this.keyService.getOrgKey(organizationId as string); - if (orgKey === null) { - throw new Error("Organization key not found"); - } - - const reportDataInJson = JSON.parse(riskInsightsReportResponse.reportData); - const reportEncrypted = reportDataInJson.data; - const wrappedReportContentEncryptionKey = reportDataInJson.key; - - const unwrappedReportContentEncryptionKey = await this.encryptService.unwrapSymmetricKey( - new EncString(wrappedReportContentEncryptionKey), - orgKey, - ); - - const reportUnencrypted = await this.encryptService.decryptString( - new EncString(reportEncrypted), - unwrappedReportContentEncryptionKey, - ); - - const reportWithSummary = JSON.parse(reportUnencrypted); - const reportJson = reportWithSummary.details; - const reportSummary = reportWithSummary.summary; - - return [reportJson, reportSummary]; - } catch { - return [null, null]; - } - } - async identifyCiphers( data: ApplicationHealthReportDetail[], cipherViews: CipherView[], ): Promise { const dataWithCiphers = data.map( - (app, index) => + (app) => ({ ...app, ciphers: cipherViews.filter((c) => app.cipherIds.some((a) => a === c.id)), diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/access-intelligence.module.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/access-intelligence.module.ts index 80cd4e7ca7f..0e2aa9a02a2 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/access-intelligence.module.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/access-intelligence.module.ts @@ -9,6 +9,7 @@ import { RiskInsightsDataService, RiskInsightsReportService, } from "@bitwarden/bit-common/dirt/reports/risk-insights/services"; +import { ReportDecipherService } from "@bitwarden/bit-common/dirt/reports/risk-insights/services/report-decipher.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; @@ -34,15 +35,16 @@ import { RiskInsightsComponent } from "./risk-insights.component"; AuditService, CipherService, MemberCipherDetailsApiService, - KeyService, - EncryptService, - CriticalAppsService, - KeyGenerationService, ], }, { provide: RiskInsightsDataService, - deps: [RiskInsightsReportService, RiskInsightsApiService, CipherService], + deps: [ + RiskInsightsReportService, + RiskInsightsApiService, + CipherService, + ReportDecipherService, + ], }, safeProvider({ provide: CriticalAppsService, @@ -58,6 +60,11 @@ import { RiskInsightsComponent } from "./risk-insights.component"; provide: RiskInsightsApiService, deps: [ApiService], }), + safeProvider({ + provide: ReportDecipherService, + useClass: ReportDecipherService, + deps: [KeyService, EncryptService, KeyGenerationService], + }), ], }) export class AccessIntelligenceModule {} diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications.component.ts index 87b4b96a906..aa576ccfb51 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications.component.ts @@ -9,7 +9,6 @@ import { firstValueFrom, map, Observable, - of, switchMap, } from "rxjs"; @@ -25,6 +24,7 @@ import { ApplicationHealthReportDetailWithCriticalFlagAndCipher, ApplicationHealthReportSummary, } from "@bitwarden/bit-common/dirt/reports/risk-insights/models/password-health"; +import { ReportDecipherService } from "@bitwarden/bit-common/dirt/reports/risk-insights/services/report-decipher.service"; import { getOrganizationById, OrganizationService, @@ -84,7 +84,7 @@ export class AllApplicationsComponent implements OnInit { }; destroyRef = inject(DestroyRef); - isLoading$: Observable = of(false); + isLoading$: Observable = this.dataService.isLoading$; private atRiskInsightsReport = new BehaviorSubject<{ data: ApplicationHealthReportDetailWithCriticalFlag[]; @@ -102,8 +102,6 @@ export class AllApplicationsComponent implements OnInit { }); async ngOnInit() { - this.isLoading$ = this.dataService.isLoading$; - this.dataService.isLoadingData(true); const organizationId = this.activatedRoute.snapshot.paramMap.get("organizationId") ?? ""; @@ -126,6 +124,7 @@ export class AllApplicationsComponent implements OnInit { this.dataService.cipherViewsForOrganization$, ]) .pipe( + takeUntilDestroyed(this.destroyRef), map( ([ report, @@ -160,7 +159,7 @@ export class AllApplicationsComponent implements OnInit { criticalApps, cipherViewsForOrg, }) => { - if (report && organization) { + if (!!report && organization) { const dataWithCiphers = await this.reportService.identifyCiphers( report, cipherViewsForOrg, @@ -178,31 +177,36 @@ export class AllApplicationsComponent implements OnInit { return { report: [], summary, isReportFromArchive, organization, criticalApps: [] }; }, ), - takeUntilDestroyed(this.destroyRef), ) - .subscribe(({ report, summary, isReportFromArchive, organization }) => { - if (report) { - this.dataSource.data = report; - } + .subscribe({ + next: ({ report, summary, isReportFromArchive, organization }) => { + if (report) { + this.dataSource.data = report; + } - if (summary) { - this.applicationSummary = summary; - } + if (summary) { + this.applicationSummary = summary; + } - if (organization) { - this.organization = organization; - } + if (organization) { + this.organization = organization; + } - if (!isReportFromArchive && report && organization && summary) { - this.atRiskInsightsReport.next({ - data: report, - organization: organization, - summary: summary, - }); - } + if (!isReportFromArchive && !!report && !!organization && !!summary) { + this.atRiskInsightsReport.next({ + data: report, + organization: organization, + summary: summary, + }); + } + + if (!!report && !!summary && !!organization) { + this.dataService.isLoadingData(false); + } + }, }); - this.dataService.isLoadingData(false); + // this.dataService.isLoadingData(false); } } @@ -218,6 +222,7 @@ export class AllApplicationsComponent implements OnInit { private accountService: AccountService, protected criticalAppsService: CriticalAppsService, protected riskInsightsApiService: RiskInsightsApiService, + protected reportDecipherService: ReportDecipherService, ) { this.searchControl.valueChanges .pipe(debounceTime(200), takeUntilDestroyed()) @@ -229,7 +234,7 @@ export class AllApplicationsComponent implements OnInit { debounceTime(500), switchMap(async (report) => { if (report && report.organization?.id && report.data && report.summary) { - const data = await this.reportService.generateEncryptedRiskInsightsReport( + const data = await this.reportDecipherService.generateEncryptedRiskInsightsReport( report.organization.id as OrganizationId, report.data, report.summary,