mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 22:03:36 +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": {
|
"notifiedMembersWithCount": {
|
||||||
"message": "Notified members ($COUNT$)",
|
"message": "Notified members ($COUNT$)",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
|
|||||||
@@ -76,7 +76,9 @@ export class AllActivitiesService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setAllAppsReportDetails(applications: ApplicationHealthReportDetailEnriched[]) {
|
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,
|
(sum, app) => sum + app.atRiskPasswordCount,
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -21,7 +21,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tw-items-baseline tw-gap-2">
|
<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>
|
||||||
|
|
||||||
<div class="tw-mt-4">
|
<div class="tw-mt-4">
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher,
|
LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher,
|
||||||
SecurityTasksApiService,
|
SecurityTasksApiService,
|
||||||
TaskMetrics,
|
TaskMetrics,
|
||||||
|
OrganizationReportSummary,
|
||||||
} from "@bitwarden/bit-common/dirt/reports/risk-insights";
|
} from "@bitwarden/bit-common/dirt/reports/risk-insights";
|
||||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||||
import { ButtonModule, ProgressModule, TypographyModule } from "@bitwarden/components";
|
import { ButtonModule, ProgressModule, TypographyModule } from "@bitwarden/components";
|
||||||
@@ -73,25 +74,8 @@ export class PasswordChangeMetricComponent implements OnInit {
|
|||||||
this.totalTasks = taskMetrics.totalTasks;
|
this.totalTasks = taskMetrics.totalTasks;
|
||||||
this.allApplicationsDetails = allApplicationsDetails;
|
this.allApplicationsDetails = allApplicationsDetails;
|
||||||
|
|
||||||
// No critical apps setup
|
// Determine render mode based on state
|
||||||
this.renderMode =
|
this.renderMode = this.determineRenderMode(summary, taskMetrics, atRiskPasswordsCount);
|
||||||
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;
|
|
||||||
|
|
||||||
this.allActivitiesService.setPasswordChangeProgressMetricHasProgressBar(
|
this.allActivitiesService.setPasswordChangeProgressMetricHasProgressBar(
|
||||||
this.renderMode === RenderMode.criticalAppsWithAtRiskAppsAndTasks,
|
this.renderMode === RenderMode.criticalAppsWithAtRiskAppsAndTasks,
|
||||||
@@ -106,6 +90,38 @@ export class PasswordChangeMetricComponent implements OnInit {
|
|||||||
protected accessIntelligenceSecurityTasksService: AccessIntelligenceSecurityTasksService,
|
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 {
|
get completedPercent(): number {
|
||||||
if (this.totalTasks === 0) {
|
if (this.totalTasks === 0) {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -144,7 +160,19 @@ export class PasswordChangeMetricComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get canAssignTasks(): boolean {
|
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() {
|
get renderModes() {
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ describe("AccessIntelligenceSecurityTasksService", () => {
|
|||||||
const organizationId = "org-1" as OrganizationId;
|
const organizationId = "org-1" as OrganizationId;
|
||||||
const apps = [
|
const apps = [
|
||||||
{
|
{
|
||||||
|
isMarkedAsCritical: true,
|
||||||
atRiskPasswordCount: 1,
|
atRiskPasswordCount: 1,
|
||||||
atRiskCipherIds: ["cid1"],
|
atRiskCipherIds: ["cid1"],
|
||||||
} as LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher,
|
} as LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher,
|
||||||
@@ -56,10 +57,12 @@ describe("AccessIntelligenceSecurityTasksService", () => {
|
|||||||
const organizationId = "org-2" as OrganizationId;
|
const organizationId = "org-2" as OrganizationId;
|
||||||
const apps = [
|
const apps = [
|
||||||
{
|
{
|
||||||
|
isMarkedAsCritical: true,
|
||||||
atRiskPasswordCount: 2,
|
atRiskPasswordCount: 2,
|
||||||
atRiskCipherIds: ["cid1", "cid2"],
|
atRiskCipherIds: ["cid1", "cid2"],
|
||||||
} as LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher,
|
} as LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher,
|
||||||
{
|
{
|
||||||
|
isMarkedAsCritical: true,
|
||||||
atRiskPasswordCount: 1,
|
atRiskPasswordCount: 1,
|
||||||
atRiskCipherIds: ["cid2"],
|
atRiskCipherIds: ["cid2"],
|
||||||
} as LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher,
|
} as LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher,
|
||||||
@@ -85,6 +88,7 @@ describe("AccessIntelligenceSecurityTasksService", () => {
|
|||||||
const organizationId = "org-3" as OrganizationId;
|
const organizationId = "org-3" as OrganizationId;
|
||||||
const apps = [
|
const apps = [
|
||||||
{
|
{
|
||||||
|
isMarkedAsCritical: true,
|
||||||
atRiskPasswordCount: 1,
|
atRiskPasswordCount: 1,
|
||||||
atRiskCipherIds: ["cid3"],
|
atRiskCipherIds: ["cid3"],
|
||||||
} as LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher,
|
} as LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher,
|
||||||
@@ -106,6 +110,7 @@ describe("AccessIntelligenceSecurityTasksService", () => {
|
|||||||
const organizationId = "org-4" as OrganizationId;
|
const organizationId = "org-4" as OrganizationId;
|
||||||
const apps = [
|
const apps = [
|
||||||
{
|
{
|
||||||
|
isMarkedAsCritical: true,
|
||||||
atRiskPasswordCount: 0,
|
atRiskPasswordCount: 0,
|
||||||
atRiskCipherIds: ["cid4"],
|
atRiskCipherIds: ["cid4"],
|
||||||
} as LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher,
|
} as LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher,
|
||||||
@@ -115,5 +120,20 @@ describe("AccessIntelligenceSecurityTasksService", () => {
|
|||||||
expect(defaultAdminTaskServiceSpy.bulkCreateTasks).toHaveBeenCalledWith(organizationId, []);
|
expect(defaultAdminTaskServiceSpy.bulkCreateTasks).toHaveBeenCalledWith(organizationId, []);
|
||||||
expect(result).toBe(0);
|
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,
|
organizationId: OrganizationId,
|
||||||
apps: LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher[],
|
apps: LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher[],
|
||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
|
// Only create tasks for CRITICAL applications with at-risk passwords
|
||||||
const cipherIds = apps
|
const cipherIds = apps
|
||||||
.filter((_) => _.atRiskPasswordCount > 0)
|
.filter((_) => _.isMarkedAsCritical && _.atRiskPasswordCount > 0)
|
||||||
.flatMap((app) => app.atRiskCipherIds);
|
.flatMap((app) => app.atRiskCipherIds);
|
||||||
|
|
||||||
const distinctCipherIds = Array.from(new Set(cipherIds));
|
const distinctCipherIds = Array.from(new Set(cipherIds));
|
||||||
|
|||||||
Reference in New Issue
Block a user