mirror of
https://github.com/bitwarden/browser
synced 2026-02-12 14:34:02 +00:00
final data aggregation for risk insights
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</div>
|
||||
<div class="tw-mt-4" *ngIf="!dataSource.data.length">
|
||||
<div class="tw-mt-4" *ngIf="!loading && !dataSource.data.length">
|
||||
<bit-no-items [icon]="noItemsIcon" class="tw-text-main">
|
||||
<ng-container slot="title">
|
||||
<h2 class="tw-font-semibold mt-4">
|
||||
@@ -34,15 +34,15 @@
|
||||
<tools-card
|
||||
class="tw-flex-1"
|
||||
[title]="'atRiskMembers' | i18n"
|
||||
[value]="mockAtRiskMembersCount"
|
||||
[maxValue]="mockTotalMembersCount"
|
||||
[value]="applicationHealthReport.totalAtRiskMembers"
|
||||
[maxValue]="applicationHealthReport.totalMembers"
|
||||
>
|
||||
</tools-card>
|
||||
<tools-card
|
||||
class="tw-flex-1"
|
||||
[title]="'atRiskApplications' | i18n"
|
||||
[value]="mockAtRiskAppsCount"
|
||||
[maxValue]="mockTotalAppsCount"
|
||||
[value]="applicationHealthReport.totalAtRiskApps"
|
||||
[maxValue]="applicationHealthReport.totalApps"
|
||||
>
|
||||
</tools-card>
|
||||
</div>
|
||||
@@ -88,7 +88,7 @@
|
||||
/>
|
||||
</td>
|
||||
<td bitCell>
|
||||
<span>{{ r.name }}</span>
|
||||
<span>{{ r.application }}</span>
|
||||
</td>
|
||||
<td bitCell>
|
||||
<span>
|
||||
|
||||
@@ -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<any>();
|
||||
protected selectedIds: Set<number> = new Set<number>();
|
||||
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));
|
||||
|
||||
@@ -29,21 +29,4 @@
|
||||
</ng-template>
|
||||
<tools-critical-applications></tools-critical-applications>
|
||||
</bit-tab>
|
||||
<bit-tab label="Raw Data">
|
||||
<tools-password-health></tools-password-health>
|
||||
</bit-tab>
|
||||
<bit-tab label="Raw Data + members">
|
||||
<tools-password-health-members></tools-password-health-members>
|
||||
</bit-tab>
|
||||
<bit-tab label="Raw Data + uri">
|
||||
<tools-password-health-members-uri></tools-password-health-members-uri>
|
||||
</bit-tab>
|
||||
<!-- <bit-tab>
|
||||
<ng-template bitTabLabel>
|
||||
<i class="bwi bwi-envelope"></i>
|
||||
{{ "notifiedMembersWithCount" | i18n: priorityApps.length }}
|
||||
</ng-template>
|
||||
<h2 bitTypography="h2">{{ "notifiedMembers" | i18n }}</h2>
|
||||
<tools-notified-members-table></tools-notified-members-table>
|
||||
</bit-tab> -->
|
||||
</bit-tab-group>
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"],
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<string, [string, BadgeVariant]>();
|
||||
usedPasswords: string[] = [];
|
||||
|
||||
passwordUseMap = new Map<string, number>();
|
||||
|
||||
exposedPasswordMap = new Map<string, number>();
|
||||
|
||||
totalMembersMap = new Map<string, number>();
|
||||
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<ApplicationHealthReport> {
|
||||
// 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<string, ApplicationHealthReportDetail>();
|
||||
|
||||
// Map cipher IDs to ciphers
|
||||
const cipherMap = new Map<string, CipherView>(ciphers.map((cipher) => [cipher.id, cipher]));
|
||||
|
||||
// Set to store at-risk member IDs
|
||||
const totalAtRiskMembers = new Set<string>();
|
||||
|
||||
// Set to store at-risk app IDs
|
||||
const totalAtRiskApps = new Set<string>();
|
||||
|
||||
// Set to store at-risk cipher IDs
|
||||
const atRiskCipherIds = new Set<string>();
|
||||
|
||||
// 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<string>();
|
||||
const atRiskApps = new Set<string>();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user