diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 5eab55e230..1f8c4ec55b 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ new passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/all-activities.service.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/all-activities.service.ts index ee7f0fae56..4044a01926 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/all-activities.service.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/all-activities.service.ts @@ -76,7 +76,9 @@ export class AllActivitiesService { } setAllAppsReportDetails(applications: ApplicationHealthReportDetailEnriched[]) { - const totalAtRiskPasswords = applications.reduce( + // Only count at-risk passwords for CRITICAL applications + const criticalApps = applications.filter((app) => app.isMarkedAsCritical); + const totalAtRiskPasswords = criticalApps.reduce( (sum, app) => sum + app.atRiskPasswordCount, 0, ); diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity-cards/password-change-metric.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity-cards/password-change-metric.component.html index 718efc4c67..674bc0b5c6 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity-cards/password-change-metric.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity-cards/password-change-metric.component.html @@ -21,7 +21,11 @@
- {{ "countOfAtRiskPasswords" | i18n: atRiskPasswordsCount }} + {{ + hasExistingTasks + ? ("newPasswordsAtRisk" | i18n: newAtRiskPasswordsCount) + : ("countOfAtRiskPasswords" | i18n: atRiskPasswordsCount) + }}
diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity-cards/password-change-metric.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity-cards/password-change-metric.component.ts index 3b8475ed5c..4d2085d7c3 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity-cards/password-change-metric.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity-cards/password-change-metric.component.ts @@ -9,6 +9,7 @@ import { LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher, SecurityTasksApiService, TaskMetrics, + OrganizationReportSummary, } from "@bitwarden/bit-common/dirt/reports/risk-insights"; import { OrganizationId } from "@bitwarden/common/types/guid"; import { ButtonModule, ProgressModule, TypographyModule } from "@bitwarden/components"; @@ -73,25 +74,8 @@ export class PasswordChangeMetricComponent implements OnInit { this.totalTasks = taskMetrics.totalTasks; this.allApplicationsDetails = allApplicationsDetails; - // No critical apps setup - this.renderMode = - summary.totalCriticalApplicationCount === 0 ? RenderMode.noCriticalApps : this.renderMode; - - // Critical apps setup with at-risk apps but no tasks - this.renderMode = - summary.totalCriticalApplicationCount > 0 && - summary.totalCriticalAtRiskApplicationCount >= 0 && - taskMetrics.totalTasks === 0 - ? RenderMode.criticalAppsWithAtRiskAppsAndNoTasks - : this.renderMode; - - // Critical apps setup with at-risk apps and tasks - this.renderMode = - summary.totalAtRiskApplicationCount > 0 && - summary.totalCriticalAtRiskApplicationCount >= 0 && - taskMetrics.totalTasks > 0 - ? RenderMode.criticalAppsWithAtRiskAppsAndTasks - : this.renderMode; + // Determine render mode based on state + this.renderMode = this.determineRenderMode(summary, taskMetrics, atRiskPasswordsCount); this.allActivitiesService.setPasswordChangeProgressMetricHasProgressBar( this.renderMode === RenderMode.criticalAppsWithAtRiskAppsAndTasks, @@ -106,6 +90,38 @@ export class PasswordChangeMetricComponent implements OnInit { protected accessIntelligenceSecurityTasksService: AccessIntelligenceSecurityTasksService, ) {} + private determineRenderMode( + summary: OrganizationReportSummary, + taskMetrics: TaskMetrics, + atRiskPasswordsCount: number, + ): RenderMode { + // State 1: No critical apps setup + if (summary.totalCriticalApplicationCount === 0) { + return RenderMode.noCriticalApps; + } + + // State 2: Critical apps with at-risk passwords but no tasks assigned yet + // OR tasks exist but NEW at-risk passwords detected (more at-risk passwords than tasks) + if ( + summary.totalCriticalApplicationCount > 0 && + (taskMetrics.totalTasks === 0 || atRiskPasswordsCount > taskMetrics.totalTasks) + ) { + return RenderMode.criticalAppsWithAtRiskAppsAndNoTasks; + } + + // State 3: Critical apps with at-risk apps and tasks (progress tracking) + if ( + summary.totalCriticalApplicationCount > 0 && + taskMetrics.totalTasks > 0 && + atRiskPasswordsCount <= taskMetrics.totalTasks + ) { + return RenderMode.criticalAppsWithAtRiskAppsAndTasks; + } + + // Default to no critical apps + return RenderMode.noCriticalApps; + } + get completedPercent(): number { if (this.totalTasks === 0) { return 0; @@ -144,7 +160,19 @@ export class PasswordChangeMetricComponent implements OnInit { } get canAssignTasks(): boolean { - return this.atRiskAppsCount > this.totalTasks ? true : false; + return this.atRiskPasswordsCount > this.totalTasks; + } + + get hasExistingTasks(): boolean { + return this.totalTasks > 0; + } + + get newAtRiskPasswordsCount(): number { + // Calculate new at-risk passwords as the difference between current count and tasks created + if (this.atRiskPasswordsCount > this.totalTasks) { + return this.atRiskPasswordsCount - this.totalTasks; + } + return 0; } get renderModes() { diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/security-tasks.service.spec.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/security-tasks.service.spec.ts index 520164c80e..a57bdfc279 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/security-tasks.service.spec.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/security-tasks.service.spec.ts @@ -40,6 +40,7 @@ describe("AccessIntelligenceSecurityTasksService", () => { const organizationId = "org-1" as OrganizationId; const apps = [ { + isMarkedAsCritical: true, atRiskPasswordCount: 1, atRiskCipherIds: ["cid1"], } as LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher, @@ -56,10 +57,12 @@ describe("AccessIntelligenceSecurityTasksService", () => { const organizationId = "org-2" as OrganizationId; const apps = [ { + isMarkedAsCritical: true, atRiskPasswordCount: 2, atRiskCipherIds: ["cid1", "cid2"], } as LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher, { + isMarkedAsCritical: true, atRiskPasswordCount: 1, atRiskCipherIds: ["cid2"], } as LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher, @@ -85,6 +88,7 @@ describe("AccessIntelligenceSecurityTasksService", () => { const organizationId = "org-3" as OrganizationId; const apps = [ { + isMarkedAsCritical: true, atRiskPasswordCount: 1, atRiskCipherIds: ["cid3"], } as LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher, @@ -106,6 +110,7 @@ describe("AccessIntelligenceSecurityTasksService", () => { const organizationId = "org-4" as OrganizationId; const apps = [ { + isMarkedAsCritical: true, atRiskPasswordCount: 0, atRiskCipherIds: ["cid4"], } as LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher, @@ -115,5 +120,20 @@ describe("AccessIntelligenceSecurityTasksService", () => { expect(defaultAdminTaskServiceSpy.bulkCreateTasks).toHaveBeenCalledWith(organizationId, []); expect(result).toBe(0); }); + + it("should not create any tasks for non-critical apps", async () => { + const organizationId = "org-5" as OrganizationId; + const apps = [ + { + isMarkedAsCritical: false, + atRiskPasswordCount: 2, + atRiskCipherIds: ["cid5", "cid6"], + } as LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher, + ]; + const result = await service.requestPasswordChange(organizationId, apps); + + expect(defaultAdminTaskServiceSpy.bulkCreateTasks).toHaveBeenCalledWith(organizationId, []); + expect(result).toBe(0); + }); }); }); diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/security-tasks.service.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/security-tasks.service.ts index c5610c638b..34fd6daa2b 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/security-tasks.service.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/security-tasks.service.ts @@ -33,8 +33,9 @@ export class AccessIntelligenceSecurityTasksService { organizationId: OrganizationId, apps: LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher[], ): Promise { + // Only create tasks for CRITICAL applications with at-risk passwords const cipherIds = apps - .filter((_) => _.atRiskPasswordCount > 0) + .filter((_) => _.isMarkedAsCritical && _.atRiskPasswordCount > 0) .flatMap((app) => app.atRiskCipherIds); const distinctCipherIds = Array.from(new Set(cipherIds));