1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-08 12:40:26 +00:00

pm-20577 encrypt and save api

This commit is contained in:
voommen-livefront
2025-05-06 16:25:09 -05:00
parent 855dad7fcc
commit d4271b49d8
8 changed files with 229 additions and 2 deletions

View File

@@ -156,4 +156,31 @@ export enum DrawerType {
OrgAtRiskApps = 3,
}
export interface RiskInsightsReport {
organizationId: OrganizationId;
date: string;
reportData: string;
totalMembers: number;
totalAtRiskMembers: number;
totalApplications: number;
totalAtRiskApplications: number;
totalCriticalApplications: number;
}
export interface SaveRiskInsightsReportRequest {
data: RiskInsightsReport;
}
export interface SaveRiskInsightsReportResponse {
id: string;
organizationId: OrganizationId;
date: string;
reportData: string;
totalMembers: number;
totalAtRiskMembers: number;
totalApplications: number;
totalAtRiskApplications: number;
totalCriticalApplications: number;
}
export type PasswordHealthReportApplicationId = Opaque<string, "PasswordHealthReportApplicationId">;

View File

@@ -49,6 +49,10 @@ export class CriticalAppsService {
// Get a list of critical apps for a given organization
getAppsListForOrg(orgId: string): Observable<PasswordHealthReportApplicationsResponse[]> {
if (this.criticalAppsList.value.length === 0) {
return of([]);
}
return this.criticalAppsList
.asObservable()
.pipe(map((apps) => apps.filter((app) => app.organizationId === orgId)));

View File

@@ -4,3 +4,4 @@ export * from "./critical-apps.service";
export * from "./critical-apps-api.service";
export * from "./risk-insights-report.service";
export * from "./risk-insights-data.service";
export * from "./risk-insights-api.service";

View File

@@ -0,0 +1,52 @@
import { mock } from "jest-mock-extended";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationId } from "@bitwarden/common/types/guid";
import { RiskInsightsApiService } from "./risk-insights-api.service";
describe("RiskInsightsApiService", () => {
let service: RiskInsightsApiService;
const apiService = mock<ApiService>();
beforeEach(() => {
service = new RiskInsightsApiService(apiService);
});
it("should be created", () => {
expect(service).toBeTruthy();
});
it("should call apiService.send with correct parameters for saveRiskInsightsReport", (done) => {
const orgId = "org1" as OrganizationId;
const request = {
data: {
organizationId: orgId,
date: new Date().toISOString(),
reportData: "test data",
totalMembers: 10,
totalAtRiskMembers: 5,
totalApplications: 100,
totalAtRiskApplications: 50,
totalCriticalApplications: 22,
},
};
const response = {
...request.data,
};
apiService.send.mockReturnValue(Promise.resolve(response));
service.saveRiskInsightsReport(orgId, request).subscribe((result) => {
expect(result).toEqual(response);
expect(apiService.send).toHaveBeenCalledWith(
"PUT",
`/reports/risk-insights-report/${orgId.toString()}`,
request.data,
true,
true,
);
done();
});
});
});

View File

@@ -0,0 +1,28 @@
import { from, Observable } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationId } from "@bitwarden/common/types/guid";
import {
SaveRiskInsightsReportRequest,
SaveRiskInsightsReportResponse,
} from "../models/password-health";
export class RiskInsightsApiService {
constructor(private apiService: ApiService) {}
saveRiskInsightsReport(
orgId: OrganizationId,
request: SaveRiskInsightsReportRequest,
): Observable<SaveRiskInsightsReportResponse> {
const dbResponse = this.apiService.send(
"PUT",
`/reports/risk-insights-report/${orgId.toString()}`,
request.data,
true,
true,
);
return from(dbResponse as Promise<SaveRiskInsightsReportResponse>);
}
}

View File

