mirror of
https://github.com/bitwarden/clients.git
synced 2026-07-10 21:03:56 +08:00
[PM-34230] Skill for HEC integration (#19914)
This commit is contained in:
parent
90cc83fbc2
commit
c60f2269bf
142
.claude/skills/create-hec-event-integration/SKILL.md
Normal file
142
.claude/skills/create-hec-event-integration/SKILL.md
Normal file
@ -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 `<ServiceName>` 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-<service-name-kebab>-color.svg` and `logo-<service-name-kebab>-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",
|
||||
<ServiceName>: "<ServiceName>", // ← 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<ServiceName> = "event-management-for-<service-name-kebab>",
|
||||
|
||||
// In the defaultFlags object:
|
||||
[FeatureFlag.EventManagementFor<ServiceName>]: 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 <serviceName>FeatureEnabled = await firstValueFrom(
|
||||
this.configService.getFeatureFlag$(FeatureFlag.EventManagementFor<ServiceName>),
|
||||
);
|
||||
|
||||
if (<serviceName>FeatureEnabled) {
|
||||
integrations.push({
|
||||
name: OrganizationIntegrationServiceName.<ServiceName>, // must match Step 1 exactly
|
||||
linkURL: "https://bitwarden.com/help/<service-name>-siem/",
|
||||
image: "../../../../../../../images/integrations/logo-<service-name>-color.svg", // TODO: add logo before shipping (if not yet provided)
|
||||
imageDarkMode: "../../../../../../../images/integrations/logo-<service-name>-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 <ServiceName> service name", () => {
|
||||
const config = OrgIntegrationBuilder.buildHecConfiguration(
|
||||
"https://test.<servicename>.com/hec",
|
||||
"test-token",
|
||||
OrganizationIntegrationServiceName.<ServiceName>,
|
||||
);
|
||||
|
||||
expect(config).toBeInstanceOf(HecConfiguration);
|
||||
expect((config as HecConfiguration).uri).toBe("https://test.<servicename>.com/hec");
|
||||
expect((config as HecConfiguration).scheme).toBe("Bearer");
|
||||
expect((config as HecConfiguration).token).toBe("test-token");
|
||||
expect(config.bw_serviceName).toBe(OrganizationIntegrationServiceName.<ServiceName>);
|
||||
});
|
||||
|
||||
// Inside describe("buildHecTemplate", ...)
|
||||
it("should work with <ServiceName> service name", () => {
|
||||
const template = OrgIntegrationBuilder.buildHecTemplate(
|
||||
"test-index",
|
||||
OrganizationIntegrationServiceName.<ServiceName>,
|
||||
);
|
||||
|
||||
expect(template).toBeInstanceOf(HecTemplate);
|
||||
expect((template as HecTemplate).index).toBe("test-index");
|
||||
expect(template.bw_serviceName).toBe(OrganizationIntegrationServiceName.<ServiceName>);
|
||||
});
|
||||
```
|
||||
|
||||
## 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 |
|
||||
@ -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**
|
||||
Loading…
Reference in New Issue
Block a user