Files
charybdis/plugins/README.md
T
Guillaume GRABÉ f1b1543f29
CI / Check (push) Successful in 13m13s
CI / Format (push) Successful in 48s
CI / Clippy (push) Successful in 12m13s
CI / Test (push) Successful in 12m45s
doc: update and cleanup
2026-06-09 11:26:47 +02:00

514 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Charybdis Plugins
Plugins extend Charybdis with integrations to external tools (security scanners, issue trackers, identity providers). Plugins are **compile-time integrated**: they live as Rust crates under `plugins/`, declare protobuf extensions in `plugins.toml`, and are linked into the `charybdis-server` binary at build time.
This document covers:
- The plugin model (event-driven vs sync, traits, lifecycle)
- Configuring shipped plugins (DefectDojo, Keycloak)
- Writing a new plugin
## Table of Contents
1. [Plugin Types](#plugin-types)
2. [Available Plugins](#available-plugins)
3. [Plugin Configuration](#plugin-configuration)
4. [Field Mapping System](#field-mapping-system)
5. [Entity Resolution](#entity-resolution)
6. [DefectDojo Reference](#defectdojo-reference)
7. [Keycloak Reference](#keycloak-reference)
8. [Writing a New Plugin](#writing-a-new-plugin)
9. [Plugin Lifecycle](#plugin-lifecycle)
10. [Annotations](#annotations)
11. [Troubleshooting](#troubleshooting)
## Plugin Types
### Event-Driven Plugins
React to entity lifecycle events (`Created`, `Updated`, `Deleted`) — typically push data to an external system.
- Implement `EventDrivenPlugin` + one or more `ResourceHandler` per resource kind.
- Example: **DefectDojo** — Component creation → DefectDojo product + engagement.
### Sync Plugins
Pull data from an external system on a schedule, creating or updating entities in Charybdis.
- Implement `SyncPlugin` (`schedule`, `on_startup`, `manual_trigger`).
- Example: **Keycloak** — periodic sync of users and groups.
Both types implement the base `Plugin` trait (`name`, `validate_config`, `health_check`).
## Available Plugins
| Plugin | Type | Status |
|---|---|---|
| DefectDojo | Event-driven | Shipped |
| Keycloak | Sync | Shipped |
Planned integrations (Slack, Jira, GitHub, Dependency-Track) are tracked in [VISION.md](../VISION.md) and [TODO.md](../TODO.md).
## Plugin Configuration
Plugins are configured in `config.toml` under `[plugins.<name>]`. Use `${VAR}` for secrets — never commit tokens.
```toml
[plugins.defectdojo]
enabled = true
base_url = "${DEFECTDOJO_URL}"
api_token = "${DEFECTDOJO_API_TOKEN}"
[plugins.keycloak]
enabled = true
base_url = "${KEYCLOAK_URL}"
realm = "charybdis"
client_id = "charybdis-sync"
client_secret = "${KEYCLOAK_CLIENT_SECRET}"
```
When `enabled = false`, the plugin is loaded but does not register handlers and does not react to events. See `config.toml.example` for the full reference.
## Field Mapping System
Event-driven plugins (currently DefectDojo) use a configurable mapper to translate Charybdis entity fields into external-tool API payloads. Mappings are defined per resource type under `[plugins.<name>.field_mappings.<resource>]`.
Three mapping types are supported:
### 1. Direct field mapping (string)
Dot-notation path into the entity.
```toml
name = "metadata.name"
description = "metadata.description"
email = "spec.profile.email"
tags = "metadata.tags"
```
### 2. Static value (object with `value` key)
```toml
business_criticality = { value = "high" }
is_active = { value = true }
priority = { value = 100 }
```
Supports strings, booleans, numbers, objects, and arrays.
### 3. Entity resolution (object)
Resolve a reference into a related entity and extract a field from it.
```toml
product_manager = {
from = "spec.owner",
resolve_entity = "User",
extract = "annotations.defectdojo.com/user-id"
}
```
| Parameter | Type | Required | Description |
|---|---|---|---|
| `from` | string | yes | Source field path |
| `resolve_entity` | string | yes | Entity kind to look up (`User`, `Component`, ...) |
| `extract` | string | yes | Field path to extract from the resolved entity |
| `resolve_array` | bool | no | Source field is an array of references |
| `lookup_entity` | string | no | Find an entity that references the source |
## Entity Resolution
Entity resolution lets a plugin navigate the entity graph at mapping time. The mapper performs a database lookup, extracts the target field, and substitutes it into the payload.
```
1. Source field spec.owner = "user:john.doe"
2. Resolve entity User entity for "john.doe"
3. Extract target field annotations.defectdojo.com/user-id = "123"
4. Result product_manager = 123
```
### Array resolution
```toml
user_ids = {
from = "spec.members",
resolve_entity = "User",
extract = "annotations.defectdojo.com/user-id",
resolve_array = true
}
```
`Group.spec.members = ["user:john", "user:jane"]``user_ids = [123, 124]`.
### Linked entity lookup
```toml
engagement_product = {
from = "id",
lookup_entity = "Component",
extract = "annotations.defectdojo.com/product-id"
}
```
Finds a `Component` that references the source entity and extracts its annotation.
### Error handling
Resolution failures (entity not found, missing annotation, invalid reference) log a warning and omit the field rather than failing the whole operation.
## DefectDojo Reference
Maps Charybdis entities to DefectDojo resources via the REST API v2. Handlers shipped: `ProductHandler`, `EngagementHandler`, `ProductTypeHandler`, `ProductMemberHandler`.
### Connection
```toml
[plugins.defectdojo]
enabled = true
base_url = "${DEFECTDOJO_URL}"
api_token = "${DEFECTDOJO_API_TOKEN}"
default_product_type_id = 1
auto_create_users = true
auto_create_product_types = false
```
To obtain the API token: in DefectDojo, **User Profile → API Key**.
### Default engagement (auto-created with each product)
```toml
[plugins.defectdojo.default_engagement]
auto_create = true
name = "CI/CD Pipeline"
description = "Automated security scans from CI/CD pipeline"
engagement_type = "CI/CD"
status = "In Progress"
duration_days = 365
deduplication_on_engagement = true
```
### Owner resolution
When a `Component` has an `owner` (typically a team name), Charybdis resolves it to DefectDojo users:
1. Find `Group` entity matching the owner.
2. Read `Group.spec.members` (usernames).
3. Look up each `User` entity.
4. Extract the user's email from the configured annotation.
5. Find the matching user in DefectDojo by email.
```toml
[plugins.defectdojo.owner_resolution]
user_email_annotation = "keycloak.com/email" # depends on your IdP
defectdojo_lookup_field = "email" # "email" or "username"
assign_all_members = true # all group members, or just first match
```
### Product mapping (Component → DefectDojo Product)
```toml
[plugins.defectdojo.field_mappings.product]
name = "metadata.name"
description = "metadata.description"
product_type_id = { value = 1 }
tags = "metadata.tags"
business_criticality = { value = "high" } # very high | high | medium | low | very low | none
platform = { value = "web" } # web | mobile | desktop | iot | ...
lifecycle = { value = "production" }
origin = { value = "internal" }
external_audience = { value = true }
internet_accessible = { value = true }
product_manager = {
from = "spec.owner",
resolve_entity = "User",
extract = "annotations.defectdojo.com/user-id"
}
```
### User mapping (User → DefectDojo User)
```toml
[plugins.defectdojo.field_mappings.user]
username = "metadata.name"
email = "spec.profile.email"
first_name = "spec.profile.displayName"
last_name = "spec.profile.displayName"
is_active = { value = true }
```
### Product Type mapping (System → DefectDojo Product Type)
```toml
[plugins.defectdojo.field_mappings.product_type]
name = "metadata.name"
description = "metadata.description"
critical_product = { value = false }
key_product = { value = true }
```
### Product Member mapping (Group → DefectDojo Product Member)
```toml
[plugins.defectdojo.field_mappings.product_member]
product_id = {
from = "spec.parent",
resolve_entity = "Component",
extract = "annotations.defectdojo.com/product-id"
}
user_id = {
from = "spec.members",
resolve_entity = "User",
extract = "annotations.defectdojo.com/user-id",
resolve_array = true
}
role_name = { value = "Reader" } # Owner | Maintainer | Writer | Reader | API_Importer
```
### Engagement mapping (Resource → DefectDojo Engagement)
```toml
[plugins.defectdojo.field_mappings.engagement]
name = "metadata.name"
description = "metadata.description"
product_id = {
from = "spec.owner",
resolve_entity = "Component",
extract = "annotations.defectdojo.com/product-id"
}
lead_id = {
from = "spec.dependsOn",
resolve_entity = "User",
extract = "annotations.defectdojo.com/user-id"
}
target_start = "spec.target_start"
target_end = "spec.target_end"
status = { value = "In Progress" }
engagement_type = { value = "CI/CD" }
version = "metadata.annotations.version"
commit_hash = "metadata.annotations.commit_hash"
branch_tag = "metadata.annotations.branch"
build_id = "metadata.annotations.build_id"
source_code_management_uri = "metadata.annotations.repo_url"
deduplication_on_engagement = { value = true }
api_test = { value = true }
pen_test = { value = false }
```
### Annotations written by the plugin
- `defectdojo.com/product-id`
- `defectdojo.com/engagement-id`
- `defectdojo.com/owner-member-ids`
## Keycloak Reference
Sync plugin: pulls users and groups from a Keycloak realm via the Admin REST API on a schedule, on startup, or via manual trigger.
```toml
[plugins.keycloak]
enabled = true
base_url = "${KEYCLOAK_URL}"
realm = "charybdis"
client_id = "charybdis-sync"
client_secret = "${KEYCLOAK_CLIENT_SECRET}"
[plugins.keycloak.sync]
schedule = "0 */5 * * * *" # optional cron; omit to sync only on startup/trigger
on_startup = true
manual_trigger = true
sync_users = true
sync_groups = true
namespace = "keycloak"
page_size = 100
```
The service account behind `client_id`/`client_secret` must have `view-users` and `view-groups` on the realm.
Synced entities carry annotations such as `keycloak.com/email`, `keycloak.com/id`, used by other plugins (e.g., the DefectDojo owner-resolution chain above).
## Writing a New Plugin
### 1. Scaffold directory
```bash
mkdir -p plugins/my_plugin/{proto,src}
```
### 2. Register in `plugins.toml`
```toml
[plugins.my_plugin]
enabled = true
proto_path = "plugins/my_plugin/proto"
metadata_field_number = 103 # next available — see registry below
spec_field_number = 203
description = "My custom integration"
```
### 3. Define the protobuf schema
`plugins/my_plugin/proto/my_plugin.proto`:
```protobuf
syntax = "proto3";
package charybdis.plugins.my_plugin;
message MyPluginMetadata {
string name = 1;
string description = 2;
}
message MyPluginSpec {
string integration_type = 1;
bool enabled = 2;
}
```
`build.rs` regenerates `proto/entities.proto` automatically on next build.
### 4. Create the crate
`plugins/my_plugin/Cargo.toml`:
```toml
[package]
name = "charybdis-my-plugin-plugin"
version = "0.1.0"
edition = "2024"
[lib]
name = "charybdis_my_plugin"
path = "src/lib.rs"
[dependencies]
charybdis = { path = "../.." }
async-trait = "0.1"
anyhow = "1.0"
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.48", features = ["full"] }
tracing = "0.1"
```
Add the crate to the workspace `members` in the top-level `Cargo.toml`.
### 5. Implement the plugin
Event-driven example:
```rust
use async_trait::async_trait;
use charybdis::plugins::{EventDrivenPlugin, Plugin, PluginConfig, PluginType, ResourceHandler};
use std::sync::Arc;
pub struct MyPlugin { /* config, client, handlers */ }
#[async_trait]
impl Plugin for MyPlugin {
fn name(&self) -> &str { "my_plugin" }
fn plugin_type(&self) -> PluginType { PluginType::EventDriven }
fn load_config(&mut self, _: PluginConfig) -> anyhow::Result<()> { Ok(()) }
fn validate_config(&self) -> anyhow::Result<()> { Ok(()) }
}
#[async_trait]
impl EventDrivenPlugin for MyPlugin {
fn resource_handlers(&self) -> Vec<Arc<dyn ResourceHandler>> {
vec![/* your handlers */]
}
}
```
### 6. Register in `charybdis-server`
Add a loader block in `charybdis-server/src/main.rs` (follow the DefectDojo block as a template) and declare the dependency in `charybdis-server/Cargo.toml`.
### Field number registry
| Range | Allocation |
|---|---|
| 199 | Core entity types |
| 100 | DefectDojo |
| 101 | DependencyTrack |
| 102 | Keycloak |
| 103+ | Available |
Field numbers MUST be unique across plugins — protobuf wire format depends on it.
## Plugin Lifecycle
### Build time
1. `build.rs` reads `plugins.toml`.
2. Generates `proto/entities.proto` with plugin-contributed `oneof` variants.
3. Compiles all protobuf files (`tonic-prost-build`).
### Runtime
1. `charybdis-server` loads each enabled plugin from `config.toml`.
2. Event-driven plugins register their `ResourceHandler`s with the dispatcher.
3. Sync plugins register with the cron scheduler (and optionally run once at startup).
4. Entity CRUD operations publish `EntityEvent` to the in-memory event bus.
5. The dispatcher fans out events to matching handlers (filtered by `trigger_kinds`).
6. Handlers call external APIs and write annotations back to Charybdis.
### Event flow
```
Client → gRPC CreateEntity → DB INSERT → EntityEvent::Created → Dispatcher
ResourceHandler.handle_create(entity)
POST /api/products → DefectDojo
update_annotations("defectdojo.com/product-id")
```
## Annotations
Plugins store external IDs in entity annotations using reverse-DNS keys:
- `defectdojo.com/product-id`
- `defectdojo.com/engagement-id`
- `keycloak.com/id`
- `keycloak.com/email`
Query by annotation via JSONB in PostgreSQL:
```sql
SELECT * FROM entities
WHERE annotations->>'defectdojo.com/product-id' = '456';
```
Use `EntityRepository::update_annotations()` for atomic JSONB merge — it avoids the read-modify-write race when multiple handlers write to the same entity.
## Troubleshooting
### Plugin not reacting to events
- `enabled = true` in `config.toml`?
- Plugin registered in `charybdis-server/src/main.rs`?
- Entity kind matches the handler's `trigger_kinds`?
- Event bus running? (`info!("Event bus started")` in startup logs)
### Field mapping not applied
- Path uses dot notation, e.g. `metadata.name`, not `meta.name`.
- Static value uses `{ value = "..." }`, not a bare string when you want a literal.
- Field actually exists on the entity (inspect via the YAML adapter or `GetEntity` gRPC).
### Entity resolution returns nothing
- Resolved entity exists in the DB.
- Resolved entity has the `extract` annotation set.
- `resolve_entity` uses the PascalCase kind (`"User"`, not `"user"`).
- For arrays, `resolve_array = true` is set.
### DefectDojo API errors
- `400 Bad Request: Invalid product type ID` → check `default_product_type_id` or your `product_type_id` mapping.
- `401 Unauthorized` → check `DEFECTDOJO_API_TOKEN`.
- `409 Conflict: user already exists` → expected, the plugin reuses existing users.
### Performance
- Minimize entity-resolution chain depth.
- Prefer static values when the data isn't entity-bound.
- Check network latency to the external API — handlers run synchronously per event.
## See Also
- [Core Concepts](../docs/core-concepts.md) — entity model, events, annotations.
- [Architecture](../docs/architecture.md) — protobuf schema, storage, scaling.
- [Field Mapper Tests](../src/plugins/field_mapper.rs) — reference behavior.