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:
@@ -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">;
|
||||
|
||||
@@ -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)));
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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>);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user