From 9372eba596e0a54109635cc6941029337786fbc7 Mon Sep 17 00:00:00 2001 From: maxkpower Date: Thu, 15 Jan 2026 20:05:32 +0100 Subject: [PATCH] Add Huntress SIEM integration and consolidate dialog result status types --- .../integrations/logo-huntress-siem.svg | 1 + apps/web/src/locales/en/messages.json | 9 + .../models/configuration/hec-configuration.ts | 10 +- .../models/integration-builder.ts | 10 +- .../organization-integration-service-type.ts | 1 + .../integration-card.component.spec.ts | 24 +- .../integration-card.component.ts | 327 +++++++++--------- .../connect-dialog-datadog.component.html | 2 +- .../connect-dialog-datadog.component.spec.ts | 5 +- .../connect-dialog-datadog.component.ts | 21 +- .../connect-dialog-hec.component.html | 2 +- .../connect-dialog-hec.component.spec.ts | 5 +- .../connect-dialog-hec.component.ts | 21 +- .../connect-dialog-huntress.component.html | 57 +++ .../connect-dialog-huntress.component.ts | 120 +++++++ .../integration-dialog/index.ts | 2 + .../integration-dialog-result-status.ts | 11 + .../integrations.component.ts | 23 ++ 18 files changed, 437 insertions(+), 214 deletions(-) create mode 100644 apps/web/src/images/integrations/logo-huntress-siem.svg create mode 100644 bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-huntress.component.html create mode 100644 bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-huntress.component.ts create mode 100644 bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/integration-dialog-result-status.ts diff --git a/apps/web/src/images/integrations/logo-huntress-siem.svg b/apps/web/src/images/integrations/logo-huntress-siem.svg new file mode 100644 index 00000000000..06f2a3443c0 --- /dev/null +++ b/apps/web/src/images/integrations/logo-huntress-siem.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 8adfaac88f2..d41f51942c2 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -10395,6 +10395,9 @@ "datadogEventIntegrationDesc": { "message": "Send vault event data to your Datadog instance" }, + "huntressEventIntegrationDesc": { + "message": "Send event data to your Huntress SIEM instance" + }, "failedToSaveIntegration": { "message": "Failed to save integration. Please try again later." }, @@ -10506,6 +10509,12 @@ "index": { "message": "Index" }, + "httpEventCollectorUrl": { + "message": "HTTP Event Collector URL" + }, + "httpEventCollectorToken": { + "message": "HTTP Event Collector Token" + }, "selectAPlan": { "message": "Select a plan" }, diff --git a/bitwarden_license/bit-common/src/dirt/organization-integrations/models/configuration/hec-configuration.ts b/bitwarden_license/bit-common/src/dirt/organization-integrations/models/configuration/hec-configuration.ts index 275ff82e9bd..2f3a2634129 100644 --- a/bitwarden_license/bit-common/src/dirt/organization-integrations/models/configuration/hec-configuration.ts +++ b/bitwarden_license/bit-common/src/dirt/organization-integrations/models/configuration/hec-configuration.ts @@ -3,15 +3,21 @@ import { OrganizationIntegrationServiceName } from "../organization-integration- export class HecConfiguration implements OrgIntegrationConfiguration { uri: string; - scheme = "Bearer"; + scheme: string; token: string; service?: string; bw_serviceName: OrganizationIntegrationServiceName; - constructor(uri: string, token: string, bw_serviceName: OrganizationIntegrationServiceName) { + constructor( + uri: string, + token: string, + bw_serviceName: OrganizationIntegrationServiceName, + scheme: string = "Bearer", + ) { this.uri = uri; this.token = token; this.bw_serviceName = bw_serviceName; + this.scheme = scheme; } toString(): string { diff --git a/bitwarden_license/bit-common/src/dirt/organization-integrations/models/integration-builder.ts b/bitwarden_license/bit-common/src/dirt/organization-integrations/models/integration-builder.ts index db682d58db4..63c04825b3c 100644 --- a/bitwarden_license/bit-common/src/dirt/organization-integrations/models/integration-builder.ts +++ b/bitwarden_license/bit-common/src/dirt/organization-integrations/models/integration-builder.ts @@ -29,8 +29,9 @@ export class OrgIntegrationBuilder { uri: string, token: string, bw_serviceName: OrganizationIntegrationServiceName, + scheme: string = "Bearer", ): OrgIntegrationConfiguration { - return new HecConfiguration(uri, token, bw_serviceName); + return new HecConfiguration(uri, token, bw_serviceName, scheme); } static buildHecTemplate( @@ -57,7 +58,12 @@ export class OrgIntegrationBuilder { switch (type) { case OrganizationIntegrationType.Hec: { const hecConfig = this.convertToJson(configuration); - return this.buildHecConfiguration(hecConfig.uri, hecConfig.token, hecConfig.bw_serviceName); + return this.buildHecConfiguration( + hecConfig.uri, + hecConfig.token, + hecConfig.bw_serviceName, + hecConfig.scheme, + ); } case OrganizationIntegrationType.Datadog: { const datadogConfig = this.convertToJson(configuration); diff --git a/bitwarden_license/bit-common/src/dirt/organization-integrations/models/organization-integration-service-type.ts b/bitwarden_license/bit-common/src/dirt/organization-integrations/models/organization-integration-service-type.ts index 9634ad7249a..5c4b851e7b1 100644 --- a/bitwarden_license/bit-common/src/dirt/organization-integrations/models/organization-integration-service-type.ts +++ b/bitwarden_license/bit-common/src/dirt/organization-integrations/models/organization-integration-service-type.ts @@ -1,6 +1,7 @@ export const OrganizationIntegrationServiceName = Object.freeze({ CrowdStrike: "CrowdStrike", Datadog: "Datadog", + Huntress: "Huntress", } as const); export type OrganizationIntegrationServiceName = diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.spec.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.spec.ts index 37bd504643c..928bb9488b3 100644 --- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.spec.ts +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.spec.ts @@ -16,13 +16,15 @@ import { DialogService, ToastService } from "@bitwarden/components"; import { I18nPipe } from "@bitwarden/ui-common"; import { SharedModule } from "@bitwarden/web-vault/app/shared"; -import { HecConnectDialogResultStatus, openHecConnectDialog } from "../integration-dialog"; +import { IntegrationDialogResultStatus, openHecConnectDialog } from "../integration-dialog"; import { IntegrationCardComponent } from "./integration-card.component"; jest.mock("../integration-dialog", () => ({ openHecConnectDialog: jest.fn(), - HecConnectDialogResultStatus: { Edited: "edit", Delete: "delete" }, + openDatadogConnectDialog: jest.fn(), + openHuntressConnectDialog: jest.fn(), + IntegrationDialogResultStatus: { Edited: "edit", Delete: "delete" }, })); describe("IntegrationCardComponent", () => { @@ -276,7 +278,7 @@ describe("IntegrationCardComponent", () => { it("should call updateHec if isUpdateAvailable is true", async () => { (openHecConnectDialog as jest.Mock).mockReturnValue({ closed: of({ - success: HecConnectDialogResultStatus.Edited, + success: IntegrationDialogResultStatus.Edited, url: "test-url", bearerToken: "token", index: "index", @@ -317,7 +319,7 @@ describe("IntegrationCardComponent", () => { (openHecConnectDialog as jest.Mock).mockReturnValue({ closed: of({ - success: HecConnectDialogResultStatus.Edited, + success: IntegrationDialogResultStatus.Edited, url: "test-url", bearerToken: "token", index: "index", @@ -354,7 +356,7 @@ describe("IntegrationCardComponent", () => { (openHecConnectDialog as jest.Mock).mockReturnValue({ closed: of({ - success: HecConnectDialogResultStatus.Delete, + success: IntegrationDialogResultStatus.Delete, url: "test-url", bearerToken: "token", index: "index", @@ -382,7 +384,7 @@ describe("IntegrationCardComponent", () => { (openHecConnectDialog as jest.Mock).mockReturnValue({ closed: of({ - success: HecConnectDialogResultStatus.Delete, + success: IntegrationDialogResultStatus.Delete, url: "test-url", bearerToken: "token", index: "index", @@ -404,7 +406,7 @@ describe("IntegrationCardComponent", () => { it("should show toast on error while saving", async () => { (openHecConnectDialog as jest.Mock).mockReturnValue({ closed: of({ - success: HecConnectDialogResultStatus.Edited, + success: IntegrationDialogResultStatus.Edited, url: "test-url", bearerToken: "token", index: "index", @@ -427,7 +429,7 @@ describe("IntegrationCardComponent", () => { it("should show mustBeOwner toast on error while inserting data", async () => { (openHecConnectDialog as jest.Mock).mockReturnValue({ closed: of({ - success: HecConnectDialogResultStatus.Edited, + success: IntegrationDialogResultStatus.Edited, url: "test-url", bearerToken: "token", index: "index", @@ -450,7 +452,7 @@ describe("IntegrationCardComponent", () => { it("should show mustBeOwner toast on error while updating data", async () => { (openHecConnectDialog as jest.Mock).mockReturnValue({ closed: of({ - success: HecConnectDialogResultStatus.Edited, + success: IntegrationDialogResultStatus.Edited, url: "test-url", bearerToken: "token", index: "index", @@ -472,7 +474,7 @@ describe("IntegrationCardComponent", () => { it("should show toast on error while deleting", async () => { (openHecConnectDialog as jest.Mock).mockReturnValue({ closed: of({ - success: HecConnectDialogResultStatus.Delete, + success: IntegrationDialogResultStatus.Delete, url: "test-url", bearerToken: "token", index: "index", @@ -495,7 +497,7 @@ describe("IntegrationCardComponent", () => { it("should show mustbeOwner toast on 404 while deleting", async () => { (openHecConnectDialog as jest.Mock).mockReturnValue({ closed: of({ - success: HecConnectDialogResultStatus.Delete, + success: IntegrationDialogResultStatus.Delete, url: "test-url", bearerToken: "token", index: "index", diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.ts index 8026e14c2fc..a924372ac5b 100644 --- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.ts @@ -12,7 +12,11 @@ import { Observable, Subject, combineLatest, lastValueFrom, takeUntil } from "rx import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens"; import { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration"; -import { OrgIntegrationBuilder } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration-builder"; +import { + OrgIntegrationBuilder, + OrgIntegrationConfiguration, + OrgIntegrationTemplate, +} from "@bitwarden/bit-common/dirt/organization-integrations/models/integration-builder"; import { OrganizationIntegrationServiceName } from "@bitwarden/bit-common/dirt/organization-integrations/models/organization-integration-service-type"; import { OrganizationIntegrationType } from "@bitwarden/bit-common/dirt/organization-integrations/models/organization-integration-type"; import { OrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/organization-integration-service"; @@ -23,7 +27,6 @@ import { OrganizationId } from "@bitwarden/common/types/guid"; import { BaseCardComponent, CardContentComponent, - DialogRef, DialogService, ToastService, } from "@bitwarden/components"; @@ -32,10 +35,11 @@ import { SharedModule } from "@bitwarden/web-vault/app/shared"; import { HecConnectDialogResult, DatadogConnectDialogResult, - HecConnectDialogResultStatus, - DatadogConnectDialogResultStatus, + HuntressConnectDialogResult, + IntegrationDialogResultStatus, openDatadogConnectDialog, openHecConnectDialog, + openHuntressConnectDialog, } from "../integration-dialog/index"; // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush @@ -164,14 +168,12 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy { } async setupConnection() { - let dialog: DialogRef; - if (this.integrationSettings?.integrationType === null) { return; } if (this.integrationSettings?.integrationType === OrganizationIntegrationType.Datadog) { - dialog = openDatadogConnectDialog(this.dialogService, { + const dialog = openDatadogConnectDialog(this.dialogService, { data: { settings: this.integrationSettings, }, @@ -179,37 +181,29 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy { const result = await lastValueFrom(dialog.closed); - // the dialog was cancelled - if (!result || !result.success) { - return; - } + await this.handleIntegrationDialogResult( + result, + () => this.deleteDatadog(), + (res) => this.saveDatadog(res), + ); + } else if (this.integrationSettings.name === OrganizationIntegrationServiceName.Huntress) { + // Huntress uses HEC protocol but has its own dialog + const dialog = openHuntressConnectDialog(this.dialogService, { + data: { + settings: this.integrationSettings, + }, + }); - try { - if (result.success === HecConnectDialogResultStatus.Delete) { - await this.deleteDatadog(); - } - } catch { - this.toastService.showToast({ - variant: "error", - title: "", - message: this.i18nService.t("failedToDeleteIntegration"), - }); - } + const result = await lastValueFrom(dialog.closed); - try { - if (result.success === DatadogConnectDialogResultStatus.Edited) { - await this.saveDatadog(result as DatadogConnectDialogResult); - } - } catch { - this.toastService.showToast({ - variant: "error", - title: "", - message: this.i18nService.t("failedToSaveIntegration"), - }); - } + await this.handleIntegrationDialogResult( + result, + () => this.deleteHuntress(), + (res) => this.saveHuntress(res), + ); } else { // invoke the dialog to connect the integration - dialog = openHecConnectDialog(this.dialogService, { + const dialog = openHecConnectDialog(this.dialogService, { data: { settings: this.integrationSettings, }, @@ -217,15 +211,113 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy { const result = await lastValueFrom(dialog.closed); - // the dialog was cancelled - if (!result || !result.success) { - return; + await this.handleIntegrationDialogResult( + result, + () => this.deleteHec(), + (res) => this.saveHec(res), + ); + } + } + + /** + * Generic save method + */ + private async saveIntegration( + integrationType: OrganizationIntegrationType, + config: OrgIntegrationConfiguration, + template: OrgIntegrationTemplate, + ): Promise { + let response = { mustBeOwner: false, success: false }; + + if (this.isUpdateAvailable) { + // retrieve org integration and configuration ids + const orgIntegrationId = this.integrationSettings.organizationIntegration?.id; + const orgIntegrationConfigurationId = + this.integrationSettings.organizationIntegration?.integrationConfiguration[0]?.id; + + if (!orgIntegrationId || !orgIntegrationConfigurationId) { + throw Error("Organization Integration ID or Configuration ID is missing"); } + // update existing integration and configuration + response = await this.organizationIntegrationService.update( + this.organizationId, + orgIntegrationId, + integrationType, + orgIntegrationConfigurationId, + config, + template, + ); + } else { + // create new integration and configuration + response = await this.organizationIntegrationService.save( + this.organizationId, + integrationType, + config, + template, + ); + } + + if (response.mustBeOwner) { + this.showMustBeOwnerToast(); + return; + } + + this.toastService.showToast({ + variant: "success", + title: "", + message: this.i18nService.t("success"), + }); + } + + /** + * Generic delete method + */ + private async deleteIntegration(): Promise { + const orgIntegrationId = this.integrationSettings.organizationIntegration?.id; + const orgIntegrationConfigurationId = + this.integrationSettings.organizationIntegration?.integrationConfiguration[0]?.id; + + if (!orgIntegrationId || !orgIntegrationConfigurationId) { + throw Error("Organization Integration ID or Configuration ID is missing"); + } + + const response = await this.organizationIntegrationService.delete( + this.organizationId, + orgIntegrationId, + orgIntegrationConfigurationId, + ); + + if (response.mustBeOwner) { + this.showMustBeOwnerToast(); + return; + } + + this.toastService.showToast({ + variant: "success", + title: "", + message: this.i18nService.t("success"), + }); + } + + /** + * Generic dialog result handler + * Handles both delete and edit actions with proper error handling + */ + private async handleIntegrationDialogResult( + result: T | undefined, + deleteCallback: () => Promise, + saveCallback: (result: T) => Promise, + ): Promise { + // User cancelled the dialog or closed it without saving + if (!result || !result.success) { + return; + } + + // Handle delete action + if (result.success === IntegrationDialogResultStatus.Delete) { try { - if (result.success === HecConnectDialogResultStatus.Delete) { - await this.deleteHec(); - } + await deleteCallback(); } catch { this.toastService.showToast({ variant: "error", @@ -233,11 +325,13 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy { message: this.i18nService.t("failedToDeleteIntegration"), }); } + return; + } + // Handle edit/save action + if (result.success === IntegrationDialogResultStatus.Edited) { try { - if (result.success === HecConnectDialogResultStatus.Edited) { - await this.saveHec(result as HecConnectDialogResult); - } + await saveCallback(result); } catch { this.toastService.showToast({ variant: "error", @@ -249,8 +343,6 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy { } async saveHec(result: HecConnectDialogResult) { - let response = { mustBeOwner: false, success: false }; - const config = OrgIntegrationBuilder.buildHecConfiguration( result.url, result.bearerToken, @@ -261,148 +353,45 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy { this.integrationSettings.name as OrganizationIntegrationServiceName, ); - if (this.isUpdateAvailable) { - // retrieve org integration and configuration ids - const orgIntegrationId = this.integrationSettings.organizationIntegration?.id; - const orgIntegrationConfigurationId = - this.integrationSettings.organizationIntegration?.integrationConfiguration[0]?.id; - - if (!orgIntegrationId || !orgIntegrationConfigurationId) { - throw Error("Organization Integration ID or Configuration ID is missing"); - } - - // update existing integration and configuration - response = await this.organizationIntegrationService.update( - this.organizationId, - orgIntegrationId, - OrganizationIntegrationType.Hec, - orgIntegrationConfigurationId, - config, - template, - ); - } else { - // create new integration and configuration - response = await this.organizationIntegrationService.save( - this.organizationId, - OrganizationIntegrationType.Hec, - config, - template, - ); - } - - if (response.mustBeOwner) { - this.showMustBeOwnerToast(); - return; - } - - this.toastService.showToast({ - variant: "success", - title: "", - message: this.i18nService.t("success"), - }); + await this.saveIntegration(OrganizationIntegrationType.Hec, config, template); } async deleteHec() { - const orgIntegrationId = this.integrationSettings.organizationIntegration?.id; - const orgIntegrationConfigurationId = - this.integrationSettings.organizationIntegration?.integrationConfiguration[0]?.id; + await this.deleteIntegration(); + } - if (!orgIntegrationId || !orgIntegrationConfigurationId) { - throw Error("Organization Integration ID or Configuration ID is missing"); - } - - const response = await this.organizationIntegrationService.delete( - this.organizationId, - orgIntegrationId, - orgIntegrationConfigurationId, + async saveHuntress(result: HuntressConnectDialogResult) { + // Huntress uses "Splunk" scheme for HEC protocol compatibility + const config = OrgIntegrationBuilder.buildHecConfiguration( + result.url, + result.token, + OrganizationIntegrationServiceName.Huntress, + "Splunk", + ); + // Huntress SIEM doesn't require the index field + const template = OrgIntegrationBuilder.buildHecTemplate( + "", + OrganizationIntegrationServiceName.Huntress, ); - if (response.mustBeOwner) { - this.showMustBeOwnerToast(); - return; - } + await this.saveIntegration(OrganizationIntegrationType.Hec, config, template); + } - this.toastService.showToast({ - variant: "success", - title: "", - message: this.i18nService.t("success"), - }); + async deleteHuntress() { + await this.deleteIntegration(); } async saveDatadog(result: DatadogConnectDialogResult) { - let response = { mustBeOwner: false, success: false }; - const config = OrgIntegrationBuilder.buildDataDogConfiguration(result.url, result.apiKey); const template = OrgIntegrationBuilder.buildDataDogTemplate( this.integrationSettings.name as OrganizationIntegrationServiceName, ); - if (this.isUpdateAvailable) { - // retrieve org integration and configuration ids - const orgIntegrationId = this.integrationSettings.organizationIntegration?.id; - const orgIntegrationConfigurationId = - this.integrationSettings.organizationIntegration?.integrationConfiguration[0]?.id; - - if (!orgIntegrationId || !orgIntegrationConfigurationId) { - throw Error("Organization Integration ID or Configuration ID is missing"); - } - - // update existing integration and configuration - response = await this.organizationIntegrationService.update( - this.organizationId, - orgIntegrationId, - OrganizationIntegrationType.Datadog, - orgIntegrationConfigurationId, - config, - template, - ); - } else { - // create new integration and configuration - response = await this.organizationIntegrationService.save( - this.organizationId, - OrganizationIntegrationType.Datadog, - config, - template, - ); - } - - if (response.mustBeOwner) { - this.showMustBeOwnerToast(); - return; - } - - this.toastService.showToast({ - variant: "success", - title: "", - message: this.i18nService.t("success"), - }); + await this.saveIntegration(OrganizationIntegrationType.Datadog, config, template); } async deleteDatadog() { - const orgIntegrationId = this.integrationSettings.organizationIntegration?.id; - const orgIntegrationConfigurationId = - this.integrationSettings.organizationIntegration?.integrationConfiguration[0]?.id; - - if (!orgIntegrationId || !orgIntegrationConfigurationId) { - throw Error("Organization Integration ID or Configuration ID is missing"); - } - - const response = await this.organizationIntegrationService.delete( - this.organizationId, - orgIntegrationId, - orgIntegrationConfigurationId, - ); - - if (response.mustBeOwner) { - this.showMustBeOwnerToast(); - return; - } - - this.toastService.showToast({ - variant: "success", - title: "", - message: this.i18nService.t("success"), - }); + await this.deleteIntegration(); } private showMustBeOwnerToast() { diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-datadog.component.html b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-datadog.component.html index ddc108201b0..523cbc66d56 100644 --- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-datadog.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-datadog.component.html @@ -23,7 +23,7 @@ {{ "apiKey" | i18n }} - + {{ "apiKey" | i18n }} diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-datadog.component.spec.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-datadog.component.spec.ts index 7298087e7e4..76fc8144309 100644 --- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-datadog.component.spec.ts +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-datadog.component.spec.ts @@ -10,11 +10,12 @@ import { DIALOG_DATA, DialogConfig, DialogRef, DialogService } from "@bitwarden/ import { I18nPipe } from "@bitwarden/ui-common"; import { SharedModule } from "@bitwarden/web-vault/app/shared"; +import { IntegrationDialogResultStatus } from "../integration-dialog-result-status"; + import { ConnectDatadogDialogComponent, DatadogConnectDialogParams, DatadogConnectDialogResult, - DatadogConnectDialogResultStatus, openDatadogConnectDialog, } from "./connect-dialog-datadog.component"; @@ -149,7 +150,7 @@ describe("ConnectDialogDatadogComponent", () => { url: "https://test.com", apiKey: "token", service: "Test Service", - success: DatadogConnectDialogResultStatus.Edited, + success: IntegrationDialogResultStatus.Edited, }); }); }); diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-datadog.component.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-datadog.component.ts index 47760c6311a..cedc8e5d3e3 100644 --- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-datadog.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-datadog.component.ts @@ -7,6 +7,11 @@ import { HecTemplate } from "@bitwarden/bit-common/dirt/organization-integration import { DIALOG_DATA, DialogConfig, DialogRef, DialogService } from "@bitwarden/components"; import { SharedModule } from "@bitwarden/web-vault/app/shared"; +import { + IntegrationDialogResultStatus, + IntegrationDialogResultStatusType, +} from "../integration-dialog-result-status"; + export type DatadogConnectDialogParams = { settings: Integration; }; @@ -16,17 +21,9 @@ export interface DatadogConnectDialogResult { url: string; apiKey: string; service: string; - success: DatadogConnectDialogResultStatusType | null; + success: IntegrationDialogResultStatusType | null; } -export const DatadogConnectDialogResultStatus = { - Edited: "edit", - Delete: "delete", -} as const; - -export type DatadogConnectDialogResultStatusType = - (typeof DatadogConnectDialogResultStatus)[keyof typeof DatadogConnectDialogResultStatus]; - // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ @@ -78,7 +75,7 @@ export class ConnectDatadogDialogComponent implements OnInit { this.formGroup.markAllAsTouched(); return; } - const result = this.getDatadogConnectDialogResult(DatadogConnectDialogResultStatus.Edited); + const result = this.getDatadogConnectDialogResult(IntegrationDialogResultStatus.Edited); this.dialogRef.close(result); @@ -95,13 +92,13 @@ export class ConnectDatadogDialogComponent implements OnInit { }); if (confirmed) { - const result = this.getDatadogConnectDialogResult(DatadogConnectDialogResultStatus.Delete); + const result = this.getDatadogConnectDialogResult(IntegrationDialogResultStatus.Delete); this.dialogRef.close(result); } }; private getDatadogConnectDialogResult( - status: DatadogConnectDialogResultStatusType, + status: IntegrationDialogResultStatusType, ): DatadogConnectDialogResult { const formJson = this.formGroup.getRawValue(); diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-hec.component.html b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-hec.component.html index 0dad1621440..1cafd7c3211 100644 --- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-hec.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-hec.component.html @@ -23,7 +23,7 @@ {{ "bearerToken" | i18n }} - + {{ "apiKey" | i18n }} diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-hec.component.spec.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-hec.component.spec.ts index 9f640ebbcc7..c337f2872d6 100644 --- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-hec.component.spec.ts +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-hec.component.spec.ts @@ -10,11 +10,12 @@ import { DIALOG_DATA, DialogConfig, DialogRef, DialogService } from "@bitwarden/ import { I18nPipe } from "@bitwarden/ui-common"; import { SharedModule } from "@bitwarden/web-vault/app/shared"; +import { IntegrationDialogResultStatus } from "../integration-dialog-result-status"; + import { ConnectHecDialogComponent, HecConnectDialogParams, HecConnectDialogResult, - HecConnectDialogResultStatus, openHecConnectDialog, } from "./connect-dialog-hec.component"; @@ -155,7 +156,7 @@ describe("ConnectDialogHecComponent", () => { bearerToken: "token", index: "1", service: "Test Service", - success: HecConnectDialogResultStatus.Edited, + success: IntegrationDialogResultStatus.Edited, }); }); }); diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-hec.component.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-hec.component.ts index 3612f2c76cb..3d38cfd1f79 100644 --- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-hec.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-hec.component.ts @@ -7,6 +7,11 @@ import { HecTemplate } from "@bitwarden/bit-common/dirt/organization-integration import { DIALOG_DATA, DialogConfig, DialogRef, DialogService } from "@bitwarden/components"; import { SharedModule } from "@bitwarden/web-vault/app/shared"; +import { + IntegrationDialogResultStatus, + IntegrationDialogResultStatusType, +} from "../integration-dialog-result-status"; + export type HecConnectDialogParams = { settings: Integration; }; @@ -17,17 +22,9 @@ export interface HecConnectDialogResult { bearerToken: string; index: string; service: string; - success: HecConnectDialogResultStatusType | null; + success: IntegrationDialogResultStatusType | null; } -export const HecConnectDialogResultStatus = { - Edited: "edit", - Delete: "delete", -} as const; - -export type HecConnectDialogResultStatusType = - (typeof HecConnectDialogResultStatus)[keyof typeof HecConnectDialogResultStatus]; - // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ @@ -81,7 +78,7 @@ export class ConnectHecDialogComponent implements OnInit { this.formGroup.markAllAsTouched(); return; } - const result = this.getHecConnectDialogResult(HecConnectDialogResultStatus.Edited); + const result = this.getHecConnectDialogResult(IntegrationDialogResultStatus.Edited); this.dialogRef.close(result); @@ -98,13 +95,13 @@ export class ConnectHecDialogComponent implements OnInit { }); if (confirmed) { - const result = this.getHecConnectDialogResult(HecConnectDialogResultStatus.Delete); + const result = this.getHecConnectDialogResult(IntegrationDialogResultStatus.Delete); this.dialogRef.close(result); } }; private getHecConnectDialogResult( - status: HecConnectDialogResultStatusType, + status: IntegrationDialogResultStatusType, ): HecConnectDialogResult { const formJson = this.formGroup.getRawValue(); diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-huntress.component.html b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-huntress.component.html new file mode 100644 index 00000000000..7c2894ff8b1 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-huntress.component.html @@ -0,0 +1,57 @@ +
+ + + {{ "connectIntegrationButtonDesc" | i18n: connectInfo.settings.name }} + +
+ @if (loading) { + + + + } + @if (!loading) { + + + {{ "httpEventCollectorUrl" | i18n }} + + + + + {{ "httpEventCollectorToken" | i18n }} + + + + } +
+ + + + + @if (canDelete) { +
+ +
+ } +
+
+
diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-huntress.component.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-huntress.component.ts new file mode 100644 index 00000000000..a9538e9f3e3 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-huntress.component.ts @@ -0,0 +1,120 @@ +import { Component, Inject, OnInit } from "@angular/core"; +import { FormBuilder, Validators } from "@angular/forms"; + +import { HecConfiguration } from "@bitwarden/bit-common/dirt/organization-integrations/models/configuration/hec-configuration"; +import { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration"; +import { HecTemplate } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration-configuration-config/configuration-template/hec-template"; +import { DIALOG_DATA, DialogConfig, DialogRef, DialogService } from "@bitwarden/components"; +import { SharedModule } from "@bitwarden/web-vault/app/shared"; + +import { + IntegrationDialogResultStatus, + IntegrationDialogResultStatusType, +} from "../integration-dialog-result-status"; + +export type HuntressConnectDialogParams = { + settings: Integration; +}; + +export interface HuntressConnectDialogResult { + integrationSettings: Integration; + url: string; + token: string; + service: string; + success: IntegrationDialogResultStatusType | null; +} + +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection +@Component({ + templateUrl: "./connect-dialog-huntress.component.html", + imports: [SharedModule], +}) +export class ConnectHuntressDialogComponent implements OnInit { + loading = false; + huntressConfig: HecConfiguration | null = null; + huntressTemplate: HecTemplate | null = null; + formGroup = this.formBuilder.group({ + url: ["", [Validators.required, Validators.minLength(7)]], + token: ["", Validators.required], + service: [""], // Programmatically set in ngOnInit, not shown to user + }); + + constructor( + @Inject(DIALOG_DATA) protected connectInfo: HuntressConnectDialogParams, + protected formBuilder: FormBuilder, + private dialogRef: DialogRef, + private dialogService: DialogService, + ) {} + + ngOnInit(): void { + this.huntressConfig = + this.connectInfo.settings.organizationIntegration?.getConfiguration() ?? + null; + this.huntressTemplate = + this.connectInfo.settings.organizationIntegration?.integrationConfiguration?.[0]?.getTemplate() ?? + null; + + this.formGroup.patchValue({ + url: this.huntressConfig?.uri || "", + token: this.huntressConfig?.token || "", + service: this.connectInfo.settings.name, + }); + } + + get isUpdateAvailable(): boolean { + return !!this.huntressConfig; + } + + get canDelete(): boolean { + return !!this.huntressConfig; + } + + submit = async (): Promise => { + if (this.formGroup.invalid) { + this.formGroup.markAllAsTouched(); + return; + } + const result = this.getHuntressConnectDialogResult(IntegrationDialogResultStatus.Edited); + + this.dialogRef.close(result); + + return; + }; + + delete = async (): Promise => { + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "deleteItem" }, + content: { + key: "deleteItemConfirmation", + }, + type: "warning", + }); + + if (confirmed) { + const result = this.getHuntressConnectDialogResult(IntegrationDialogResultStatus.Delete); + this.dialogRef.close(result); + } + }; + + private getHuntressConnectDialogResult( + status: IntegrationDialogResultStatusType, + ): HuntressConnectDialogResult { + const formJson = this.formGroup.getRawValue(); + + return { + integrationSettings: this.connectInfo.settings, + url: formJson.url || "", + token: formJson.token || "", + service: formJson.service || "", + success: status, + }; + } +} + +export function openHuntressConnectDialog( + dialogService: DialogService, + config: DialogConfig>, +) { + return dialogService.open(ConnectHuntressDialogComponent, config); +} diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/index.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/index.ts index 9852f3fe5c8..a41ee826cbc 100644 --- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/index.ts +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/index.ts @@ -1,2 +1,4 @@ export * from "./connect-dialog/connect-dialog-hec.component"; export * from "./connect-dialog/connect-dialog-datadog.component"; +export * from "./connect-dialog/connect-dialog-huntress.component"; +export * from "./integration-dialog-result-status"; diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/integration-dialog-result-status.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/integration-dialog-result-status.ts new file mode 100644 index 00000000000..1774088c203 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/integration-dialog-result-status.ts @@ -0,0 +1,11 @@ +/** + * Shared status types for integration dialog results + * Used across all SIEM integration dialogs (HEC, Datadog, Huntress, etc.) + */ +export const IntegrationDialogResultStatus = { + Edited: "edit", + Delete: "delete", +} as const; + +export type IntegrationDialogResultStatusType = + (typeof IntegrationDialogResultStatus)[keyof typeof IntegrationDialogResultStatus]; diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integrations.component.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integrations.component.ts index 6517182b21e..5485410f735 100644 --- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integrations.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integrations.component.ts @@ -32,6 +32,7 @@ export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy { tabIndex: number = 0; organization$: Observable = new Observable(); isEventManagementForDataDogAndCrowdStrikeEnabled: boolean = false; + isEventManagementForHuntressEnabled: boolean = false; private destroy$ = new Subject(); // initialize the integrations list with default integrations @@ -258,6 +259,13 @@ export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy { this.isEventManagementForDataDogAndCrowdStrikeEnabled = isEnabled; }); + this.configService + .getFeatureFlag$(FeatureFlag.EventManagementForHuntress) + .pipe(takeUntil(this.destroy$)) + .subscribe((isEnabled) => { + this.isEventManagementForHuntressEnabled = isEnabled; + }); + // Add the new event based items to the list if (this.isEventManagementForDataDogAndCrowdStrikeEnabled) { const crowdstrikeIntegration: Integration = { @@ -285,6 +293,21 @@ export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy { this.integrationsList.push(datadogIntegration); } + // Add Huntress SIEM integration (separate feature flag) + if (this.isEventManagementForHuntressEnabled) { + const huntressIntegration: Integration = { + name: OrganizationIntegrationServiceName.Huntress, + linkURL: "https://bitwarden.com/help/huntress-siem/", + image: "../../../../../../../images/integrations/logo-huntress-siem.svg", + type: IntegrationType.EVENT, + description: "huntressEventIntegrationDesc", + canSetupConnection: true, + integrationType: OrganizationIntegrationType.Hec, + }; + + this.integrationsList.push(huntressIntegration); + } + // For all existing event based configurations loop through and assign the // organizationIntegration for the correct services. this.organizationIntegrationService.integrations$