1
0
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:
maxkpower
2026-01-15 20:05:32 +01:00
parent aec049aa84
commit 9372eba596
18 changed files with 437 additions and 214 deletions

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@@ -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"
},

View File

@@ -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 {

View File

@@ -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);

View File

@@ -1,6 +1,7 @@
export const OrganizationIntegrationServiceName = Object.freeze({
CrowdStrike: "CrowdStrike",
Datadog: "Datadog",
Huntress: "Huntress",
} as const);
export type OrganizationIntegrationServiceName =

View File

@@ -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",

View File

@@ -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() {

View File

@@ -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>

View File

@@ -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,
});
});
});

View File

@@ -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();

View File

@@ -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>

View File

@@ -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,
});
});
});

View File

@@ -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();

View File

@@ -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>

View File

@@ -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);
}

View File

@@ -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";

View File

@@ -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];

View File

@@ -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$