mirror of
https://github.com/bitwarden/browser.git
synced 2024-12-18 15:47:57 +01:00
[PM-13455] Risk insights aggregation in a new service. (#12071)
* Risk insights aggregation in a new service. Initial PR. * Ignoring all non-login items and refactoring into a method * Cleaning up the documentation a little * logic for generating the report summary * application summary to list at risk applications not passwords * Adding more documentation and moving types to it's own file * Awaiting the raw data report and adding the start of the test file * Adding more test cases * Removing unnecessary file * Test cases update * Fixing memeber details test to have new member * Fixing password health tests * Moving to observables * removing commented code * commented code * Switching from ternary to if/else * nullable types * one more nullable type * Adding the fixme for strict types * moving the fixme --------- Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>
This commit is contained in:
parent
1b6b5d3110
commit
30c151f44a
@ -0,0 +1,92 @@
|
|||||||
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
|
// @ts-strict-ignore
|
||||||
|
|
||||||
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
|
import { BadgeVariant } from "@bitwarden/components";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All applications report summary. The total members,
|
||||||
|
* total at risk members, application, and at risk application
|
||||||
|
* counts. Aggregated from all calculated applications
|
||||||
|
*/
|
||||||
|
export type ApplicationHealthReportSummary = {
|
||||||
|
totalMemberCount: number;
|
||||||
|
totalAtRiskMemberCount: number;
|
||||||
|
totalApplicationCount: number;
|
||||||
|
totalAtRiskApplicationCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All applications report detail. Application is the cipher
|
||||||
|
* uri. Has the at risk, password, and member information
|
||||||
|
*/
|
||||||
|
export type ApplicationHealthReportDetail = {
|
||||||
|
applicationName: string;
|
||||||
|
passwordCount: number;
|
||||||
|
atRiskPasswordCount: number;
|
||||||
|
memberCount: number;
|
||||||
|
|
||||||
|
memberDetails: MemberDetailsFlat[];
|
||||||
|
atRiskMemberDetails: MemberDetailsFlat[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Breaks the cipher health info out by uri and passes
|
||||||
|
* along the password health and member info
|
||||||
|
*/
|
||||||
|
export type CipherHealthReportUriDetail = {
|
||||||
|
cipherId: string;
|
||||||
|
reusedPasswordCount: number;
|
||||||
|
weakPasswordDetail: WeakPasswordDetail;
|
||||||
|
exposedPasswordDetail: ExposedPasswordDetail;
|
||||||
|
cipherMembers: MemberDetailsFlat[];
|
||||||
|
trimmedUri: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Associates a cipher with it's essential information.
|
||||||
|
* Gets the password health details, cipher members, and
|
||||||
|
* the trimmed uris for the cipher
|
||||||
|
*/
|
||||||
|
export type CipherHealthReportDetail = CipherView & {
|
||||||
|
reusedPasswordCount: number;
|
||||||
|
weakPasswordDetail: WeakPasswordDetail;
|
||||||
|
exposedPasswordDetail: ExposedPasswordDetail;
|
||||||
|
cipherMembers: MemberDetailsFlat[];
|
||||||
|
trimmedUris: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Weak password details containing the score
|
||||||
|
* and the score type for the label and badge
|
||||||
|
*/
|
||||||
|
export type WeakPasswordDetail = {
|
||||||
|
score: number;
|
||||||
|
detailValue: WeakPasswordScore;
|
||||||
|
} | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Weak password details containing the badge and
|
||||||
|
* the label for the password score
|
||||||
|
*/
|
||||||
|
export type WeakPasswordScore = {
|
||||||
|
label: string;
|
||||||
|
badgeVariant: BadgeVariant;
|
||||||
|
} | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How many times a password has been exposed
|
||||||
|
*/
|
||||||
|
export type ExposedPasswordDetail = {
|
||||||
|
exposedXTimes: number;
|
||||||
|
} | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flattened member details that associates an
|
||||||
|
* organization member to a cipher
|
||||||
|
*/
|
||||||
|
export type MemberDetailsFlat = {
|
||||||
|
userName: string;
|
||||||
|
email: string;
|
||||||
|
cipherId: string;
|
||||||
|
};
|
@ -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[] = [
|
export const mockCiphers: any[] = [
|
||||||
{
|
{
|
||||||
initializerKey: 1,
|
initializerKey: 1,
|
||||||
id: "cbea34a8-bde4-46ad-9d19-b05001228ab1",
|
id: "cbea34a8-bde4-46ad-9d19-b05001228ab1",
|
||||||
organizationId: null,
|
organizationId: null,
|
||||||
folderId: null,
|
folderId: null,
|
||||||
name: "Cannot Be Edited",
|
name: "Weak Password Cipher",
|
||||||
notes: null,
|
notes: null,
|
||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
type: 1,
|
type: 1,
|
||||||
@ -14,10 +22,11 @@ export const mockCiphers: any[] = [
|
|||||||
password: "123",
|
password: "123",
|
||||||
hasUris: true,
|
hasUris: true,
|
||||||
uris: [
|
uris: [
|
||||||
{ uri: "www.google.com" },
|
createLoginUriView("101domain.com"),
|
||||||
{ uri: "accounts.google.com" },
|
createLoginUriView("www.google.com"),
|
||||||
{ uri: "https://www.google.com" },
|
createLoginUriView("accounts.google.com"),
|
||||||
{ uri: "https://www.google.com/login" },
|
createLoginUriView("https://www.google.com"),
|
||||||
|
createLoginUriView("https://www.google.com/login"),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
edit: false,
|
edit: false,
|
||||||
@ -31,23 +40,18 @@ export const mockCiphers: any[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
initializerKey: 1,
|
initializerKey: 1,
|
||||||
id: "cbea34a8-bde4-46ad-9d19-b05001228ab2",
|
id: "cbea34a8-bde4-46ad-9d19-b05001228cd3",
|
||||||
organizationId: null,
|
organizationId: null,
|
||||||
folderId: null,
|
folderId: null,
|
||||||
name: "Can Be Edited id ending 2",
|
name: "Strong Password Cipher",
|
||||||
notes: null,
|
notes: null,
|
||||||
isDeleted: false,
|
|
||||||
type: 1,
|
type: 1,
|
||||||
favorite: false,
|
favorite: false,
|
||||||
organizationUseTotp: false,
|
organizationUseTotp: false,
|
||||||
login: {
|
login: {
|
||||||
password: "123",
|
password: "Password!123",
|
||||||
hasUris: true,
|
hasUris: true,
|
||||||
uris: [
|
uris: [createLoginUriView("http://example.com")],
|
||||||
{
|
|
||||||
uri: "http://nothing.com",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
edit: true,
|
edit: true,
|
||||||
viewPassword: true,
|
viewPassword: true,
|
||||||
@ -60,22 +64,18 @@ export const mockCiphers: any[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
initializerKey: 1,
|
initializerKey: 1,
|
||||||
id: "cbea34a8-bde4-46ad-9d19-b05001228cd3",
|
id: "cbea34a8-bde4-46ad-9d19-b05001228ab2",
|
||||||
organizationId: null,
|
organizationId: null,
|
||||||
folderId: null,
|
folderId: null,
|
||||||
name: "Can Be Edited id ending 3",
|
name: "Strong password Cipher",
|
||||||
notes: null,
|
notes: null,
|
||||||
type: 1,
|
type: 1,
|
||||||
favorite: false,
|
favorite: false,
|
||||||
organizationUseTotp: false,
|
organizationUseTotp: false,
|
||||||
login: {
|
login: {
|
||||||
password: "123",
|
|
||||||
hasUris: true,
|
hasUris: true,
|
||||||
uris: [
|
password: "Password!1234",
|
||||||
{
|
uris: [createLoginUriView("101domain.com")],
|
||||||
uri: "http://example.com",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
edit: true,
|
edit: true,
|
||||||
viewPassword: true,
|
viewPassword: true,
|
||||||
@ -91,14 +91,15 @@ export const mockCiphers: any[] = [
|
|||||||
id: "cbea34a8-bde4-46ad-9d19-b05001228xy4",
|
id: "cbea34a8-bde4-46ad-9d19-b05001228xy4",
|
||||||
organizationId: null,
|
organizationId: null,
|
||||||
folderId: null,
|
folderId: null,
|
||||||
name: "Can Be Edited id ending 4",
|
name: "Strong password Cipher",
|
||||||
notes: null,
|
notes: null,
|
||||||
type: 1,
|
type: 1,
|
||||||
favorite: false,
|
favorite: false,
|
||||||
organizationUseTotp: false,
|
organizationUseTotp: false,
|
||||||
login: {
|
login: {
|
||||||
hasUris: true,
|
hasUris: true,
|
||||||
uris: [{ uri: "101domain.com" }],
|
password: "Password!123",
|
||||||
|
uris: [createLoginUriView("example.com")],
|
||||||
},
|
},
|
||||||
edit: true,
|
edit: true,
|
||||||
viewPassword: true,
|
viewPassword: true,
|
||||||
@ -114,14 +115,39 @@ export const mockCiphers: any[] = [
|
|||||||
id: "cbea34a8-bde4-46ad-9d19-b05001227nm5",
|
id: "cbea34a8-bde4-46ad-9d19-b05001227nm5",
|
||||||
organizationId: null,
|
organizationId: null,
|
||||||
folderId: null,
|
folderId: null,
|
||||||
name: "Can Be Edited id ending 5",
|
name: "Exposed password Cipher",
|
||||||
notes: null,
|
notes: null,
|
||||||
type: 1,
|
type: 1,
|
||||||
favorite: false,
|
favorite: false,
|
||||||
organizationUseTotp: false,
|
organizationUseTotp: false,
|
||||||
login: {
|
login: {
|
||||||
hasUris: true,
|
hasUris: true,
|
||||||
uris: [{ uri: "123formbuilder.com" }],
|
password: "123",
|
||||||
|
uris: [createLoginUriView("123formbuilder.com"), createLoginUriView("www.google.com")],
|
||||||
|
},
|
||||||
|
edit: true,
|
||||||
|
viewPassword: true,
|
||||||
|
collectionIds: [],
|
||||||
|
revisionDate: "2023-08-03T17:40:59.793Z",
|
||||||
|
creationDate: "2023-08-03T17:40:59.793Z",
|
||||||
|
deletedDate: null,
|
||||||
|
reprompt: 0,
|
||||||
|
localData: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
initializerKey: 1,
|
||||||
|
id: "cbea34a8-bde4-46ad-9d19-b05001227tt1",
|
||||||
|
organizationId: null,
|
||||||
|
folderId: null,
|
||||||
|
name: "Secure Co Login",
|
||||||
|
notes: null,
|
||||||
|
type: 1,
|
||||||
|
favorite: false,
|
||||||
|
organizationUseTotp: false,
|
||||||
|
login: {
|
||||||
|
hasUris: true,
|
||||||
|
password: "4gRyhhOX2Og2p0",
|
||||||
|
uris: [createLoginUriView("SecureCo.com")],
|
||||||
},
|
},
|
||||||
edit: true,
|
edit: true,
|
||||||
viewPassword: true,
|
viewPassword: true,
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
export * from "./member-cipher-details-api.service";
|
export * from "./member-cipher-details-api.service";
|
||||||
export * from "./password-health.service";
|
export * from "./password-health.service";
|
||||||
|
export * from "./risk-insights-report.service";
|
||||||
|
@ -69,6 +69,12 @@ export const mockMemberCipherDetails: any = [
|
|||||||
"cbea34a8-bde4-46ad-9d19-b05001228xy4",
|
"cbea34a8-bde4-46ad-9d19-b05001228xy4",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
userName: "Mister Secure",
|
||||||
|
email: "mister.secure@secureco.com",
|
||||||
|
usesKeyConnector: true,
|
||||||
|
cipherIds: ["cbea34a8-bde4-46ad-9d19-b05001227tt1"],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
describe("Member Cipher Details API Service", () => {
|
describe("Member Cipher Details API Service", () => {
|
||||||
@ -91,7 +97,7 @@ describe("Member Cipher Details API Service", () => {
|
|||||||
const orgId = "1234";
|
const orgId = "1234";
|
||||||
const result = await memberCipherDetailsApiService.getMemberCipherDetails(orgId);
|
const result = await memberCipherDetailsApiService.getMemberCipherDetails(orgId);
|
||||||
expect(result).not.toBeNull();
|
expect(result).not.toBeNull();
|
||||||
expect(result).toHaveLength(6);
|
expect(result).toHaveLength(7);
|
||||||
expect(apiService.send).toHaveBeenCalledWith(
|
expect(apiService.send).toHaveBeenCalledWith(
|
||||||
"GET",
|
"GET",
|
||||||
"/reports/member-cipher-details/" + orgId,
|
"/reports/member-cipher-details/" + orgId,
|
||||||
|
@ -3,18 +3,15 @@ import { TestBed } from "@angular/core/testing";
|
|||||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
|
||||||
|
|
||||||
import { mockCiphers } from "./ciphers.mock";
|
import { mockCiphers } from "./ciphers.mock";
|
||||||
import { MemberCipherDetailsApiService } from "./member-cipher-details-api.service";
|
import { MemberCipherDetailsApiService } from "./member-cipher-details-api.service";
|
||||||
import { mockMemberCipherDetails } from "./member-cipher-details-api.service.spec";
|
import { mockMemberCipherDetails } from "./member-cipher-details-api.service.spec";
|
||||||
import { PasswordHealthService } from "./password-health.service";
|
import { PasswordHealthService } from "./password-health.service";
|
||||||
|
|
||||||
|
// FIXME: Remove password-health report service after PR-15498 completion
|
||||||
describe("PasswordHealthService", () => {
|
describe("PasswordHealthService", () => {
|
||||||
let service: PasswordHealthService;
|
let service: PasswordHealthService;
|
||||||
let cipherService: CipherService;
|
|
||||||
let memberCipherDetailsApiService: MemberCipherDetailsApiService;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
providers: [
|
providers: [
|
||||||
@ -51,8 +48,6 @@ describe("PasswordHealthService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
service = TestBed.inject(PasswordHealthService);
|
service = TestBed.inject(PasswordHealthService);
|
||||||
cipherService = TestBed.inject(CipherService);
|
|
||||||
memberCipherDetailsApiService = TestBed.inject(MemberCipherDetailsApiService);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be created", () => {
|
it("should be created", () => {
|
||||||
@ -67,83 +62,4 @@ describe("PasswordHealthService", () => {
|
|||||||
expect(service.exposedPasswordMap.size).toBe(0);
|
expect(service.exposedPasswordMap.size).toBe(0);
|
||||||
expect(service.totalMembersMap.size).toBe(0);
|
expect(service.totalMembersMap.size).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("generateReport", () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
await service.generateReport();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should fetch all ciphers for the organization", () => {
|
|
||||||
expect(cipherService.getAllFromApiForOrganization).toHaveBeenCalledWith("org1");
|
|
||||||
});
|
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
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("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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@ -0,0 +1,148 @@
|
|||||||
|
import { TestBed } from "@angular/core/testing";
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
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 { mockCiphers } from "./ciphers.mock";
|
||||||
|
import { MemberCipherDetailsApiService } from "./member-cipher-details-api.service";
|
||||||
|
import { mockMemberCipherDetails } from "./member-cipher-details-api.service.spec";
|
||||||
|
import { RiskInsightsReportService } from "./risk-insights-report.service";
|
||||||
|
|
||||||
|
describe("RiskInsightsReportService", () => {
|
||||||
|
let service: RiskInsightsReportService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
RiskInsightsReportService,
|
||||||
|
{
|
||||||
|
provide: PasswordStrengthServiceAbstraction,
|
||||||
|
useValue: {
|
||||||
|
getPasswordStrength: (password: string) => {
|
||||||
|
const score = password.length < 4 ? 1 : 4;
|
||||||
|
return { score };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: AuditService,
|
||||||
|
useValue: {
|
||||||
|
passwordLeaked: (password: string) => Promise.resolve(password === "123" ? 100 : 0),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: CipherService,
|
||||||
|
useValue: {
|
||||||
|
getAllFromApiForOrganization: jest.fn().mockResolvedValue(mockCiphers),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: MemberCipherDetailsApiService,
|
||||||
|
useValue: {
|
||||||
|
getMemberCipherDetails: jest.fn().mockResolvedValue(mockMemberCipherDetails),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
service = TestBed.inject(RiskInsightsReportService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should generate the raw data report correctly", async () => {
|
||||||
|
const result = await firstValueFrom(service.generateRawDataReport$("orgId"));
|
||||||
|
|
||||||
|
expect(result).toHaveLength(6);
|
||||||
|
|
||||||
|
let testCaseResults = result.filter((x) => x.id === "cbea34a8-bde4-46ad-9d19-b05001228ab1");
|
||||||
|
expect(testCaseResults).toHaveLength(1);
|
||||||
|
let testCase = testCaseResults[0];
|
||||||
|
expect(testCase).toBeTruthy();
|
||||||
|
expect(testCase.cipherMembers).toHaveLength(2);
|
||||||
|
expect(testCase.trimmedUris).toHaveLength(3);
|
||||||
|
expect(testCase.weakPasswordDetail).toBeTruthy();
|
||||||
|
expect(testCase.exposedPasswordDetail).toBeTruthy();
|
||||||
|
expect(testCase.reusedPasswordCount).toEqual(2);
|
||||||
|
|
||||||
|
testCaseResults = result.filter((x) => x.id === "cbea34a8-bde4-46ad-9d19-b05001227tt1");
|
||||||
|
expect(testCaseResults).toHaveLength(1);
|
||||||
|
testCase = testCaseResults[0];
|
||||||
|
expect(testCase).toBeTruthy();
|
||||||
|
expect(testCase.cipherMembers).toHaveLength(1);
|
||||||
|
expect(testCase.trimmedUris).toHaveLength(1);
|
||||||
|
expect(testCase.weakPasswordDetail).toBeFalsy();
|
||||||
|
expect(testCase.exposedPasswordDetail).toBeFalsy();
|
||||||
|
expect(testCase.reusedPasswordCount).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should generate the raw data + uri report correctly", async () => {
|
||||||
|
const result = await firstValueFrom(service.generateRawDataUriReport$("orgId"));
|
||||||
|
|
||||||
|
expect(result).toHaveLength(9);
|
||||||
|
|
||||||
|
// Two ciphers that have google.com as their uri. There should be 2 results
|
||||||
|
const googleResults = result.filter((x) => x.trimmedUri === "google.com");
|
||||||
|
expect(googleResults).toHaveLength(2);
|
||||||
|
|
||||||
|
// Verify the details for one of the googles matches the password health info
|
||||||
|
// expected
|
||||||
|
const firstGoogle = googleResults.filter(
|
||||||
|
(x) => x.cipherId === "cbea34a8-bde4-46ad-9d19-b05001228ab1" && x.trimmedUri === "google.com",
|
||||||
|
)[0];
|
||||||
|
expect(firstGoogle.weakPasswordDetail).toBeTruthy();
|
||||||
|
expect(firstGoogle.exposedPasswordDetail).toBeTruthy();
|
||||||
|
expect(firstGoogle.reusedPasswordCount).toEqual(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should generate applications health report data correctly", async () => {
|
||||||
|
const result = await firstValueFrom(service.generateApplicationsReport$("orgId"));
|
||||||
|
|
||||||
|
expect(result).toHaveLength(6);
|
||||||
|
|
||||||
|
// Two ciphers have google.com associated with them. The first cipher
|
||||||
|
// has 2 members and the second has 4. However, the 2 members in the first
|
||||||
|
// cipher are also associated with the second. The total amount of members
|
||||||
|
// should be 4 not 6
|
||||||
|
const googleTestResults = result.filter((x) => x.applicationName === "google.com");
|
||||||
|
expect(googleTestResults).toHaveLength(1);
|
||||||
|
const googleTest = googleTestResults[0];
|
||||||
|
expect(googleTest.memberCount).toEqual(4);
|
||||||
|
|
||||||
|
// Both ciphers have at risk passwords
|
||||||
|
expect(googleTest.passwordCount).toEqual(2);
|
||||||
|
|
||||||
|
// All members are at risk since both ciphers are at risk
|
||||||
|
expect(googleTest.atRiskMemberDetails).toHaveLength(4);
|
||||||
|
expect(googleTest.atRiskPasswordCount).toEqual(2);
|
||||||
|
|
||||||
|
// There are 2 ciphers associated with 101domain.com
|
||||||
|
const domain101TestResults = result.filter((x) => x.applicationName === "101domain.com");
|
||||||
|
expect(domain101TestResults).toHaveLength(1);
|
||||||
|
const domain101Test = domain101TestResults[0];
|
||||||
|
expect(domain101Test.passwordCount).toEqual(2);
|
||||||
|
|
||||||
|
// The first cipher is at risk. The second cipher is not at risk
|
||||||
|
expect(domain101Test.atRiskPasswordCount).toEqual(1);
|
||||||
|
|
||||||
|
// The first cipher has 2 members. The second cipher the second
|
||||||
|
// cipher has 4. One of the members in the first cipher is associated
|
||||||
|
// with the second. So there should be 5 members total.
|
||||||
|
expect(domain101Test.memberCount).toEqual(5);
|
||||||
|
|
||||||
|
// The first cipher is at risk. The total at risk members is 2 and
|
||||||
|
// at risk password count is 1.
|
||||||
|
expect(domain101Test.atRiskMemberDetails).toHaveLength(2);
|
||||||
|
expect(domain101Test.atRiskPasswordCount).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should generate applications summary data correctly", async () => {
|
||||||
|
const reportResult = await firstValueFrom(service.generateApplicationsReport$("orgId"));
|
||||||
|
const reportSummary = service.generateApplicationsSummary(reportResult);
|
||||||
|
|
||||||
|
expect(reportSummary.totalMemberCount).toEqual(7);
|
||||||
|
expect(reportSummary.totalAtRiskMemberCount).toEqual(6);
|
||||||
|
expect(reportSummary.totalApplicationCount).toEqual(6);
|
||||||
|
expect(reportSummary.totalAtRiskApplicationCount).toEqual(5);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,395 @@
|
|||||||
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
|
// @ts-strict-ignore
|
||||||
|
|
||||||
|
import { Injectable } from "@angular/core";
|
||||||
|
import { concatMap, first, from, map, Observable, zip } from "rxjs";
|
||||||
|
|
||||||
|
import { AuditService } from "@bitwarden/common/abstractions/audit.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 {
|
||||||
|
ApplicationHealthReportDetail,
|
||||||
|
ApplicationHealthReportSummary,
|
||||||
|
CipherHealthReportDetail,
|
||||||
|
CipherHealthReportUriDetail,
|
||||||
|
ExposedPasswordDetail,
|
||||||
|
MemberDetailsFlat,
|
||||||
|
WeakPasswordDetail,
|
||||||
|
WeakPasswordScore,
|
||||||
|
} from "../models/password-health";
|
||||||
|
|
||||||
|
import { MemberCipherDetailsApiService } from "./member-cipher-details-api.service";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RiskInsightsReportService {
|
||||||
|
constructor(
|
||||||
|
private passwordStrengthService: PasswordStrengthServiceAbstraction,
|
||||||
|
private auditService: AuditService,
|
||||||
|
private cipherService: CipherService,
|
||||||
|
private memberCipherDetailsApiService: MemberCipherDetailsApiService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Report data from raw cipher health data.
|
||||||
|
* Can be used in the Raw Data diagnostic tab (just exclude the members in the view)
|
||||||
|
* and can be used in the raw data + members tab when including the members in the view
|
||||||
|
* @param organizationId
|
||||||
|
* @returns Cipher health report data with members and trimmed uris
|
||||||
|
*/
|
||||||
|
generateRawDataReport$(organizationId: string): Observable<CipherHealthReportDetail[]> {
|
||||||
|
const allCiphers$ = from(this.cipherService.getAllFromApiForOrganization(organizationId));
|
||||||
|
const memberCiphers$ = from(
|
||||||
|
this.memberCipherDetailsApiService.getMemberCipherDetails(organizationId),
|
||||||
|
);
|
||||||
|
|
||||||
|
const results$ = zip(allCiphers$, memberCiphers$).pipe(
|
||||||
|
map(([allCiphers, memberCiphers]) => {
|
||||||
|
const details: MemberDetailsFlat[] = memberCiphers.flatMap((dtl) =>
|
||||||
|
dtl.cipherIds.map((c) => this.getMemberDetailsFlat(dtl.userName, dtl.email, c)),
|
||||||
|
);
|
||||||
|
return [allCiphers, details] as const;
|
||||||
|
}),
|
||||||
|
concatMap(([ciphers, flattenedDetails]) => this.getCipherDetails(ciphers, flattenedDetails)),
|
||||||
|
first(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return results$;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Report data for raw cipher health broken out into the uris
|
||||||
|
* Can be used in the raw data + members + uri diagnostic report
|
||||||
|
* @param organizationId Id of the organization
|
||||||
|
* @returns Cipher health report data flattened to the uris
|
||||||
|
*/
|
||||||
|
generateRawDataUriReport$(organizationId: string): Observable<CipherHealthReportUriDetail[]> {
|
||||||
|
const cipherHealthDetails$ = this.generateRawDataReport$(organizationId);
|
||||||
|
const results$ = cipherHealthDetails$.pipe(
|
||||||
|
map((healthDetails) => this.getCipherUriDetails(healthDetails)),
|
||||||
|
first(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return results$;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Report data for the aggregation of uris to like uris and getting password/member counts,
|
||||||
|
* members, and at risk statuses.
|
||||||
|
* @param organizationId Id of the organization
|
||||||
|
* @returns The all applications health report data
|
||||||
|
*/
|
||||||
|
generateApplicationsReport$(organizationId: string): Observable<ApplicationHealthReportDetail[]> {
|
||||||
|
const cipherHealthUriReport$ = this.generateRawDataUriReport$(organizationId);
|
||||||
|
const results$ = cipherHealthUriReport$.pipe(
|
||||||
|
map((uriDetails) => this.getApplicationHealthReport(uriDetails)),
|
||||||
|
first(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return results$;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the summary from the application health report. Returns total members and applications as well
|
||||||
|
* as the total at risk members and at risk applications
|
||||||
|
* @param reports The previously calculated application health report data
|
||||||
|
* @returns A summary object containing report totals
|
||||||
|
*/
|
||||||
|
generateApplicationsSummary(
|
||||||
|
reports: ApplicationHealthReportDetail[],
|
||||||
|
): ApplicationHealthReportSummary {
|
||||||
|
const totalMembers = reports.flatMap((x) => x.memberDetails);
|
||||||
|
const uniqueMembers = this.getUniqueMembers(totalMembers);
|
||||||
|
|
||||||
|
const atRiskMembers = reports.flatMap((x) => x.atRiskMemberDetails);
|
||||||
|
const uniqueAtRiskMembers = this.getUniqueMembers(atRiskMembers);
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalMemberCount: uniqueMembers.length,
|
||||||
|
totalAtRiskMemberCount: uniqueAtRiskMembers.length,
|
||||||
|
totalApplicationCount: reports.length,
|
||||||
|
totalAtRiskApplicationCount: reports.filter((app) => app.atRiskPasswordCount > 0).length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Associates the members with the ciphers they have access to. Calculates the password health.
|
||||||
|
* Finds the trimmed uris.
|
||||||
|
* @param ciphers Org ciphers
|
||||||
|
* @param memberDetails Org members
|
||||||
|
* @returns Cipher password health data with trimmed uris and associated members
|
||||||
|
*/
|
||||||
|
private async getCipherDetails(
|
||||||
|
ciphers: CipherView[],
|
||||||
|
memberDetails: MemberDetailsFlat[],
|
||||||
|
): Promise<CipherHealthReportDetail[]> {
|
||||||
|
const cipherHealthReports: CipherHealthReportDetail[] = [];
|
||||||
|
const passwordUseMap = new Map<string, number>();
|
||||||
|
for (const cipher of ciphers) {
|
||||||
|
if (this.validateCipher(cipher)) {
|
||||||
|
const weakPassword = this.findWeakPassword(cipher);
|
||||||
|
// Looping over all ciphers needs to happen first to determine reused passwords over all ciphers.
|
||||||
|
// Store in the set and evaluate later
|
||||||
|
if (passwordUseMap.has(cipher.login.password)) {
|
||||||
|
passwordUseMap.set(
|
||||||
|
cipher.login.password,
|
||||||
|
(passwordUseMap.get(cipher.login.password) || 0) + 1,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
passwordUseMap.set(cipher.login.password, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const exposedPassword = await this.findExposedPassword(cipher);
|
||||||
|
|
||||||
|
// Get the cipher members
|
||||||
|
const cipherMembers = memberDetails.filter((x) => x.cipherId === cipher.id);
|
||||||
|
|
||||||
|
// Trim uris to host name and create the cipher health report
|
||||||
|
const cipherTrimmedUris = this.getTrimmedCipherUris(cipher);
|
||||||
|
const cipherHealth = {
|
||||||
|
...cipher,
|
||||||
|
weakPasswordDetail: weakPassword,
|
||||||
|
exposedPasswordDetail: exposedPassword,
|
||||||
|
cipherMembers: cipherMembers,
|
||||||
|
trimmedUris: cipherTrimmedUris,
|
||||||
|
} as CipherHealthReportDetail;
|
||||||
|
|
||||||
|
cipherHealthReports.push(cipherHealth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// loop for reused passwords
|
||||||
|
cipherHealthReports.forEach((detail) => {
|
||||||
|
detail.reusedPasswordCount = passwordUseMap.get(detail.login.password) ?? 0;
|
||||||
|
});
|
||||||
|
return cipherHealthReports;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flattens the cipher to trimmed uris. Used for the raw data + uri
|
||||||
|
* @param cipherHealthReport Cipher health report with uris and members
|
||||||
|
* @returns Flattened cipher health details to uri
|
||||||
|
*/
|
||||||
|
private getCipherUriDetails(
|
||||||
|
cipherHealthReport: CipherHealthReportDetail[],
|
||||||
|
): CipherHealthReportUriDetail[] {
|
||||||
|
return cipherHealthReport.flatMap((rpt) =>
|
||||||
|
rpt.trimmedUris.map((u) => this.getFlattenedCipherDetails(rpt, u)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loop through the flattened cipher to uri data. If the item exists it's values need to be updated with the new item.
|
||||||
|
* If the item is new, create and add the object with the flattened details
|
||||||
|
* @param cipherHealthUriReport Cipher and password health info broken out into their uris
|
||||||
|
* @returns Application health reports
|
||||||
|
*/
|
||||||
|
private getApplicationHealthReport(
|
||||||
|
cipherHealthUriReport: CipherHealthReportUriDetail[],
|
||||||
|
): ApplicationHealthReportDetail[] {
|
||||||
|
const appReports: ApplicationHealthReportDetail[] = [];
|
||||||
|
cipherHealthUriReport.forEach((uri) => {
|
||||||
|
const index = appReports.findIndex((item) => item.applicationName === uri.trimmedUri);
|
||||||
|
|
||||||
|
let atRisk: boolean = false;
|
||||||
|
if (uri.exposedPasswordDetail || uri.weakPasswordDetail || uri.reusedPasswordCount > 1) {
|
||||||
|
atRisk = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index === -1) {
|
||||||
|
appReports.push(this.getApplicationReportDetail(uri, atRisk));
|
||||||
|
} else {
|
||||||
|
appReports[index] = this.getApplicationReportDetail(uri, atRisk, appReports[index]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return appReports;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async findExposedPassword(cipher: CipherView): Promise<ExposedPasswordDetail> {
|
||||||
|
const exposedCount = await this.auditService.passwordLeaked(cipher.login.password);
|
||||||
|
if (exposedCount > 0) {
|
||||||
|
const exposedDetail = { exposedXTimes: exposedCount } as ExposedPasswordDetail;
|
||||||
|
return exposedDetail;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private findWeakPassword(cipher: CipherView): WeakPasswordDetail {
|
||||||
|
const hasUserName = this.isUserNameNotEmpty(cipher);
|
||||||
|
let userInput: string[] = [];
|
||||||
|
if (hasUserName) {
|
||||||
|
const atPosition = cipher.login.username.indexOf("@");
|
||||||
|
if (atPosition > -1) {
|
||||||
|
userInput = userInput
|
||||||
|
.concat(
|
||||||
|
cipher.login.username
|
||||||
|
.substring(0, atPosition)
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.split(/[^A-Za-z0-9]/),
|
||||||
|
)
|
||||||
|
.filter((i) => i.length >= 3);
|
||||||
|
} else {
|
||||||
|
userInput = cipher.login.username
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.split(/[^A-Za-z0-9]/)
|
||||||
|
.filter((i) => i.length >= 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const { score } = this.passwordStrengthService.getPasswordStrength(
|
||||||
|
cipher.login.password,
|
||||||
|
null,
|
||||||
|
userInput.length > 0 ? userInput : null,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (score != null && score <= 2) {
|
||||||
|
const scoreValue = this.weakPasswordScore(score);
|
||||||
|
const weakPasswordDetail = { score: score, detailValue: scoreValue } as WeakPasswordDetail;
|
||||||
|
return weakPasswordDetail;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private weakPasswordScore(score: number): WeakPasswordScore {
|
||||||
|
switch (score) {
|
||||||
|
case 4:
|
||||||
|
return { label: "strong", badgeVariant: "success" };
|
||||||
|
case 3:
|
||||||
|
return { label: "good", badgeVariant: "primary" };
|
||||||
|
case 2:
|
||||||
|
return { label: "weak", badgeVariant: "warning" };
|
||||||
|
default:
|
||||||
|
return { label: "veryWeak", badgeVariant: "danger" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the new application health report detail object with the details from the cipher health report uri detail object
|
||||||
|
* update or create the at risk values if the item is at risk.
|
||||||
|
* @param newUriDetail New cipher uri detail
|
||||||
|
* @param isAtRisk If the cipher has a weak, exposed, or reused password it is at risk
|
||||||
|
* @param existingUriDetail The previously processed Uri item
|
||||||
|
* @returns The new or updated application health report detail
|
||||||
|
*/
|
||||||
|
private getApplicationReportDetail(
|
||||||
|
newUriDetail: CipherHealthReportUriDetail,
|
||||||
|
isAtRisk: boolean,
|
||||||
|
existingUriDetail?: ApplicationHealthReportDetail,
|
||||||
|
): ApplicationHealthReportDetail {
|
||||||
|
const reportDetail = {
|
||||||
|
applicationName: existingUriDetail
|
||||||
|
? existingUriDetail.applicationName
|
||||||
|
: newUriDetail.trimmedUri,
|
||||||
|
passwordCount: existingUriDetail ? existingUriDetail.passwordCount + 1 : 1,
|
||||||
|
memberDetails: existingUriDetail
|
||||||
|
? this.getUniqueMembers(existingUriDetail.memberDetails.concat(newUriDetail.cipherMembers))
|
||||||
|
: newUriDetail.cipherMembers,
|
||||||
|
atRiskMemberDetails: existingUriDetail ? existingUriDetail.atRiskMemberDetails : [],
|
||||||
|
atRiskPasswordCount: existingUriDetail ? existingUriDetail.atRiskPasswordCount : 0,
|
||||||
|
} as ApplicationHealthReportDetail;
|
||||||
|
|
||||||
|
if (isAtRisk) {
|
||||||
|
(reportDetail.atRiskPasswordCount = reportDetail.atRiskPasswordCount + 1),
|
||||||
|
(reportDetail.atRiskMemberDetails = this.getUniqueMembers(
|
||||||
|
reportDetail.atRiskMemberDetails.concat(newUriDetail.cipherMembers),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
reportDetail.memberCount = reportDetail.memberDetails.length;
|
||||||
|
|
||||||
|
return reportDetail;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a distinct array of members from a combined list. Input list may contain
|
||||||
|
* duplicate members.
|
||||||
|
* @param orgMembers Input list of members
|
||||||
|
* @returns Distinct array of members
|
||||||
|
*/
|
||||||
|
private getUniqueMembers(orgMembers: MemberDetailsFlat[]): MemberDetailsFlat[] {
|
||||||
|
const existingEmails = new Set<string>();
|
||||||
|
const distinctUsers = orgMembers.filter((member) => {
|
||||||
|
if (existingEmails.has(member.email)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
existingEmails.add(member.email);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
return distinctUsers;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getFlattenedCipherDetails(
|
||||||
|
detail: CipherHealthReportDetail,
|
||||||
|
uri: string,
|
||||||
|
): CipherHealthReportUriDetail {
|
||||||
|
return {
|
||||||
|
cipherId: detail.id,
|
||||||
|
reusedPasswordCount: detail.reusedPasswordCount,
|
||||||
|
weakPasswordDetail: detail.weakPasswordDetail,
|
||||||
|
exposedPasswordDetail: detail.exposedPasswordDetail,
|
||||||
|
cipherMembers: detail.cipherMembers,
|
||||||
|
trimmedUri: uri,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private getMemberDetailsFlat(
|
||||||
|
userName: string,
|
||||||
|
email: string,
|
||||||
|
cipherId: string,
|
||||||
|
): MemberDetailsFlat {
|
||||||
|
return {
|
||||||
|
userName: userName,
|
||||||
|
email: email,
|
||||||
|
cipherId: cipherId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trim the cipher uris down to get the password health application.
|
||||||
|
* The uri should only exist once after being trimmed. No duplication.
|
||||||
|
* Example:
|
||||||
|
* - Untrimmed Uris: https://gmail.com, gmail.com/login
|
||||||
|
* - Both would trim to gmail.com
|
||||||
|
* - The cipher trimmed uri list should only return on instance in the list
|
||||||
|
* @param cipher
|
||||||
|
* @returns distinct list of trimmed cipher uris
|
||||||
|
*/
|
||||||
|
private getTrimmedCipherUris(cipher: CipherView): string[] {
|
||||||
|
const cipherUris: string[] = [];
|
||||||
|
const uris = cipher.login?.uris ?? [];
|
||||||
|
uris.map((u: { uri: string }) => {
|
||||||
|
const uri = Utils.getHostname(u.uri).replace("www.", "");
|
||||||
|
if (!cipherUris.includes(uri)) {
|
||||||
|
cipherUris.push(uri);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return cipherUris;
|
||||||
|
}
|
||||||
|
|
||||||
|
private isUserNameNotEmpty(c: CipherView): boolean {
|
||||||
|
return !Utils.isNullOrWhitespace(c.login.username);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates that the cipher is a login item, has a password
|
||||||
|
* is not deleted, and the user can view the password
|
||||||
|
* @param c the input cipher
|
||||||
|
*/
|
||||||
|
private validateCipher(c: CipherView): boolean {
|
||||||
|
const { type, login, isDeleted, viewPassword } = c;
|
||||||
|
if (
|
||||||
|
type !== CipherType.Login ||
|
||||||
|
login.password == null ||
|
||||||
|
login.password === "" ||
|
||||||
|
isDeleted ||
|
||||||
|
!viewPassword
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user