diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 9150028f4d6..83ff06cd463 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -75,7 +75,7 @@ } }, "noAppsInOrgTitle": { - "message": "No applications found in $ORG NAME$", + "message": "No application reports found in $ORG NAME$", "placeholders": { "org name": { "content": "$1", @@ -101,6 +101,9 @@ "applicationsMarkedAsCriticalSuccess": { "message": "Applications marked as critical" }, + "applicationsMarkedAsCriticalFail": { + "message": "Failed to mark applications as critical" + }, "application": { "message": "Application" }, @@ -10850,5 +10853,8 @@ "example": "12-3456789" } } + }, + "runRiskInsightsReport": { + "message": "Run Report" } } diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/helpers/index.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/helpers/index.ts new file mode 100644 index 00000000000..5508eba65e5 --- /dev/null +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/helpers/index.ts @@ -0,0 +1 @@ +export * from "./risk-insights-data-mappers"; diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/helpers/risk-insights-data-mappers.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/helpers/risk-insights-data-mappers.ts new file mode 100644 index 00000000000..6d56273b06a --- /dev/null +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/helpers/risk-insights-data-mappers.ts @@ -0,0 +1,51 @@ +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; + +import { MemberDetails } from "../models/report-models"; +import { MemberCipherDetailsResponse } from "../response/member-cipher-details.response"; + +export function flattenMemberDetails( + memberCiphers: MemberCipherDetailsResponse[], +): MemberDetails[] { + return memberCiphers.flatMap((member) => + member.cipherIds.map((cipherId) => ({ + userGuid: member.userGuid, + userName: member.userName, + email: member.email, + 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 + */ +export function getTrimmedCipherUris(cipher: CipherView): string[] { + const uris = cipher.login?.uris ?? []; + + const uniqueDomains = new Set(); + + uris.forEach((u: { uri: string }) => { + const domain = Utils.getDomain(u.uri) ?? u.uri; + uniqueDomains.add(domain); + }); + return Array.from(uniqueDomains); +} + +// Returns a deduplicated array of members by email +export function getUniqueMembers(orgMembers: MemberDetails[]): MemberDetails[] { + const existingEmails = new Set(); + return orgMembers.filter((member) => { + if (existingEmails.has(member.email)) { + return false; + } + existingEmails.add(member.email); + return true; + }); +} diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/index.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/index.ts index b2221a94a89..a800cb2cf4a 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/index.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/index.ts @@ -1 +1,2 @@ export * from "./services"; +export * from "./helpers"; diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/api-models.types.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/api-models.types.ts new file mode 100644 index 00000000000..f28a8ee504d --- /dev/null +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/api-models.types.ts @@ -0,0 +1,49 @@ +import { EncryptedString } from "@bitwarden/common/platform/models/domain/enc-string"; +import { OrganizationId } from "@bitwarden/common/types/guid"; + +import { PasswordHealthReportApplicationId, RiskInsightsReport } from "./report-models"; + +// -------------------- Password Health Report Models -------------------- +/** + * Request to drop a password health report application + * Model is expected by the API endpoint + */ +export interface PasswordHealthReportApplicationDropRequest { + organizationId: OrganizationId; + passwordHealthReportApplicationIds: string[]; +} + +/** + * Response from the API after marking an app as critical + */ +export interface PasswordHealthReportApplicationsResponse { + id: PasswordHealthReportApplicationId; + organizationId: OrganizationId; + uri: string; +} +/* + * Request to save a password health report application + * Model is expected by the API endpoint + */ +export interface PasswordHealthReportApplicationsRequest { + organizationId: OrganizationId; + url: string; +} + +// -------------------- Risk Insights Report Models -------------------- +export interface SaveRiskInsightsReportRequest { + data: RiskInsightsReport; +} + +export interface SaveRiskInsightsReportResponse { + id: string; +} + +export interface GetRiskInsightsReportResponse { + id: string; + organizationId: OrganizationId; + // TODO Update to use creationDate from server + date: string; + reportData: EncryptedString; + contentEncryptionKey: EncryptedString; +} diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/password-health.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/password-health.ts index fea9e2c9046..ffa2d49e8e0 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/password-health.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/password-health.ts @@ -1,76 +1,9 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Opaque } from "type-fest"; - import { EncryptedString } from "@bitwarden/common/platform/models/domain/enc-string"; import { OrganizationId } from "@bitwarden/common/types/guid"; -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; - atRiskCipherIds: string[]; - memberCount: number; - atRiskMemberCount: number; - memberDetails: MemberDetailsFlat[]; - atRiskMemberDetails: MemberDetailsFlat[]; - cipherIds: string[]; -}; - -export type ApplicationHealthReportDetailWithCriticalFlag = ApplicationHealthReportDetail & { - isMarkedAsCritical: boolean; -}; - -export type ApplicationHealthReportDetailWithCriticalFlagAndCipher = - ApplicationHealthReportDetailWithCriticalFlag & { - ciphers: CipherView[]; - }; - -/** - * 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; - cipher: CipherView; -}; - -/** - * 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 @@ -97,41 +30,6 @@ export type ExposedPasswordDetail = { exposedXTimes: number; } | null; -/** - * Flattened member details that associates an - * organization member to a cipher - */ -export type MemberDetailsFlat = { - userGuid: string; - userName: string; - email: string; - cipherId: string; -}; - -/** - * Member email with the number of at risk passwords - * At risk member detail that contains the email - * and the count of at risk ciphers - */ -export type AtRiskMemberDetail = { - email: string; - atRiskPasswordCount: number; -}; - -/* - * A list of applications and the count of - * at risk passwords for each application - */ -export type AtRiskApplicationDetail = { - applicationName: string; - atRiskPasswordCount: number; -}; - -export type AppAtRiskMembersDialogParams = { - members: MemberDetailsFlat[]; - applicationName: string; -}; - /* * After data is encrypted, it is returned with the * encryption key used to encrypt the data. @@ -139,70 +37,5 @@ export type AppAtRiskMembersDialogParams = { export interface EncryptedDataWithKey { organizationId: OrganizationId; encryptedData: EncryptedString; - encryptionKey: EncryptedString; + contentEncryptionKey: EncryptedString; } - -/** - * Request to drop a password health report application - * Model is expected by the API endpoint - */ -export interface PasswordHealthReportApplicationDropRequest { - organizationId: OrganizationId; - passwordHealthReportApplicationIds: string[]; -} - -/** - * Response from the API after marking an app as critical - */ -export interface PasswordHealthReportApplicationsResponse { - id: PasswordHealthReportApplicationId; - organizationId: OrganizationId; - uri: string; -} -/* - * Request to save a password health report application - * Model is expected by the API endpoint - */ -export interface PasswordHealthReportApplicationsRequest { - organizationId: OrganizationId; - url: string; -} - -// FIXME: update to use a const object instead of a typescript enum -// eslint-disable-next-line @bitwarden/platform/no-enums -export enum DrawerType { - None = 0, - AppAtRiskMembers = 1, - OrgAtRiskMembers = 2, - OrgAtRiskApps = 3, -} - -export interface RiskInsightsReport { - organizationId: OrganizationId; - date: string; - reportData: string; - reportKey: string; -} - -export interface ReportInsightsReportData { - data: ApplicationHealthReportDetail[]; - summary: ApplicationHealthReportSummary; -} - -export interface SaveRiskInsightsReportRequest { - data: RiskInsightsReport; -} - -export interface SaveRiskInsightsReportResponse { - id: string; -} - -export interface GetRiskInsightsReportResponse { - id: string; - organizationId: OrganizationId; - date: string; - reportData: EncryptedString; - reportKey: EncryptedString; -} - -export type PasswordHealthReportApplicationId = Opaque; diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/report-models.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/report-models.ts new file mode 100644 index 00000000000..631170be4f9 --- /dev/null +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/report-models.ts @@ -0,0 +1,162 @@ +import { Opaque } from "type-fest"; + +import { OrganizationId } from "@bitwarden/common/types/guid"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { BadgeVariant } from "@bitwarden/components"; + +import { ExposedPasswordDetail, WeakPasswordDetail } from "./password-health"; + +// -------------------- Drawer and UI Models -------------------- +// FIXME: update to use a const object instead of a typescript enum +// eslint-disable-next-line @bitwarden/platform/no-enums +export enum DrawerType { + None = 0, + AppAtRiskMembers = 1, + OrgAtRiskMembers = 2, + OrgAtRiskApps = 3, +} + +export type DrawerDetails = { + open: boolean; + invokerId: string; + activeDrawerType: DrawerType; + atRiskMemberDetails?: AtRiskMemberDetail[]; + appAtRiskMembers?: AppAtRiskMembersDialogParams | null; + atRiskAppDetails?: AtRiskApplicationDetail[] | null; +}; + +export type AppAtRiskMembersDialogParams = { + members: MemberDetails[]; + applicationName: string; +}; + +// -------------------- Member Models -------------------- +/** + * Member email with the number of at risk passwords + * At risk member detail that contains the email + * and the count of at risk ciphers + */ +export type AtRiskMemberDetail = { + email: string; + atRiskPasswordCount: number; +}; + +/** + * Flattened member details that associates an + * organization member to a cipher + */ +export type MemberDetails = { + userGuid: string; + userName: string; + email: string; + cipherId: string; +}; + +// -------------------- Cipher Models -------------------- + +export type PasswordHealthData = { + reusedPasswordCount: number; + weakPasswordDetail: WeakPasswordDetail; + exposedPasswordDetail: ExposedPasswordDetail; +}; + +/** + * Associates a cipher with it's essential information. + * Gets the password health details, cipher members, and + * the trimmed uris for the cipher + */ +export type CipherHealthReport = { + applications: string[]; + cipherMembers: MemberDetails[]; + healthData: PasswordHealthData; + cipher: CipherView; +}; + +/** + * Breaks the cipher health info out by uri and passes + * along the password health and member info + */ +export type CipherApplicationView = { + cipherId: string; + cipher: CipherView; + cipherMembers: MemberDetails[]; + application: string; + healthData: PasswordHealthData; +}; + +// -------------------- Application Health Report Models -------------------- +/** + * 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; +}; + +export type CriticalSummaryDetails = { + totalCriticalMembersCount: number; + totalCriticalApplicationsCount: 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; + atRiskCipherIds: string[]; + memberCount: number; + atRiskMemberCount: number; + memberDetails: MemberDetails[]; + atRiskMemberDetails: MemberDetails[]; + cipherIds: string[]; +}; + +export type ApplicationHealthReportDetailEnriched = ApplicationHealthReportDetail & { + isMarkedAsCritical: boolean; + ciphers: CipherView[]; +}; + +/* + * A list of applications and the count of + * at risk passwords for each application + */ +export type AtRiskApplicationDetail = { + applicationName: string; + atRiskPasswordCount: number; +}; + +// -------------------- Password Health Report Models -------------------- +export type PasswordHealthReportApplicationId = Opaque; + +// -------------------- Risk Insights Report Models -------------------- +export interface RiskInsightsReportData { + data: ApplicationHealthReportDetailEnriched[]; + summary: ApplicationHealthReportSummary; +} +export interface RiskInsightsReport { + organizationId: OrganizationId; + date: string; + reportData: string; + reportKey: string; +} + +export type ReportScore = { label: string; badgeVariant: BadgeVariant; sortOrder: number }; + +export type ReportResult = CipherView & { + score: number; + reportValue: ReportScore; + scoreKey: number; +}; + +export type ReportDetailsAndSummary = { + data: ApplicationHealthReportDetailEnriched[]; + summary: ApplicationHealthReportSummary; + dateCreated: Date; +};