1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-18 09:13:33 +00:00

[PM-23680] Report Applications data (#16819)

* Move files to folders. Delete unused component. Move model to file

* Move risk insights services to folder structure capturing domains, api, and view organization. Move mock data

* Remove legacy risk insight report code

* Move api model to file

* Separate data service and orchestration of data to make the data service a facade

* Add orchestration updates for fetching applications as well as migrating data.

* Updated migration of critical applications and merged old saved data to new critical applications on report object

* Update test cases

* Fixed test case after merge. Cleaned up per comments on review

* Fixed decryption and encryption issue when not using existing content key

* Fix type errors

* Fix test update

* Fixe remove critical applications

* Fix report generating flag not being reset

* Removed extra logs
This commit is contained in:
Leslie Tilton
2025-10-22 10:36:51 -05:00
committed by GitHub
parent cc954ed123
commit 03d636108d
59 changed files with 2142 additions and 1864 deletions

View File

@@ -12,7 +12,8 @@ import {
RiskInsightsReportService,
SecurityTasksApiService,
} from "@bitwarden/bit-common/dirt/reports/risk-insights/services";
import { RiskInsightsEncryptionService } from "@bitwarden/bit-common/dirt/reports/risk-insights/services/risk-insights-encryption.service";
import { RiskInsightsEncryptionService } from "@bitwarden/bit-common/dirt/reports/risk-insights/services/domain/risk-insights-encryption.service";
import { RiskInsightsOrchestratorService } from "@bitwarden/bit-common/dirt/reports/risk-insights/services/domain/risk-insights-orchestrator.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
@@ -24,6 +25,7 @@ import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/pass
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { ToastService } from "@bitwarden/components";
import { KeyService } from "@bitwarden/key-management";
import { LogService } from "@bitwarden/logging";
import { DefaultAdminTaskService } from "../../vault/services/default-admin-task.service";
@@ -52,28 +54,31 @@ import { AccessIntelligenceSecurityTasksService } from "./shared/security-tasks.
safeProvider({
provide: RiskInsightsReportService,
useClass: RiskInsightsReportService,
deps: [RiskInsightsApiService, RiskInsightsEncryptionService],
}),
safeProvider({
provide: RiskInsightsOrchestratorService,
deps: [
AccountServiceAbstraction,
CipherService,
CriticalAppsService,
LogService,
MemberCipherDetailsApiService,
OrganizationService,
PasswordHealthService,
RiskInsightsApiService,
RiskInsightsReportService,
RiskInsightsEncryptionService,
],
}),
safeProvider({
provide: RiskInsightsDataService,
deps: [
AccountServiceAbstraction,
CriticalAppsService,
OrganizationService,
RiskInsightsReportService,
],
deps: [RiskInsightsOrchestratorService],
}),
{
safeProvider({
provide: RiskInsightsEncryptionService,
useClass: RiskInsightsEncryptionService,
deps: [KeyService, EncryptService, KeyGenerationService],
},
deps: [KeyService, EncryptService, KeyGenerationService, LogService],
}),
safeProvider({
provide: CriticalAppsService,
useClass: CriticalAppsService,

View File

@@ -1,12 +1,12 @@
import { CommonModule } from "@angular/common";
import { Component, OnInit } from "@angular/core";
import { Component, OnInit, ChangeDetectionStrategy } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { Subject, switchMap, takeUntil, of, BehaviorSubject, combineLatest } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import {
AllActivitiesService,
LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher,
ApplicationHealthReportDetailEnriched,
SecurityTasksApiService,
TaskMetrics,
OrganizationReportSummary,
@@ -14,17 +14,12 @@ import {
import { OrganizationId } from "@bitwarden/common/types/guid";
import { ButtonModule, ProgressModule, TypographyModule } from "@bitwarden/components";
import { DefaultAdminTaskService } from "../../../vault/services/default-admin-task.service";
import { AccessIntelligenceSecurityTasksService } from "../shared/security-tasks.service";
export const RenderMode = {
noCriticalApps: "noCriticalApps",
criticalAppsWithAtRiskAppsAndNoTasks: "criticalAppsWithAtRiskAppsAndNoTasks",
criticalAppsWithAtRiskAppsAndTasks: "criticalAppsWithAtRiskAppsAndTasks",
} as const;
export type RenderMode = (typeof RenderMode)[keyof typeof RenderMode];
import { DefaultAdminTaskService } from "../../../../vault/services/default-admin-task.service";
import { RenderMode } from "../../models/activity.models";
import { AccessIntelligenceSecurityTasksService } from "../../shared/security-tasks.service";
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: "dirt-password-change-metric",
imports: [CommonModule, TypographyModule, JslibModule, ProgressModule, ButtonModule],
templateUrl: "./password-change-metric.component.html",
@@ -34,8 +29,7 @@ export class PasswordChangeMetricComponent implements OnInit {
protected taskMetrics$ = new BehaviorSubject<TaskMetrics>({ totalTasks: 0, completedTasks: 0 });
private completedTasks: number = 0;
private totalTasks: number = 0;
private allApplicationsDetails: LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher[] =
[];
private allApplicationsDetails: ApplicationHealthReportDetailEnriched[] = [];
atRiskAppsCount: number = 0;
atRiskPasswordsCount: number = 0;
@@ -43,6 +37,13 @@ export class PasswordChangeMetricComponent implements OnInit {
private destroyRef = new Subject<void>();
renderMode: RenderMode = "noCriticalApps";
constructor(
private activatedRoute: ActivatedRoute,
private securityTasksApiService: SecurityTasksApiService,
private allActivitiesService: AllActivitiesService,
protected accessIntelligenceSecurityTasksService: AccessIntelligenceSecurityTasksService,
) {}
async ngOnInit(): Promise<void> {
combineLatest([this.activatedRoute.paramMap, this.allActivitiesService.taskCreatedCount$])
.pipe(
@@ -83,13 +84,6 @@ export class PasswordChangeMetricComponent implements OnInit {
});
}
constructor(
private activatedRoute: ActivatedRoute,
private securityTasksApiService: SecurityTasksApiService,
private allActivitiesService: AllActivitiesService,
protected accessIntelligenceSecurityTasksService: AccessIntelligenceSecurityTasksService,
) {}
private determineRenderMode(
summary: OrganizationReportSummary,
taskMetrics: TaskMetrics,

View File

@@ -11,16 +11,16 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { getById } from "@bitwarden/common/platform/misc";
import { ToastService, DialogService } from "@bitwarden/components";
import { DialogService } from "@bitwarden/components";
import { SharedModule } from "@bitwarden/web-vault/app/shared";
import { RiskInsightsTabType } from "../models/risk-insights.models";
import { ApplicationsLoadingComponent } from "../shared/risk-insights-loading.component";
import { ActivityCardComponent } from "./activity-card.component";
import { PasswordChangeMetricComponent } from "./activity-cards/password-change-metric.component";
import { NewApplicationsDialogComponent } from "./new-applications-dialog.component";
import { ApplicationsLoadingComponent } from "./risk-insights-loading.component";
import { RiskInsightsTabType } from "./risk-insights.component";
@Component({
selector: "dirt-all-activity",
@@ -43,6 +43,15 @@ export class AllActivityComponent implements OnInit {
destroyRef = inject(DestroyRef);
constructor(
private accountService: AccountService,
protected activatedRoute: ActivatedRoute,
protected allActivitiesService: AllActivitiesService,
protected dataService: RiskInsightsDataService,
private dialogService: DialogService,
protected organizationService: OrganizationService,
) {}
async ngOnInit(): Promise<void> {
const organizationId = this.activatedRoute.snapshot.paramMap.get("organizationId");
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
@@ -71,17 +80,6 @@ export class AllActivityComponent implements OnInit {
}
}
constructor(
protected activatedRoute: ActivatedRoute,
private accountService: AccountService,
protected organizationService: OrganizationService,
protected dataService: RiskInsightsDataService,
protected allActivitiesService: AllActivitiesService,
private toastService: ToastService,
private i18nService: I18nService,
private dialogService: DialogService,
) {}
get RiskInsightsTabType() {
return RiskInsightsTabType;
}

View File

@@ -25,8 +25,8 @@ import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.mod
import { SharedModule } from "@bitwarden/web-vault/app/shared";
import { PipesModule } from "@bitwarden/web-vault/app/vault/individual-vault/pipes/pipes.module";
import { AppTableRowScrollableComponent } from "./app-table-row-scrollable.component";
import { ApplicationsLoadingComponent } from "./risk-insights-loading.component";
import { AppTableRowScrollableComponent } from "../shared/app-table-row-scrollable.component";
import { ApplicationsLoadingComponent } from "../shared/risk-insights-loading.component";
@Component({
selector: "dirt-all-applications",
@@ -67,7 +67,7 @@ export class AllApplicationsComponent implements OnInit {
}
async ngOnInit() {
this.dataService.reportResults$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
this.dataService.enrichedReportData$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
next: (report) => {
this.applicationSummary = report?.summaryData ?? createNewSummaryData();
this.dataSource.data = report?.reportData ?? [];

View File

@@ -23,11 +23,10 @@ import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.mod
import { SharedModule } from "@bitwarden/web-vault/app/shared";
import { PipesModule } from "@bitwarden/web-vault/app/vault/individual-vault/pipes/pipes.module";
import { DefaultAdminTaskService } from "../../vault/services/default-admin-task.service";
import { AppTableRowScrollableComponent } from "./app-table-row-scrollable.component";
import { RiskInsightsTabType } from "./risk-insights.component";
import { AccessIntelligenceSecurityTasksService } from "./shared/security-tasks.service";
import { DefaultAdminTaskService } from "../../../vault/services/default-admin-task.service";
import { RiskInsightsTabType } from "../models/risk-insights.models";
import { AppTableRowScrollableComponent } from "../shared/app-table-row-scrollable.component";
import { AccessIntelligenceSecurityTasksService } from "../shared/security-tasks.service";
@Component({
selector: "dirt-critical-applications",

View File

@@ -0,0 +1,7 @@
export const RenderMode = {
noCriticalApps: "noCriticalApps",
criticalAppsWithAtRiskAppsAndNoTasks: "criticalAppsWithAtRiskAppsAndNoTasks",
criticalAppsWithAtRiskAppsAndTasks: "criticalAppsWithAtRiskAppsAndTasks",
} as const;
export type RenderMode = (typeof RenderMode)[keyof typeof RenderMode];

View File

@@ -0,0 +1,8 @@
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum RiskInsightsTabType {
AllActivity = 0,
AllApps = 1,
CriticalApps = 2,
NotifiedMembers = 3,
}

View File

@@ -1,11 +0,0 @@
<!-- <bit-table [dataSource]="dataSource"> -->
<ng-container header>
<tr>
<th bitCell>{{ "member" | i18n }}</th>
<th bitCell>{{ "atRiskPasswords" | i18n }}</th>
<th bitCell>{{ "totalPasswords" | i18n }}</th>
<th bitCell>{{ "atRiskApplications" | i18n }}</th>
<th bitCell>{{ "totalApplications" | i18n }}</th>
</tr>
</ng-container>
<!-- </bit-table> -->

View File

@@ -1,18 +0,0 @@
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { TableDataSource, TableModule } from "@bitwarden/components";
@Component({
selector: "tools-notified-members-table",
templateUrl: "./notified-members-table.component.html",
imports: [CommonModule, JslibModule, TableModule],
})
export class NotifiedMembersTableComponent {
dataSource = new TableDataSource<any>();
constructor() {
this.dataSource.data = [];
}
}

View File

@@ -17,7 +17,7 @@
} @else {
<span class="tw-mx-4">{{ "noReportRan" | i18n }}</span>
}
@let isRunningReport = dataService.isRunningReport$ | async;
@let isRunningReport = dataService.isGeneratingReport$ | async;
<span class="tw-flex tw-justify-center">
<button
*ngIf="!isRunningReport"
@@ -26,7 +26,7 @@
buttonType="secondary"
class="tw-border-none !tw-font-normal tw-cursor-pointer !tw-py-0"
tabindex="0"
[bitAction]="refreshData.bind(this)"
[bitAction]="generateReport.bind(this)"
>
{{ "riskInsightsRunReport" | i18n }}
</button>

View File

@@ -1,13 +1,15 @@
import { CommonModule } from "@angular/common";
import { Component, DestroyRef, OnInit, inject } from "@angular/core";
import { Component, DestroyRef, OnDestroy, OnInit, inject } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ActivatedRoute, Router } from "@angular/router";
import { EMPTY } from "rxjs";
import { map, switchMap } from "rxjs/operators";
import { map, tap } from "rxjs/operators";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { RiskInsightsDataService } from "@bitwarden/bit-common/dirt/reports/risk-insights";
import { DrawerType } from "@bitwarden/bit-common/dirt/reports/risk-insights/models/report-models";
import {
DrawerType,
RiskInsightsDataService,
} from "@bitwarden/bit-common/dirt/reports/risk-insights";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { OrganizationId } from "@bitwarden/common/types/guid";
@@ -21,18 +23,10 @@ import {
} from "@bitwarden/components";
import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module";
import { AllActivityComponent } from "./all-activity.component";
import { AllApplicationsComponent } from "./all-applications.component";
import { CriticalApplicationsComponent } from "./critical-applications.component";
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum RiskInsightsTabType {
AllActivity = 0,
AllApps = 1,
CriticalApps = 2,
NotifiedMembers = 3,
}
import { AllActivityComponent } from "./activity/all-activity.component";
import { AllApplicationsComponent } from "./all-applications/all-applications.component";
import { CriticalApplicationsComponent } from "./critical-applications/critical-applications.component";
import { RiskInsightsTabType } from "./models/risk-insights.models";
@Component({
templateUrl: "./risk-insights.component.html",
@@ -51,7 +45,7 @@ export enum RiskInsightsTabType {
AllActivityComponent,
],
})
export class RiskInsightsComponent implements OnInit {
export class RiskInsightsComponent implements OnInit, OnDestroy {
private destroyRef = inject(DestroyRef);
private _isDrawerOpen: boolean = false;
@@ -65,7 +59,6 @@ export class RiskInsightsComponent implements OnInit {
private organizationId: OrganizationId = "" as OrganizationId;
dataLastUpdated: Date | null = null;
refetching: boolean = false;
constructor(
private route: ActivatedRoute,
@@ -91,11 +84,10 @@ export class RiskInsightsComponent implements OnInit {
.pipe(
takeUntilDestroyed(this.destroyRef),
map((params) => params.get("organizationId")),
switchMap(async (orgId) => {
tap((orgId) => {
if (orgId) {
// Initialize Data Service
await this.dataService.initializeForOrganization(orgId as OrganizationId);
this.dataService.initializeForOrganization(orgId as OrganizationId);
this.organizationId = orgId as OrganizationId;
} else {
return EMPTY;
@@ -105,7 +97,7 @@ export class RiskInsightsComponent implements OnInit {
.subscribe();
// Subscribe to report result details
this.dataService.reportResults$
this.dataService.enrichedReportData$
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((report) => {
this.appsCount = report?.reportData.length ?? 0;
@@ -119,15 +111,16 @@ export class RiskInsightsComponent implements OnInit {
this._isDrawerOpen = details.open;
});
}
runReport = () => {
this.dataService.triggerReport();
};
ngOnDestroy(): void {
this.dataService.destroy();
}
/**
* Refreshes the data by re-fetching the applications report.
* This will automatically notify child components subscribed to the RiskInsightsDataService observables.
*/
refreshData(): void {
generateReport(): void {
if (this.organizationId) {
this.dataService.triggerReport();
}

View File

@@ -3,7 +3,7 @@ import { mock } from "jest-mock-extended";
import {
AllActivitiesService,
LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher,
ApplicationHealthReportDetailEnriched,
} from "@bitwarden/bit-common/dirt/reports/risk-insights";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { OrganizationId } from "@bitwarden/common/types/guid";
@@ -43,7 +43,7 @@ describe("AccessIntelligenceSecurityTasksService", () => {
isMarkedAsCritical: true,
atRiskPasswordCount: 1,
atRiskCipherIds: ["cid1"],
} as LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher,
} as ApplicationHealthReportDetailEnriched,
];
const spy = jest.spyOn(service, "requestPasswordChange").mockResolvedValue(2);
await service.assignTasks(organizationId, apps);
@@ -60,12 +60,12 @@ describe("AccessIntelligenceSecurityTasksService", () => {
isMarkedAsCritical: true,
atRiskPasswordCount: 2,
atRiskCipherIds: ["cid1", "cid2"],
} as LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher,
} as ApplicationHealthReportDetailEnriched,
{
isMarkedAsCritical: true,
atRiskPasswordCount: 1,
atRiskCipherIds: ["cid2"],
} as LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher,
} as ApplicationHealthReportDetailEnriched,
];
defaultAdminTaskServiceSpy.bulkCreateTasks.mockResolvedValue(undefined);
i18nServiceSpy.t.mockImplementation((key) => key);
@@ -91,7 +91,7 @@ describe("AccessIntelligenceSecurityTasksService", () => {
isMarkedAsCritical: true,
atRiskPasswordCount: 1,
atRiskCipherIds: ["cid3"],
} as LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher,
} as ApplicationHealthReportDetailEnriched,
];
defaultAdminTaskServiceSpy.bulkCreateTasks.mockRejectedValue(new Error("fail"));
i18nServiceSpy.t.mockImplementation((key) => key);
@@ -113,7 +113,7 @@ describe("AccessIntelligenceSecurityTasksService", () => {
isMarkedAsCritical: true,
atRiskPasswordCount: 0,
atRiskCipherIds: ["cid4"],
} as LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher,
} as ApplicationHealthReportDetailEnriched,
];
const result = await service.requestPasswordChange(organizationId, apps);
@@ -128,7 +128,7 @@ describe("AccessIntelligenceSecurityTasksService", () => {
isMarkedAsCritical: false,
atRiskPasswordCount: 2,
atRiskCipherIds: ["cid5", "cid6"],
} as LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher,
} as ApplicationHealthReportDetailEnriched,
];
const result = await service.requestPasswordChange(organizationId, apps);

View File

@@ -2,7 +2,7 @@ import { Injectable } from "@angular/core";
import {
AllActivitiesService,
LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher,
ApplicationHealthReportDetailEnriched,
} from "@bitwarden/bit-common/dirt/reports/risk-insights";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CipherId, OrganizationId } from "@bitwarden/common/types/guid";
@@ -20,10 +20,7 @@ export class AccessIntelligenceSecurityTasksService {
private toastService: ToastService,
private i18nService: I18nService,
) {}
async assignTasks(
organizationId: OrganizationId,
apps: LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher[],
) {
async assignTasks(organizationId: OrganizationId, apps: ApplicationHealthReportDetailEnriched[]) {
const taskCount = await this.requestPasswordChange(organizationId, apps);
this.allActivitiesService.setTaskCreatedCount(taskCount);
}
@@ -31,7 +28,7 @@ export class AccessIntelligenceSecurityTasksService {
// TODO: this method is shared between here and critical-applications.component.ts
async requestPasswordChange(
organizationId: OrganizationId,
apps: LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher[],
apps: ApplicationHealthReportDetailEnriched[],
): Promise<number> {
// Only create tasks for CRITICAL applications with at-risk passwords
const cipherIds = apps