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 }} - +