diff --git a/apps/browser/src/auth/popup/hint.component.ts b/apps/browser/src/auth/popup/hint.component.ts
index bc1f68f4c43..e97236fe6a8 100644
--- a/apps/browser/src/auth/popup/hint.component.ts
+++ b/apps/browser/src/auth/popup/hint.component.ts
@@ -34,7 +34,7 @@ export class HintComponent extends BaseHintComponent {
toastService,
);
- super.onSuccessfulSubmit = async () => {
+ this.onSuccessfulSubmit = async () => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate([this.successRoute]);
diff --git a/apps/browser/src/auth/popup/lock.component.ts b/apps/browser/src/auth/popup/lock.component.ts
index 96bda7012d1..75fcfc58f6a 100644
--- a/apps/browser/src/auth/popup/lock.component.ts
+++ b/apps/browser/src/auth/popup/lock.component.ts
@@ -105,7 +105,7 @@ export class LockComponent extends BaseLockComponent implements OnInit {
this.successRoute = "/tabs/current";
this.isInitialLockScreen = (window as any).previousPopupUrl == null;
- super.onSuccessfulSubmit = async () => {
+ this.onSuccessfulSubmit = async () => {
const previousUrl = this.routerService.getPreviousUrl();
if (previousUrl) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
diff --git a/apps/browser/src/auth/popup/login-via-auth-request.component.ts b/apps/browser/src/auth/popup/login-via-auth-request.component.ts
index 53f29badee6..33ec2acf387 100644
--- a/apps/browser/src/auth/popup/login-via-auth-request.component.ts
+++ b/apps/browser/src/auth/popup/login-via-auth-request.component.ts
@@ -74,7 +74,7 @@ export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent {
loginStrategyService,
toastService,
);
- super.onSuccessfulLogin = async () => {
+ this.onSuccessfulLogin = async () => {
await syncService.fullSync(true);
};
}
diff --git a/apps/browser/src/auth/popup/login.component.ts b/apps/browser/src/auth/popup/login.component.ts
index ea72fb61f5f..fd4d9bc547a 100644
--- a/apps/browser/src/auth/popup/login.component.ts
+++ b/apps/browser/src/auth/popup/login.component.ts
@@ -78,10 +78,10 @@ export class LoginComponent extends BaseLoginComponent implements OnInit {
registerRouteService,
toastService,
);
- super.onSuccessfulLogin = async () => {
+ this.onSuccessfulLogin = async () => {
await syncService.fullSync(true);
};
- super.successRoute = "/tabs/vault";
+ this.successRoute = "/tabs/vault";
this.showPasswordless = flagEnabled("showPasswordless");
}
diff --git a/apps/browser/src/auth/popup/sso.component.ts b/apps/browser/src/auth/popup/sso.component.ts
index 42222c42b97..988563c2fe6 100644
--- a/apps/browser/src/auth/popup/sso.component.ts
+++ b/apps/browser/src/auth/popup/sso.component.ts
@@ -79,7 +79,7 @@ export class SsoComponent extends BaseSsoComponent {
});
this.clientId = "browser";
- super.onSuccessfulLogin = async () => {
+ this.onSuccessfulLogin = async () => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
syncService.fullSync(true);
@@ -92,13 +92,13 @@ export class SsoComponent extends BaseSsoComponent {
this.win.close();
};
- super.onSuccessfulLoginTde = async () => {
+ this.onSuccessfulLoginTde = async () => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
syncService.fullSync(true);
};
- super.onSuccessfulLoginTdeNavigate = async () => {
+ this.onSuccessfulLoginTdeNavigate = async () => {
this.win.close();
};
}
diff --git a/apps/browser/src/auth/popup/two-factor-auth.component.ts b/apps/browser/src/auth/popup/two-factor-auth.component.ts
index 27c95321100..9e755746e6f 100644
--- a/apps/browser/src/auth/popup/two-factor-auth.component.ts
+++ b/apps/browser/src/auth/popup/two-factor-auth.component.ts
@@ -118,7 +118,7 @@ export class TwoFactorAuthComponent
win,
toastService,
);
- super.onSuccessfulLoginTdeNavigate = async () => {
+ this.onSuccessfulLoginTdeNavigate = async () => {
this.win.close();
};
this.onSuccessfulLoginNavigate = this.goAfterLogIn;
@@ -131,7 +131,7 @@ export class TwoFactorAuthComponent
// WebAuthn fallback response
this.selectedProviderType = TwoFactorProviderType.WebAuthn;
this.token = this.route.snapshot.paramMap.get("webAuthnResponse");
- super.onSuccessfulLogin = async () => {
+ this.onSuccessfulLogin = async () => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.syncService.fullSync(true);
diff --git a/apps/browser/src/auth/popup/two-factor.component.ts b/apps/browser/src/auth/popup/two-factor.component.ts
index e9167a5087a..27c4604be91 100644
--- a/apps/browser/src/auth/popup/two-factor.component.ts
+++ b/apps/browser/src/auth/popup/two-factor.component.ts
@@ -87,23 +87,23 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnInit
accountService,
toastService,
);
- super.onSuccessfulLogin = async () => {
+ this.onSuccessfulLogin = async () => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
syncService.fullSync(true);
};
- super.onSuccessfulLoginTde = async () => {
+ this.onSuccessfulLoginTde = async () => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
syncService.fullSync(true);
};
- super.onSuccessfulLoginTdeNavigate = async () => {
+ this.onSuccessfulLoginTdeNavigate = async () => {
this.win.close();
};
- super.successRoute = "/tabs/vault";
+ this.successRoute = "/tabs/vault";
// FIXME: Chromium 110 has broken WebAuthn support in extensions via an iframe
this.webAuthnNewTab = true;
}
@@ -113,7 +113,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnInit
// WebAuthn fallback response
this.selectedProviderType = TwoFactorProviderType.WebAuthn;
this.token = this.route.snapshot.paramMap.get("webAuthnResponse");
- super.onSuccessfulLogin = async () => {
+ this.onSuccessfulLogin = async () => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.syncService.fullSync(true);
@@ -155,7 +155,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnInit
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
if (qParams.sso === "true") {
- super.onSuccessfulLogin = async () => {
+ this.onSuccessfulLogin = async () => {
// This is not awaited so we don't pause the application while the sync is happening.
// This call is executed by the service that lives in the background script so it will continue
// the sync even if this tab closes.
diff --git a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts
index 1f0a38ad806..4109662fd66 100644
--- a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts
+++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts
@@ -1158,7 +1158,6 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
}
if (
- this.showInlineMenuIdentities &&
this.inlineMenuFieldQualificationService.isFieldForAccountCreationForm(
autofillFieldData,
pageDetails,
diff --git a/apps/cli/package.json b/apps/cli/package.json
index a609224dcb5..c5aeb306230 100644
--- a/apps/cli/package.json
+++ b/apps/cli/package.json
@@ -80,7 +80,7 @@
"papaparse": "5.4.1",
"proper-lockfile": "4.1.2",
"rxjs": "7.8.1",
- "tldts": "6.1.51",
+ "tldts": "6.1.52",
"zxcvbn": "4.4.2"
}
}
diff --git a/apps/desktop/src/auth/login/login-via-auth-request.component.ts b/apps/desktop/src/auth/login/login-via-auth-request.component.ts
index c0a6a51b907..12be2f01c08 100644
--- a/apps/desktop/src/auth/login/login-via-auth-request.component.ts
+++ b/apps/desktop/src/auth/login/login-via-auth-request.component.ts
@@ -83,7 +83,7 @@ export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent {
toastService,
);
- super.onSuccessfulLogin = () => {
+ this.onSuccessfulLogin = () => {
return syncService.fullSync(true);
};
}
diff --git a/apps/desktop/src/auth/login/login.component.ts b/apps/desktop/src/auth/login/login.component.ts
index b43e5bc84f0..6ba143421ca 100644
--- a/apps/desktop/src/auth/login/login.component.ts
+++ b/apps/desktop/src/auth/login/login.component.ts
@@ -99,7 +99,7 @@ export class LoginComponent extends BaseLoginComponent implements OnInit, OnDest
registerRouteService,
toastService,
);
- super.onSuccessfulLogin = () => {
+ this.onSuccessfulLogin = () => {
return syncService.fullSync(true);
};
}
diff --git a/apps/desktop/src/auth/sso.component.ts b/apps/desktop/src/auth/sso.component.ts
index 6821a548945..760eef14e80 100644
--- a/apps/desktop/src/auth/sso.component.ts
+++ b/apps/desktop/src/auth/sso.component.ts
@@ -65,13 +65,13 @@ export class SsoComponent extends BaseSsoComponent {
accountService,
toastService,
);
- super.onSuccessfulLogin = async () => {
+ this.onSuccessfulLogin = async () => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
syncService.fullSync(true);
};
- super.onSuccessfulLoginTde = async () => {
+ this.onSuccessfulLoginTde = async () => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
syncService.fullSync(true);
diff --git a/apps/desktop/src/auth/two-factor.component.ts b/apps/desktop/src/auth/two-factor.component.ts
index d2c5efe929f..0050ec65608 100644
--- a/apps/desktop/src/auth/two-factor.component.ts
+++ b/apps/desktop/src/auth/two-factor.component.ts
@@ -89,13 +89,13 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnDest
accountService,
toastService,
);
- super.onSuccessfulLogin = async () => {
+ this.onSuccessfulLogin = async () => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
syncService.fullSync(true);
};
- super.onSuccessfulLoginTde = async () => {
+ this.onSuccessfulLoginTde = async () => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
syncService.fullSync(true);
diff --git a/apps/web/src/app/admin-console/organizations/policies/personal-ownership.component.html b/apps/web/src/app/admin-console/organizations/policies/personal-ownership.component.html
index 85fb04730d4..2b6c86b1fdc 100644
--- a/apps/web/src/app/admin-console/organizations/policies/personal-ownership.component.html
+++ b/apps/web/src/app/admin-console/organizations/policies/personal-ownership.component.html
@@ -2,15 +2,7 @@
{{ "personalOwnershipExemption" | i18n }}
-
+
+
+ {{ "turnOn" | i18n }}
+
diff --git a/apps/web/src/app/auth/register-form/register-form.component.ts b/apps/web/src/app/auth/register-form/register-form.component.ts
index bf4a3e8203f..9982af2ab5d 100644
--- a/apps/web/src/app/auth/register-form/register-form.component.ts
+++ b/apps/web/src/app/auth/register-form/register-form.component.ts
@@ -71,7 +71,7 @@ export class RegisterFormComponent extends BaseRegisterComponent implements OnIn
dialogService,
toastService,
);
- super.modifyRegisterRequest = async (request: RegisterRequest) => {
+ this.modifyRegisterRequest = async (request: RegisterRequest) => {
// Org invites are deep linked. Non-existent accounts are redirected to the register page.
// Org user id and token are included here only for validation and two factor purposes.
const orgInvite = await acceptOrgInviteService.getOrganizationInvite();
diff --git a/apps/web/src/app/tools/access-intelligence/access-intelligence-routing.module.ts b/apps/web/src/app/tools/access-intelligence/access-intelligence-routing.module.ts
index b35b1fa64a3..88efb2b4832 100644
--- a/apps/web/src/app/tools/access-intelligence/access-intelligence-routing.module.ts
+++ b/apps/web/src/app/tools/access-intelligence/access-intelligence-routing.module.ts
@@ -1,7 +1,6 @@
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
-import { unauthGuardFn } from "@bitwarden/angular/auth/guards";
import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
@@ -11,7 +10,7 @@ const routes: Routes = [
{
path: "",
component: AccessIntelligenceComponent,
- canActivate: [canAccessFeature(FeatureFlag.AccessIntelligence), unauthGuardFn()],
+ canActivate: [canAccessFeature(FeatureFlag.AccessIntelligence)],
data: {
titleId: "accessIntelligence",
},
diff --git a/apps/web/src/app/tools/access-intelligence/access-intelligence.component.html b/apps/web/src/app/tools/access-intelligence/access-intelligence.component.html
index 665f8f6b0c5..df3eee389f6 100644
--- a/apps/web/src/app/tools/access-intelligence/access-intelligence.component.html
+++ b/apps/web/src/app/tools/access-intelligence/access-intelligence.component.html
@@ -1,6 +1,9 @@
-
+
+
+
+
diff --git a/apps/web/src/app/tools/access-intelligence/access-intelligence.component.ts b/apps/web/src/app/tools/access-intelligence/access-intelligence.component.ts
index 9e5eff6f629..8bdaadbd7e4 100644
--- a/apps/web/src/app/tools/access-intelligence/access-intelligence.component.ts
+++ b/apps/web/src/app/tools/access-intelligence/access-intelligence.component.ts
@@ -11,6 +11,7 @@ import { HeaderModule } from "../../layouts/header/header.module";
import { ApplicationTableComponent } from "./application-table.component";
import { NotifiedMembersTableComponent } from "./notified-members-table.component";
+import { PasswordHealthComponent } from "./password-health.component";
export enum AccessIntelligenceTabType {
AllApps = 0,
@@ -26,6 +27,7 @@ export enum AccessIntelligenceTabType {
CommonModule,
JslibModule,
HeaderModule,
+ PasswordHealthComponent,
NotifiedMembersTableComponent,
TabsModule,
],
diff --git a/apps/web/src/app/tools/access-intelligence/password-health.component.html b/apps/web/src/app/tools/access-intelligence/password-health.component.html
new file mode 100644
index 00000000000..32459706449
--- /dev/null
+++ b/apps/web/src/app/tools/access-intelligence/password-health.component.html
@@ -0,0 +1,57 @@
+
+ {{ "passwordsReportDesc" | i18n }}
+
+
+ {{ "loading" | i18n }}
+
+
+
+
+
+ |
+ {{ "name" | i18n }} |
+ {{ "weakness" | i18n }} |
+ {{ "timesReused" | i18n }} |
+ {{ "timesExposed" | i18n }} |
+
+
+
+
+ |
+
+ |
+
+
+ {{ r.name }}
+
+
+ {{ r.subTitle }}
+ |
+
+
+ {{ passwordStrengthMap.get(r.id)[0] | i18n }}
+
+ |
+
+
+ {{ "reusedXTimes" | i18n: passwordUseMap.get(r.login.password) }}
+
+ |
+
+
+ {{ "exposedXTimes" | i18n: exposedPasswordMap.get(r.id) }}
+
+ |
+
+
+
+
+
diff --git a/apps/web/src/app/tools/access-intelligence/password-health.component.spec.ts b/apps/web/src/app/tools/access-intelligence/password-health.component.spec.ts
new file mode 100644
index 00000000000..4a6d5c50ee1
--- /dev/null
+++ b/apps/web/src/app/tools/access-intelligence/password-health.component.spec.ts
@@ -0,0 +1,114 @@
+import { ComponentFixture, TestBed } from "@angular/core/testing";
+import { ActivatedRoute, convertToParamMap } from "@angular/router";
+import { MockProxy, mock } from "jest-mock-extended";
+import { of } from "rxjs";
+
+import { AuditService } from "@bitwarden/common/abstractions/audit.service";
+import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
+import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
+import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
+import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
+import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
+import { TableModule } from "@bitwarden/components";
+import { TableBodyDirective } from "@bitwarden/components/src/table/table.component";
+
+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";
+
+import { PasswordHealthComponent } from "./password-health.component";
+
+describe("PasswordHealthComponent", () => {
+ let component: PasswordHealthComponent;
+ let fixture: ComponentFixture;
+ let passwordStrengthService: MockProxy;
+ let organizationService: MockProxy;
+ let cipherServiceMock: MockProxy;
+ let auditServiceMock: MockProxy;
+ const activeRouteParams = convertToParamMap({ organizationId: "orgId" });
+
+ beforeEach(async () => {
+ passwordStrengthService = mock();
+ auditServiceMock = mock();
+ organizationService = mock({
+ get: jest.fn().mockResolvedValue({ id: "orgId" } as Organization),
+ });
+ cipherServiceMock = mock({
+ getAllFromApiForOrganization: jest.fn().mockResolvedValue(cipherData),
+ });
+
+ await TestBed.configureTestingModule({
+ imports: [PasswordHealthComponent, PipesModule, TableModule, LooseComponentsModule],
+ declarations: [TableBodyDirective],
+ providers: [
+ { provide: CipherService, useValue: cipherServiceMock },
+ { provide: PasswordStrengthServiceAbstraction, useValue: passwordStrengthService },
+ { provide: OrganizationService, useValue: organizationService },
+ { provide: I18nService, useValue: mock() },
+ { provide: AuditService, useValue: auditServiceMock },
+ {
+ provide: ActivatedRoute,
+ useValue: {
+ paramMap: of(activeRouteParams),
+ url: of([]),
+ },
+ },
+ ],
+ }).compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(PasswordHealthComponent);
+ component = fixture.componentInstance;
+
+ fixture.detectChanges();
+ });
+
+ it("should initialize component", () => {
+ expect(component).toBeTruthy();
+ });
+
+ it("should populate reportCiphers with ciphers that have password issues", async () => {
+ passwordStrengthService.getPasswordStrength.mockReturnValue({ score: 1 } as any);
+
+ auditServiceMock.passwordLeaked.mockResolvedValue(5);
+
+ await component.setCiphers();
+
+ const cipherIds = component.reportCiphers.map((c) => c.id);
+
+ expect(cipherIds).toEqual([
+ "cbea34a8-bde4-46ad-9d19-b05001228ab1",
+ "cbea34a8-bde4-46ad-9d19-b05001228ab2",
+ "cbea34a8-bde4-46ad-9d19-b05001228cd3",
+ ]);
+ expect(component.reportCiphers.length).toEqual(3);
+ });
+
+ it("should correctly populate passwordStrengthMap", async () => {
+ passwordStrengthService.getPasswordStrength.mockImplementation((password) => {
+ let score = 0;
+ if (password === "123") {
+ score = 1;
+ } else {
+ score = 4;
+ }
+ return { score } as any;
+ });
+
+ auditServiceMock.passwordLeaked.mockResolvedValue(0);
+
+ await component.setCiphers();
+
+ expect(component.passwordStrengthMap.size).toBeGreaterThan(0);
+ expect(component.passwordStrengthMap.get("cbea34a8-bde4-46ad-9d19-b05001228ab2")).toEqual([
+ "veryWeak",
+ "danger",
+ ]);
+ expect(component.passwordStrengthMap.get("cbea34a8-bde4-46ad-9d19-b05001228cd3")).toEqual([
+ "veryWeak",
+ "danger",
+ ]);
+ });
+});
diff --git a/apps/web/src/app/tools/access-intelligence/password-health.component.ts b/apps/web/src/app/tools/access-intelligence/password-health.component.ts
new file mode 100644
index 00000000000..6e8e62c50db
--- /dev/null
+++ b/apps/web/src/app/tools/access-intelligence/password-health.component.ts
@@ -0,0 +1,229 @@
+import { CommonModule } from "@angular/common";
+import { Component, DestroyRef, inject, OnInit } from "@angular/core";
+import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
+import { ActivatedRoute } from "@angular/router";
+import { from, map, switchMap, tap } from "rxjs";
+
+import { JslibModule } from "@bitwarden/angular/jslib.module";
+import { AuditService } from "@bitwarden/common/abstractions/audit.service";
+import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
+import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
+import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
+import { Utils } from "@bitwarden/common/platform/misc/utils";
+import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
+import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
+import { CipherType } from "@bitwarden/common/vault/enums";
+import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
+import {
+ BadgeModule,
+ BadgeVariant,
+ ContainerComponent,
+ TableDataSource,
+ TableModule,
+} from "@bitwarden/components";
+
+// eslint-disable-next-line no-restricted-imports
+import { HeaderModule } from "../../layouts/header/header.module";
+// eslint-disable-next-line no-restricted-imports
+import { OrganizationBadgeModule } from "../../vault/individual-vault/organization-badge/organization-badge.module";
+// eslint-disable-next-line no-restricted-imports
+import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module";
+
+@Component({
+ standalone: true,
+ selector: "tools-password-health",
+ templateUrl: "password-health.component.html",
+ imports: [
+ BadgeModule,
+ OrganizationBadgeModule,
+ CommonModule,
+ ContainerComponent,
+ PipesModule,
+ JslibModule,
+ HeaderModule,
+ TableModule,
+ ],
+})
+export class PasswordHealthComponent implements OnInit {
+ passwordStrengthMap = new Map();
+
+ weakPasswordCiphers: CipherView[] = [];
+
+ passwordUseMap = new Map();
+
+ exposedPasswordMap = new Map();
+
+ dataSource = new TableDataSource();
+
+ reportCiphers: CipherView[] = [];
+ reportCipherIds: string[] = [];
+
+ organization: Organization;
+
+ loading = true;
+
+ private destroyRef = inject(DestroyRef);
+
+ constructor(
+ protected cipherService: CipherService,
+ protected passwordStrengthService: PasswordStrengthServiceAbstraction,
+ protected organizationService: OrganizationService,
+ protected auditService: AuditService,
+ protected i18nService: I18nService,
+ protected activatedRoute: ActivatedRoute,
+ ) {}
+
+ ngOnInit() {
+ this.activatedRoute.paramMap
+ .pipe(
+ takeUntilDestroyed(this.destroyRef),
+ map((params) => params.get("organizationId")),
+ switchMap((organizationId) => {
+ return from(this.organizationService.get(organizationId));
+ }),
+ tap((organization) => {
+ this.organization = organization;
+ }),
+ switchMap(() => from(this.setCiphers())),
+ )
+ .subscribe();
+ }
+
+ async setCiphers() {
+ const allCiphers = await this.cipherService.getAllFromApiForOrganization(this.organization.id);
+ allCiphers.forEach(async (cipher) => {
+ this.findWeakPassword(cipher);
+ this.findReusedPassword(cipher);
+ await this.findExposedPassword(cipher);
+ });
+ this.dataSource.data = this.reportCiphers;
+ this.loading = false;
+
+ // const reportIssues = allCiphers.map((c) => {
+ // if (this.passwordStrengthMap.has(c.id)) {
+ // return c;
+ // }
+
+ // if (this.passwordUseMap.has(c.id)) {
+ // return c;
+ // }
+
+ // if (this.exposedPasswordMap.has(c.id)) {
+ // return c;
+ // }
+ // });
+ }
+
+ protected checkForExistingCipher(ciph: CipherView) {
+ if (!this.reportCipherIds.includes(ciph.id)) {
+ this.reportCipherIds.push(ciph.id);
+ this.reportCiphers.push(ciph);
+ }
+ }
+
+ 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 exposedCount = await this.auditService.passwordLeaked(login.password);
+ if (exposedCount > 0) {
+ this.exposedPasswordMap.set(id, exposedCount);
+ this.checkForExistingCipher(cipher);
+ }
+ }
+
+ 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;
+ }
+
+ 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.checkForExistingCipher(cipher);
+ }
+
+ 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(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) => i.length >= 3);
+ } else {
+ userInput = login.username
+ .trim()
+ .toLowerCase()
+ .split(/[^A-Za-z0-9]/)
+ .filter((i) => i.length >= 3);
+ }
+ }
+ const { score } = this.passwordStrengthService.getPasswordStrength(
+ login.password,
+ null,
+ userInput.length > 0 ? userInput : null,
+ );
+
+ 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 scoreKey(score: number): [string, BadgeVariant] {
+ switch (score) {
+ case 4:
+ return ["strong", "success"];
+ case 3:
+ return ["good", "primary"];
+ case 2:
+ return ["weak", "warning"];
+ default:
+ return ["veryWeak", "danger"];
+ }
+ }
+}
diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts b/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts
index 96089d2b156..f74b73b1030 100644
--- a/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts
+++ b/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts
@@ -288,6 +288,7 @@ function createCollectionView(i: number): CollectionAdminView {
view.id = `collection-${i}`;
view.name = `Collection ${i}`;
view.organizationId = organization?.id;
+ view.manage = true;
if (group !== undefined) {
view.groups = [
diff --git a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component.ts b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component.ts
index e0b76c7f5c3..7141f867882 100644
--- a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component.ts
+++ b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component.ts
@@ -105,6 +105,11 @@ export class DomainAddEditDialogComponent implements OnInit, OnDestroy {
copyDnsTxt(): void {
this.orgDomainService.copyDnsTxt(this.txtCtrl.value);
+ this.toastService.showToast({
+ variant: "success",
+ title: null,
+ message: this.i18nService.t("valueCopied", this.i18nService.t("dnsTxtRecord")),
+ });
}
// End Form methods
diff --git a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.ts b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.ts
index bc68bdaaf54..703808900c9 100644
--- a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.ts
+++ b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.ts
@@ -101,6 +101,11 @@ export class DomainVerificationComponent implements OnInit, OnDestroy {
copyDnsTxt(dnsTxt: string): void {
this.orgDomainService.copyDnsTxt(dnsTxt);
+ this.toastService.showToast({
+ variant: "success",
+ title: null,
+ message: this.i18nService.t("valueCopied", this.i18nService.t("dnsTxtRecord")),
+ });
}
async verifyDomain(orgDomainId: string, domainName: string): Promise {
diff --git a/libs/common/src/admin-console/services/organization-domain/org-domain.service.spec.ts b/libs/common/src/admin-console/services/organization-domain/org-domain.service.spec.ts
index 6721ea3a808..21027334fb8 100644
--- a/libs/common/src/admin-console/services/organization-domain/org-domain.service.spec.ts
+++ b/libs/common/src/admin-console/services/organization-domain/org-domain.service.spec.ts
@@ -180,6 +180,5 @@ describe("Org Domain Service", () => {
it("copyDnsTxt copies DNS TXT to clipboard and shows toast", () => {
orgDomainService.copyDnsTxt("fakeTxt");
expect(jest.spyOn(platformUtilService, "copyToClipboard")).toHaveBeenCalled();
- expect(jest.spyOn(platformUtilService, "showToast")).toHaveBeenCalled();
});
});
diff --git a/libs/common/src/admin-console/services/organization-domain/org-domain.service.ts b/libs/common/src/admin-console/services/organization-domain/org-domain.service.ts
index 5a5a2e4288f..ebdc098c855 100644
--- a/libs/common/src/admin-console/services/organization-domain/org-domain.service.ts
+++ b/libs/common/src/admin-console/services/organization-domain/org-domain.service.ts
@@ -23,11 +23,6 @@ export class OrgDomainService implements OrgDomainInternalServiceAbstraction {
copyDnsTxt(dnsTxt: string): void {
this.platformUtilsService.copyToClipboard(dnsTxt);
- this.platformUtilsService.showToast(
- "success",
- null,
- this.i18nService.t("valueCopied", this.i18nService.t("dnsTxtRecord")),
- );
}
upsert(orgDomains: OrganizationDomainResponse[]): void {
diff --git a/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html b/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html
index bcda8b57107..4da3466f708 100644
--- a/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html
+++ b/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html
@@ -43,7 +43,7 @@
>
{{ "sendPasswordDescV2" | i18n }}
-
+