diff --git a/apps/web/src/app/tools/access-intelligence/all-applications.component.html b/apps/web/src/app/tools/access-intelligence/all-applications.component.html
index 4ed31adea78..730da309aad 100644
--- a/apps/web/src/app/tools/access-intelligence/all-applications.component.html
+++ b/apps/web/src/app/tools/access-intelligence/all-applications.component.html
@@ -6,7 +6,7 @@
>
{{ "loading" | i18n }}
-
+
@@ -34,15 +34,15 @@
@@ -88,7 +88,7 @@
/>
- {{ r.name }}
+ {{ r.application }}
|
diff --git a/apps/web/src/app/tools/access-intelligence/all-applications.component.ts b/apps/web/src/app/tools/access-intelligence/all-applications.component.ts
index 5d76403f46b..5cfa348ca3b 100644
--- a/apps/web/src/app/tools/access-intelligence/all-applications.component.ts
+++ b/apps/web/src/app/tools/access-intelligence/all-applications.component.ts
@@ -4,6 +4,12 @@ import { FormControl } from "@angular/forms";
import { ActivatedRoute } from "@angular/router";
import { debounceTime, firstValueFrom, map } from "rxjs";
+// eslint-disable-next-line no-restricted-imports
+import {
+ PasswordHealthService,
+ MemberCipherDetailsApiService,
+ ApplicationHealthReport,
+} from "@bitwarden/bit-common/tools/reports/risk-insights";
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";
@@ -26,31 +32,30 @@ import { HeaderModule } from "../../layouts/header/header.module";
import { SharedModule } from "../../shared";
import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module";
-import { applicationTableMockData } from "./application-table.mock";
-
@Component({
standalone: true,
selector: "tools-all-applications",
templateUrl: "./all-applications.component.html",
imports: [HeaderModule, CardComponent, SearchModule, PipesModule, NoItemsModule, SharedModule],
+ providers: [MemberCipherDetailsApiService],
})
export class AllApplicationsComponent implements OnInit {
protected dataSource = new TableDataSource();
protected selectedIds: Set = new Set();
protected searchControl = new FormControl("", { nonNullable: true });
private destroyRef = inject(DestroyRef);
- protected loading = false;
+ protected loading = true;
protected organization: Organization;
noItemsIcon = Icons.Security;
protected markingAsCritical = false;
+ protected applicationHealthReport: ApplicationHealthReport;
isCritialAppsFeatureEnabled = false;
-
- // MOCK DATA
- protected mockData = applicationTableMockData;
- protected mockAtRiskMembersCount = 0;
- protected mockAtRiskAppsCount = 0;
- protected mockTotalMembersCount = 0;
- protected mockTotalAppsCount = 0;
+ passwordHealthService = new PasswordHealthService(
+ this.passwordStrengthService,
+ this.auditService,
+ this.cipherService,
+ this.memberCipherDetailsApiService,
+ );
async ngOnInit() {
this.activatedRoute.paramMap
@@ -59,7 +64,10 @@ export class AllApplicationsComponent implements OnInit {
map(async (params) => {
const organizationId = params.get("organizationId");
this.organization = await firstValueFrom(this.organizationService.get$(organizationId));
- // TODO: use organizationId to fetch data
+ this.applicationHealthReport =
+ await this.passwordHealthService.generateReportDetails(organizationId);
+ this.dataSource.data = this.applicationHealthReport.details;
+ this.loading = false;
}),
)
.subscribe();
@@ -78,8 +86,8 @@ export class AllApplicationsComponent implements OnInit {
protected toastService: ToastService,
protected organizationService: OrganizationService,
protected configService: ConfigService,
+ protected memberCipherDetailsApiService: MemberCipherDetailsApiService,
) {
- this.dataSource.data = applicationTableMockData;
this.searchControl.valueChanges
.pipe(debounceTime(200), takeUntilDestroyed())
.subscribe((v) => (this.dataSource.filter = v));
diff --git a/apps/web/src/app/tools/access-intelligence/risk-insights.component.html b/apps/web/src/app/tools/access-intelligence/risk-insights.component.html
index 067207160d4..66cdb41aaf8 100644
--- a/apps/web/src/app/tools/access-intelligence/risk-insights.component.html
+++ b/apps/web/src/app/tools/access-intelligence/risk-insights.component.html
@@ -29,21 +29,4 @@
-
-
-
-
-
-
-
-
-
-
diff --git a/apps/web/src/app/tools/access-intelligence/risk-insights.component.ts b/apps/web/src/app/tools/access-intelligence/risk-insights.component.ts
index 1c6a36b4454..b76f0064d36 100644
--- a/apps/web/src/app/tools/access-intelligence/risk-insights.component.ts
+++ b/apps/web/src/app/tools/access-intelligence/risk-insights.component.ts
@@ -13,9 +13,6 @@ import { HeaderModule } from "../../layouts/header/header.module";
import { AllApplicationsComponent } from "./all-applications.component";
import { CriticalApplicationsComponent } from "./critical-applications.component";
import { NotifiedMembersTableComponent } from "./notified-members-table.component";
-import { PasswordHealthMembersURIComponent } from "./password-health-members-uri.component";
-import { PasswordHealthMembersComponent } from "./password-health-members.component";
-import { PasswordHealthComponent } from "./password-health.component";
export enum RiskInsightsTabType {
AllApps = 0,
@@ -34,9 +31,6 @@ export enum RiskInsightsTabType {
CriticalApplicationsComponent,
JslibModule,
HeaderModule,
- PasswordHealthComponent,
- PasswordHealthMembersComponent,
- PasswordHealthMembersURIComponent,
NotifiedMembersTableComponent,
TabsModule,
],
diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/ciphers.mock.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/ciphers.mock.ts
index e7693e46a32..7538d7c92b9 100644
--- a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/ciphers.mock.ts
+++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/ciphers.mock.ts
@@ -1,10 +1,18 @@
+import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
+
+const createLoginUriView = (uri: string): LoginUriView => {
+ const view = new LoginUriView();
+ view.uri = uri;
+ return view;
+};
+
export const mockCiphers: any[] = [
{
initializerKey: 1,
id: "cbea34a8-bde4-46ad-9d19-b05001228ab1",
organizationId: null,
folderId: null,
- name: "Cannot Be Edited",
+ name: "Weak Password Cipher",
notes: null,
isDeleted: false,
type: 1,
@@ -14,10 +22,11 @@ export const mockCiphers: any[] = [
password: "123",
hasUris: true,
uris: [
- { uri: "www.google.com" },
- { uri: "accounts.google.com" },
- { uri: "https://www.google.com" },
- { uri: "https://www.google.com/login" },
+ createLoginUriView("101domain.com"),
+ createLoginUriView("www.google.com"),
+ createLoginUriView("accounts.google.com"),
+ createLoginUriView("https://www.google.com"),
+ createLoginUriView("https://www.google.com/login"),
],
},
edit: false,
@@ -31,23 +40,18 @@ export const mockCiphers: any[] = [
},
{
initializerKey: 1,
- id: "cbea34a8-bde4-46ad-9d19-b05001228ab2",
+ id: "cbea34a8-bde4-46ad-9d19-b05001228cd3",
organizationId: null,
folderId: null,
- name: "Can Be Edited id ending 2",
+ name: "Strong Password Cipher",
notes: null,
- isDeleted: false,
type: 1,
favorite: false,
organizationUseTotp: false,
login: {
- password: "123",
+ password: "Password!123",
hasUris: true,
- uris: [
- {
- uri: "http://nothing.com",
- },
- ],
+ uris: [createLoginUriView("http://example.com")],
},
edit: true,
viewPassword: true,
@@ -60,22 +64,18 @@ export const mockCiphers: any[] = [
},
{
initializerKey: 1,
- id: "cbea34a8-bde4-46ad-9d19-b05001228cd3",
+ id: "cbea34a8-bde4-46ad-9d19-b05001228ab2",
organizationId: null,
folderId: null,
- name: "Can Be Edited id ending 3",
+ name: "Strong password Cipher",
notes: null,
type: 1,
favorite: false,
organizationUseTotp: false,
login: {
- password: "123",
hasUris: true,
- uris: [
- {
- uri: "http://example.com",
- },
- ],
+ password: "Password!1234",
+ uris: [createLoginUriView("101domain.com")],
},
edit: true,
viewPassword: true,
@@ -91,14 +91,15 @@ export const mockCiphers: any[] = [
id: "cbea34a8-bde4-46ad-9d19-b05001228xy4",
organizationId: null,
folderId: null,
- name: "Can Be Edited id ending 4",
+ name: "Weak password Cipher",
notes: null,
type: 1,
favorite: false,
organizationUseTotp: false,
login: {
hasUris: true,
- uris: [{ uri: "101domain.com" }],
+ password: "Password!123",
+ uris: [createLoginUriView("example.com")],
},
edit: true,
viewPassword: true,
@@ -114,14 +115,14 @@ export const mockCiphers: any[] = [
id: "cbea34a8-bde4-46ad-9d19-b05001227nm5",
organizationId: null,
folderId: null,
- name: "Can Be Edited id ending 5",
+ name: "No password Cipher",
notes: null,
type: 1,
favorite: false,
organizationUseTotp: false,
login: {
hasUris: true,
- uris: [{ uri: "123formbuilder.com" }],
+ uris: [createLoginUriView("123formbuilder.com")],
},
edit: true,
viewPassword: true,
diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/member-cipher-details-api.service.spec.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/member-cipher-details-api.service.spec.ts
index 872a4cdff55..6ea49551239 100644
--- a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/member-cipher-details-api.service.spec.ts
+++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/member-cipher-details-api.service.spec.ts
@@ -63,11 +63,7 @@ export const mockMemberCipherDetails: any = [
userName: "Chris Finch",
email: "chris.finch@wernhamhogg.uk",
usesKeyConnector: true,
- cipherIds: [
- "cbea34a8-bde4-46ad-9d19-b05001228ab2",
- "cbea34a8-bde4-46ad-9d19-b05001228cd3",
- "cbea34a8-bde4-46ad-9d19-b05001228xy4",
- ],
+ cipherIds: ["cbea34a8-bde4-46ad-9d19-b05001227nm5"],
},
];
diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/password-health.service.spec.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/password-health.service.spec.ts
index c0f77abeb79..b3b36072fd9 100644
--- a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/password-health.service.spec.ts
+++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/password-health.service.spec.ts
@@ -3,7 +3,9 @@ import { TestBed } from "@angular/core/testing";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
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 { LoginView } from "@bitwarden/common/vault/models/view/login.view";
import { mockCiphers } from "./ciphers.mock";
import { MemberCipherDetailsApiService } from "./member-cipher-details-api.service";
@@ -12,8 +14,6 @@ import { PasswordHealthService } from "./password-health.service";
describe("PasswordHealthService", () => {
let service: PasswordHealthService;
- let cipherService: CipherService;
- let memberCipherDetailsApiService: MemberCipherDetailsApiService;
beforeEach(() => {
TestBed.configureTestingModule({
@@ -46,104 +46,101 @@ describe("PasswordHealthService", () => {
getMemberCipherDetails: jest.fn().mockResolvedValue(mockMemberCipherDetails),
},
},
- { provide: "organizationId", useValue: "org1" },
],
});
service = TestBed.inject(PasswordHealthService);
- cipherService = TestBed.inject(CipherService);
- memberCipherDetailsApiService = TestBed.inject(MemberCipherDetailsApiService);
});
- it("should be created", () => {
- expect(service).toBeTruthy();
+ it("should build the application health report correctly", async () => {
+ // Execute the method
+ const result = await service.generateReportDetails("orgId");
+
+ const expected = [
+ {
+ application: "101domain.com",
+ atRiskPasswords: 1,
+ totalPasswords: 2,
+ atRiskMembers: 2,
+ totalMembers: 4,
+ },
+ {
+ application: "123formbuilder.com",
+ atRiskPasswords: 0,
+ totalPasswords: 1,
+ atRiskMembers: 0,
+ totalMembers: 5,
+ },
+ {
+ application: "example.com",
+ atRiskPasswords: 1,
+ totalPasswords: 2,
+ atRiskMembers: 5,
+ totalMembers: 5,
+ },
+ {
+ application: "google.com",
+ atRiskPasswords: 1,
+ totalPasswords: 1,
+ atRiskMembers: 2,
+ totalMembers: 2,
+ },
+ ];
+
+ // Sort for comparison
+ const sortFn = (a: any, b: any) => a.application.localeCompare(b.application);
+
+ expect(result.details.sort(sortFn)).toEqual(expected.sort(sortFn));
+ expect(result.totalAtRiskMembers).toBe(5);
+ expect(result.totalMembers).toBe(6);
+ expect(result.totalAtRiskApps).toBe(3);
+ expect(result.totalApps).toBe(4);
});
- it("should initialize properties", () => {
- expect(service.reportCiphers).toEqual([]);
- expect(service.reportCipherIds).toEqual([]);
- expect(service.passwordStrengthMap.size).toBe(0);
- expect(service.passwordUseMap.size).toBe(0);
- expect(service.exposedPasswordMap.size).toBe(0);
- expect(service.totalMembersMap.size).toBe(0);
- });
+ describe("isWeakPassword", () => {
+ it("should return true for a weak password", () => {
+ const cipher = new CipherView();
+ cipher.type = CipherType.Login;
+ cipher.login = { password: "123", username: "user" } as LoginView;
+ cipher.viewPassword = true;
- describe("generateReport", () => {
- beforeEach(async () => {
- await service.generateReport();
+ expect(service.isWeakPassword(cipher)).toBe(true);
});
- it("should fetch all ciphers for the organization", () => {
- expect(cipherService.getAllFromApiForOrganization).toHaveBeenCalledWith("org1");
- });
+ it("should return false for a strong password", () => {
+ const cipher = new CipherView();
+ cipher.type = CipherType.Login;
+ cipher.login = { password: "StrongPass!123", username: "user" } as LoginView;
+ cipher.viewPassword = true;
- it("should fetch member cipher details", () => {
- expect(memberCipherDetailsApiService.getMemberCipherDetails).toHaveBeenCalledWith("org1");
- });
-
- it("should populate reportCiphers with ciphers that have issues", () => {
- expect(service.reportCiphers.length).toBeGreaterThan(0);
- });
-
- it("should detect weak passwords", () => {
- expect(service.passwordStrengthMap.size).toBeGreaterThan(0);
- expect(service.passwordStrengthMap.get("cbea34a8-bde4-46ad-9d19-b05001228ab1")).toEqual([
- "veryWeak",
- "danger",
- ]);
- expect(service.passwordStrengthMap.get("cbea34a8-bde4-46ad-9d19-b05001228ab2")).toEqual([
- "veryWeak",
- "danger",
- ]);
- expect(service.passwordStrengthMap.get("cbea34a8-bde4-46ad-9d19-b05001228cd3")).toEqual([
- "veryWeak",
- "danger",
- ]);
- });
-
- it("should detect reused passwords", () => {
- expect(service.passwordUseMap.get("123")).toBe(3);
- });
-
- it("should detect exposed passwords", () => {
- expect(service.exposedPasswordMap.size).toBeGreaterThan(0);
- expect(service.exposedPasswordMap.get("cbea34a8-bde4-46ad-9d19-b05001228ab1")).toBe(100);
- });
-
- it("should calculate total members per cipher", () => {
- expect(service.totalMembersMap.size).toBeGreaterThan(0);
- expect(service.totalMembersMap.get("cbea34a8-bde4-46ad-9d19-b05001228ab1")).toBe(2);
- expect(service.totalMembersMap.get("cbea34a8-bde4-46ad-9d19-b05001228ab2")).toBe(4);
- expect(service.totalMembersMap.get("cbea34a8-bde4-46ad-9d19-b05001228cd3")).toBe(5);
- expect(service.totalMembersMap.get("cbea34a8-bde4-46ad-9d19-b05001227nm5")).toBe(4);
- expect(service.totalMembersMap.get("cbea34a8-bde4-46ad-9d19-b05001227nm7")).toBe(1);
- expect(service.totalMembersMap.get("cbea34a8-bde4-46ad-9d19-b05001228xy4")).toBe(6);
+ expect(service.isWeakPassword(cipher)).toBe(false);
});
});
- describe("findWeakPassword", () => {
- it("should add weak passwords to passwordStrengthMap", () => {
- const weakCipher = mockCiphers.find((c) => c.login?.password === "123") as CipherView;
- service.findWeakPassword(weakCipher);
- expect(service.passwordStrengthMap.get(weakCipher.id)).toEqual(["veryWeak", "danger"]);
- });
- });
+ describe("isReusedPassword", () => {
+ it("should return false for a new password", () => {
+ const cipher = new CipherView();
+ cipher.type = CipherType.Login;
+ cipher.login = { password: "uniquePassword", username: "user" } as LoginView;
+ cipher.viewPassword = true;
- describe("findReusedPassword", () => {
- it("should detect password reuse", () => {
- mockCiphers.forEach((cipher) => {
- service.findReusedPassword(cipher as CipherView);
- });
- const reuseCounts = Array.from(service.passwordUseMap.values()).filter((count) => count > 1);
- expect(reuseCounts.length).toBeGreaterThan(0);
+ expect(service.isReusedPassword(cipher)).toBe(false);
});
- });
- describe("findExposedPassword", () => {
- it("should add exposed passwords to exposedPasswordMap", async () => {
- const exposedCipher = mockCiphers.find((c) => c.login?.password === "123") as CipherView;
- await service.findExposedPassword(exposedCipher);
- expect(service.exposedPasswordMap.get(exposedCipher.id)).toBe(100);
+ it("should return true for a reused password", () => {
+ const cipher1 = new CipherView();
+ cipher1.type = CipherType.Login;
+ cipher1.login = { password: "reusedPassword", username: "user" } as LoginView;
+ cipher1.viewPassword = true;
+
+ const cipher2 = new CipherView();
+ cipher2.type = CipherType.Login;
+ cipher2.login = { password: "reusedPassword", username: "user" } as LoginView;
+ cipher2.viewPassword = true;
+
+ service.isReusedPassword(cipher1); // Adds 'reusedPassword' to usedPasswords
+
+ expect(service.isReusedPassword(cipher2)).toBe(true);
});
});
});
diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/password-health.service.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/password-health.service.ts
index 4070b23d29e..1161415ea58 100644
--- a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/password-health.service.ts
+++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/password-health.service.ts
@@ -1,4 +1,4 @@
-import { Inject, Injectable } from "@angular/core";
+import { Injectable } from "@angular/core";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
@@ -6,57 +6,157 @@ import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/pass
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 { BadgeVariant } from "@bitwarden/components";
+import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
import { MemberCipherDetailsApiService } from "./member-cipher-details-api.service";
+export interface ApplicationHealthReport {
+ details: ApplicationHealthReportDetail[];
+ totalAtRiskMembers: number;
+ totalMembers: number;
+ totalAtRiskApps: number;
+ totalApps: number;
+}
+
+interface ApplicationHealthReportDetail {
+ application: string;
+ atRiskPasswords: number;
+ totalPasswords: number;
+ atRiskMembers: number;
+ totalMembers: number;
+}
+
@Injectable()
export class PasswordHealthService {
reportCiphers: CipherView[] = [];
reportCipherIds: string[] = [];
- passwordStrengthMap = new Map();
+ usedPasswords: string[] = [];
- passwordUseMap = new Map();
-
- exposedPasswordMap = new Map();
-
- totalMembersMap = new Map();
+ applicationHealthReport: ApplicationHealthReportDetail[] = [];
constructor(
private passwordStrengthService: PasswordStrengthServiceAbstraction,
private auditService: AuditService,
private cipherService: CipherService,
private memberCipherDetailsApiService: MemberCipherDetailsApiService,
- @Inject("organizationId") private organizationId: string,
) {}
- async generateReport() {
- const allCiphers = await this.cipherService.getAllFromApiForOrganization(this.organizationId);
- allCiphers.forEach(async (cipher) => {
- this.findWeakPassword(cipher);
- this.findReusedPassword(cipher);
- await this.findExposedPassword(cipher);
- });
+ async generateReportDetails(organizationId: string): Promise {
+ // Helper function to normalize hostnames to TLDs
+ const hostnameToTLD = (uriView: LoginUriView): string => {
+ const match = uriView.hostname.match(/([^.]+\.[^.]+)$/);
+ return match ? match[1] : uriView.hostname;
+ };
- const memberCipherDetails = await this.memberCipherDetailsApiService.getMemberCipherDetails(
- this.organizationId,
- );
+ const members = await this.memberCipherDetailsApiService.getMemberCipherDetails(organizationId);
- memberCipherDetails.forEach((user) => {
- user.cipherIds.forEach((cipherId: string) => {
- if (this.totalMembersMap.has(cipherId)) {
- this.totalMembersMap.set(cipherId, (this.totalMembersMap.get(cipherId) || 0) + 1);
- } else {
- this.totalMembersMap.set(cipherId, 1);
+ const ciphers = await this.cipherService.getAllFromApiForOrganization(organizationId);
+
+ // Map to store application data
+ const appDataMap = new Map();
+
+ // Map cipher IDs to ciphers
+ const cipherMap = new Map(ciphers.map((cipher) => [cipher.id, cipher]));
+
+ // Set to store at-risk member IDs
+ const totalAtRiskMembers = new Set();
+
+ // Set to store at-risk app IDs
+ const totalAtRiskApps = new Set();
+
+ // Set to store at-risk cipher IDs
+ const atRiskCipherIds = new Set();
+
+ // Determine at-risk ciphers
+ for (const cipher of ciphers) {
+ const isWeak = this.isWeakPassword(cipher);
+ const isReused = this.isReusedPassword(cipher);
+ const isExposed = await this.isExposedPassword(cipher);
+ if (isWeak || isReused || isExposed) {
+ atRiskCipherIds.add(cipher.id);
+ }
+ }
+
+ // Group ciphers by application
+ for (const cipher of ciphers) {
+ const applications = new Set(cipher.login.uris.map(hostnameToTLD));
+
+ for (const app of applications) {
+ if (!appDataMap.has(app)) {
+ appDataMap.set(app, {
+ application: app,
+ atRiskPasswords: 0,
+ totalPasswords: 0,
+ atRiskMembers: 0,
+ totalMembers: 0,
+ });
}
- });
- });
+
+ const appData = appDataMap.get(app)!;
+ appData.totalPasswords += 1;
+
+ if (atRiskCipherIds.has(cipher.id)) {
+ appData.atRiskPasswords += 1;
+ }
+ }
+ }
+
+ // Associate members with applications
+ for (const member of members) {
+ const memberApps = new Set();
+ const atRiskApps = new Set();
+
+ for (const cipherId of member.cipherIds) {
+ const cipher = cipherMap.get(cipherId);
+ if (!cipher) {
+ continue;
+ }
+
+ const applications = new Set(cipher.login.uris.map(hostnameToTLD));
+
+ for (const app of applications) {
+ memberApps.add(app);
+ if (atRiskCipherIds.has(cipherId)) {
+ atRiskApps.add(app);
+ }
+ }
+ }
+
+ for (const app of memberApps) {
+ const appData = appDataMap.get(app);
+ if (appData) {
+ appData.totalMembers += 1;
+ }
+ }
+
+ for (const app of atRiskApps) {
+ const appData = appDataMap.get(app);
+ if (appData) {
+ if (!totalAtRiskMembers.has(member.userName)) {
+ totalAtRiskMembers.add(member.userName);
+ }
+ if (!totalAtRiskApps.has(app)) {
+ totalAtRiskApps.add(app);
+ }
+ appData.atRiskMembers += 1;
+ }
+ }
+ }
+
+ // Convert map to array
+ return {
+ totalAtRiskMembers: totalAtRiskMembers.size,
+ totalMembers: members.length,
+ totalAtRiskApps: totalAtRiskApps.size,
+ totalApps: appDataMap.size,
+ details: Array.from(appDataMap.values()),
+ };
}
- async findExposedPassword(cipher: CipherView) {
- const { type, login, isDeleted, viewPassword, id } = cipher;
+ async isExposedPassword(cipher: CipherView) {
+ const { type, login, isDeleted, viewPassword } = cipher;
if (
type !== CipherType.Login ||
login.password == null ||
@@ -68,13 +168,10 @@ export class PasswordHealthService {
}
const exposedCount = await this.auditService.passwordLeaked(login.password);
- if (exposedCount > 0) {
- this.exposedPasswordMap.set(id, exposedCount);
- this.checkForExistingCipher(cipher);
- }
+ return exposedCount > 0;
}
- findReusedPassword(cipher: CipherView) {
+ isReusedPassword(cipher: CipherView) {
const { type, login, isDeleted, viewPassword } = cipher;
if (
type !== CipherType.Login ||
@@ -86,16 +183,15 @@ export class PasswordHealthService {
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);
+ if (this.usedPasswords.includes(login.password)) {
+ return true;
}
- this.checkForExistingCipher(cipher);
+ this.usedPasswords.push(login.password);
+ return false;
}
- findWeakPassword(cipher: CipherView): void {
+ isWeakPassword(cipher: CipherView) {
const { type, login, isDeleted, viewPassword } = cipher;
if (
type !== CipherType.Login ||
@@ -107,7 +203,7 @@ export class PasswordHealthService {
return;
}
- const hasUserName = this.isUserNameNotEmpty(cipher);
+ const hasUserName = !Utils.isNullOrWhitespace(cipher.login.username);
let userInput: string[] = [];
if (hasUserName) {
const atPosition = login.username.indexOf("@");
@@ -135,50 +231,6 @@ export class PasswordHealthService {
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"];
- }
- }
-
- checkForExistingCipher(ciph: CipherView) {
- if (!this.reportCipherIds.includes(ciph.id)) {
- this.reportCipherIds.push(ciph.id);
- this.reportCiphers.push(ciph);
- }
- }
-
- groupCiphersByLoginUri(): CipherView[] {
- const cipherViews: CipherView[] = [];
- const cipherUris: string[] = [];
- const ciphers = this.reportCiphers;
-
- ciphers.forEach((ciph) => {
- const uris = ciph.login?.uris ?? [];
- uris.map((u: { uri: string }) => {
- const uri = Utils.getHostname(u.uri).replace("www.", "");
- cipherUris.push(uri);
- cipherViews.push({ ...ciph, hostURI: uri } as CipherView & { hostURI: string });
- });
- });
-
- return cipherViews;
+ return score != null && score <= 2;
}
}
|