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:
committed by
GitHub
parent
eb7eb614f5
commit
9eeaf0a61f
@@ -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) {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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" };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user