1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-15 15:53:27 +00:00

[PM-25611][PM-25612] Update components to use persistance code (#16655)

* Add password trigger logic to report service. Also updated api to use classes that properly handle encstring with placeholders for upcoming usage

* Fix merged test case conflict

* Fix type errors and test cases. Make create data functions for report and summary

* Update Risk Insights Report Data Type

* Update encryption usage and test cases. Moved mock data

* Remove unused variable

* Move all-application constructor

* Update all applications and risk insights to look at fetched logic

* Fix name of variable. Fetch last report run

* Cleanup all and critical application tabs drawer dependencies

* Rename components from tool to dirt. Hook up all applications to use reportResult summary

* Critical application cleanup. Trigger refetch of report for enriching when critical applications change

* Fix type errors

* Rename loader from tools to dirt. Cleanup

* Add activity tab updates using data service

* Use safeProviders in access intelligence

* Fix refresh button not appearing. Change "refresh" to "run report"

* Remove multiple async calls for isRunningReport

* Fix report button not showing

* Add no report ran message

* Fix password change on critical applications
This commit is contained in:
Leslie Tilton
2025-10-06 12:01:06 -05:00
committed by GitHub
parent 6d98360b04
commit 8c81ccc1c5
30 changed files with 1055 additions and 846 deletions

View File

@@ -29,28 +29,32 @@ import { RiskInsightsComponent } from "./risk-insights.component";
@NgModule({
imports: [RiskInsightsComponent, AccessIntelligenceRoutingModule],
providers: [
{
safeProvider({
provide: MemberCipherDetailsApiService,
useClass: MemberCipherDetailsApiService,
deps: [ApiService],
},
{
}),
safeProvider({
provide: PasswordHealthService,
useClass: PasswordHealthService,
deps: [PasswordStrengthServiceAbstraction, AuditService],
},
{
}),
safeProvider({
provide: RiskInsightsApiService,
useClass: RiskInsightsApiService,
deps: [ApiService],
},
{
}),
safeProvider({
provide: RiskInsightsReportService,
useClass: RiskInsightsReportService,
deps: [
CipherService,
MemberCipherDetailsApiService,
PasswordHealthService,
RiskInsightsApiService,
RiskInsightsEncryptionService,
PasswordHealthService,
],
},
}),
safeProvider({
provide: RiskInsightsDataService,
deps: [
@@ -78,7 +82,7 @@ import { RiskInsightsComponent } from "./risk-insights.component";
safeProvider({
provide: AllActivitiesService,
useClass: AllActivitiesService,
deps: [],
deps: [RiskInsightsDataService],
}),
safeProvider({
provide: SecurityTasksApiService,

View File

@@ -1,10 +1,6 @@
@if (isLoading$ | async) {
<div *ngIf="isLoading$ | async">
<tools-risk-insights-loading></tools-risk-insights-loading>
</div>
}
@if (!(isLoading$ | async)) {
@if (dataService.isLoading$ | async) {
<dirt-risk-insights-loading></dirt-risk-insights-loading>
} @else {
<ul
class="tw-inline-grid tw-grid-cols-3 tw-gap-6 tw-m-0 tw-p-0 tw-w-full tw-auto-cols-auto tw-list-none"
>

View File

@@ -20,7 +20,7 @@ import { ApplicationsLoadingComponent } from "./risk-insights-loading.component"
import { RiskInsightsTabType } from "./risk-insights.component";
@Component({
selector: "tools-all-activity",
selector: "dirt-all-activity",
imports: [
ApplicationsLoadingComponent,
SharedModule,
@@ -30,7 +30,6 @@ import { RiskInsightsTabType } from "./risk-insights.component";
templateUrl: "./all-activity.component.html",
})
export class AllActivityComponent implements OnInit {
protected isLoading$ = this.dataService.isLoading$;
organization: Organization | null = null;
totalCriticalAppsAtRiskMemberCount = 0;
totalCriticalAppsCount = 0;

View File

@@ -1,96 +1,102 @@
<div *ngIf="isLoading$ | async">
<tools-risk-insights-loading></tools-risk-insights-loading>
</div>
<div class="tw-mt-4" *ngIf="!(isLoading$ | async) && !dataSource.data.length">
<bit-no-items [icon]="noItemsIcon" class="tw-text-main">
<ng-container slot="title">
<h2 class="tw-font-semibold tw-mt-4">
{{ "noAppsInOrgTitle" | i18n: organization?.name }}
</h2>
</ng-container>
<ng-container slot="description">
<div class="tw-flex tw-flex-col tw-mb-2">
<span class="tw-text-muted">
{{ "noAppsInOrgDescription" | i18n }}
</span>
<a class="tw-text-primary-600" routerLink="/login">{{ "learnMore" | i18n }}</a>
@if (dataService.isLoading$ | async) {
<dirt-risk-insights-loading></dirt-risk-insights-loading>
} @else {
@let drawerDetails = dataService.drawerDetails$ | async;
@if (!dataSource.data.length) {
<div class="tw-mt-4">
<bit-no-items [icon]="noItemsIcon" class="tw-text-main">
<ng-container slot="title">
<h2 class="tw-font-semibold tw-mt-4">
{{
"noAppsInOrgTitle"
| i18n: (dataService.organizationDetails$ | async)?.organizationName || ""
}}
</h2>
</ng-container>
<ng-container slot="description">
<div class="tw-flex tw-flex-col tw-mb-2">
<span class="tw-text-muted">
{{ "noAppsInOrgDescription" | i18n }}
</span>
<a class="tw-text-primary-600" routerLink="/login">{{ "learnMore" | i18n }}</a>
</div>
</ng-container>
<ng-container slot="button">
<button (click)="goToCreateNewLoginItem()" bitButton buttonType="primary" type="button">
{{ "createNewLoginItem" | i18n }}
</button>
</ng-container>
</bit-no-items>
</div>
} @else {
<div class="tw-mt-4 tw-flex tw-flex-col">
<h2 class="tw-mb-6" bitTypography="h2">{{ "allApplications" | i18n }}</h2>
<div class="tw-flex tw-gap-6">
<button
type="button"
class="tw-flex-1"
tabindex="0"
(click)="dataService.setDrawerForOrgAtRiskMembers('allAppsOrgAtRiskMembers')"
>
<dirt-card
#allAppsOrgAtRiskMembers
class="tw-w-full"
[ngClass]="{
'tw-bg-primary-100': drawerDetails.invokerId === 'allAppsOrgAtRiskMembers',
}"
[title]="'atRiskMembers' | i18n"
[value]="applicationSummary.totalAtRiskMemberCount"
[maxValue]="applicationSummary.totalMemberCount"
>
</dirt-card>
</button>
<button
type="button"
class="tw-flex-1"
tabindex="0"
(click)="dataService.setDrawerForOrgAtRiskApps('allAppsOrgAtRiskApplications')"
>
<dirt-card
#allAppsOrgAtRiskApplications
class="tw-w-full"
[ngClass]="{
'tw-bg-primary-100': drawerDetails.invokerId === 'allAppsOrgAtRiskApplications',
}"
[title]="'atRiskApplications' | i18n"
[value]="applicationSummary.totalAtRiskApplicationCount"
[maxValue]="applicationSummary.totalApplicationCount"
>
</dirt-card>
</button>
</div>
</ng-container>
<ng-container slot="button">
<button (click)="goToCreateNewLoginItem()" bitButton buttonType="primary" type="button">
{{ "createNewLoginItem" | i18n }}
</button>
</ng-container>
</bit-no-items>
</div>
<div class="tw-mt-4 tw-flex tw-flex-col" *ngIf="!(isLoading$ | async) && dataSource.data.length">
<h2 class="tw-mb-6" bitTypography="h2">{{ "allApplications" | i18n }}</h2>
@if (dataService.drawerDetails$ | async; as drawerDetails) {
<div class="tw-flex tw-gap-6">
<button
type="button"
class="tw-flex-1"
tabindex="0"
(click)="showOrgAtRiskMembers('allAppsOrgAtRiskMembers')"
>
<dirt-card
#allAppsOrgAtRiskMembers
class="tw-w-full"
[ngClass]="{
'tw-bg-primary-100': drawerDetails.invokerId === 'allAppsOrgAtRiskMembers',
}"
[title]="'atRiskMembers' | i18n"
[value]="applicationSummary.totalAtRiskMemberCount"
[maxValue]="applicationSummary.totalMemberCount"
<div class="tw-flex tw-mt-8 tw-mb-4 tw-gap-4">
<bit-search
[placeholder]="'searchApps' | i18n"
class="tw-grow"
[formControl]="searchControl"
></bit-search>
<button
type="button"
[buttonType]="'primary'"
bitButton
[disabled]="!selectedUrls.size"
[loading]="markingAsCritical"
(click)="markAppsAsCritical()"
>
</dirt-card>
</button>
<button
type="button"
class="tw-flex-1"
tabindex="0"
(click)="showOrgAtRiskApps('allAppsOrgAtRiskApplications')"
>
<dirt-card
#allAppsOrgAtRiskApplications
class="tw-w-full"
[ngClass]="{
'tw-bg-primary-100': drawerDetails.invokerId === 'allAppsOrgAtRiskApplications',
}"
[title]="'atRiskApplications' | i18n"
[value]="applicationSummary.totalAtRiskApplicationCount"
[maxValue]="applicationSummary.totalApplicationCount"
>
</dirt-card>
</button>
</div>
<div class="tw-flex tw-mt-8 tw-mb-4 tw-gap-4">
<bit-search
[placeholder]="'searchApps' | i18n"
class="tw-grow"
[formControl]="searchControl"
></bit-search>
<button
type="button"
[buttonType]="'primary'"
bitButton
[disabled]="!selectedUrls.size"
[loading]="markingAsCritical"
(click)="markAppsAsCritical()"
>
<i class="bwi tw-mr-2" [ngClass]="selectedUrls.size ? 'bwi-star-f' : 'bwi-star'"></i>
{{ "markAppAsCritical" | i18n }}
</button>
</div>
<i class="bwi tw-mr-2" [ngClass]="selectedUrls.size ? 'bwi-star-f' : 'bwi-star'"></i>
{{ "markAppAsCritical" | i18n }}
</button>
</div>
<app-table-row-scrollable
[dataSource]="dataSource"
[showRowCheckBox]="true"
[showRowMenuForCriticalApps]="false"
[selectedUrls]="selectedUrls"
[openApplication]="drawerDetails.invokerId || ''"
[checkboxChange]="onCheckboxChange"
[showAppAtRiskMembers]="showAppAtRiskMembers"
></app-table-row-scrollable>
<app-table-row-scrollable
[dataSource]="dataSource"
[showRowCheckBox]="true"
[showRowMenuForCriticalApps]="false"
[selectedUrls]="selectedUrls"
[openApplication]="drawerDetails.invokerId || ''"
[checkboxChange]="onCheckboxChange"
[showAppAtRiskMembers]="showAppAtRiskMembers"
></app-table-row-scrollable>
</div>
}
</div>
}

View File

@@ -2,33 +2,17 @@ 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, switchMap } from "rxjs";
import { debounceTime } from "rxjs";
import { Security } from "@bitwarden/assets/svg";
import {
AllActivitiesService,
CriticalAppsService,
ApplicationHealthReportDetailEnriched,
RiskInsightsDataService,
RiskInsightsReportService,
} from "@bitwarden/bit-common/dirt/reports/risk-insights";
import { createNewSummaryData } from "@bitwarden/bit-common/dirt/reports/risk-insights/helpers";
import {
LEGACY_ApplicationHealthReportDetailWithCriticalFlag,
LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher,
} from "@bitwarden/bit-common/dirt/reports/risk-insights/models/password-health";
import { OrganizationReportSummary } from "@bitwarden/bit-common/dirt/reports/risk-insights/models/report-models";
import { RiskInsightsEncryptionService } from "@bitwarden/bit-common/dirt/reports/risk-insights/services/risk-insights-encryption.service";
import {
getOrganizationById,
OrganizationService,
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.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 { 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 {
IconButtonModule,
NoItemsModule,
@@ -45,7 +29,7 @@ import { AppTableRowScrollableComponent } from "./app-table-row-scrollable.compo
import { ApplicationsLoadingComponent } from "./risk-insights-loading.component";
@Component({
selector: "tools-all-applications",
selector: "dirt-all-applications",
templateUrl: "./all-applications.component.html",
imports: [
ApplicationsLoadingComponent,
@@ -60,97 +44,44 @@ import { ApplicationsLoadingComponent } from "./risk-insights-loading.component"
],
})
export class AllApplicationsComponent implements OnInit {
protected dataSource =
new TableDataSource<LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher>();
protected dataSource = new TableDataSource<ApplicationHealthReportDetailEnriched>();
protected selectedUrls: Set<string> = new Set<string>();
protected searchControl = new FormControl("", { nonNullable: true });
protected loading = true;
protected organization = new Organization();
noItemsIcon = Security;
protected markingAsCritical = false;
protected applicationSummary: OrganizationReportSummary = createNewSummaryData();
destroyRef = inject(DestroyRef);
isLoading$: Observable<boolean> = of(false);
async ngOnInit() {
const organizationId = this.activatedRoute.snapshot.paramMap.get("organizationId");
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
if (organizationId) {
const organization$ = this.organizationService
.organizations$(userId)
.pipe(getOrganizationById(organizationId));
combineLatest([
this.dataService.applications$,
this.criticalAppsService.getAppsListForOrg(organizationId as OrganizationId),
organization$,
])
.pipe(
takeUntilDestroyed(this.destroyRef),
map(([applications, criticalApps, organization]) => {
if (applications && applications.length === 0 && criticalApps && criticalApps) {
const criticalUrls = criticalApps.map((ca) => ca.uri);
const data = applications?.map((app) => ({
...app,
isMarkedAsCritical: criticalUrls.includes(app.applicationName),
})) as LEGACY_ApplicationHealthReportDetailWithCriticalFlag[];
return { data, organization };
}
return { data: applications, organization };
}),
switchMap(async ({ data, organization }) => {
if (data && organization) {
const dataWithCiphers = await this.reportService.identifyCiphers(
data,
organization.id as OrganizationId,
);
return {
data: dataWithCiphers,
organization,
};
}
return { data: [], organization };
}),
)
.subscribe(({ data, organization }) => {
if (data) {
this.dataSource.data = data;
this.applicationSummary = this.reportService.generateApplicationsSummary(data);
this.allActivitiesService.setAllAppsReportSummary(this.applicationSummary);
}
if (organization) {
this.organization = organization;
}
});
this.isLoading$ = this.dataService.isLoading$;
}
}
constructor(
protected cipherService: CipherService,
protected i18nService: I18nService,
protected activatedRoute: ActivatedRoute,
protected toastService: ToastService,
protected configService: ConfigService,
protected dataService: RiskInsightsDataService,
protected organizationService: OrganizationService,
protected reportService: RiskInsightsReportService,
private accountService: AccountService,
protected criticalAppsService: CriticalAppsService,
protected riskInsightsEncryptionService: RiskInsightsEncryptionService,
protected allActivitiesService: AllActivitiesService,
// protected allActivitiesService: AllActivitiesService,
) {
this.searchControl.valueChanges
.pipe(debounceTime(200), takeUntilDestroyed())
.subscribe((v) => (this.dataSource.filter = v));
}
async ngOnInit() {
this.dataService.reportResults$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
next: (report) => {
this.applicationSummary = report?.summaryData ?? createNewSummaryData();
this.dataSource.data = report?.reportData ?? [];
},
error: () => {
this.dataSource.data = [];
},
});
// TODO
// this.applicationSummary = this.reportService.generateApplicationsSummary(data);
// this.allActivitiesService.setAllAppsReportSummary(this.applicationSummary);
}
goToCreateNewLoginItem = async () => {
// TODO: implement
this.toastService.showToast({
@@ -167,41 +98,31 @@ export class AllApplicationsComponent implements OnInit {
markAppsAsCritical = async () => {
this.markingAsCritical = true;
try {
await this.criticalAppsService.setCriticalApps(
this.organization.id as OrganizationId,
Array.from(this.selectedUrls),
);
this.toastService.showToast({
variant: "success",
title: "",
message: this.i18nService.t("applicationsMarkedAsCriticalSuccess"),
this.dataService
.saveCriticalApplications(Array.from(this.selectedUrls))
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({
next: () => {
this.toastService.showToast({
variant: "success",
title: "",
message: this.i18nService.t("applicationsMarkedAsCriticalSuccess"),
});
this.selectedUrls.clear();
this.markingAsCritical = false;
},
error: () => {
this.toastService.showToast({
variant: "error",
title: "",
message: this.i18nService.t("applicationsMarkedAsCriticalFail"),
});
},
});
} finally {
this.selectedUrls.clear();
this.markingAsCritical = false;
}
};
showAppAtRiskMembers = async (applicationName: string) => {
const info = {
members:
this.dataSource.data.find((app) => app.applicationName === applicationName)
?.atRiskMemberDetails ?? [],
applicationName,
};
this.dataService.setDrawerForAppAtRiskMembers(info, applicationName);
};
showOrgAtRiskMembers = async (invokerId: string) => {
const dialogData = this.reportService.generateAtRiskMemberList(this.dataSource.data);
this.dataService.setDrawerForOrgAtRiskMembers(dialogData, invokerId);
};
showOrgAtRiskApps = async (invokerId: string) => {
const data = this.reportService.generateAtRiskApplicationList(this.dataSource.data);
this.dataService.setDrawerForOrgAtRiskApps(data, invokerId);
await this.dataService.setDrawerForAppAtRiskMembers(applicationName);
};
onCheckboxChange = (applicationName: string, event: Event) => {

View File

@@ -2,7 +2,7 @@ import { CommonModule } from "@angular/common";
import { Component, Input } from "@angular/core";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher } from "@bitwarden/bit-common/dirt/reports/risk-insights/models/password-health";
import { ApplicationHealthReportDetailEnriched } from "@bitwarden/bit-common/dirt/reports/risk-insights";
import { MenuModule, TableDataSource, TableModule } from "@bitwarden/components";
import { SharedModule } from "@bitwarden/web-vault/app/shared";
import { PipesModule } from "@bitwarden/web-vault/app/vault/individual-vault/pipes/pipes.module";
@@ -14,7 +14,7 @@ import { PipesModule } from "@bitwarden/web-vault/app/vault/individual-vault/pip
})
export class AppTableRowScrollableComponent {
@Input()
dataSource!: TableDataSource<LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher>;
dataSource!: TableDataSource<ApplicationHealthReportDetailEnriched>;
@Input() showRowMenuForCriticalApps: boolean = false;
@Input() showRowCheckBox: boolean = false;
@Input() selectedUrls: Set<string> = new Set<string>();

View File

@@ -49,7 +49,7 @@
type="button"
class="tw-flex-1"
tabindex="0"
(click)="showOrgAtRiskMembers('criticalAppsAtRiskMembers')"
(click)="dataService.setDrawerForOrgAtRiskMembers('criticalAppsAtRiskMembers')"
>
<dirt-card
#criticalAppsAtRiskMembers
@@ -67,7 +67,7 @@
type="button"
class="tw-flex-1"
tabindex="0"
(click)="showOrgAtRiskApps('criticalAppsAtRiskApplications')"
(click)="dataService.setDrawerForOrgAtRiskApps('criticalAppsAtRiskApplications')"
>
<dirt-card
#criticalAppsAtRiskApplications
@@ -96,7 +96,7 @@
[showRowMenuForCriticalApps]="true"
[openApplication]="drawerDetails.invokerId || ''"
[showAppAtRiskMembers]="showAppAtRiskMembers"
[unmarkAsCritical]="unmarkAsCritical"
[unmarkAsCritical]="removeCriticalApplication"
></app-table-row-scrollable>
}
</div>

View File

@@ -4,23 +4,15 @@ import { Component, DestroyRef, inject, OnInit } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { FormControl } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
import { combineLatest, debounceTime, firstValueFrom, map, switchMap } from "rxjs";
import { debounceTime, EMPTY, map, switchMap } from "rxjs";
import { Security } from "@bitwarden/assets/svg";
import {
AllActivitiesService,
CriticalAppsService,
ApplicationHealthReportDetailEnriched,
RiskInsightsDataService,
RiskInsightsReportService,
} from "@bitwarden/bit-common/dirt/reports/risk-insights";
import {
LEGACY_ApplicationHealthReportDetailWithCriticalFlag,
LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher,
} from "@bitwarden/bit-common/dirt/reports/risk-insights/models/password-health";
import { createNewSummaryData } from "@bitwarden/bit-common/dirt/reports/risk-insights/helpers";
import { OrganizationReportSummary } from "@bitwarden/bit-common/dirt/reports/risk-insights/models/report-models";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CipherId, OrganizationId } from "@bitwarden/common/types/guid";
import { SecurityTaskType } from "@bitwarden/common/vault/tasks";
@@ -37,7 +29,7 @@ import { AppTableRowScrollableComponent } from "./app-table-row-scrollable.compo
import { RiskInsightsTabType } from "./risk-insights.component";
@Component({
selector: "tools-critical-applications",
selector: "dirt-critical-applications",
templateUrl: "./critical-applications.component.html",
imports: [
CardComponent,
@@ -51,60 +43,57 @@ import { RiskInsightsTabType } from "./risk-insights.component";
providers: [DefaultAdminTaskService],
})
export class CriticalApplicationsComponent implements OnInit {
protected dataSource =
new TableDataSource<LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher>();
protected selectedIds: Set<number> = new Set<number>();
protected searchControl = new FormControl("", { nonNullable: true });
private destroyRef = inject(DestroyRef);
protected loading = false;
protected enableRequestPasswordChange = false;
protected organizationId: OrganizationId;
protected applicationSummary = {} as OrganizationReportSummary;
noItemsIcon = Security;
enableRequestPasswordChange = false;
protected dataSource = new TableDataSource<ApplicationHealthReportDetailEnriched>();
protected applicationSummary = {} as OrganizationReportSummary;
protected selectedIds: Set<number> = new Set<number>();
protected searchControl = new FormControl("", { nonNullable: true });
constructor(
protected activatedRoute: ActivatedRoute,
protected router: Router,
protected toastService: ToastService,
protected dataService: RiskInsightsDataService,
protected i18nService: I18nService,
private adminTaskService: DefaultAdminTaskService,
) {
this.searchControl.valueChanges
.pipe(debounceTime(200), takeUntilDestroyed())
.subscribe((v) => (this.dataSource.filter = v));
}
async ngOnInit() {
this.organizationId = this.activatedRoute.snapshot.paramMap.get(
"organizationId",
) as OrganizationId;
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
this.criticalAppsService.loadOrganizationContext(this.organizationId as OrganizationId, userId);
if (this.organizationId) {
combineLatest([
this.dataService.applications$,
this.criticalAppsService.getAppsListForOrg(this.organizationId as OrganizationId),
])
.pipe(
takeUntilDestroyed(this.destroyRef),
map(([applications, criticalApps]) => {
const criticalUrls = criticalApps.map((ca) => ca.uri);
const data = applications?.map((app) => ({
...app,
isMarkedAsCritical: criticalUrls.includes(app.applicationName),
})) as LEGACY_ApplicationHealthReportDetailWithCriticalFlag[];
return data?.filter((app) => app.isMarkedAsCritical);
}),
switchMap(async (data) => {
if (data) {
const dataWithCiphers = await this.reportService.identifyCiphers(
data,
this.organizationId,
);
return dataWithCiphers;
}
return null;
}),
)
.subscribe((applications) => {
if (applications) {
this.dataSource.data = applications;
this.applicationSummary = this.reportService.generateApplicationsSummary(applications);
this.enableRequestPasswordChange = this.applicationSummary.totalAtRiskMemberCount > 0;
this.allActivitiesService.setCriticalAppsReportSummary(this.applicationSummary);
this.allActivitiesService.setAllAppsReportDetails(applications);
this.dataService.criticalReportResults$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
next: (criticalReport) => {
this.dataSource.data = criticalReport?.reportData ?? [];
this.applicationSummary = criticalReport?.summaryData ?? createNewSummaryData();
this.enableRequestPasswordChange = criticalReport?.summaryData?.totalAtRiskMemberCount > 0;
},
error: () => {
this.dataSource.data = [];
this.applicationSummary = createNewSummaryData();
this.enableRequestPasswordChange = false;
},
});
this.activatedRoute.paramMap
.pipe(
takeUntilDestroyed(this.destroyRef),
map((params) => params.get("organizationId")),
switchMap(async (orgId) => {
if (orgId) {
this.organizationId = orgId as OrganizationId;
} else {
return EMPTY;
}
});
}
}),
)
.subscribe();
}
goToAllAppsTab = async () => {
@@ -117,26 +106,25 @@ export class CriticalApplicationsComponent implements OnInit {
);
};
unmarkAsCritical = async (hostname: string) => {
try {
await this.criticalAppsService.dropCriticalApp(
this.organizationId as OrganizationId,
hostname,
);
} catch {
this.toastService.showToast({
message: this.i18nService.t("unexpectedError"),
variant: "error",
title: this.i18nService.t("error"),
removeCriticalApplication = async (hostname: string) => {
this.dataService
.removeCriticalApplication(hostname)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({
next: () => {
this.toastService.showToast({
message: this.i18nService.t("criticalApplicationUnmarkedSuccessfully"),
variant: "success",
});
},
error: () => {
this.toastService.showToast({
message: this.i18nService.t("unexpectedError"),
variant: "error",
title: this.i18nService.t("error"),
});
},
});
return;
}
this.toastService.showToast({
message: this.i18nService.t("criticalApplicationUnmarkedSuccessfully"),
variant: "success",
});
this.dataSource.data = this.dataSource.data.filter((app) => app.applicationName !== hostname);
};
async requestPasswordChange() {
@@ -167,42 +155,7 @@ export class CriticalApplicationsComponent implements OnInit {
});
}
}
constructor(
protected activatedRoute: ActivatedRoute,
protected router: Router,
protected toastService: ToastService,
protected dataService: RiskInsightsDataService,
protected criticalAppsService: CriticalAppsService,
protected reportService: RiskInsightsReportService,
protected i18nService: I18nService,
private configService: ConfigService,
private adminTaskService: DefaultAdminTaskService,
private accountService: AccountService,
private allActivitiesService: AllActivitiesService,
) {
this.searchControl.valueChanges
.pipe(debounceTime(200), takeUntilDestroyed())
.subscribe((v) => (this.dataSource.filter = v));
}
showAppAtRiskMembers = async (applicationName: string) => {
const data = {
members:
this.dataSource.data.find((app) => app.applicationName === applicationName)
?.atRiskMemberDetails ?? [],
applicationName,
};
this.dataService.setDrawerForAppAtRiskMembers(data, applicationName);
};
showOrgAtRiskMembers = async (invokerId: string) => {
const data = this.reportService.generateAtRiskMemberList(this.dataSource.data);
this.dataService.setDrawerForOrgAtRiskMembers(data, invokerId);
};
showOrgAtRiskApps = async (invokerId: string) => {
const data = this.reportService.generateAtRiskApplicationList(this.dataSource.data);
this.dataService.setDrawerForOrgAtRiskApps(data, invokerId);
await this.dataService.setDrawerForAppAtRiskMembers(applicationName);
};
}

View File

@@ -4,7 +4,7 @@ import { Component } from "@angular/core";
import { JslibModule } from "@bitwarden/angular/jslib.module";
@Component({
selector: "tools-risk-insights-loading",
selector: "dirt-risk-insights-loading",
imports: [CommonModule, JslibModule],
templateUrl: "./risk-insights-loading.component.html",
})

View File

@@ -4,19 +4,23 @@
{{ "reviewAtRiskPasswords" | i18n }}
</div>
<div
*ngIf="dataLastUpdated$ | async"
class="tw-bg-primary-100 tw-rounded-lg tw-w-full tw-px-8 tw-py-4 tw-my-4 tw-flex tw-items-center"
>
<i
class="bwi bwi-exclamation-triangle bwi-lg tw-text-[1.2rem] tw-text-muted"
aria-hidden="true"
></i>
<span class="tw-mx-4">{{
"dataLastUpdated" | i18n: (dataLastUpdated$ | async | date: "MMMM d, y 'at' h:mm a")
}}</span>
<span class="tw-flex tw-justify-center tw-w-16">
@if (dataLastUpdated) {
<span class="tw-mx-4">{{
"dataLastUpdated" | i18n: (dataLastUpdated | date: "MMMM d, y 'at' h:mm a")
}}</span>
} @else {
<span class="tw-mx-4">{{ "noReportRan" | i18n }}</span>
}
@let isRunningReport = dataService.isRunningReport$ | async;
<span class="tw-flex tw-justify-center">
<button
*ngIf="!(isRefreshing$ | async)"
*ngIf="!isRunningReport"
type="button"
bitButton
buttonType="secondary"
@@ -24,11 +28,11 @@
tabindex="0"
[bitAction]="refreshData.bind(this)"
>
{{ "refresh" | i18n }}
{{ "riskInsightsRunReport" | i18n }}
</button>
<span>
<i
*ngIf="isRefreshing$ | async"
*ngIf="isRunningReport"
class="bwi bwi-spinner bwi-spin tw-text-muted tw-text-[1.2rem]"
aria-hidden="true"
></i>
@@ -38,18 +42,21 @@
<bit-tab-group [(selectedIndex)]="tabIndex" (selectedIndexChange)="onTabChange($event)">
@if (isRiskInsightsActivityTabFeatureEnabled) {
<bit-tab label="{{ 'activity' | i18n }}">
<tools-all-activity></tools-all-activity>
<dirt-all-activity></dirt-all-activity>
</bit-tab>
}
<bit-tab label="{{ 'allApplicationsWithCount' | i18n: appsCount }}">
<tools-all-applications></tools-all-applications>
<dirt-all-applications></dirt-all-applications>
</bit-tab>
<bit-tab>
<ng-template bitTabLabel>
<i class="bwi bwi-star"></i>
{{ "criticalApplicationsWithCount" | i18n: (criticalApps$ | async)?.length ?? 0 }}
{{
"criticalApplicationsWithCount"
| i18n: (dataService.criticalReportResults$ | async)?.reportData?.length ?? 0
}}
</ng-template>
<tools-critical-applications></tools-critical-applications>
<dirt-critical-applications></dirt-critical-applications>
</bit-tab>
</bit-tab-group>
@@ -69,7 +76,9 @@
}}</span>
<ng-container *ngIf="drawerDetails.atRiskMemberDetails.length > 0">
<div class="tw-flex tw-justify-between tw-mt-2 tw-text-muted">
<div bitTypography="body2" class="tw-text-sm tw-font-bold">{{ "email" | i18n }}</div>
<div bitTypography="body2" class="tw-text-sm tw-font-bold">
{{ "email" | i18n }}
</div>
<div bitTypography="body2" class="tw-text-sm tw-font-bold">
{{ "atRiskPasswords" | i18n }}
</div>

View File

@@ -2,21 +2,12 @@ import { CommonModule } from "@angular/common";
import { Component, DestroyRef, OnInit, inject } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ActivatedRoute, Router } from "@angular/router";
import { EMPTY, firstValueFrom, Observable } from "rxjs";
import { EMPTY } from "rxjs";
import { map, switchMap } from "rxjs/operators";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import {
CriticalAppsService,
RiskInsightsDataService,
} from "@bitwarden/bit-common/dirt/reports/risk-insights";
import { PasswordHealthReportApplicationsResponse } from "@bitwarden/bit-common/dirt/reports/risk-insights/models/api-models.types";
import {
ApplicationHealthReportDetail,
DrawerType,
} from "@bitwarden/bit-common/dirt/reports/risk-insights/models/report-models";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { RiskInsightsDataService } from "@bitwarden/bit-common/dirt/reports/risk-insights";
import { DrawerType } from "@bitwarden/bit-common/dirt/reports/risk-insights/models/report-models";
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";
@@ -67,19 +58,13 @@ export class RiskInsightsComponent implements OnInit {
tabIndex: RiskInsightsTabType = RiskInsightsTabType.AllApps;
isRiskInsightsActivityTabFeatureEnabled: boolean = false;
dataLastUpdated: Date = new Date();
criticalApps$: Observable<PasswordHealthReportApplicationsResponse[]> = new Observable();
appsCount: number = 0;
criticalAppsCount: number = 0;
notifiedMembersCount: number = 0;
// Leaving this commented because it's not used but seems important
// notifiedMembersCount: number = 0;
private organizationId: OrganizationId = "" as OrganizationId;
isLoading$: Observable<boolean> = new Observable<boolean>();
isRefreshing$: Observable<boolean> = new Observable<boolean>();
dataLastUpdated$: Observable<Date | null> = new Observable<Date | null>();
dataLastUpdated: Date | null = null;
refetching: boolean = false;
constructor(
@@ -87,8 +72,6 @@ export class RiskInsightsComponent implements OnInit {
private router: Router,
private configService: ConfigService,
protected dataService: RiskInsightsDataService,
private criticalAppsService: CriticalAppsService,
private accountService: AccountService,
) {
this.route.queryParams.pipe(takeUntilDestroyed()).subscribe(({ tabIndex }) => {
this.tabIndex = !isNaN(Number(tabIndex)) ? Number(tabIndex) : RiskInsightsTabType.AllApps;
@@ -104,39 +87,29 @@ export class RiskInsightsComponent implements OnInit {
}
async ngOnInit() {
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
this.route.paramMap
.pipe(
takeUntilDestroyed(this.destroyRef),
map((params) => params.get("organizationId")),
switchMap((orgId) => {
switchMap(async (orgId) => {
if (orgId) {
// Initialize Data Service
await this.dataService.initializeForOrganization(orgId as OrganizationId);
this.organizationId = orgId as OrganizationId;
this.dataService.fetchApplicationsReport(this.organizationId);
this.isLoading$ = this.dataService.isLoading$;
this.isRefreshing$ = this.dataService.isRefreshing$;
this.dataLastUpdated$ = this.dataService.dataLastUpdated$;
return this.dataService.applications$;
} else {
return EMPTY;
}
}),
)
.subscribe({
next: (applications: ApplicationHealthReportDetail[] | null) => {
if (applications) {
this.appsCount = applications.length;
}
.subscribe();
this.criticalAppsService.loadOrganizationContext(
this.organizationId as OrganizationId,
userId,
);
this.criticalApps$ = this.criticalAppsService.getAppsListForOrg(
this.organizationId as OrganizationId,
);
},
// Subscribe to report result details
this.dataService.reportResults$
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((report) => {
this.appsCount = report?.reportData.length ?? 0;
this.dataLastUpdated = report?.creationDate ?? null;
});
// Subscribe to drawer state changes
@@ -156,7 +129,7 @@ export class RiskInsightsComponent implements OnInit {
*/
refreshData(): void {
if (this.organizationId) {
this.dataService.refreshApplicationsReport(this.organizationId);
this.dataService.triggerReport();
}
}