1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-12 14:23:32 +00:00

[PM-12066] Add sorting to weak password report (#11027)

* Simplify the filter(toggle group) to filter by organizationId instead of a orgFilterStatus property which is not present on the CipherView

* Add sorting to weak password report table

- Create new type to represent a row within the report
- Add types and remove usage of any
- Include the score/badge within the data passed to the datasource/table instead of looking it up via the `passwordStrengthMap`
- Remove unneeded passwordStrengthCache
-  Enable sorting via bitSortable
- Set default sort to order by weakness

* Show headers and sort also within AC version of weak-password report, but hide the Owner column

* Clarify that we are filtering by OrgId

* Use a typed object for the reportValue instead of an array

---------

Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>
This commit is contained in:
Daniel James Smith
2024-09-27 01:38:18 +02:00
committed by GitHub
parent eb7eb614f5
commit 9eeaf0a61f
3 changed files with 61 additions and 78 deletions

View File

@@ -5,6 +5,7 @@ import { ModalService } from "@bitwarden/angular/services/modal.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
@@ -81,7 +82,7 @@ export class CipherReportComponent implements OnDestroy {
if (filterId === 0) { if (filterId === 0) {
cipherCount = this.allCiphers.length; cipherCount = this.allCiphers.length;
} else if (filterId === 1) { } else if (filterId === 1) {
cipherCount = this.allCiphers.filter((c: any) => c.orgFilterStatus === null).length; cipherCount = this.allCiphers.filter((c) => c.organizationId === null).length;
} else { } else {
this.organizations.filter((org: Organization) => { this.organizations.filter((org: Organization) => {
if (org.id === filterId) { if (org.id === filterId) {
@@ -89,22 +90,20 @@ export class CipherReportComponent implements OnDestroy {
return org; return org;
} }
}); });
cipherCount = this.allCiphers.filter( cipherCount = this.allCiphers.filter((c) => c.organizationId === orgFilterStatus).length;
(c: any) => c.orgFilterStatus === orgFilterStatus,
).length;
} }
return cipherCount; return cipherCount;
} }
async filterOrgToggle(status: any) { async filterOrgToggle(status: any) {
this.currentFilterStatus = status; let filter = null;
if (status === 0) { if (typeof status === "number" && status === 1) {
this.dataSource.filter = null; filter = (c: CipherView) => c.organizationId == null;
} else if (status === 1) { } else if (typeof status === "string") {
this.dataSource.filter = (c: any) => c.orgFilterStatus == null; const orgId = status as OrganizationId;
} else { filter = (c: CipherView) => c.organizationId === orgId;
this.dataSource.filter = (c: any) => c.orgFilterStatus === status;
} }
this.dataSource.filter = filter;
} }
async load() { async load() {
@@ -183,9 +182,7 @@ export class CipherReportComponent implements OnDestroy {
protected filterCiphersByOrg(ciphersList: CipherView[]) { protected filterCiphersByOrg(ciphersList: CipherView[]) {
this.allCiphers = [...ciphersList]; this.allCiphers = [...ciphersList];
this.ciphers = ciphersList.map((ciph: any) => { this.ciphers = ciphersList.map((ciph) => {
ciph.orgFilterStatus = ciph.organizationId;
if (this.filterStatus.indexOf(ciph.organizationId) === -1 && ciph.organizationId != null) { if (this.filterStatus.indexOf(ciph.organizationId) === -1 && ciph.organizationId != null) {
this.filterStatus.push(ciph.organizationId); this.filterStatus.push(ciph.organizationId);
} else if (this.filterStatus.indexOf(1) === -1 && ciph.organizationId == null) { } else if (this.filterStatus.indexOf(1) === -1 && ciph.organizationId == null) {
@@ -193,7 +190,6 @@ export class CipherReportComponent implements OnDestroy {
} }
return ciph; return ciph;
}); });
this.dataSource.data = this.ciphers; this.dataSource.data = this.ciphers;
if (this.filterStatus.length > 2) { if (this.filterStatus.length > 2) {

View File

@@ -32,12 +32,14 @@
</ng-container> </ng-container>
</bit-toggle-group> </bit-toggle-group>
<bit-table [dataSource]="dataSource"> <bit-table [dataSource]="dataSource">
<ng-container header *ngIf="!isAdminConsoleActive"> <ng-container header>
<tr bitRow> <tr bitRow>
<th bitCell></th> <th bitCell></th>
<th bitCell>{{ "name" | i18n }}</th> <th bitCell bitSortable="name">{{ "name" | i18n }}</th>
<th bitCell>{{ "owner" | i18n }}</th> <th bitCell bitSortable="organizationId" *ngIf="!isAdminConsoleActive">
<th bitCell></th> {{ "owner" | i18n }}
</th>
<th bitCell class="tw-text-right" bitSortable="reportValue" default></th>
</tr> </tr>
</ng-container> </ng-container>
<ng-template body let-rows$> <ng-template body let-rows$>
@@ -80,7 +82,7 @@
<br /> <br />
<small>{{ r.subTitle }}</small> <small>{{ r.subTitle }}</small>
</td> </td>
<td bitCell> <td bitCell *ngIf="!isAdminConsoleActive">
<app-org-badge <app-org-badge
*ngIf="!organization" *ngIf="!organization"
[disabled]="disabled" [disabled]="disabled"
@@ -91,8 +93,8 @@
</app-org-badge> </app-org-badge>
</td> </td>
<td bitCell class="tw-text-right"> <td bitCell class="tw-text-right">
<span bitBadge [variant]="passwordStrengthMap.get(r.id)[1]"> <span bitBadge [variant]="r.reportValue.badgeVariant">
{{ passwordStrengthMap.get(r.id)[0] | i18n }} {{ r.reportValue.label | i18n }}
</span> </span>
</td> </td>
</tr> </tr>

View File

@@ -14,16 +14,17 @@ import { PasswordRepromptService } from "@bitwarden/vault";
import { CipherReportComponent } from "./cipher-report.component"; import { CipherReportComponent } from "./cipher-report.component";
type ReportScore = { label: string; badgeVariant: BadgeVariant };
type ReportResult = CipherView & { reportValue: ReportScore };
@Component({ @Component({
selector: "app-weak-passwords-report", selector: "app-weak-passwords-report",
templateUrl: "weak-passwords-report.component.html", templateUrl: "weak-passwords-report.component.html",
}) })
export class WeakPasswordsReportComponent extends CipherReportComponent implements OnInit { export class WeakPasswordsReportComponent extends CipherReportComponent implements OnInit {
passwordStrengthMap = new Map<string, [string, BadgeVariant]>();
disabled = true; disabled = true;
private passwordStrengthCache = new Map<string, number>(); weakPasswordCiphers: ReportResult[] = [];
weakPasswordCiphers: CipherView[] = [];
constructor( constructor(
protected cipherService: CipherService, protected cipherService: CipherService,
@@ -49,16 +50,15 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen
} }
async setCiphers() { async setCiphers() {
const allCiphers: any = await this.getAllCiphers(); const allCiphers = await this.getAllCiphers();
this.passwordStrengthCache = new Map<string, number>();
this.weakPasswordCiphers = []; this.weakPasswordCiphers = [];
this.filterStatus = [0]; this.filterStatus = [0];
this.findWeakPasswords(allCiphers); this.findWeakPasswords(allCiphers);
} }
protected findWeakPasswords(ciphers: any[]): void { protected findWeakPasswords(ciphers: CipherView[]): void {
ciphers.forEach((ciph) => { ciphers.forEach((ciph) => {
const { type, login, isDeleted, edit, viewPassword, id } = ciph; const { type, login, isDeleted, edit, viewPassword } = ciph;
if ( if (
type !== CipherType.Login || type !== CipherType.Login ||
login.password == null || login.password == null ||
@@ -71,50 +71,39 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen
} }
const hasUserName = this.isUserNameNotEmpty(ciph); const hasUserName = this.isUserNameNotEmpty(ciph);
const cacheKey = this.getCacheKey(ciph); let userInput: string[] = [];
if (!this.passwordStrengthCache.has(cacheKey)) { if (hasUserName) {
let userInput: string[] = []; const atPosition = login.username.indexOf("@");
if (hasUserName) { if (atPosition > -1) {
const atPosition = login.username.indexOf("@"); userInput = userInput
if (atPosition > -1) { .concat(
userInput = userInput login.username
.concat( .substr(0, atPosition)
login.username .trim()
.substr(0, atPosition) .toLowerCase()
.trim() .split(/[^A-Za-z0-9]/),
.toLowerCase() )
.split(/[^A-Za-z0-9]/), .filter((i) => i.length >= 3);
) } else {
.filter((i) => i.length >= 3); userInput = login.username
} else { .trim()
userInput = login.username .toLowerCase()
.trim() .split(/[^A-Za-z0-9]/)
.toLowerCase() .filter((i) => i.length >= 3);
.split(/[^A-Za-z0-9]/)
.filter((i: any) => i.length >= 3);
}
} }
const result = this.passwordStrengthService.getPasswordStrength(
login.password,
null,
userInput.length > 0 ? userInput : null,
);
this.passwordStrengthCache.set(cacheKey, result.score);
} }
const score = this.passwordStrengthCache.get(cacheKey); const result = this.passwordStrengthService.getPasswordStrength(
login.password,
if (score != null && score <= 2) { null,
this.passwordStrengthMap.set(id, this.scoreKey(score)); userInput.length > 0 ? userInput : null,
this.weakPasswordCiphers.push(ciph);
}
});
this.weakPasswordCiphers.sort((a, b) => {
return (
this.passwordStrengthCache.get(this.getCacheKey(a)) -
this.passwordStrengthCache.get(this.getCacheKey(b))
); );
});
if (result.score != null && result.score <= 2) {
const scoreValue = this.scoreKey(result.score);
const row = { ...ciph, reportValue: scoreValue } as ReportResult;
this.weakPasswordCiphers.push(row);
}
});
this.filterCiphersByOrg(this.weakPasswordCiphers); this.filterCiphersByOrg(this.weakPasswordCiphers);
} }
@@ -127,20 +116,16 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen
return !Utils.isNullOrWhitespace(c.login.username); return !Utils.isNullOrWhitespace(c.login.username);
} }
private getCacheKey(c: CipherView): string { private scoreKey(score: number): ReportScore {
return c.login.password + "_____" + (this.isUserNameNotEmpty(c) ? c.login.username : "");
}
private scoreKey(score: number): [string, BadgeVariant] {
switch (score) { switch (score) {
case 4: case 4:
return ["strong", "success"]; return { label: "strong", badgeVariant: "success" };
case 3: case 3:
return ["good", "primary"]; return { label: "good", badgeVariant: "primary" };
case 2: case 2:
return ["weak", "warning"]; return { label: "weak", badgeVariant: "warning" };
default: default:
return ["veryWeak", "danger"]; return { label: "veryWeak", badgeVariant: "danger" };
} }
} }
} }