@@ -1,13 +1,16 @@
// FIXME: Update this file to be type safe
// @ts-strict-ignore
import { concatMap, first, from, map, Observable, zip } from "rxjs";
import { concatMap, first, firstValueFrom, from, map, Observable, takeWhile, zip } from "rxjs";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
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,
@@ -20,8 +23,10 @@ import {
MemberDetailsFlat,
WeakPasswordDetail,
WeakPasswordScore,
RiskInsightsReport,
} from "../models/password-health";
import { CriticalAppsService } from "./critical-apps.service";
import { MemberCipherDetailsApiService } from "./member-cipher-details-api.service";
export class RiskInsightsReportService {
@@ -30,6 +35,9 @@ export class RiskInsightsReportService {
private auditService: AuditService,
private cipherService: CipherService,
private memberCipherDetailsApiService: MemberCipherDetailsApiService,
private keyService: KeyService,
private encryptService: EncryptService,
private criticalAppsService: CriticalAppsService,
) {}
/**
@@ -162,6 +170,38 @@ export class RiskInsightsReportService {
};
}
async generateRiskInsightsReport(
organizationId: OrganizationId,
details: ApplicationHealthReportDetail[],
summary: ApplicationHealthReportSummary,
): Promise<RiskInsightsReport> {
const key = await this.keyService.getOrgKey(organizationId as string);
if (key === null) {
throw new Error("Organization key not found");
}
const reportData = await this.encryptService.encryptString(JSON.stringify(details), key);
// const atRiskMembers = this.generateAtRiskMemberList(details);
// const atRiskApplications = this.generateAtRiskApplicationList(details);
const criticalApps = await firstValueFrom(
this.criticalAppsService
.getAppsListForOrg(organizationId)
.pipe(takeWhile((apps) => apps !== null && apps.length > 0)),
);
return {
organizationId: organizationId,
date: new Date().toISOString(),
reportData: reportData?.encryptedString?.toString() ?? "",
totalMembers: summary.totalMemberCount,
totalAtRiskMembers: summary.totalAtRiskMemberCount,
totalApplications: summary.totalApplicationCount,
totalAtRiskApplications: summary.totalAtRiskApplicationCount,
totalCriticalApplications: criticalApps.length,
};
}
/**
* Associates the members with the ciphers they have access to. Calculates the password health.
* Finds the trimmed uris.

View File

@@ -5,6 +5,7 @@ import { CriticalAppsService } from "@bitwarden/bit-common/dirt/reports/risk-ins
import {
CriticalAppsApiService,
MemberCipherDetailsApiService,
RiskInsightsApiService,
RiskInsightsDataService,
RiskInsightsReportService,
} from "@bitwarden/bit-common/dirt/reports/risk-insights/services";
@@ -32,6 +33,9 @@ import { RiskInsightsComponent } from "./risk-insights.component";
AuditService,
CipherService,
MemberCipherDetailsApiService,
KeyService,
EncryptService,
CriticalAppsService,
],
},
{
@@ -48,6 +52,10 @@ import { RiskInsightsComponent } from "./risk-insights.component";
useClass: CriticalAppsApiService,
deps: [ApiService],
}),
safeProvider({
provide: RiskInsightsApiService,
deps: [ApiService],
}),
],
})
export class AccessIntelligenceModule {}

View File

@@ -2,10 +2,21 @@ import { Component, DestroyRef, inject, OnInit } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { FormControl } from "@angular/forms";
import { ActivatedRoute } from "@angular/router";
import { combineLatest, debounceTime, firstValueFrom, map, Observable, of, skipWhile } from "rxjs";
import {
BehaviorSubject,
combineLatest,
debounceTime,
firstValueFrom,
map,
Observable,
of,
skipWhile,
switchMap,
} from "rxjs";
import {
CriticalAppsService,
RiskInsightsApiService,
RiskInsightsDataService,
RiskInsightsReportService,
} from "@bitwarden/bit-common/dirt/reports/risk-insights";
@@ -24,6 +35,7 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { OrganizationId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import {
Icons,
@@ -74,6 +86,12 @@ export class AllApplicationsComponent implements OnInit {
isLoading$: Observable<boolean> = of(false);
isCriticalAppsFeatureEnabled = false;
private atRiskInsightsReport = new BehaviorSubject<{
data: ApplicationHealthReportDetailWithCriticalFlag[];
organization: Organization;
summary: ApplicationHealthReportSummary;
}>(null);
async ngOnInit() {
this.isCriticalAppsFeatureEnabled = await this.configService.getFeatureFlag(
FeatureFlag.CriticalApps,
@@ -112,6 +130,16 @@ export class AllApplicationsComponent implements OnInit {
if (organization) {
this.organization = organization;
}
if (data && organization) {
this.atRiskInsightsReport.next({
data,
organization,
summary: this.applicationSummary,
});
}
// this.persistReportData();
});
this.isLoading$ = this.dataService.isLoading$;
@@ -129,10 +157,49 @@ export class AllApplicationsComponent implements OnInit {
protected reportService: RiskInsightsReportService,
private accountService: AccountService,
protected criticalAppsService: CriticalAppsService,
protected riskInsightsApiService: RiskInsightsApiService,
) {
this.searchControl.valueChanges
.pipe(debounceTime(200), takeUntilDestroyed())
.subscribe((v) => (this.dataSource.filter = v));
this.atRiskInsightsReport
.asObservable()
.pipe(
debounceTime(500),
switchMap(async (report) => {
if (report && report.organization?.id && report.data && report.summary) {
const data = await this.reportService.generateRiskInsightsReport(
report.organization.id as OrganizationId,
report.data,
report.summary,
);
return data;
}
return null;
}),
switchMap(async (reportData) => {
if (reportData) {
const request = { data: reportData };
try {
const response = await firstValueFrom(
this.riskInsightsApiService.saveRiskInsightsReport(
this.organization.id as OrganizationId,
request,
),
);
// console.log("Risk insights report saved successfully.", JSON.stringify(response));
return response;
} catch {
// console.error("Error saving risk insights report:", error);
}
return null;
}
}),
takeUntilDestroyed(),
)
.subscribe();
}
goToCreateNewLoginItem = async () => {