Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -167,4 +167,40 @@ export enum DrawerType {
OrgAtRiskApps = 3,
}

export interface RiskInsightsReport {
organizationId: OrganizationId;
date: string;
reportData: string;
totalMembers: number;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question โ“ Will this report be automatically generated and submitted on a schedule, or manually created only? If the former, are we OK with this unencrypted metadata?

โš ๏ธโš ๏ธโš ๏ธ Assuming it can be run automated: I don't know the underlying security guarantees that are expected to be achieved here. From the fact that we encrypt reports, but do not encrypt summaries, I assume the underlying assumption here is that summaries do not leak individual data so it's "safe to store". That (assumed) assumption is not valid.

Unless not possible, I would recommend encrypting all metadata here. Specifically, reportData should be one blob including both the summary data and detail data. Below, I show a theoretical attack that may abuse the summary data to decrypt all org vault data remotely.

Specifically, the following attacks (non exhaustive, and non validated) may be made possible. I have not implemented or validated them, and they are purely on the basis of reading the code.

  • Assumption: There is a automated schedule / way to trigger the generation
  • Threat model: the attacker controls the API server.

Leak individiual item/application weakness status

Before every report generation, the server forces a sync to the client that will create the report, and present a false view of the ciphers, specifically, just show 1 cipher. The total will then reflect just that cipher, and thus the safety presumed by the use of summarized data is broken. This attack can be optimized to be much faster than a linear amount of queries.

Remote predicate evaluation on arbitrary org encrypted strings

Further, it allows the server to remotely run certain predicates such as findWeakPassword on any arbitrary org-key encrypted data (not just passwords) and obtain the result, by swapping encrypted strings within the cipher.

  • Possible full plaintext recovery, breaking organization e2e encryption: Doing a brief evaluation, without further validation, this.passwordStrengthService.getPasswordStrength( is called, which uses zxcvbn to compare a security score, of the password compared to, including the encrypted "username" field. If the server has access to known plaintext ciphertext pairs here, they may be able to create a decryption oracle capable of remotely decrypting all org vault data with this. NOTE: This is theoretical, I have not validated this thoroughly. Roughly, the attacker would remotely, via the previous attack make the client evaluate password strength. As the password, the server sets the target encstring. As the username, they set one of the known plaintext / ciphertext pairs. zxcvbn will return a different score based on the similarity of the two, and depending on if it reaches a threshold, it will be a cipher at risk. This can be used, given enough plaintext ciphertext pairs to fully recovery the target plaintext, thus making full decryption of all vault data possible. One would find a plaintext-ciphertext pair close to the threshold, then flip each character to figure out which character is the correct character for that position. However, this requires a lot of plaintext ciphertext pairs, or an encryption oracle.

Remote predicate evaluation - equality check

Using the same attack as above, the server can perform an equality check on any two arbitrary org-key encrypted org strings, by presenting 2 items, with the target encstrings set as the password.

Remote clustering of encrypted vault items by app

Further, it allows the server to cluster any ciphers and confirm if the are for the same domain. Example: Server provides cipher A, cipher B. If only one app is reported, then both are the same app and get clustered. This can be improved to be much faster than linear time.

(Out of scope note) Clusters / individual weak items can be tied to real domains

Combining this with a compromised icon server (out of scope) would allow tying encrypted ciphers to domains, and thus leak which accounts are weak.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will cc @mandreko-bitwarden, @jlf0dev on this, because these (presumed) attacks are security relevant, possibly threatening all of organization vault encryption confidentiality. I'm keeping these comments public because this is not released code, but just draft.

@jlf0dev I have not implemented these attacks because I looked at this as an early code review for the unrelated issue bellow, but it felt unethical not to look closer here. I don't know if it's worth to spend the time to confirm the validity of the attacks above. If we just encrypt the metadata, then all of them are invalid etiher way.

totalAtRiskMembers: number;
totalApplications: number;
totalAtRiskApplications: number;
totalCriticalApplications: number;
}

export interface ReportInsightsReportData {
data: string;
key: string;
}

export interface SaveRiskInsightsReportRequest {
data: RiskInsightsReport;
}

export interface SaveRiskInsightsReportResponse {
id: string;
}

export interface GetRiskInsightsReportResponse {
id: string;
organizationId: OrganizationId;
date: string;
reportData: string;
totalMembers: number;
totalAtRiskMembers: number;
totalApplications: number;
totalAtRiskApplications: number;
totalCriticalApplications: number;
}

export type PasswordHealthReportApplicationId = Opaque<string, "PasswordHealthReportApplicationId">;
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
export * from "./critical-apps-api.service";
export * from "./risk-insights-report.service";
export * from "./risk-insights-data.service";
export * from "./risk-insights-api.service";

Check warning on line 7 in bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/index.ts

View check run for this annotation

Codecov / codecov/patch

bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/index.ts#L7

Added line #L7 was not covered by tests
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { mock } from "jest-mock-extended";

import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationId } from "@bitwarden/common/types/guid";

