From c60f2269bfc0ac68e985f5ea8f3c481dde0e619d Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Mon, 6 Apr 2026 08:26:00 -0500 Subject: [PATCH] [PM-34230] Skill for HEC integration (#19914) --- .../create-hec-event-integration/SKILL.md | 142 ++++++++++++++++++ .../example/token-auth.md | 118 +++++++++++++++ 2 files changed, 260 insertions(+) create mode 100644 .claude/skills/create-hec-event-integration/SKILL.md create mode 100644 .claude/skills/create-hec-event-integration/example/token-auth.md diff --git a/.claude/skills/create-hec-event-integration/SKILL.md b/.claude/skills/create-hec-event-integration/SKILL.md new file mode 100644 index 00000000000..e730a34fc9d --- /dev/null +++ b/.claude/skills/create-hec-event-integration/SKILL.md @@ -0,0 +1,142 @@ +--- +name: create-hec-event-integration +description: Use when adding a new HEC (HTTP Event Collector) event integration to the Bitwarden web client. Implements the Splunk token authentication model (Bearer token + URI). Covers feature flag setup and card registration behind the flag. Does not apply to API key integrations or integrations requiring a custom connect dialog. +--- + +# Create HEC Event Integration (Token Auth) + +## Step 1 - Prompts + +Ask these questions one at a time — wait for each answer before proceeding. + +**Prompt 1 — Service name:** "What is the service name for this integration?" (e.g. `Splunk`, `CrowdStrike`, `Panther`) + +Use the answer as `` throughout. The string value in the constant must exactly match what you use as the card's `name` in Step 4 — a mismatch silently saves the config with the wrong service name. + +**Prompt 2 — Authentication:** "How is this integration authenticated?" (e.g. `Token`, `API key`) + +- **If Token** — continue with the steps below. +- **If anything else** — stop and inform the user: "This skill currently only supports token-based authentication. Support for other authentication methods hasn't been added yet." + +**Prompt 3 — Logos:** "Do you have the integration logo(s) ready to provide?" + +- **If yes** — ask for the light-mode SVG file path, and optionally a dark-mode SVG path. Copy both to `apps/web/src/images/integrations/` using the naming convention `logo--color.svg` and `logo--darkmode.svg`. Use those filenames in Step 4. +- **If no** — use placeholder paths in Step 4 and add a `// TODO: add logo before shipping` comment. + +## Step 2 — Add service name constant + +**File:** `bitwarden_license/bit-common/src/dirt/organization-integrations/models/organization-integration-service-type.ts` + +Add to `OrganizationIntegrationServiceName`: + +```typescript +export const OrganizationIntegrationServiceName = Object.freeze({ + CrowdStrike: "CrowdStrike", + Datadog: "Datadog", + Huntress: "Huntress", + : "", // ← add here +} as const); +``` + +## Step 3 — Add feature flag + +**File:** `libs/common/src/enums/feature-flag.enum.ts` + +Add the enum entry and its default. The enum key is PascalCase; the string value is kebab-case (e.g. `CrowdStrike` → `crowdstrike`, `Sumo Logic` → `sumo-logic`): + +```typescript +// In the FeatureFlag enum: +EventManagementFor = "event-management-for-", + +// In the defaultFlags object: +[FeatureFlag.EventManagementFor]: FALSE, +``` + +Example for `Panther`: + +```typescript +EventManagementForPanther = "event-management-for-panther", +[FeatureFlag.EventManagementForPanther]: FALSE, +``` + +## Step 4 — Register the card behind the feature flag + +**File:** `bitwarden_license/bit-web/src/app/dirt/organization-integrations/organization-integrations.resolver.ts` + +If logos were provided, copy them to `apps/web/src/images/integrations/` first, then use the actual filenames below. If not, use the placeholder paths with the TODO comment: + +```typescript +const FeatureEnabled = await firstValueFrom( + this.configService.getFeatureFlag$(FeatureFlag.EventManagementFor), +); + +if (FeatureEnabled) { + integrations.push({ + name: OrganizationIntegrationServiceName., // must match Step 1 exactly + linkURL: "https://bitwarden.com/help/-siem/", + image: "../../../../../../../images/integrations/logo--color.svg", // TODO: add logo before shipping (if not yet provided) + imageDarkMode: "../../../../../../../images/integrations/logo--darkmode.svg", // TODO: add logo before shipping (omit if no dark mode variant) + type: IntegrationType.EVENT, + canSetupConnection: true, + integrationType: OrganizationIntegrationType.Hec, + }); +} +``` + +No changes needed to `IntegrationCardComponent` — new HEC services fall into the existing `else` branch, which calls `openHecConnectDialog` → `saveHec` → `deleteHec`. These methods already call `buildHecConfiguration` and `buildHecTemplate` using the card's `name` as the service name. + +## Step 5 — Add tests + +**File:** `bitwarden_license/bit-common/src/dirt/organization-integrations/models/integration-builder.spec.ts` + +Add one `it` block inside the existing `describe("buildHecConfiguration", ...)` block, and one inside `describe("buildHecTemplate", ...)`. Use typed property access — do not use `JSON.parse`: + +```typescript +// Inside describe("buildHecConfiguration", ...) +it("should work with service name", () => { + const config = OrgIntegrationBuilder.buildHecConfiguration( + "https://test..com/hec", + "test-token", + OrganizationIntegrationServiceName., + ); + + expect(config).toBeInstanceOf(HecConfiguration); + expect((config as HecConfiguration).uri).toBe("https://test..com/hec"); + expect((config as HecConfiguration).scheme).toBe("Bearer"); + expect((config as HecConfiguration).token).toBe("test-token"); + expect(config.bw_serviceName).toBe(OrganizationIntegrationServiceName.); +}); + +// Inside describe("buildHecTemplate", ...) +it("should work with service name", () => { + const template = OrgIntegrationBuilder.buildHecTemplate( + "test-index", + OrganizationIntegrationServiceName., + ); + + expect(template).toBeInstanceOf(HecTemplate); + expect((template as HecTemplate).index).toBe("test-index"); + expect(template.bw_serviceName).toBe(OrganizationIntegrationServiceName.); +}); +``` + +## Step 6 — Run unit tests + +Run the unit tests for the spec file and confirm they all pass before finishing: + +```bash +npx jest bitwarden_license/bit-common/src/dirt/organization-integrations/models/integration-builder.spec.ts +``` + +All tests must pass. If any fail, fix them before proceeding. + +## Common Mistakes + +| Mistake | Fix | +| ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | +| `name` in card doesn't match `OrganizationIntegrationServiceName` value | They must be identical strings — `saveHec()` casts the name directly | +| Feature flag default not set to `FALSE` | Always add the default entry in `defaultFlags`; new flags without a default will not work correctly | +| Kebab-case mismatch in flag string | Convert consistently: lowercase, spaces → hyphens | +| Adding a new `OrganizationIntegrationType` | Not needed — all HEC services share `OrganizationIntegrationType.Hec` | +| Creating a new config/template class | Not needed — `HecConfiguration` and `HecTemplate` handle all HEC services | +| Referencing an image path without copying the file | Copy SVGs to `apps/web/src/images/integrations/` first; if logos aren't ready, leave the TODO comment | diff --git a/.claude/skills/create-hec-event-integration/example/token-auth.md b/.claude/skills/create-hec-event-integration/example/token-auth.md new file mode 100644 index 00000000000..1498b2b8dc9 --- /dev/null +++ b/.claude/skills/create-hec-event-integration/example/token-auth.md @@ -0,0 +1,118 @@ +# Example: Blumira HEC Integration (Token Auth) + +A completed walkthrough of the skill using **Blumira** as the service name. + +--- + +## Step 1 - Prompts + +| Prompt | Answer | +| -------------- | -------------------------------- | +| Service name | `Blumira` | +| Authentication | Token | +| Logos ready? | Yes — light + dark SVGs provided | + +--- + +## Step 2 — Service name constant + +**File:** `bitwarden_license/bit-common/src/dirt/organization-integrations/models/organization-integration-service-type.ts` + +```typescript +export const OrganizationIntegrationServiceName = Object.freeze({ + Blumira: "Blumira", + CrowdStrike: "CrowdStrike", + Datadog: "Datadog", + Huntress: "Huntress", +} as const); +``` + +--- + +## Step 3 — Feature flag + +**File:** `libs/common/src/enums/feature-flag.enum.ts` + +```typescript +// In the FeatureFlag enum (DIRT section): +EventManagementForBlumira = "event-management-for-blumira", + +// In defaultFlags (DIRT section): +[FeatureFlag.EventManagementForBlumira]: FALSE, +``` + +--- + +## Step 4 — Card registration + +Logos were provided in Step 1, so copy them first: + +- `apps/web/src/images/integrations/logo-blumira-color.svg` +- `apps/web/src/images/integrations/logo-blumira-darkmode.svg` + +**File:** `bitwarden_license/bit-web/src/app/dirt/organization-integrations/organization-integrations.resolver.ts` + +```typescript +const blumiraFeatureEnabled = await firstValueFrom( + this.configService.getFeatureFlag$(FeatureFlag.EventManagementForBlumira), +); + +if (blumiraFeatureEnabled) { + integrations.push({ + name: OrganizationIntegrationServiceName.Blumira, + linkURL: "https://bitwarden.com/help/blumira-siem/", + image: "../../../../../../../images/integrations/logo-blumira-color.svg", + imageDarkMode: "../../../../../../../images/integrations/logo-blumira-darkmode.svg", + type: IntegrationType.EVENT, + canSetupConnection: true, + integrationType: OrganizationIntegrationType.Hec, + }); +} +``` + +--- + +## Step 5 — Add tests + +**File:** `bitwarden_license/bit-common/src/dirt/organization-integrations/models/integration-builder.spec.ts` + +Added inside the existing `describe("buildHecConfiguration", ...)` and `describe("buildHecTemplate", ...)` blocks: + +```typescript +// Inside describe("buildHecConfiguration", ...) +it("should work with Blumira service name", () => { + const config = OrgIntegrationBuilder.buildHecConfiguration( + "https://test.blumira.com/hec", + "test-token", + OrganizationIntegrationServiceName.Blumira, + ); + + expect(config).toBeInstanceOf(HecConfiguration); + expect((config as HecConfiguration).uri).toBe("https://test.blumira.com/hec"); + expect((config as HecConfiguration).scheme).toBe("Bearer"); + expect((config as HecConfiguration).token).toBe("test-token"); + expect(config.bw_serviceName).toBe(OrganizationIntegrationServiceName.Blumira); +}); + +// Inside describe("buildHecTemplate", ...) +it("should work with Blumira service name", () => { + const template = OrgIntegrationBuilder.buildHecTemplate( + "test-index", + OrganizationIntegrationServiceName.Blumira, + ); + + expect(template).toBeInstanceOf(HecTemplate); + expect((template as HecTemplate).index).toBe("test-index"); + expect(template.bw_serviceName).toBe(OrganizationIntegrationServiceName.Blumira); +}); +``` + +--- + +## Step 6 — Run unit tests + +```bash +npx jest bitwarden_license/bit-common/src/dirt/organization-integrations/models/integration-builder.spec.ts +``` + +Result: **32 tests passed**