mirror of
https://github.com/bitwarden/browser
synced 2026-02-09 13:10:17 +00:00
Add Huntress SIEM integration and consolidate dialog result status types
This commit is contained in:
1
apps/web/src/images/integrations/logo-huntress-siem.svg
Normal file
1
apps/web/src/images/integrations/logo-huntress-siem.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 6.0 KiB |
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<HecConfiguration>(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<DatadogConfiguration>(configuration);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export const OrganizationIntegrationServiceName = Object.freeze({
|
||||
CrowdStrike: "CrowdStrike",
|
||||
Datadog: "Datadog",
|
||||
Huntress: "Huntress",
|
||||
} as const);
|
||||
|
||||
export type OrganizationIntegrationServiceName =
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<DatadogConnectDialogResult | HecConnectDialogResult, unknown>;
|
||||
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<T extends { success: string | null }>(
|
||||
result: T | undefined,
|
||||
deleteCallback: () => Promise<void>,
|
||||
saveCallback: (result: T) => Promise<void>,
|
||||
): Promise<void> {
|
||||
// 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() {
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "apiKey" | i18n }}</bit-label>
|
||||
<input bitInput type="text" formControlName="apiKey" />
|
||||
<input bitInput type="password" formControlName="apiKey" />
|
||||
<bit-hint>{{ "apiKey" | i18n }}</bit-hint>
|
||||
</bit-form-field>
|
||||
</ng-container>
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "bearerToken" | i18n }}</bit-label>
|
||||
<input bitInput type="text" formControlName="bearerToken" />
|
||||
<input bitInput type="password" formControlName="bearerToken" />
|
||||
<bit-hint>{{ "apiKey" | i18n }}</bit-hint>
|
||||
</bit-form-field>
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
<bit-dialog dialogSize="large" [loading]="loading">
|
||||
<span bitDialogTitle>
|
||||
{{ "connectIntegrationButtonDesc" | i18n: connectInfo.settings.name }}
|
||||
</span>
|
||||
<div bitDialogContent class="tw-flex tw-flex-col tw-gap-4">
|
||||
@if (loading) {
|
||||
<ng-container #spinner>
|
||||
<i class="bwi bwi-spinner bwi-lg bwi-spin" aria-hidden="true"></i>
|
||||
</ng-container>
|
||||
}
|
||||
@if (!loading) {
|
||||
<ng-container>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "httpEventCollectorUrl" | i18n }}</bit-label>
|
||||
<input
|
||||
bitInput
|
||||
type="text"
|
||||
formControlName="url"
|
||||
placeholder="https://hec.huntress.io/services/collector"
|
||||
/>
|
||||
</bit-form-field>
|
||||
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "httpEventCollectorToken" | i18n }}</bit-label>
|
||||
<input bitInput type="password" formControlName="token" />
|
||||
</bit-form-field>
|
||||
</ng-container>
|
||||
}
|
||||
</div>
|
||||
<ng-container bitDialogFooter>
|
||||
<button type="submit" bitButton bitFormButton buttonType="primary" [disabled]="loading">
|
||||
@if (isUpdateAvailable) {
|
||||
{{ "update" | i18n }}
|
||||
} @else {
|
||||
{{ "save" | i18n }}
|
||||
}
|
||||
</button>
|
||||
<button type="button" bitButton bitDialogClose buttonType="secondary" [disabled]="loading">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
|
||||
@if (canDelete) {
|
||||
<div class="tw-ml-auto">
|
||||
<button
|
||||
bitIconButton="bwi-trash"
|
||||
type="button"
|
||||
buttonType="danger"
|
||||
label="{{ 'delete' | i18n }}"
|
||||
appA11yTitle="{{ 'delete' | i18n }}"
|
||||
[bitAction]="delete"
|
||||
></button>
|
||||
</div>
|
||||
}
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
</form>
|
||||
@@ -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<HuntressConnectDialogResult>,
|
||||
private dialogService: DialogService,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.huntressConfig =
|
||||
this.connectInfo.settings.organizationIntegration?.getConfiguration<HecConfiguration>() ??
|
||||
null;
|
||||
this.huntressTemplate =
|
||||
this.connectInfo.settings.organizationIntegration?.integrationConfiguration?.[0]?.getTemplate<HecTemplate>() ??
|
||||
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<void> => {
|
||||
if (this.formGroup.invalid) {
|
||||
this.formGroup.markAllAsTouched();
|
||||
return;
|
||||
}
|
||||
const result = this.getHuntressConnectDialogResult(IntegrationDialogResultStatus.Edited);
|
||||
|
||||
this.dialogRef.close(result);
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
delete = async (): Promise<void> => {
|
||||
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<HuntressConnectDialogParams, DialogRef<HuntressConnectDialogResult>>,
|
||||
) {
|
||||
return dialogService.open<HuntressConnectDialogResult>(ConnectHuntressDialogComponent, config);
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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];
|
||||
@@ -32,6 +32,7 @@ export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy {
|
||||
tabIndex: number = 0;
|
||||
organization$: Observable<Organization> = new Observable<Organization>();
|
||||
isEventManagementForDataDogAndCrowdStrikeEnabled: boolean = false;
|
||||
isEventManagementForHuntressEnabled: boolean = false;
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
// 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$
|
||||
|
||||
Reference in New Issue
Block a user