Skip to content

Commit

Permalink
simplify logic. fix tests
Browse files Browse the repository at this point in the history
  • Loading branch information
jaasen-livefront committed Oct 14, 2024
1 parent 9251828 commit 1721255
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 171 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
<tr bitRow>
<th bitCell></th>
<th bitCell bitSortable="name">{{ "name" | i18n }}</th>
<th bitCell bitSortable="organizationId">{{ "organization" | i18n }}</th>
<th bitCell class="tw-text-right">{{ "weakness" | i18n }}</th>
<th bitCell class="tw-text-right">{{ "timesReused" | i18n }}</th>
<th bitCell class="tw-text-right">{{ "timesExposed" | i18n }}</th>
Expand All @@ -26,44 +25,12 @@
<app-vault-icon [cipher]="r"></app-vault-icon>
</td>
<td bitCell>
<ng-container *ngIf="!organization || canManageCipher(r); else cantManage">
<a href="#" appStopClick (click)="selectCipher(r)" title="{{ 'editItem' | i18n }}">{{
r.name
}}</a>
</ng-container>
<ng-template #cantManage>
<ng-container>
<span>{{ r.name }}</span>
</ng-template>
<ng-container *ngIf="!organization && r.organizationId">
<i
class="bwi bwi-collection"
appStopProp
title="{{ 'shared' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "shared" | i18n }}</span>
</ng-container>
<ng-container *ngIf="r.hasAttachments">
<i
class="bwi bwi-paperclip"
appStopProp
title="{{ 'attachments' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "attachments" | i18n }}</span>
</ng-container>
<br />
<small>{{ r.subTitle }}</small>
</td>
<td bitCell>
<app-org-badge
*ngIf="!organization"
[organizationId]="r.organizationId"
[organizationName]="r.organizationId | orgNameFromId: (organizations$ | async)"
appStopProp
>
</app-org-badge>
</td>
<td bitCell class="tw-text-right">
<span
bitBadge
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { ComponentFixture, TestBed } from "@angular/core/testing";
import { MockProxy, mock } from "jest-mock-extended";
import { of } from "rxjs";

import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
Expand All @@ -13,9 +12,9 @@ import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.serv
import { TableModule } from "@bitwarden/components";
import { TableBodyDirective } from "@bitwarden/components/src/table/table.component";
import { PasswordRepromptService } from "@bitwarden/vault";
// eslint-disable-next-line no-restricted-imports
import { PipesModule } from "@bitwarden/web-vault/app/vault/individual-vault/pipes/pipes.module";

import { LooseComponentsModule } from "../../shared";
import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module";
// eslint-disable-next-line no-restricted-imports
import { cipherData } from "../reports/pages/reports-ciphers.mock";

