1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-06 19:53:59 +00:00

PM-20578 retrieve report from DB

This commit is contained in:
voommen-livefront
2025-06-24 14:47:30 -05:00
parent 02e0eb699d
commit 8960acbe0b
6 changed files with 178 additions and 163 deletions

View File

@@ -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<RiskInsightsReport> {
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];
}
}
}

View File

@@ -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 => {

View File

@@ -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<AuditService>();
const cipherService = mock<CipherService>();
const memberCipherDetailsService = mock<MemberCipherDetailsApiService>();
const keyService = mock<KeyService>();
const encryptService = mock<EncryptService>();
const criticalAppsService = mock<CriticalAppsService>();
const keyGenerationService = mock<KeyGenerationService>();
beforeEach(() => {
pwdStrengthService.getPasswordStrength.mockImplementation((password: string) => {
@@ -45,10 +37,6 @@ describe("RiskInsightsReportService", () => {
auditService,
cipherService,
memberCipherDetailsService,
keyService,
encryptService,
criticalAppsService,
keyGenerationService,
);
});

View File

@@ -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<RiskInsightsReport> {
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<ApplicationHealthReportDetailWithCriticalFlagAndCipher[]> {
const dataWithCiphers = data.map(
(app, index) =>
(app) =>
({
...app,
ciphers: cipherViews.filter((c) => app.cipherIds.some((a) => a === c.id)),

View File

@@ -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 {}

View File

@@ -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<boolean> = of(false);
isLoading$: Observable<boolean> = 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,