import { SaveRiskInsightsReportRequest } from "../models/password-health";

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: SaveRiskInsightsReportRequest = {
data: {
organizationId: orgId,
date: new Date().toISOString(),
reportData: "test",
totalMembers: 10,
totalAtRiskMembers: 5,
totalApplications: 100,
totalAtRiskApplications: 50,
totalCriticalApplications: 22,
},
};
const response = {
...request.data,
};

apiService.send.mockReturnValue(Promise.resolve(response));

service.saveRiskInsightsReport(request).subscribe((result) => {
expect(result).toEqual(response);
expect(apiService.send).toHaveBeenCalledWith(
"POST",
`/reports/organization-reports`,
request.data,
true,
true,
);
done();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { from, Observable, of } from "rxjs";

import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationId } from "@bitwarden/common/types/guid";

import {
GetRiskInsightsReportResponse,
SaveRiskInsightsReportRequest,
SaveRiskInsightsReportResponse,
} from "../models/password-health";

export class RiskInsightsApiService {
constructor(private apiService: ApiService) {}

saveRiskInsightsReport(
request: SaveRiskInsightsReportRequest,
): Observable<SaveRiskInsightsReportResponse> {
const dbResponse = this.apiService.send(
"POST",
`/reports/organization-reports`,
request.data,
true,
true,
);

return from(dbResponse as Promise<SaveRiskInsightsReportResponse>);
}

getRiskInsightsReport(orgId: OrganizationId): Observable<GetRiskInsightsReportResponse | null> {
const dbResponse = this.apiService

Check warning on line 30 in bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-api.service.ts

View check run for this annotation

Codecov / codecov/patch

bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-api.service.ts#L30

Added line #L30 was not covered by tests
.send("GET", `/reports/organization-reports/latest/${orgId.toString()}`, null, true, true)
.catch((error: any): any => {
if (error.statusCode === 404) {
return null; // Handle 404 by returning null or an appropriate default value

Check warning on line 34 in bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-api.service.ts

View check run for this annotation

Codecov / codecov/patch

bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-api.service.ts#L34

Added line #L34 was not covered by tests
}
throw error; // Re-throw other errors

Check warning on line 36 in bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-api.service.ts

View check run for this annotation

Codecov / codecov/patch

bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-api.service.ts#L36

Added line #L36 was not covered by tests
});

if (dbResponse instanceof Error) {
return of(null);

Check warning on line 40 in bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-api.service.ts

View check run for this annotation

Codecov / codecov/patch

bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-api.service.ts#L40

Added line #L40 was not covered by tests
}
return from(dbResponse as Promise<GetRiskInsightsReportResponse>);

Check warning on line 42 in bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-api.service.ts

View check run for this annotation

Codecov / codecov/patch

bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-api.service.ts#L42

Added line #L42 was not covered by tests
}
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,30 @@
import { BehaviorSubject } from "rxjs";
import { finalize } from "rxjs/operators";
import { BehaviorSubject, firstValueFrom } from "rxjs";
import { map, switchMap } from "rxjs/operators";

Check warning on line 2 in bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts

View check run for this annotation

Codecov / codecov/patch

bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts#L1-L2

Added lines #L1 - L2 were not covered by tests

import { OrganizationId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";

import {
AppAtRiskMembersDialogParams,
ApplicationHealthReportDetail,
ApplicationHealthReportSummary,
AtRiskApplicationDetail,
AtRiskMemberDetail,
DrawerType,
} from "../models/password-health";

import { RiskInsightsApiService } from "./risk-insights-api.service";
import { RiskInsightsEncryptionService } from "./risk-insights-encryption.service";
import { RiskInsightsReportService } from "./risk-insights-report.service";
export class RiskInsightsDataService {
private applicationsSubject = new BehaviorSubject<ApplicationHealthReportDetail[] | null>(null);
private appsSummarySubject = new BehaviorSubject<ApplicationHealthReportSummary | null>(null);
private isReportFromArchiveSubject = new BehaviorSubject<boolean>(true); // True by default

Check warning on line 23 in bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts

View check run for this annotation

Codecov / codecov/patch

bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts#L22-L23

Added lines #L22 - L23 were not covered by tests

applications$ = this.applicationsSubject.asObservable();
appsSummary$ = this.appsSummarySubject.asObservable();
isReportFromArchive$ = this.isReportFromArchiveSubject.asObservable();

Check warning on line 27 in bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts

View check run for this annotation

Codecov / codecov/patch

bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts#L26-L27

Added lines #L26 - L27 were not covered by tests

private isLoadingSubject = new BehaviorSubject<boolean>(false);
isLoading$ = this.isLoadingSubject.asObservable();
Expand All @@ -24,50 +35,128 @@
private errorSubject = new BehaviorSubject<string | null>(null);
error$ = this.errorSubject.asObservable();

private dataLastUpdatedSubject = new BehaviorSubject<Date | null>(null);
private dataLastUpdatedSubject = new BehaviorSubject<Date>(new Date());

Check warning on line 38 in bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts

View check run for this annotation

Codecov / codecov/patch

bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts#L38

Added line #L38 was not covered by tests
dataLastUpdated$ = this.dataLastUpdatedSubject.asObservable();

private cipherViewsForOrganizationSubject = new BehaviorSubject<CipherView[]>([]);
cipherViewsForOrganization$ = this.cipherViewsForOrganizationSubject.asObservable();

Check warning on line 42 in bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts

View check run for this annotation

Codecov / codecov/patch

bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts#L41-L42

Added lines #L41 - L42 were not covered by tests

openDrawer = false;
drawerInvokerId: string = "";
activeDrawerType: DrawerType = DrawerType.None;
atRiskMemberDetails: AtRiskMemberDetail[] = [];
appAtRiskMembers: AppAtRiskMembersDialogParams | null = null;
atRiskAppDetails: AtRiskApplicationDetail[] | null = null;

constructor(private reportService: RiskInsightsReportService) {}
constructor(
private reportService: RiskInsightsReportService,
private riskInsightsApiService: RiskInsightsApiService,
private cipherService: CipherService,
private riskInsightsEncryptionService: RiskInsightsEncryptionService,

Check warning on line 55 in bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts

View check run for this annotation

Codecov / codecov/patch

bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts#L52-L55

Added lines #L52 - L55 were not covered by tests
) {}

/**
* Fetches the applications report and updates the applicationsSubject.
* @param organizationId The ID of the organization.
*/
fetchApplicationsReport(organizationId: string, isRefresh?: boolean): void {
if (isRefresh) {
this.isRefreshingSubject.next(true);
} else {
this.isLoadingSubject.next(true);
}
this.reportService
.generateApplicationsReport$(organizationId)
this.reportService.generateApplicationsReport$(organizationId).subscribe({

Check warning on line 63 in bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts

View check run for this annotation

Codecov / codecov/patch

bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts#L63

Added line #L63 was not covered by tests
next: (reports: ApplicationHealthReportDetail[]) => {
this.applicationsSubject.next(reports);
this.errorSubject.next(null);
this.appsSummarySubject.next(this.reportService.generateApplicationsSummary(reports));
this.dataLastUpdatedSubject.next(new Date());

Check warning on line 68 in bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts

View check run for this annotation

Codecov / codecov/patch

bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts#L65-L68

Added lines #L65 - L68 were not covered by tests
},
error: () => {
this.applicationsSubject.next([]);

Check warning on line 71 in bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts

View check run for this annotation

Codecov / codecov/patch

bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts#L71

Added line #L71 was not covered by tests
},
});
}

fetchApplicationsReportFromCache(organizationId: string, isRefresh: boolean = false) {
return this.riskInsightsApiService

Check warning on line 77 in bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts

View check run for this annotation

Codecov / codecov/patch

bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts#L77

Added line #L77 was not covered by tests
.getRiskInsightsReport(organizationId as OrganizationId)
.pipe(
finalize(() => {
this.isLoadingSubject.next(false);
this.isRefreshingSubject.next(false);
this.dataLastUpdatedSubject.next(new Date());
map((reportFromArchive) => {
if (isRefresh) {
// we force a refresh if isRefresh is true
// ignore all data from the server
return null;

Check warning on line 84 in bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts

View check run for this annotation

Codecov / codecov/patch

bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts#L84

Added line #L84 was not covered by tests
}
return reportFromArchive;

Check warning on line 86 in bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts

View check run for this annotation

Codecov / codecov/patch

bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts#L86

Added line #L86 was not covered by tests
}),
switchMap(async (reportFromArchive) => {

Check warning on line 88 in bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts

View check run for this annotation

Codecov / codecov/patch

bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts#L88

Added line #L88 was not covered by tests
if (!reportFromArchive || !reportFromArchive?.date) {
const report = await firstValueFrom(

Check warning on line 90 in bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts

View check run for this annotation

Codecov / codecov/patch

bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts#L90

Added line #L90 was not covered by tests
this.reportService.generateApplicationsReport$(organizationId),
);
const summary = this.reportService.generateApplicationsSummary(report);

Check warning on line 93 in bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts

View check run for this annotation

Codecov / codecov/patch

bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts#L93

Added line #L93 was not covered by tests

return {

Check warning on line 95 in bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts

View check run for this annotation

Codecov / codecov/patch

bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts#L95

Added line #L95 was not covered by tests
report,
summary,
fromArchive: false,
lastUpdated: new Date(),
};
} else {
const [report, summary] =
await this.riskInsightsEncryptionService.decryptRiskInsightsReport(

Check warning on line 103 in bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts

View check run for this annotation

Codecov / codecov/patch

bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts#L103

Added line #L103 was not covered by tests
organizationId as OrganizationId,
reportFromArchive,
);

return {

Check warning on line 108 in bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts

View check run for this annotation

Codecov / codecov/patch

bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts#L108

Added line #L108 was not covered by tests
report,
summary,
fromArchive: true,
lastUpdated: new Date(reportFromArchive.date),
};
}
}),
)
.subscribe({
next: (reports: ApplicationHealthReportDetail[]) => {
this.applicationsSubject.next(reports);
next: ({ report, summary, fromArchive, lastUpdated }) => {
this.applicationsSubject.next(report);

Check warning on line 119 in bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts

View check run for this annotation

Codecov / codecov/patch

bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts#L119

Added line #L119 was not covered by tests
this.errorSubject.next(null);
this.appsSummarySubject.next(summary);
this.isReportFromArchiveSubject.next(fromArchive);
this.dataLastUpdatedSubject.next(lastUpdated);

Check warning on line 123 in bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts

View check run for this annotation

Codecov / codecov/patch

bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts#L121-L123

Added lines #L121 - L123 were not covered by tests
},
error: () => {
error: (error: unknown) => {
this.errorSubject.next((error as Error).message);

Check warning on line 126 in bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts

View check run for this annotation

Codecov / codecov/patch

bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts#L126

Added line #L126 was not covered by tests
this.applicationsSubject.next([]);
},
});
}

async fetchCipherViewsForOrganization(
organizationId: OrganizationId,

Check warning on line 133 in bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts

View check run for this annotation

Codecov / codecov/patch

bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts#L133

Added line #L133 was not covered by tests
isRefresh: boolean = false,
): Promise<void> {
if (isRefresh) {
this.cipherViewsForOrganizationSubject.next([]);

Check warning on line 137 in bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts

View check run for this annotation

Codecov / codecov/patch

bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts#L137

Added line #L137 was not covered by tests
}

if (this.cipherViewsForOrganizationSubject.value) {
return;

Check warning on line 141 in bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts

View check run for this annotation

Codecov / codecov/patch

bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts#L141

Added line #L141 was not covered by tests
}

const cipherViews = await this.cipherService.getAllFromApiForOrganization(organizationId);
this.cipherViewsForOrganizationSubject.next(cipherViews);

Check warning on line 145 in bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts

View check run for this annotation

Codecov / codecov/patch

bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts#L144-L145

Added lines #L144 - L145 were not covered by tests
}

isLoadingData(started: boolean): void {
this.isLoadingSubject.next(started);
this.isRefreshingSubject.next(started);

Check warning on line 150 in bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts

View check run for this annotation

Codecov / codecov/patch

bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts#L149-L150

Added lines #L149 - L150 were not covered by tests
}

setReportFromArchiveStatus(isFromArchive: boolean): void {
this.isReportFromArchiveSubject.next(isFromArchive);

Check warning on line 154 in bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts

View check run for this annotation

Codecov / codecov/patch

bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts#L154

Added line #L154 was not covered by tests
}

refreshApplicationsReport(organizationId: string): void {
this.fetchApplicationsReport(organizationId, true);
this.isLoadingData(true);
this.fetchApplicationsReportFromCache(organizationId, true);

Check warning on line 159 in bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts

View check run for this annotation

Codecov / codecov/patch

bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts#L158-L159

Added lines #L158 - L159 were not covered by tests
}

isActiveDrawerType = (drawerType: DrawerType): boolean => {
Expand Down
Loading
Loading