mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +00:00
[PM-27024] password progress card at risk detection (#16955)
* [PM-27024] Fix password change progress card to track only critical apps and detect new at-risk passwords - Filter at-risk password count to critical applications only - Update state logic to transition back to assign tasks when new at-risk passwords detected - Only create security tasks for critical applications with at-risk passwords - Show 'X new passwords at-risk' message when tasks exist and new at-risk passwords appear * spec
This commit is contained in:
@@ -154,6 +154,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"newPasswordsAtRisk": {
|
||||
"message": "$COUNT$ new passwords at-risk",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"content": "$1",
|
||||
"example": "5"
|
||||
}
|
||||
}
|
||||
},
|
||||
"notifiedMembersWithCount": {
|
||||
"message": "Notified members ($COUNT$)",
|
||||
"placeholders": {
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -21,7 +21,11 @@
|
||||
</div>
|
||||
|
||||
<div class="tw-items-baseline tw-gap-2">
|
||||
<span bitTypography="body2">{{ "countOfAtRiskPasswords" | i18n: atRiskPasswordsCount }}</span>
|
||||
<span bitTypography="body2">{{
|
||||
hasExistingTasks
|
||||
? ("newPasswordsAtRisk" | i18n: newAtRiskPasswordsCount)
|
||||
: ("countOfAtRiskPasswords" | i18n: atRiskPasswordsCount)
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<div class="tw-mt-4">
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -33,8 +33,9 @@ export class AccessIntelligenceSecurityTasksService {
|
||||
organizationId: OrganizationId,
|
||||
apps: LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher[],
|
||||
): Promise<number> {
|
||||
// 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));
|
||||
|
||||
Reference in New Issue
Block a user