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));