Expand All @@ -36,13 +35,15 @@ describe("PasswordHealthComponent", () => {
auditServiceMock = mock<AuditService>();
organizationService = mock<OrganizationService>();
syncServiceMock = mock<SyncService>();
cipherServiceMock = mock<CipherService>();
cipherServiceMock = mock<CipherService>({
getAllDecrypted: jest.fn().mockResolvedValue(cipherData),
});

organizationService.organizations$ = of([]);

await TestBed.configureTestingModule({
imports: [PipesModule, TableModule, TableBodyDirective],
declarations: [I18nPipe],
imports: [PasswordHealthComponent, PipesModule, TableModule, LooseComponentsModule],
declarations: [TableBodyDirective],
providers: [
{ provide: CipherService, useValue: cipherServiceMock },
{ provide: PasswordStrengthServiceAbstraction, useValue: passwordStrengthService },
Expand Down
218 changes: 87 additions & 131 deletions apps/web/src/app/tools/access-intelligence/password-health.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,10 @@ import { CipherReportComponent } from "../reports/pages/cipher-report.component"
export class PasswordHealthComponent extends CipherReportComponent implements OnInit {
passwordStrengthMap = new Map<string, [string, BadgeVariant]>();

private passwordStrengthCache = new Map<string, number>();
weakPasswordCiphers: CipherView[] = [];

reusedPasswordCiphers: CipherView[] = [];
passwordUseMap: Map<string, number>;
passwordUseMap = new Map<string, number>();

exposedPasswordCiphers: CipherView[] = [];
exposedPasswordMap = new Map<string, number>();

reportCiphers: CipherView[] = [];
Expand Down Expand Up @@ -80,10 +77,12 @@ export class PasswordHealthComponent extends CipherReportComponent implements On

async setCiphers() {
const allCiphers = await this.getAllCiphers();
this.filterStatus = [0];
this.setWeakPasswordMap(allCiphers);
this.setReusedPasswordMap(allCiphers);
await this.setExposedPasswordMap(allCiphers);
allCiphers.forEach(async (cipher) => {
this.findWeakPassword(cipher);
this.findReusedPassword(cipher);
await this.findExposedPassword(cipher);
});
this.filterCiphersByOrg(this.reportCiphers);

// const reportIssues = allCiphers.map((c) => {
// if (this.passwordStrengthMap.has(c.id)) {
Expand All @@ -98,151 +97,108 @@ export class PasswordHealthComponent extends CipherReportComponent implements On
// return c;
// }
// });

this.filterCiphersByOrg(this.reportCiphers);
}

protected setWeakPasswordMap(ciphers: any[]) {
this.weakPasswordCiphers = [];
this.filterStatus = [0];
this.findWeakPasswords(ciphers);
protected checkForExistingCipher(ciph: CipherView) {
if (!this.reportCipherIds.includes(ciph.id)) {
this.reportCipherIds.push(ciph.id);
this.reportCiphers.push(ciph);
}
}

protected async setExposedPasswordMap(ciphers: any[]) {
const promises: Promise<void>[] = [];

ciphers.forEach((ciph: any) => {
const { type, login, isDeleted, edit, viewPassword, id } = ciph;
if (
type !== CipherType.Login ||
login.password == null ||
login.password === "" ||
isDeleted ||
(!this.organization && !edit) ||
!viewPassword
) {
return;
}
protected async findExposedPassword(cipher: CipherView) {
const { type, login, isDeleted, edit, viewPassword, id } = cipher;
if (
type !== CipherType.Login ||
login.password == null ||
login.password === "" ||
isDeleted ||
(!this.organization && !edit) ||
!viewPassword
) {
return;
}

const promise = this.auditService.passwordLeaked(login.password).then((exposedCount) => {
if (exposedCount > 0) {
this.exposedPasswordCiphers.push(ciph);
this.exposedPasswordMap.set(id, exposedCount);
if (!this.reportCipherIds.includes(ciph.id)) {
this.reportCipherIds.push(ciph.id);
this.reportCiphers.push(ciph);
}
}
});
promises.push(promise);
});
await Promise.all(promises);
const exposedCount = await this.auditService.passwordLeaked(login.password);
if (exposedCount > 0) {
this.exposedPasswordMap.set(id, exposedCount);
this.checkForExistingCipher(cipher);
}
}

protected setReusedPasswordMap(ciphers: any[]): void {
const ciphersWithPasswords: CipherView[] = [];
this.passwordUseMap = new Map<string, number>();
protected findReusedPassword(cipher: CipherView) {
const { type, login, isDeleted, edit, viewPassword } = cipher;
if (
type !== CipherType.Login ||
login.password == null ||
login.password === "" ||
isDeleted ||
(!this.organization && !edit) ||
!viewPassword
) {
return;
}

ciphers.forEach((ciph) => {
const { type, login, isDeleted, edit, viewPassword } = ciph;
if (
type !== CipherType.Login ||
login.password == null ||
login.password === "" ||
isDeleted ||
(!this.organization && !edit) ||
!viewPassword
) {
return;
}
if (this.passwordUseMap.has(login.password)) {
this.passwordUseMap.set(login.password, this.passwordUseMap.get(login.password) || 0 + 1);
} else {
this.passwordUseMap.set(login.password, 1);
}

ciphersWithPasswords.push(ciph);
if (this.passwordUseMap.has(login.password)) {
this.passwordUseMap.set(login.password, this.passwordUseMap.get(login.password) || 0 + 1);
} else {
this.passwordUseMap.set(login.password, 1);
}
});
this.reusedPasswordCiphers = ciphersWithPasswords.filter(
(c) =>
(this.passwordUseMap.has(c.login.password) && this.passwordUseMap.get(c.login.password)) ||
0 > 1,
);
this.checkForExistingCipher(cipher);
}

protected findWeakPasswords(ciphers: any[]): void {
ciphers.forEach((ciph) => {
const { type, login, isDeleted, edit, viewPassword, id } = ciph;
if (
type !== CipherType.Login ||
login.password == null ||
login.password === "" ||
isDeleted ||
(!this.organization && !edit) ||
!viewPassword
) {
return;
}
protected findWeakPassword(cipher: CipherView): void {
const { type, login, isDeleted, edit, viewPassword } = cipher;
if (
type !== CipherType.Login ||
login.password == null ||
login.password === "" ||
isDeleted ||
(!this.organization && !edit) ||
!viewPassword
) {
return;
}

const hasUserName = this.isUserNameNotEmpty(ciph);
const cacheKey = this.getCacheKey(ciph);
if (!this.passwordStrengthCache.has(cacheKey)) {
let userInput: string[] = [];
if (hasUserName) {
const atPosition = login.username.indexOf("@");
if (atPosition > -1) {
userInput = userInput
.concat(
login.username
.substr(0, atPosition)
.trim()
.toLowerCase()
.split(/[^A-Za-z0-9]/),
)
.filter((i) => i.length >= 3);
} else {
userInput = login.username
const hasUserName = this.isUserNameNotEmpty(cipher);
let userInput: string[] = [];
if (hasUserName) {
const atPosition = login.username.indexOf("@");
if (atPosition > -1) {
userInput = userInput
.concat(
login.username
.substring(0, atPosition)
.trim()
.toLowerCase()
.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);

if (score != null && score <= 2) {
this.passwordStrengthMap.set(id, this.scoreKey(score));
this.weakPasswordCiphers.push(ciph);
.split(/[^A-Za-z0-9]/),
)
.filter((i) => i.length >= 3);
} else {
userInput = login.username
.trim()
.toLowerCase()
.split(/[^A-Za-z0-9]/)
.filter((i) => i.length >= 3);
}
});
this.weakPasswordCiphers.sort((a, b) => {
return (
(this.passwordStrengthCache.get(this.getCacheKey(a)) || 0) -
(this.passwordStrengthCache.get(this.getCacheKey(b)) || 0)
);
});
}
}
const { score } = this.passwordStrengthService.getPasswordStrength(
login.password,
null,
userInput.length > 0 ? userInput : null,
);

protected canManageCipher(c: CipherView): boolean {
// this will only ever be false from the org view;
return true;
if (score != null && score <= 2) {
this.passwordStrengthMap.set(cipher.id, this.scoreKey(score));
this.checkForExistingCipher(cipher);
}
}

private isUserNameNotEmpty(c: CipherView): boolean {
return !Utils.isNullOrWhitespace(c.login.username);
}

private getCacheKey(c: CipherView): string {
return c.login.password + "_____" + (this.isUserNameNotEmpty(c) ? c.login.username : "");
}

private scoreKey(score: number): [string, BadgeVariant] {
switch (score) {
case 4:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ export class DefaultConfigService implements ConfigService {
return DefaultFeatureFlagValue[flag];
}

serverConfig.featureStates[FeatureFlag.AccessIntelligence] = true;

return serverConfig.featureStates[flag] as FeatureFlagValueType<Flag>;
}

Expand Down

0 comments on commit 1721255

Please sign in to comment.