fix: format and lint
CI / Format (push) Successful in 48s
CI / Test (push) Failing after 7m9s
CI / Check (push) Successful in 12m6s
CI / Clippy (push) Successful in 12m10s

This commit is contained in:
Guillaume GRABÉ
2026-06-08 23:51:29 +02:00
parent 6400518beb
commit 1302ce412c
27 changed files with 563 additions and 395 deletions
+8 -8
View File
@@ -85,10 +85,10 @@ fn parse_plugins_config(config: &str) -> Result<Vec<PluginConfig>, Box<dyn std::
// Section header [plugins.name] // Section header [plugins.name]
if line.starts_with('[') && line.ends_with(']') { if line.starts_with('[') && line.ends_with(']') {
// Save previous plugin if exists // Save previous plugin if exists
if let Some(plugin_data) = current_plugin.take() { if let Some(plugin_data) = current_plugin.take()
if let Some(plugin) = build_plugin_config(&current_section, plugin_data) { && let Some(plugin) = build_plugin_config(&current_section, plugin_data)
plugins.push(plugin); {
} plugins.push(plugin);
} }
current_section = line[1..line.len() - 1].to_string(); current_section = line[1..line.len() - 1].to_string();
@@ -115,10 +115,10 @@ fn parse_plugins_config(config: &str) -> Result<Vec<PluginConfig>, Box<dyn std::
} }
// Don't forget the last plugin // Don't forget the last plugin
if let Some(plugin_data) = current_plugin.take() { if let Some(plugin_data) = current_plugin.take()
if let Some(plugin) = build_plugin_config(&current_section, plugin_data) { && let Some(plugin) = build_plugin_config(&current_section, plugin_data)
plugins.push(plugin); {
} plugins.push(plugin);
} }
Ok(plugins) Ok(plugins)
+8 -6
View File
@@ -6,8 +6,8 @@ use tracing::{info, warn};
// Plugin imports // Plugin imports
extern crate charybdis_defectdojo; extern crate charybdis_defectdojo;
extern crate charybdis_keycloak;
extern crate charybdis_dependencytrack; extern crate charybdis_dependencytrack;
extern crate charybdis_keycloak;
// Import MyEntityService and EntityServiceServer from the library crate // Import MyEntityService and EntityServiceServer from the library crate
use charybdis::{ use charybdis::{
@@ -70,7 +70,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Load DefectDojo plugin if configured // Load DefectDojo plugin if configured
if let Some(dd_config) = &config.plugins.defectdojo { if let Some(dd_config) = &config.plugins.defectdojo {
let enabled = dd_config.get("enabled") let enabled = dd_config
.get("enabled")
.and_then(|v| v.as_bool()) .and_then(|v| v.as_bool())
.unwrap_or(false); .unwrap_or(false);
if !enabled { if !enabled {
@@ -101,7 +102,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Load Keycloak plugin if configured // Load Keycloak plugin if configured
if let Some(kc_config) = &config.plugins.keycloak { if let Some(kc_config) = &config.plugins.keycloak {
let enabled = kc_config.get("enabled") let enabled = kc_config
.get("enabled")
.and_then(|v| v.as_bool()) .and_then(|v| v.as_bool())
.unwrap_or(false); .unwrap_or(false);
if !enabled { if !enabled {
@@ -132,7 +134,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Load Dependency-Track plugin if configured // Load Dependency-Track plugin if configured
if let Some(dt_config) = &config.plugins.dependencytrack { if let Some(dt_config) = &config.plugins.dependencytrack {
let enabled = dt_config.get("enabled") let enabled = dt_config
.get("enabled")
.and_then(|v| v.as_bool()) .and_then(|v| v.as_bool())
.unwrap_or(false); .unwrap_or(false);
if !enabled { if !enabled {
@@ -213,8 +216,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Initialize parser registry and ingestion service // Initialize parser registry and ingestion service
let parser_registry = Arc::new(ParserRegistry::with_builtins()); let parser_registry = Arc::new(ParserRegistry::with_builtins());
let my_ingestion_service = let my_ingestion_service = MyIngestionService::new(entity_repository.clone(), parser_registry);
MyIngestionService::new(entity_repository.clone(), parser_registry);
// Configure and build the reflection service using the embedded descriptor set. // Configure and build the reflection service using the embedded descriptor set.
// In tonic-reflection 0.14+, use build_v1() instead of build() // In tonic-reflection 0.14+, use build_v1() instead of build()
+35 -24
View File
@@ -95,17 +95,18 @@ impl ProductHandler {
let mapped = self.field_mapper.map_all(entity).await?; let mapped = self.field_mapper.map_all(entity).await?;
// Extract name/description from mapped fields or directly from entity metadata // Extract name/description from mapped fields or directly from entity metadata
let name = mapped.get("name").cloned() let name = mapped
.get("name")
.cloned()
.unwrap_or_else(|| json!(self.extract_name(entity))); .unwrap_or_else(|| json!(self.extract_name(entity)));
let description = mapped.get("description").cloned() let description = mapped.get("description").cloned().unwrap_or_else(|| {
.unwrap_or_else(|| { let desc = self.extract_description(entity);
let desc = self.extract_description(entity); if desc.is_empty() {
if desc.is_empty() { json!(format!("Managed by Charybdis (entity: {})", entity.id))
json!(format!("Managed by Charybdis (entity: {})", entity.id)) } else {
} else { json!(desc)
json!(desc) }
} });
});
// Build DefectDojo product payload // Build DefectDojo product payload
let mut payload = json!({ let mut payload = json!({
@@ -315,7 +316,10 @@ impl ProductHandler {
match &group.spec { match &group.spec {
Some(Spec::GroupSpec(s)) => s.members.clone(), Some(Spec::GroupSpec(s)) => s.members.clone(),
_ => { _ => {
warn!("Group '{}' has no GroupSpec — cannot resolve members", owner); warn!(
"Group '{}' has no GroupSpec — cannot resolve members",
owner
);
return Ok(resolved_users); return Ok(resolved_users);
} }
} }
@@ -382,11 +386,7 @@ impl ProductHandler {
} }
/// Assign resolved users as product members in DefectDojo /// Assign resolved users as product members in DefectDojo
async fn assign_owner_to_product( async fn assign_owner_to_product(&self, product_id: i32, owner: &str) -> Result<Vec<i32>> {
&self,
product_id: i32,
owner: &str,
) -> Result<Vec<i32>> {
let resolved = self.resolve_owner_to_users(owner).await?; let resolved = self.resolve_owner_to_users(owner).await?;
if resolved.is_empty() { if resolved.is_empty() {
@@ -471,9 +471,7 @@ impl ProductHandler {
fn extract_owner(&self, entity: &Entity) -> Option<String> { fn extract_owner(&self, entity: &Entity) -> Option<String> {
use charybdis::charybdis::entities::entity; use charybdis::charybdis::entities::entity;
match &entity.spec { match &entity.spec {
Some(entity::Spec::ComponentSpec(s)) if !s.owner.is_empty() => { Some(entity::Spec::ComponentSpec(s)) if !s.owner.is_empty() => Some(s.owner.clone()),
Some(s.owner.clone())
}
_ => None, _ => None,
} }
} }
@@ -501,7 +499,10 @@ impl ResourceHandler for ProductHandler {
// Build annotations to store DefectDojo product ID // Build annotations to store DefectDojo product ID
let mut annotations = HashMap::new(); let mut annotations = HashMap::new();
annotations.insert("defectdojo.com/product-id".to_string(), product_id.to_string()); annotations.insert(
"defectdojo.com/product-id".to_string(),
product_id.to_string(),
);
// Auto-create engagement if enabled // Auto-create engagement if enabled
if self.config.default_engagement.auto_create { if self.config.default_engagement.auto_create {
@@ -516,7 +517,10 @@ impl ResourceHandler for ProductHandler {
.await .await
{ {
Ok(engagement_id) => { Ok(engagement_id) => {
annotations.insert("defectdojo.com/engagement-id".to_string(), engagement_id.to_string()); annotations.insert(
"defectdojo.com/engagement-id".to_string(),
engagement_id.to_string(),
);
info!( info!(
"Auto-created engagement {} for product {} (entity: {})", "Auto-created engagement {} for product {} (entity: {})",
engagement_id, product_id, entity.id engagement_id, product_id, entity.id
@@ -552,7 +556,9 @@ impl ResourceHandler for ProductHandler {
} }
// Update only annotations in Charybdis // Update only annotations in Charybdis
self.repository.update_annotations(&entity.id, annotations).await?; self.repository
.update_annotations(&entity.id, annotations)
.await?;
// Don't create a new entity, just return None // Don't create a new entity, just return None
Ok(None) Ok(None)
@@ -571,8 +577,13 @@ impl ResourceHandler for ProductHandler {
// Update only annotations with product ID // Update only annotations with product ID
let mut annotations = HashMap::new(); let mut annotations = HashMap::new();
annotations.insert("defectdojo.com/product-id".to_string(), product_id.to_string()); annotations.insert(
self.repository.update_annotations(&entity.id, annotations).await?; "defectdojo.com/product-id".to_string(),
product_id.to_string(),
);
self.repository
.update_annotations(&entity.id, annotations)
.await?;
} }
Ok(()) Ok(())
+70 -61
View File
@@ -8,7 +8,9 @@ use charybdis::charybdis::entities::entity::{Metadata, Spec};
use charybdis::charybdis::entities::Entity; use charybdis::charybdis::entities::Entity;
use charybdis::database::{ensure_schema, EntityRepository}; use charybdis::database::{ensure_schema, EntityRepository};
use charybdis::plugins::ResourceHandler; use charybdis::plugins::ResourceHandler;
use charybdis_defectdojo::{DefectDojoClient, DefectDojoConfig, EngagementConfig, OwnerResolutionConfig}; use charybdis_defectdojo::{
DefectDojoClient, DefectDojoConfig, EngagementConfig, OwnerResolutionConfig,
};
use serde_json::json; use serde_json::json;
use sqlx::PgPool; use sqlx::PgPool;
use std::collections::HashMap; use std::collections::HashMap;
@@ -17,9 +19,11 @@ use wiremock::matchers::{method, path, query_param};
use wiremock::{Mock, MockServer, ResponseTemplate}; use wiremock::{Mock, MockServer, ResponseTemplate};
async fn setup_db() -> PgPool { async fn setup_db() -> PgPool {
let url = std::env::var("DATABASE_URL") let url =
.expect("DATABASE_URL must be set for integration tests"); std::env::var("DATABASE_URL").expect("DATABASE_URL must be set for integration tests");
let pool = PgPool::connect(&url).await.expect("Failed to connect to test database"); let pool = PgPool::connect(&url)
.await
.expect("Failed to connect to test database");
ensure_schema(&pool).await.expect("Failed to create schema"); ensure_schema(&pool).await.expect("Failed to create schema");
// Clean up from previous test runs // Clean up from previous test runs
@@ -118,13 +122,11 @@ async fn test_product_creation_on_component_create() {
config.owner_resolution.assign_all_members = false; config.owner_resolution.assign_all_members = false;
let client = DefectDojoClient::new(mock_server.uri(), "test-token".to_string()).unwrap(); let client = DefectDojoClient::new(mock_server.uri(), "test-token".to_string()).unwrap();
let engagement_handler = Arc::new( let engagement_handler = Arc::new(charybdis_defectdojo::handlers::EngagementHandler::new(
charybdis_defectdojo::handlers::EngagementHandler::new( client.clone(),
client.clone(), repository.clone(),
repository.clone(), HashMap::new(),
HashMap::new(), ));
),
);
let handler = charybdis_defectdojo::handlers::ProductHandler::new( let handler = charybdis_defectdojo::handlers::ProductHandler::new(
client, client,
@@ -136,7 +138,10 @@ async fn test_product_creation_on_component_create() {
// Create the entity in the database first (returns entity with real UUID) // Create the entity in the database first (returns entity with real UUID)
let entity = make_component_entity("", "payment-api", "team-payments"); let entity = make_component_entity("", "payment-api", "team-payments");
let entity = repository.create(&entity).await.expect("Failed to create entity"); let entity = repository
.create(&entity)
.await
.expect("Failed to create entity");
// Trigger handler // Trigger handler
let result = handler.handle_create(&entity).await; let result = handler.handle_create(&entity).await;
@@ -178,13 +183,11 @@ async fn test_product_update_with_existing_product_id() {
let config = make_config(&mock_server.uri()); let config = make_config(&mock_server.uri());
let client = DefectDojoClient::new(mock_server.uri(), "test-token".to_string()).unwrap(); let client = DefectDojoClient::new(mock_server.uri(), "test-token".to_string()).unwrap();
let engagement_handler = Arc::new( let engagement_handler = Arc::new(charybdis_defectdojo::handlers::EngagementHandler::new(
charybdis_defectdojo::handlers::EngagementHandler::new( client.clone(),
client.clone(), repository.clone(),
repository.clone(), HashMap::new(),
HashMap::new(), ));
),
);
let handler = charybdis_defectdojo::handlers::ProductHandler::new( let handler = charybdis_defectdojo::handlers::ProductHandler::new(
client, client,
@@ -196,11 +199,13 @@ async fn test_product_update_with_existing_product_id() {
// Create entity with existing product-id annotation // Create entity with existing product-id annotation
let mut entity = make_component_entity("", "payment-api", "team-payments"); let mut entity = make_component_entity("", "payment-api", "team-payments");
entity.annotations.insert( entity
"defectdojo.com/product-id".to_string(), .annotations
"42".to_string(), .insert("defectdojo.com/product-id".to_string(), "42".to_string());
); let entity = repository
let entity = repository.create(&entity).await.expect("Failed to create entity"); .create(&entity)
.await
.expect("Failed to create entity");
// Trigger update handler // Trigger update handler
let result = handler.handle_update(&entity).await; let result = handler.handle_update(&entity).await;
@@ -228,13 +233,11 @@ async fn test_product_deletion() {
let config = make_config(&mock_server.uri()); let config = make_config(&mock_server.uri());
let client = DefectDojoClient::new(mock_server.uri(), "test-token".to_string()).unwrap(); let client = DefectDojoClient::new(mock_server.uri(), "test-token".to_string()).unwrap();
let engagement_handler = Arc::new( let engagement_handler = Arc::new(charybdis_defectdojo::handlers::EngagementHandler::new(
charybdis_defectdojo::handlers::EngagementHandler::new( client.clone(),
client.clone(), repository.clone(),
repository.clone(), HashMap::new(),
HashMap::new(), ));
),
);
let handler = charybdis_defectdojo::handlers::ProductHandler::new( let handler = charybdis_defectdojo::handlers::ProductHandler::new(
client, client,
@@ -246,11 +249,13 @@ async fn test_product_deletion() {
// Entity with product-id annotation // Entity with product-id annotation
let mut entity = make_component_entity("", "payment-api", "team-payments"); let mut entity = make_component_entity("", "payment-api", "team-payments");
entity.annotations.insert( entity
"defectdojo.com/product-id".to_string(), .annotations
"42".to_string(), .insert("defectdojo.com/product-id".to_string(), "42".to_string());
); let entity = repository
let entity = repository.create(&entity).await.expect("Failed to create entity"); .create(&entity)
.await
.expect("Failed to create entity");
// Trigger delete handler // Trigger delete handler
let result = handler.handle_delete(&entity).await; let result = handler.handle_delete(&entity).await;
@@ -423,7 +428,10 @@ async fn test_owner_resolution_assigns_product_members() {
member_of: vec!["backend-team".to_string()], member_of: vec!["backend-team".to_string()],
})), })),
}; };
alice.annotations.insert("keycloak.com/email".to_string(), "alice@example.com".to_string()); alice.annotations.insert(
"keycloak.com/email".to_string(),
"alice@example.com".to_string(),
);
repository.create(&alice).await.unwrap(); repository.create(&alice).await.unwrap();
// Create User "bob" with keycloak email annotation // Create User "bob" with keycloak email annotation
@@ -448,18 +456,19 @@ async fn test_owner_resolution_assigns_product_members() {
member_of: vec!["backend-team".to_string()], member_of: vec!["backend-team".to_string()],
})), })),
}; };
bob.annotations.insert("keycloak.com/email".to_string(), "bob@example.com".to_string()); bob.annotations.insert(
"keycloak.com/email".to_string(),
"bob@example.com".to_string(),
);
repository.create(&bob).await.unwrap(); repository.create(&bob).await.unwrap();
// Create handlers // Create handlers
let client = DefectDojoClient::new(mock_server.uri(), "test-token".to_string()).unwrap(); let client = DefectDojoClient::new(mock_server.uri(), "test-token".to_string()).unwrap();
let engagement_handler = Arc::new( let engagement_handler = Arc::new(charybdis_defectdojo::handlers::EngagementHandler::new(
charybdis_defectdojo::handlers::EngagementHandler::new( client.clone(),
client.clone(), repository.clone(),
repository.clone(), HashMap::new(),
HashMap::new(), ));
),
);
let handler = charybdis_defectdojo::handlers::ProductHandler::new( let handler = charybdis_defectdojo::handlers::ProductHandler::new(
client, client,
@@ -489,7 +498,9 @@ async fn test_owner_resolution_assigns_product_members() {
); );
// Owner member IDs should be set // Owner member IDs should be set
assert!( assert!(
updated.annotations.contains_key("defectdojo.com/owner-member-ids"), updated
.annotations
.contains_key("defectdojo.com/owner-member-ids"),
"Expected owner-member-ids annotation" "Expected owner-member-ids annotation"
); );
} }
@@ -529,13 +540,11 @@ async fn test_product_creation_without_engagement() {
config.default_engagement.auto_create = false; config.default_engagement.auto_create = false;
let client = DefectDojoClient::new(mock_server.uri(), "test-token".to_string()).unwrap(); let client = DefectDojoClient::new(mock_server.uri(), "test-token".to_string()).unwrap();
let engagement_handler = Arc::new( let engagement_handler = Arc::new(charybdis_defectdojo::handlers::EngagementHandler::new(
charybdis_defectdojo::handlers::EngagementHandler::new( client.clone(),
client.clone(), repository.clone(),
repository.clone(), HashMap::new(),
HashMap::new(), ));
),
);
let handler = charybdis_defectdojo::handlers::ProductHandler::new( let handler = charybdis_defectdojo::handlers::ProductHandler::new(
client, client,
@@ -557,7 +566,9 @@ async fn test_product_creation_without_engagement() {
updated.annotations.get("defectdojo.com/product-id"), updated.annotations.get("defectdojo.com/product-id"),
Some(&"55".to_string()) Some(&"55".to_string())
); );
assert!(!updated.annotations.contains_key("defectdojo.com/engagement-id")); assert!(!updated
.annotations
.contains_key("defectdojo.com/engagement-id"));
} }
// ────────────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────────────
@@ -583,13 +594,11 @@ async fn test_product_creation_handles_api_error() {
let config = make_config(&mock_server.uri()); let config = make_config(&mock_server.uri());
let client = DefectDojoClient::new(mock_server.uri(), "test-token".to_string()).unwrap(); let client = DefectDojoClient::new(mock_server.uri(), "test-token".to_string()).unwrap();
let engagement_handler = Arc::new( let engagement_handler = Arc::new(charybdis_defectdojo::handlers::EngagementHandler::new(
charybdis_defectdojo::handlers::EngagementHandler::new( client.clone(),
client.clone(), repository.clone(),
repository.clone(), HashMap::new(),
HashMap::new(), ));
),
);
let handler = charybdis_defectdojo::handlers::ProductHandler::new( let handler = charybdis_defectdojo::handlers::ProductHandler::new(
client, client,
+14 -4
View File
@@ -179,7 +179,8 @@ impl KeycloakClient {
pub async fn authenticate(&self) -> Result<()> { pub async fn authenticate(&self) -> Result<()> {
info!("Authenticating with Keycloak (client credentials grant)"); info!("Authenticating with Keycloak (client credentials grant)");
let response = self.raw_client let response = self
.raw_client
.post(&self.token_url) .post(&self.token_url)
.form(&[ .form(&[
("grant_type", "client_credentials"), ("grant_type", "client_credentials"),
@@ -222,7 +223,8 @@ impl KeycloakClient {
let url = format!("{}/{}", self.http.base_url(), path.trim_start_matches('/')); let url = format!("{}/{}", self.http.base_url(), path.trim_start_matches('/'));
let response = self.raw_client let response = self
.raw_client
.get(&url) .get(&url)
.header("Authorization", format!("Bearer {}", token)) .header("Authorization", format!("Bearer {}", token))
.header("Accept", "application/json") .header("Accept", "application/json")
@@ -258,7 +260,11 @@ impl KeycloakClient {
first += page_size; first += page_size;
} }
info!("Fetched {} users from Keycloak realm '{}'", all_users.len(), self.realm); info!(
"Fetched {} users from Keycloak realm '{}'",
all_users.len(),
self.realm
);
Ok(all_users) Ok(all_users)
} }
@@ -266,7 +272,11 @@ impl KeycloakClient {
pub async fn fetch_groups(&self) -> Result<Vec<KeycloakGroup>> { pub async fn fetch_groups(&self) -> Result<Vec<KeycloakGroup>> {
let response = self.get("groups?briefRepresentation=false").await?; let response = self.get("groups?briefRepresentation=false").await?;
let groups: Vec<KeycloakGroup> = serde_json::from_value(response)?; let groups: Vec<KeycloakGroup> = serde_json::from_value(response)?;
info!("Fetched {} top-level groups from Keycloak realm '{}'", groups.len(), self.realm); info!(
"Fetched {} top-level groups from Keycloak realm '{}'",
groups.len(),
self.realm
);
Ok(groups) Ok(groups)
} }
+1 -4
View File
@@ -194,10 +194,7 @@ fn build_group_entity(
let mut annotations = HashMap::new(); let mut annotations = HashMap::new();
annotations.insert("keycloak.com/group-id".to_string(), kc_group.id.clone()); annotations.insert("keycloak.com/group-id".to_string(), kc_group.id.clone());
annotations.insert( annotations.insert("keycloak.com/group-path".to_string(), kc_group.path.clone());
"keycloak.com/group-path".to_string(),
kc_group.path.clone(),
);
annotations.insert("keycloak.com/realm".to_string(), config.realm.clone()); annotations.insert("keycloak.com/realm".to_string(), config.realm.clone());
annotations.insert( annotations.insert(
"keycloak.com/member-count".to_string(), "keycloak.com/member-count".to_string(),
+16 -13
View File
@@ -308,7 +308,10 @@ fn extract_metadata(entity: &Entity) -> Result<Value, Box<dyn std::error::Error>
} }
let mut annotations = serde_json::Map::new(); let mut annotations = serde_json::Map::new();
if !m.resource_type.is_empty() { if !m.resource_type.is_empty() {
annotations.insert("keycloak.org/resource-type".to_string(), json!(m.resource_type)); annotations.insert(
"keycloak.org/resource-type".to_string(),
json!(m.resource_type),
);
} }
if !m.realm.is_empty() { if !m.realm.is_empty() {
annotations.insert("keycloak.org/realm".to_string(), json!(m.realm)); annotations.insert("keycloak.org/realm".to_string(), json!(m.realm));
@@ -465,10 +468,10 @@ fn extract_spec(entity: &Entity) -> Option<Value> {
Some(Spec::DefectdojoSpec(s)) => { Some(Spec::DefectdojoSpec(s)) => {
// DefectDojo plugin spec - parse config_json if present // DefectDojo plugin spec - parse config_json if present
let mut spec = json!({}); let mut spec = json!({});
if !s.config_json.is_empty() { if !s.config_json.is_empty()
if let Ok(config) = serde_json::from_str::<Value>(&s.config_json) { && let Ok(config) = serde_json::from_str::<Value>(&s.config_json)
spec = config; {
} spec = config;
} }
if !s.sync_status.is_empty() { if !s.sync_status.is_empty() {
spec["sync_status"] = json!(s.sync_status); spec["sync_status"] = json!(s.sync_status);
@@ -478,10 +481,10 @@ fn extract_spec(entity: &Entity) -> Option<Value> {
Some(Spec::KeycloakSpec(s)) => { Some(Spec::KeycloakSpec(s)) => {
// Keycloak plugin spec - parse config_json if present // Keycloak plugin spec - parse config_json if present
let mut spec = json!({}); let mut spec = json!({});
if !s.config_json.is_empty() { if !s.config_json.is_empty()
if let Ok(config) = serde_json::from_str::<Value>(&s.config_json) { && let Ok(config) = serde_json::from_str::<Value>(&s.config_json)
spec = config; {
} spec = config;
} }
if !s.sync_status.is_empty() { if !s.sync_status.is_empty() {
spec["sync_status"] = json!(s.sync_status); spec["sync_status"] = json!(s.sync_status);
@@ -497,10 +500,10 @@ fn extract_spec(entity: &Entity) -> Option<Value> {
Some(Spec::DependencytrackSpec(s)) => { Some(Spec::DependencytrackSpec(s)) => {
// Dependency-Track plugin spec - parse config_json if present // Dependency-Track plugin spec - parse config_json if present
let mut spec = json!({}); let mut spec = json!({});
if !s.config_json.is_empty() { if !s.config_json.is_empty()
if let Ok(config) = serde_json::from_str::<Value>(&s.config_json) { && let Ok(config) = serde_json::from_str::<Value>(&s.config_json)
spec = config; {
} spec = config;
} }
if !s.sync_status.is_empty() { if !s.sync_status.is_empty() {
spec["sync_status"] = json!(s.sync_status); spec["sync_status"] = json!(s.sync_status);
+10 -10
View File
@@ -71,16 +71,16 @@ async fn get_locations(State(state): State<YamlAdapterState>) -> Response {
// Check cache first // Check cache first
{ {
let cache = state.cache.read().await; let cache = state.cache.read().await;
if let Some(ref cached) = *cache { if let Some(ref cached) = *cache
if cached.cached_at.elapsed() < state.cache_ttl { && cached.cached_at.elapsed() < state.cache_ttl
debug!("Serving /yaml/locations from cache"); {
return ( debug!("Serving /yaml/locations from cache");
StatusCode::OK, return (
[("content-type", "text/yaml; charset=utf-8")], StatusCode::OK,
cached.data.clone(), [("content-type", "text/yaml; charset=utf-8")],
) cached.data.clone(),
.into_response(); )
} .into_response();
} }
} }
+1 -2
View File
@@ -231,8 +231,7 @@ impl Config {
fn substitute_env_vars(content: &str) -> Result<String> { fn substitute_env_vars(content: &str) -> Result<String> {
let mut result = content.to_string(); let mut result = content.to_string();
// Match ${VAR_NAME} or ${VAR_NAME:-default_value} // Match ${VAR_NAME} or ${VAR_NAME:-default_value}
let var_pattern = let var_pattern = regex::Regex::new(r"\$\{([A-Z_][A-Z0-9_]*)(?::-((?:[^}])*))?\}").unwrap();
regex::Regex::new(r"\$\{([A-Z_][A-Z0-9_]*)(?::-((?:[^}])*))?\}").unwrap();
for capture in var_pattern.captures_iter(content) { for capture in var_pattern.captures_iter(content) {
let full_match = &capture[0]; let full_match = &capture[0];
+200 -99
View File
@@ -403,10 +403,14 @@ impl EntityRepository {
// Decode cursor: "timestamp|uuid" (pipe separator since RFC3339 contains colons) // Decode cursor: "timestamp|uuid" (pipe separator since RFC3339 contains colons)
let cursor = page_token.and_then(|token| { let cursor = page_token.and_then(|token| {
use base64::Engine; use base64::Engine;
let decoded = base64::engine::general_purpose::STANDARD.decode(token).ok()?; let decoded = base64::engine::general_purpose::STANDARD
.decode(token)
.ok()?;
let s = String::from_utf8(decoded).ok()?; let s = String::from_utf8(decoded).ok()?;
let (ts_str, id_str) = s.rsplit_once('|')?; let (ts_str, id_str) = s.rsplit_once('|')?;
let ts = DateTime::parse_from_rfc3339(ts_str).ok()?.with_timezone(&Utc); let ts = DateTime::parse_from_rfc3339(ts_str)
.ok()?
.with_timezone(&Utc);
let id = Uuid::parse_str(id_str).ok()?; let id = Uuid::parse_str(id_str).ok()?;
Some((ts, id)) Some((ts, id))
}); });
@@ -673,7 +677,10 @@ impl EntityRepository {
Ok(Some((entity_data_bytes, merged_annotations_json))) => { Ok(Some((entity_data_bytes, merged_annotations_json))) => {
// Rebuild protobuf blob with merged annotations // Rebuild protobuf blob with merged annotations
let mut entity = Entity::decode(entity_data_bytes.as_slice())?; let mut entity = Entity::decode(entity_data_bytes.as_slice())?;
if let Ok(merged) = serde_json::from_value::<std::collections::HashMap<String, String>>(merged_annotations_json) { if let Ok(merged) = serde_json::from_value::<
std::collections::HashMap<String, String>,
>(merged_annotations_json)
{
entity.annotations = merged; entity.annotations = merged;
} }
entity.updated_at = Some(prost_types::Timestamp { entity.updated_at = Some(prost_types::Timestamp {
@@ -872,56 +879,112 @@ fn apply_field_mask(
// Nested fields - annotations // Nested fields - annotations
path if path.starts_with("annotations.") => { path if path.starts_with("annotations.") => {
if let Some(key) = path.strip_prefix("annotations.") { if let Some(key) = path.strip_prefix("annotations.")
if let Some(value) = partial.annotations.get(key) { && let Some(value) = partial.annotations.get(key)
existing.annotations.insert(key.to_string(), value.clone()); {
} existing.annotations.insert(key.to_string(), value.clone());
} }
} }
// Component metadata fields // Component metadata fields
"component_metadata.name" => apply_metadata_field!(partial, existing, ComponentMetadata, name), "component_metadata.name" => {
"component_metadata.namespace" => apply_metadata_field!(partial, existing, ComponentMetadata, namespace), apply_metadata_field!(partial, existing, ComponentMetadata, name)
"component_metadata.description" => apply_metadata_field!(partial, existing, ComponentMetadata, description), }
"component_metadata.labels" => apply_metadata_field!(partial, existing, ComponentMetadata, labels), "component_metadata.namespace" => {
"component_metadata.tags" => apply_metadata_field!(partial, existing, ComponentMetadata, tags), apply_metadata_field!(partial, existing, ComponentMetadata, namespace)
"component_metadata.links" => apply_metadata_field!(partial, existing, ComponentMetadata, links), }
"component_metadata.description" => {
apply_metadata_field!(partial, existing, ComponentMetadata, description)
}
"component_metadata.labels" => {
apply_metadata_field!(partial, existing, ComponentMetadata, labels)
}
"component_metadata.tags" => {
apply_metadata_field!(partial, existing, ComponentMetadata, tags)
}
"component_metadata.links" => {
apply_metadata_field!(partial, existing, ComponentMetadata, links)
}
// Component spec fields // Component spec fields
"component_spec.type" => apply_spec_field!(partial, existing, ComponentSpec, r#type), "component_spec.type" => apply_spec_field!(partial, existing, ComponentSpec, r#type),
"component_spec.lifecycle" => apply_spec_field!(partial, existing, ComponentSpec, lifecycle), "component_spec.lifecycle" => {
apply_spec_field!(partial, existing, ComponentSpec, lifecycle)
}
"component_spec.owner" => apply_spec_field!(partial, existing, ComponentSpec, owner), "component_spec.owner" => apply_spec_field!(partial, existing, ComponentSpec, owner),
"component_spec.system" => apply_spec_field!(partial, existing, ComponentSpec, system), "component_spec.system" => apply_spec_field!(partial, existing, ComponentSpec, system),
"component_spec.subcomponent_of" => apply_spec_field!(partial, existing, ComponentSpec, subcomponent_of), "component_spec.subcomponent_of" => {
"component_spec.depends_on" => apply_spec_field!(partial, existing, ComponentSpec, depends_on), apply_spec_field!(partial, existing, ComponentSpec, subcomponent_of)
"component_spec.provides_apis" => apply_spec_field!(partial, existing, ComponentSpec, provides_apis), }
"component_spec.consumes_apis" => apply_spec_field!(partial, existing, ComponentSpec, consumes_apis), "component_spec.depends_on" => {
apply_spec_field!(partial, existing, ComponentSpec, depends_on)
}
"component_spec.provides_apis" => {
apply_spec_field!(partial, existing, ComponentSpec, provides_apis)
}
"component_spec.consumes_apis" => {
apply_spec_field!(partial, existing, ComponentSpec, consumes_apis)
}
// Service metadata fields // Service metadata fields
"service_metadata.name" => apply_metadata_field!(partial, existing, ServiceMetadata, name), "service_metadata.name" => {
"service_metadata.namespace" => apply_metadata_field!(partial, existing, ServiceMetadata, namespace), apply_metadata_field!(partial, existing, ServiceMetadata, name)
"service_metadata.description" => apply_metadata_field!(partial, existing, ServiceMetadata, description), }
"service_metadata.labels" => apply_metadata_field!(partial, existing, ServiceMetadata, labels), "service_metadata.namespace" => {
"service_metadata.tags" => apply_metadata_field!(partial, existing, ServiceMetadata, tags), apply_metadata_field!(partial, existing, ServiceMetadata, namespace)
"service_metadata.links" => apply_metadata_field!(partial, existing, ServiceMetadata, links), }
"service_metadata.description" => {
apply_metadata_field!(partial, existing, ServiceMetadata, description)
}
"service_metadata.labels" => {
apply_metadata_field!(partial, existing, ServiceMetadata, labels)
}
"service_metadata.tags" => {
apply_metadata_field!(partial, existing, ServiceMetadata, tags)
}
"service_metadata.links" => {
apply_metadata_field!(partial, existing, ServiceMetadata, links)
}
// Service spec fields // Service spec fields
"service_spec.type" => apply_spec_field!(partial, existing, ServiceSpec, r#type), "service_spec.type" => apply_spec_field!(partial, existing, ServiceSpec, r#type),
"service_spec.lifecycle" => apply_spec_field!(partial, existing, ServiceSpec, lifecycle), "service_spec.lifecycle" => {
apply_spec_field!(partial, existing, ServiceSpec, lifecycle)
}
"service_spec.owner" => apply_spec_field!(partial, existing, ServiceSpec, owner), "service_spec.owner" => apply_spec_field!(partial, existing, ServiceSpec, owner),
"service_spec.system" => apply_spec_field!(partial, existing, ServiceSpec, system), "service_spec.system" => apply_spec_field!(partial, existing, ServiceSpec, system),
"service_spec.subcomponent_of" => apply_spec_field!(partial, existing, ServiceSpec, subcomponent_of), "service_spec.subcomponent_of" => {
"service_spec.depends_on" => apply_spec_field!(partial, existing, ServiceSpec, depends_on), apply_spec_field!(partial, existing, ServiceSpec, subcomponent_of)
"service_spec.consumes_apis" => apply_spec_field!(partial, existing, ServiceSpec, consumes_apis), }
"service_spec.provides_apis" => apply_spec_field!(partial, existing, ServiceSpec, provides_apis), "service_spec.depends_on" => {
apply_spec_field!(partial, existing, ServiceSpec, depends_on)
}
"service_spec.consumes_apis" => {
apply_spec_field!(partial, existing, ServiceSpec, consumes_apis)
}
"service_spec.provides_apis" => {
apply_spec_field!(partial, existing, ServiceSpec, provides_apis)
}
// System metadata fields // System metadata fields
"system_metadata.name" => apply_metadata_field!(partial, existing, SystemMetadata, name), "system_metadata.name" => {
"system_metadata.namespace" => apply_metadata_field!(partial, existing, SystemMetadata, namespace), apply_metadata_field!(partial, existing, SystemMetadata, name)
"system_metadata.description" => apply_metadata_field!(partial, existing, SystemMetadata, description), }
"system_metadata.labels" => apply_metadata_field!(partial, existing, SystemMetadata, labels), "system_metadata.namespace" => {
"system_metadata.tags" => apply_metadata_field!(partial, existing, SystemMetadata, tags), apply_metadata_field!(partial, existing, SystemMetadata, namespace)
"system_metadata.links" => apply_metadata_field!(partial, existing, SystemMetadata, links), }
"system_metadata.description" => {
apply_metadata_field!(partial, existing, SystemMetadata, description)
}
"system_metadata.labels" => {
apply_metadata_field!(partial, existing, SystemMetadata, labels)
}
"system_metadata.tags" => {
apply_metadata_field!(partial, existing, SystemMetadata, tags)
}
"system_metadata.links" => {
apply_metadata_field!(partial, existing, SystemMetadata, links)
}
// System spec fields // System spec fields
"system_spec.owner" => apply_spec_field!(partial, existing, SystemSpec, owner), "system_spec.owner" => apply_spec_field!(partial, existing, SystemSpec, owner),
@@ -929,8 +992,12 @@ fn apply_field_mask(
// API metadata fields // API metadata fields
"api_metadata.name" => apply_metadata_field!(partial, existing, ApiMetadata, name), "api_metadata.name" => apply_metadata_field!(partial, existing, ApiMetadata, name),
"api_metadata.namespace" => apply_metadata_field!(partial, existing, ApiMetadata, namespace), "api_metadata.namespace" => {
"api_metadata.description" => apply_metadata_field!(partial, existing, ApiMetadata, description), apply_metadata_field!(partial, existing, ApiMetadata, namespace)
}
"api_metadata.description" => {
apply_metadata_field!(partial, existing, ApiMetadata, description)
}
"api_metadata.labels" => apply_metadata_field!(partial, existing, ApiMetadata, labels), "api_metadata.labels" => apply_metadata_field!(partial, existing, ApiMetadata, labels),
"api_metadata.tags" => apply_metadata_field!(partial, existing, ApiMetadata, tags), "api_metadata.tags" => apply_metadata_field!(partial, existing, ApiMetadata, tags),
"api_metadata.links" => apply_metadata_field!(partial, existing, ApiMetadata, links), "api_metadata.links" => apply_metadata_field!(partial, existing, ApiMetadata, links),
@@ -944,9 +1011,15 @@ fn apply_field_mask(
// User metadata fields // User metadata fields
"user_metadata.name" => apply_metadata_field!(partial, existing, UserMetadata, name), "user_metadata.name" => apply_metadata_field!(partial, existing, UserMetadata, name),
"user_metadata.namespace" => apply_metadata_field!(partial, existing, UserMetadata, namespace), "user_metadata.namespace" => {
"user_metadata.description" => apply_metadata_field!(partial, existing, UserMetadata, description), apply_metadata_field!(partial, existing, UserMetadata, namespace)
"user_metadata.labels" => apply_metadata_field!(partial, existing, UserMetadata, labels), }
"user_metadata.description" => {
apply_metadata_field!(partial, existing, UserMetadata, description)
}
"user_metadata.labels" => {
apply_metadata_field!(partial, existing, UserMetadata, labels)
}
"user_metadata.tags" => apply_metadata_field!(partial, existing, UserMetadata, tags), "user_metadata.tags" => apply_metadata_field!(partial, existing, UserMetadata, tags),
"user_metadata.links" => apply_metadata_field!(partial, existing, UserMetadata, links), "user_metadata.links" => apply_metadata_field!(partial, existing, UserMetadata, links),
@@ -956,43 +1029,48 @@ fn apply_field_mask(
// User spec nested profile fields // User spec nested profile fields
"user_spec.profile.display_name" => { "user_spec.profile.display_name" => {
if let Some(Spec::UserSpec(ref partial_spec)) = partial.spec { if let Some(Spec::UserSpec(ref partial_spec)) = partial.spec
if let Some(Spec::UserSpec(ref mut existing_spec)) = existing.spec { && let Some(Spec::UserSpec(ref mut existing_spec)) = existing.spec
if let Some(ref partial_profile) = partial_spec.profile { && let Some(ref partial_profile) = partial_spec.profile
let profile = existing_spec.profile.get_or_insert_default(); {
profile.display_name = partial_profile.display_name.clone(); let profile = existing_spec.profile.get_or_insert_default();
} profile.display_name = partial_profile.display_name.clone();
}
} }
} }
"user_spec.profile.email" => { "user_spec.profile.email" => {
if let Some(Spec::UserSpec(ref partial_spec)) = partial.spec { if let Some(Spec::UserSpec(ref partial_spec)) = partial.spec
if let Some(Spec::UserSpec(ref mut existing_spec)) = existing.spec { && let Some(Spec::UserSpec(ref mut existing_spec)) = existing.spec
if let Some(ref partial_profile) = partial_spec.profile { && let Some(ref partial_profile) = partial_spec.profile
let profile = existing_spec.profile.get_or_insert_default(); {
profile.email = partial_profile.email.clone(); let profile = existing_spec.profile.get_or_insert_default();
} profile.email = partial_profile.email.clone();
}
} }
} }
"user_spec.profile.picture" => { "user_spec.profile.picture" => {
if let Some(Spec::UserSpec(ref partial_spec)) = partial.spec { if let Some(Spec::UserSpec(ref partial_spec)) = partial.spec
if let Some(Spec::UserSpec(ref mut existing_spec)) = existing.spec { && let Some(Spec::UserSpec(ref mut existing_spec)) = existing.spec
if let Some(ref partial_profile) = partial_spec.profile { && let Some(ref partial_profile) = partial_spec.profile
let profile = existing_spec.profile.get_or_insert_default(); {
profile.picture = partial_profile.picture.clone(); let profile = existing_spec.profile.get_or_insert_default();
} profile.picture = partial_profile.picture.clone();
}
} }
} }
// Group metadata fields // Group metadata fields
"group_metadata.name" => apply_metadata_field!(partial, existing, GroupMetadata, name), "group_metadata.name" => apply_metadata_field!(partial, existing, GroupMetadata, name),
"group_metadata.namespace" => apply_metadata_field!(partial, existing, GroupMetadata, namespace), "group_metadata.namespace" => {
"group_metadata.description" => apply_metadata_field!(partial, existing, GroupMetadata, description), apply_metadata_field!(partial, existing, GroupMetadata, namespace)
"group_metadata.labels" => apply_metadata_field!(partial, existing, GroupMetadata, labels), }
"group_metadata.description" => {
apply_metadata_field!(partial, existing, GroupMetadata, description)
}
"group_metadata.labels" => {
apply_metadata_field!(partial, existing, GroupMetadata, labels)
}
"group_metadata.tags" => apply_metadata_field!(partial, existing, GroupMetadata, tags), "group_metadata.tags" => apply_metadata_field!(partial, existing, GroupMetadata, tags),
"group_metadata.links" => apply_metadata_field!(partial, existing, GroupMetadata, links), "group_metadata.links" => {
apply_metadata_field!(partial, existing, GroupMetadata, links)
}
// Group spec fields // Group spec fields
"group_spec.type" => apply_spec_field!(partial, existing, GroupSpec, r#type), "group_spec.type" => apply_spec_field!(partial, existing, GroupSpec, r#type),
@@ -1003,60 +1081,83 @@ fn apply_field_mask(
// Group spec nested profile fields // Group spec nested profile fields
"group_spec.profile.display_name" => { "group_spec.profile.display_name" => {
if let Some(Spec::GroupSpec(ref partial_spec)) = partial.spec { if let Some(Spec::GroupSpec(ref partial_spec)) = partial.spec
if let Some(Spec::GroupSpec(ref mut existing_spec)) = existing.spec { && let Some(Spec::GroupSpec(ref mut existing_spec)) = existing.spec
if let Some(ref partial_profile) = partial_spec.profile { && let Some(ref partial_profile) = partial_spec.profile
let profile = existing_spec.profile.get_or_insert_default(); {
profile.display_name = partial_profile.display_name.clone(); let profile = existing_spec.profile.get_or_insert_default();
} profile.display_name = partial_profile.display_name.clone();
}
} }
} }
"group_spec.profile.email" => { "group_spec.profile.email" => {
if let Some(Spec::GroupSpec(ref partial_spec)) = partial.spec { if let Some(Spec::GroupSpec(ref partial_spec)) = partial.spec
if let Some(Spec::GroupSpec(ref mut existing_spec)) = existing.spec { && let Some(Spec::GroupSpec(ref mut existing_spec)) = existing.spec
if let Some(ref partial_profile) = partial_spec.profile { && let Some(ref partial_profile) = partial_spec.profile
let profile = existing_spec.profile.get_or_insert_default(); {
profile.email = partial_profile.email.clone(); let profile = existing_spec.profile.get_or_insert_default();
} profile.email = partial_profile.email.clone();
}
} }
} }
"group_spec.profile.picture" => { "group_spec.profile.picture" => {
if let Some(Spec::GroupSpec(ref partial_spec)) = partial.spec { if let Some(Spec::GroupSpec(ref partial_spec)) = partial.spec
if let Some(Spec::GroupSpec(ref mut existing_spec)) = existing.spec { && let Some(Spec::GroupSpec(ref mut existing_spec)) = existing.spec
if let Some(ref partial_profile) = partial_spec.profile { && let Some(ref partial_profile) = partial_spec.profile
let profile = existing_spec.profile.get_or_insert_default(); {
profile.picture = partial_profile.picture.clone(); let profile = existing_spec.profile.get_or_insert_default();
} profile.picture = partial_profile.picture.clone();
}
} }
} }
// Domain metadata fields // Domain metadata fields
"domain_metadata.name" => apply_metadata_field!(partial, existing, DomainMetadata, name), "domain_metadata.name" => {
"domain_metadata.namespace" => apply_metadata_field!(partial, existing, DomainMetadata, namespace), apply_metadata_field!(partial, existing, DomainMetadata, name)
"domain_metadata.description" => apply_metadata_field!(partial, existing, DomainMetadata, description), }
"domain_metadata.labels" => apply_metadata_field!(partial, existing, DomainMetadata, labels), "domain_metadata.namespace" => {
"domain_metadata.tags" => apply_metadata_field!(partial, existing, DomainMetadata, tags), apply_metadata_field!(partial, existing, DomainMetadata, namespace)
"domain_metadata.links" => apply_metadata_field!(partial, existing, DomainMetadata, links), }
"domain_metadata.description" => {
apply_metadata_field!(partial, existing, DomainMetadata, description)
}
"domain_metadata.labels" => {
apply_metadata_field!(partial, existing, DomainMetadata, labels)
}
"domain_metadata.tags" => {
apply_metadata_field!(partial, existing, DomainMetadata, tags)
}
"domain_metadata.links" => {
apply_metadata_field!(partial, existing, DomainMetadata, links)
}
// Domain spec fields // Domain spec fields
"domain_spec.owner" => apply_spec_field!(partial, existing, DomainSpec, owner), "domain_spec.owner" => apply_spec_field!(partial, existing, DomainSpec, owner),
// Resource metadata fields // Resource metadata fields
"resource_metadata.name" => apply_metadata_field!(partial, existing, ResourceMetadata, name), "resource_metadata.name" => {
"resource_metadata.namespace" => apply_metadata_field!(partial, existing, ResourceMetadata, namespace), apply_metadata_field!(partial, existing, ResourceMetadata, name)
"resource_metadata.description" => apply_metadata_field!(partial, existing, ResourceMetadata, description), }
"resource_metadata.labels" => apply_metadata_field!(partial, existing, ResourceMetadata, labels), "resource_metadata.namespace" => {
"resource_metadata.tags" => apply_metadata_field!(partial, existing, ResourceMetadata, tags), apply_metadata_field!(partial, existing, ResourceMetadata, namespace)
"resource_metadata.links" => apply_metadata_field!(partial, existing, ResourceMetadata, links), }
"resource_metadata.description" => {
apply_metadata_field!(partial, existing, ResourceMetadata, description)
}
"resource_metadata.labels" => {
apply_metadata_field!(partial, existing, ResourceMetadata, labels)
}
"resource_metadata.tags" => {
apply_metadata_field!(partial, existing, ResourceMetadata, tags)
}
"resource_metadata.links" => {
apply_metadata_field!(partial, existing, ResourceMetadata, links)
}
// Resource spec fields // Resource spec fields
"resource_spec.type" => apply_spec_field!(partial, existing, ResourceSpec, r#type), "resource_spec.type" => apply_spec_field!(partial, existing, ResourceSpec, r#type),
"resource_spec.owner" => apply_spec_field!(partial, existing, ResourceSpec, owner), "resource_spec.owner" => apply_spec_field!(partial, existing, ResourceSpec, owner),
"resource_spec.system" => apply_spec_field!(partial, existing, ResourceSpec, system), "resource_spec.system" => apply_spec_field!(partial, existing, ResourceSpec, system),
"resource_spec.depends_on" => apply_spec_field!(partial, existing, ResourceSpec, depends_on), "resource_spec.depends_on" => {
apply_spec_field!(partial, existing, ResourceSpec, depends_on)
}
_ => { _ => {
tracing::warn!("Unknown field mask path: {}", path); tracing::warn!("Unknown field mask path: {}", path);
+4 -4
View File
@@ -51,10 +51,10 @@ impl EventBus for MemoryEventBus {
); );
// Send through broadcast channel if we have one // Send through broadcast channel if we have one
if let Some(sender) = &self.sender { if let Some(sender) = &self.sender
if let Err(e) = sender.send(event.clone()) { && let Err(e) = sender.send(event.clone())
error!("Failed to send event through broadcast channel: {}", e); {
} error!("Failed to send event through broadcast channel: {}", e);
} }
// Always dispatch directly to handlers // Always dispatch directly to handlers
+5 -5
View File
@@ -4,11 +4,11 @@
//! Events are published when entities are created, updated, or deleted. //! Events are published when entities are created, updated, or deleted.
//! Plugins and other components can subscribe to react to these events. //! Plugins and other components can subscribe to react to these events.
pub mod types;
pub mod handler;
pub mod bus;
pub mod backends; pub mod backends;
pub mod bus;
pub mod handler;
pub mod types;
pub use types::*;
pub use handler::*;
pub use bus::*; pub use bus::*;
pub use handler::*;
pub use types::*;
+5 -5
View File
@@ -10,10 +10,7 @@ use crate::scanners::NormalizedFinding;
/// 2. Fallback: hash of (scanner + rule_id + file_path) — deliberately excludes line number /// 2. Fallback: hash of (scanner + rule_id + file_path) — deliberately excludes line number
/// to avoid ghost resolve/create when lines shift. Trade-off: if the same rule fires /// to avoid ghost resolve/create when lines shift. Trade-off: if the same rule fires
/// twice in the same file, they collapse into one finding. /// twice in the same file, they collapse into one finding.
pub fn compute_fingerprint( pub fn compute_fingerprint(scanner: &str, finding: &NormalizedFinding) -> String {
scanner: &str,
finding: &NormalizedFinding,
) -> String {
if let Some(ref fp) = finding.scanner_fingerprint { if let Some(ref fp) = finding.scanner_fingerprint {
return fp.clone(); return fp.clone();
} }
@@ -79,7 +76,10 @@ mod tests {
let fp1 = compute_fingerprint("scanner", &f1); let fp1 = compute_fingerprint("scanner", &f1);
let fp2 = compute_fingerprint("scanner", &f2); let fp2 = compute_fingerprint("scanner", &f2);
assert_eq!(fp1, fp2, "line number should NOT affect fallback fingerprint"); assert_eq!(
fp1, fp2,
"line number should NOT affect fallback fingerprint"
);
} }
#[test] #[test]
+19 -12
View File
@@ -10,7 +10,7 @@ use crate::charybdis::ingestion::{
ReconciliationSummary, ReconciliationSummary,
}; };
use crate::database::EntityRepository; use crate::database::EntityRepository;
use crate::findings::reconciler::{ReconciliationEngine, ReconciledFinding}; use crate::findings::reconciler::{ReconciledFinding, ReconciliationEngine};
use crate::scanners::{ParserRegistry, Severity}; use crate::scanners::{ParserRegistry, Severity};
pub struct MyIngestionService { pub struct MyIngestionService {
@@ -61,7 +61,6 @@ impl MyIngestionService {
} }
} }
#[tonic::async_trait] #[tonic::async_trait]
impl IngestionService for MyIngestionService { impl IngestionService for MyIngestionService {
#[instrument(skip(self, request), fields(component_ref, lifecycle, format))] #[instrument(skip(self, request), fields(component_ref, lifecycle, format))]
@@ -85,10 +84,9 @@ impl IngestionService for MyIngestionService {
let component_id = self.resolve_component_id(&req.component_ref).await?; let component_id = self.resolve_component_id(&req.component_ref).await?;
let parser = self let parser = self.parser_registry.get(&req.format).ok_or_else(|| {
.parser_registry Status::invalid_argument(format!("Unsupported format: {}", req.format))
.get(&req.format) })?;
.ok_or_else(|| Status::invalid_argument(format!("Unsupported format: {}", req.format)))?;
let report = parser let report = parser
.parse(&req.data) .parse(&req.data)
@@ -108,7 +106,12 @@ impl IngestionService for MyIngestionService {
let result = self let result = self
.reconciler .reconciler
.reconcile(&component_id, &req.lifecycle, &scanner_name, &report.findings) .reconcile(
&component_id,
&req.lifecycle,
&scanner_name,
&report.findings,
)
.await .await
.map_err(|e| { .map_err(|e| {
error!(error = %e, "Reconciliation failed"); error!(error = %e, "Reconciliation failed");
@@ -175,10 +178,9 @@ impl IngestionService for MyIngestionService {
let component_id = self.resolve_component_id(&req.component_ref).await?; let component_id = self.resolve_component_id(&req.component_ref).await?;
let parser = self let parser = self.parser_registry.get(&req.format).ok_or_else(|| {
.parser_registry Status::invalid_argument(format!("Unsupported format: {}", req.format))
.get(&req.format) })?;
.ok_or_else(|| Status::invalid_argument(format!("Unsupported format: {}", req.format)))?;
let report = parser let report = parser
.parse(&req.data) .parse(&req.data)
@@ -192,7 +194,12 @@ impl IngestionService for MyIngestionService {
let result = self let result = self
.reconciler .reconciler
.reconcile(&component_id, &req.lifecycle, &scanner_name, &report.findings) .reconcile(
&component_id,
&req.lifecycle,
&scanner_name,
&report.findings,
)
.await .await
.map_err(|e| { .map_err(|e| {
error!(error = %e, "Reconciliation failed"); error!(error = %e, "Reconciliation failed");
+28 -18
View File
@@ -5,9 +5,11 @@ use anyhow::Result;
use chrono::Utc; use chrono::Utc;
use tracing::{info, instrument}; use tracing::{info, instrument};
use crate::charybdis::core::{FindingMetadata, FindingSpec, FindingState, Severity as ProtoSeverity}; use crate::charybdis::core::{
use crate::charybdis::entities::entity::{Metadata, Spec}; FindingMetadata, FindingSpec, FindingState, Severity as ProtoSeverity,
};
use crate::charybdis::entities::Entity; use crate::charybdis::entities::Entity;
use crate::charybdis::entities::entity::{Metadata, Spec};
use crate::database::EntityRepository; use crate::database::EntityRepository;
use crate::findings::fingerprint::compute_fingerprint; use crate::findings::fingerprint::compute_fingerprint;
use crate::scanners::{NormalizedFinding, Severity}; use crate::scanners::{NormalizedFinding, Severity};
@@ -51,7 +53,9 @@ impl ReconciliationEngine {
scanner: &str, scanner: &str,
incoming: &[NormalizedFinding], incoming: &[NormalizedFinding],
) -> Result<ReconciliationResult> { ) -> Result<ReconciliationResult> {
let existing = self.load_existing_findings(component_ref, lifecycle).await?; let existing = self
.load_existing_findings(component_ref, lifecycle)
.await?;
let mut existing_by_fingerprint: HashMap<String, Entity> = HashMap::new(); let mut existing_by_fingerprint: HashMap<String, Entity> = HashMap::new();
for entity in existing { for entity in existing {
@@ -140,8 +144,16 @@ impl ReconciliationEngine {
.unwrap_or_default(), .unwrap_or_default(),
proto_severity_to_domain(spec.severity), proto_severity_to_domain(spec.severity),
spec.rule_id.clone(), spec.rule_id.clone(),
if spec.file_path.is_empty() { None } else { Some(spec.file_path.clone()) }, if spec.file_path.is_empty() {
if spec.line_start == 0 { None } else { Some(spec.line_start) }, None
} else {
Some(spec.file_path.clone())
},
if spec.line_start == 0 {
None
} else {
Some(spec.line_start)
},
), ),
_ => (String::new(), Severity::Medium, String::new(), None, None), _ => (String::new(), Severity::Medium, String::new(), None, None),
}) })
@@ -216,29 +228,27 @@ impl ReconciliationEngine {
for reconciled in &result.unchanged { for reconciled in &result.unchanged {
if let Some(ref entity_id) = reconciled.entity_id { if let Some(ref entity_id) = reconciled.entity_id {
let mut annotations = std::collections::HashMap::new(); let mut annotations = std::collections::HashMap::new();
annotations.insert( annotations.insert("charybdis.io/last-seen".to_string(), now.to_rfc3339());
"charybdis.io/last-seen".to_string(), annotations.insert("charybdis.io/scan-id".to_string(), scan_id.to_string());
now.to_rfc3339(), self.repository
); .update_annotations(entity_id, annotations)
annotations.insert( .await?;
"charybdis.io/scan-id".to_string(),
scan_id.to_string(),
);
self.repository.update_annotations(entity_id, annotations).await?;
} }
} }
// Resolve findings no longer detected // Resolve findings no longer detected
for reconciled in &result.resolved { for reconciled in &result.resolved {
if let Some(ref entity_id) = reconciled.entity_id { if let Some(ref entity_id) = reconciled.entity_id {
self.mark_finding_state(entity_id, FindingState::Resolved).await?; self.mark_finding_state(entity_id, FindingState::Resolved)
.await?;
} }
} }
// Reopen findings detected again // Reopen findings detected again
for reconciled in &result.reopened { for reconciled in &result.reopened {
if let Some(ref entity_id) = reconciled.entity_id { if let Some(ref entity_id) = reconciled.entity_id {
self.mark_finding_state(entity_id, FindingState::Reopened).await?; self.mark_finding_state(entity_id, FindingState::Reopened)
.await?;
} }
} }
@@ -323,8 +333,8 @@ fn build_finding_entity(
package_version: finding.package_version.clone().unwrap_or_default(), package_version: finding.package_version.clone().unwrap_or_default(),
fixed_version: finding.fixed_version.clone().unwrap_or_default(), fixed_version: finding.fixed_version.clone().unwrap_or_default(),
details_url: finding.details_url.clone().unwrap_or_default(), details_url: finding.details_url.clone().unwrap_or_default(),
first_seen: Some(timestamp.clone()), first_seen: Some(*timestamp),
last_seen: Some(timestamp.clone()), last_seen: Some(*timestamp),
resolved_at: None, resolved_at: None,
scan_id: scan_id.to_string(), scan_id: scan_id.to_string(),
}; };
+31 -19
View File
@@ -16,6 +16,7 @@ pub mod security;
pub mod telemetry; pub mod telemetry;
// Include the generated protobuf code with serde support // Include the generated protobuf code with serde support
#[allow(clippy::large_enum_variant)]
pub mod charybdis { pub mod charybdis {
pub mod core { pub mod core {
tonic::include_proto!("charybdis.core"); tonic::include_proto!("charybdis.core");
@@ -106,14 +107,10 @@ impl EntityService for MyEntityService {
let entity_kind = entity_data.kind.clone(); let entity_kind = entity_data.kind.clone();
tracing::Span::current().record("entity.kind", &entity_kind); tracing::Span::current().record("entity.kind", &entity_kind);
let created_entity = self let created_entity = self.repository.create(&entity_data).await.map_err(|e| {
.repository error!(error = %e, "Failed to create entity");
.create(&entity_data) Status::from(e)
.await })?;
.map_err(|e| {
error!(error = %e, "Failed to create entity");
Status::from(e)
})?;
let entity_id = created_entity.id.clone(); let entity_id = created_entity.id.clone();
info!(entity_id = %entity_id, entity_kind = %entity_kind, "Entity created"); info!(entity_id = %entity_id, entity_kind = %entity_kind, "Entity created");
@@ -214,9 +211,7 @@ impl EntityService for MyEntityService {
error!(entity_id = %entity_id, error = %e, "Failed to update entity"); error!(entity_id = %entity_id, error = %e, "Failed to update entity");
Status::from(e) Status::from(e)
})? })?
.ok_or_else(|| { .ok_or_else(|| Status::not_found(format!("Entity not found: {}", entity_id)))?;
Status::not_found(format!("Entity not found: {}", entity_id))
})?;
info!(entity_id = %entity_id, entity_kind = %entity_kind, "Entity updated"); info!(entity_id = %entity_id, entity_kind = %entity_kind, "Entity updated");
@@ -265,9 +260,7 @@ impl EntityService for MyEntityService {
error!(entity_id = %entity_id, error = %e, "Failed to get entity for deletion"); error!(entity_id = %entity_id, error = %e, "Failed to get entity for deletion");
Status::from(e) Status::from(e)
})? })?
.ok_or_else(|| { .ok_or_else(|| Status::not_found(format!("Entity not found: {}", entity_id)))?;
Status::not_found(format!("Entity not found: {}", entity_id))
})?;
let entity_kind = entity.kind.clone(); let entity_kind = entity.kind.clone();
tracing::Span::current().record("entity.kind", &entity_kind); tracing::Span::current().record("entity.kind", &entity_kind);
@@ -278,7 +271,10 @@ impl EntityService for MyEntityService {
})?; })?;
if !deleted { if !deleted {
return Err(Status::not_found(format!("Entity not found: {}", entity_id))); return Err(Status::not_found(format!(
"Entity not found: {}",
entity_id
)));
} }
info!(entity_id = %entity_id, entity_kind = %entity_kind, "Entity deleted"); info!(entity_id = %entity_id, entity_kind = %entity_kind, "Entity deleted");
@@ -314,10 +310,26 @@ impl EntityService for MyEntityService {
}; };
let req = request.into_inner(); let req = request.into_inner();
let kind = if req.kind.is_empty() { None } else { Some(req.kind.as_str()) }; let kind = if req.kind.is_empty() {
let name = if req.name.is_empty() { None } else { Some(req.name.as_str()) }; None
let page_size = if req.page_size == 0 { 100 } else { req.page_size }; } else {
let page_token = if req.page_token.is_empty() { None } else { Some(req.page_token.as_str()) }; Some(req.kind.as_str())
};
let name = if req.name.is_empty() {
None
} else {
Some(req.name.as_str())
};
let page_size = if req.page_size == 0 {
100
} else {
req.page_size
};
let page_token = if req.page_token.is_empty() {
None
} else {
Some(req.page_token.as_str())
};
let paginated = self let paginated = self
.repository .repository
+1 -2
View File
@@ -135,8 +135,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Initialize parser registry and ingestion service // Initialize parser registry and ingestion service
let parser_registry = Arc::new(ParserRegistry::with_builtins()); let parser_registry = Arc::new(ParserRegistry::with_builtins());
let my_ingestion_service = let my_ingestion_service = MyIngestionService::new(entity_repository.clone(), parser_registry);
MyIngestionService::new(entity_repository.clone(), parser_registry);
// Configure and build the reflection service using the embedded descriptor set. // Configure and build the reflection service using the embedded descriptor set.
// In tonic-reflection 0.14+, use build_v1() instead of build() // In tonic-reflection 0.14+, use build_v1() instead of build()
+5 -3
View File
@@ -275,7 +275,7 @@ impl FieldMapper {
pub async fn map_all(&self, entity: &Entity) -> Result<HashMap<String, Value>> { pub async fn map_all(&self, entity: &Entity) -> Result<HashMap<String, Value>> {
let mut result = HashMap::new(); let mut result = HashMap::new();
for (field_name, _) in &self.mappings { for field_name in self.mappings.keys() {
match self.map(field_name, entity).await { match self.map(field_name, entity).await {
Ok(value) => { Ok(value) => {
result.insert(field_name.clone(), value); result.insert(field_name.clone(), value);
@@ -540,7 +540,8 @@ fn entity_to_json(entity: &Entity) -> Result<Value> {
if let Some(profile) = &s.profile { if let Some(profile) = &s.profile {
let mut profile_obj = serde_json::Map::new(); let mut profile_obj = serde_json::Map::new();
if !profile.display_name.is_empty() { if !profile.display_name.is_empty() {
profile_obj.insert("display_name".to_string(), json!(&profile.display_name)); profile_obj
.insert("display_name".to_string(), json!(&profile.display_name));
} }
if !profile.email.is_empty() { if !profile.email.is_empty() {
profile_obj.insert("email".to_string(), json!(&profile.email)); profile_obj.insert("email".to_string(), json!(&profile.email));
@@ -563,7 +564,8 @@ fn entity_to_json(entity: &Entity) -> Result<Value> {
if let Some(profile) = &s.profile { if let Some(profile) = &s.profile {
let mut profile_obj = serde_json::Map::new(); let mut profile_obj = serde_json::Map::new();
if !profile.display_name.is_empty() { if !profile.display_name.is_empty() {
profile_obj.insert("display_name".to_string(), json!(&profile.display_name)); profile_obj
.insert("display_name".to_string(), json!(&profile.display_name));
} }
if !profile.email.is_empty() { if !profile.email.is_empty() {
profile_obj.insert("email".to_string(), json!(&profile.email)); profile_obj.insert("email".to_string(), json!(&profile.email));
+10 -11
View File
@@ -285,20 +285,19 @@ impl PluginHttpClient {
let request = build_request(); let request = build_request();
match request.send().await { match request.send().await {
Ok(response) => { Ok(response) => {
if response.status() == StatusCode::TOO_MANY_REQUESTS if (response.status() == StatusCode::TOO_MANY_REQUESTS
|| response.status() == StatusCode::BAD_GATEWAY || response.status() == StatusCode::BAD_GATEWAY
|| response.status() == StatusCode::SERVICE_UNAVAILABLE || response.status() == StatusCode::SERVICE_UNAVAILABLE
|| response.status() == StatusCode::GATEWAY_TIMEOUT || response.status() == StatusCode::GATEWAY_TIMEOUT)
&& attempt < MAX_RETRIES
{ {
if attempt < MAX_RETRIES { debug!("Retryable status {}, will retry", response.status());
debug!("Retryable status {}, will retry", response.status()); last_error = Some(anyhow!(
last_error = Some(anyhow!( "HTTP {} (attempt {})",
"HTTP {} (attempt {})", response.status(),
response.status(), attempt + 1
attempt + 1 ));
)); continue;
continue;
}
} }
return Ok(response); return Ok(response);
} }
+4 -4
View File
@@ -107,10 +107,10 @@ impl SyncScheduler {
info!("Sync scheduler started with tokio_cron_scheduler"); info!("Sync scheduler started with tokio_cron_scheduler");
self.scheduler = Some(scheduler); self.scheduler = Some(scheduler);
if let Some(s) = &self.scheduler { if let Some(s) = &self.scheduler
if let Err(e) = s.start().await { && let Err(e) = s.start().await
error!("Failed to start scheduler: {}", e); {
} error!("Failed to start scheduler: {}", e);
} }
} else { } else {
info!("Sync scheduler started with manual cron implementation"); info!("Sync scheduler started with manual cron implementation");
+4 -4
View File
@@ -149,10 +149,10 @@ fn extract_scanner_fingerprint(result: &SarifResult) -> Option<String> {
return Some(v.clone()); return Some(v.clone());
} }
} }
if let Some(ref fps) = result.fingerprints { if let Some(ref fps) = result.fingerprints
if let Some(v) = fps.values().next() { && let Some(v) = fps.values().next()
return Some(v.clone()); {
} return Some(v.clone());
} }
None None
} }
+2 -22
View File
@@ -1,7 +1,7 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct SecurityConfig { pub struct SecurityConfig {
#[serde(default)] #[serde(default)]
pub mtls: MtlsConfig, pub mtls: MtlsConfig,
@@ -10,15 +10,6 @@ pub struct SecurityConfig {
pub rbac: RbacConfig, pub rbac: RbacConfig,
} }
impl Default for SecurityConfig {
fn default() -> Self {
Self {
mtls: MtlsConfig::default(),
rbac: RbacConfig::default(),
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize, Serialize)]
pub struct MtlsConfig { pub struct MtlsConfig {
#[serde(default)] #[serde(default)]
@@ -57,7 +48,7 @@ impl Default for MtlsConfig {
} }
} }
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct RbacConfig { pub struct RbacConfig {
#[serde(default)] #[serde(default)]
pub enabled: bool, pub enabled: bool,
@@ -72,17 +63,6 @@ pub struct RbacConfig {
pub audit: AuditConfig, pub audit: AuditConfig,
} }
impl Default for RbacConfig {
fn default() -> Self {
Self {
enabled: false,
role_mappings: vec![],
permissions: HashMap::new(),
audit: AuditConfig::default(),
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize, Serialize)]
pub struct RoleMapping { pub struct RoleMapping {
pub role: String, pub role: String,
+6 -3
View File
@@ -1,3 +1,5 @@
use std::fmt;
use thiserror::Error; use thiserror::Error;
use tracing::debug; use tracing::debug;
@@ -72,9 +74,10 @@ impl ClientIdentity {
certificate_serial: serial, certificate_serial: serial,
}) })
} }
}
/// Display identity for logging impl fmt::Display for ClientIdentity {
pub fn to_string(&self) -> String { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut parts = vec![format!("CN={}", self.common_name)]; let mut parts = vec![format!("CN={}", self.common_name)];
if let Some(ref ou) = self.organizational_unit { if let Some(ref ou) = self.organizational_unit {
@@ -85,7 +88,7 @@ impl ClientIdentity {
parts.push(format!("O={}", o)); parts.push(format!("O={}", o));
} }
parts.join(", ") f.write_str(&parts.join(", "))
} }
} }
+9 -13
View File
@@ -45,7 +45,7 @@ impl AuthInterceptor {
tracing::Span::current().record("client.cn", &identity.common_name); tracing::Span::current().record("client.cn", &identity.common_name);
debug!( debug!(
identity = %identity.to_string(), identity = %identity,
"Extracted client identity from certificate" "Extracted client identity from certificate"
); );
@@ -62,10 +62,7 @@ impl AuthInterceptor {
"No role assigned to identity", "No role assigned to identity",
duration, duration,
); );
Status::permission_denied(format!( Status::permission_denied(format!("No role assigned to identity: {}", identity))
"No role assigned to identity: {}",
identity.to_string()
))
})?; })?;
tracing::Span::current().record("role", &role); tracing::Span::current().record("role", &role);
@@ -135,7 +132,7 @@ impl AuthInterceptor {
} }
debug!( debug!(
identity = %identity.to_string(), identity = %identity,
role = %role, role = %role,
"Request authorized" "Request authorized"
); );
@@ -150,13 +147,12 @@ impl AuthInterceptor {
// Try 1: Extract from Tonic's TlsConnectInfo (direct mTLS) // Try 1: Extract from Tonic's TlsConnectInfo (direct mTLS)
// This requires the tls-connect-info feature enabled in Tonic // This requires the tls-connect-info feature enabled in Tonic
// Note: Must use TlsConnectInfo<TcpConnectInfo>, not TlsConnectInfo<SocketAddr> // Note: Must use TlsConnectInfo<TcpConnectInfo>, not TlsConnectInfo<SocketAddr>
if let Some(connect_info) = request.extensions().get::<TlsConnectInfo<TcpConnectInfo>>() { if let Some(connect_info) = request.extensions().get::<TlsConnectInfo<TcpConnectInfo>>()
if let Some(certs) = connect_info.peer_certs() { && let Some(certs) = connect_info.peer_certs()
if let Some(cert) = certs.first() { && let Some(cert) = certs.first()
debug!("Extracted certificate from TlsConnectInfo (direct mTLS)"); {
return Ok(cert.as_ref().to_vec()); debug!("Extracted certificate from TlsConnectInfo (direct mTLS)");
} return Ok(cert.as_ref().to_vec());
}
} }
// Try 2: Extract from headers (reverse proxy injected certificate) // Try 2: Extract from headers (reverse proxy injected certificate)
+4 -4
View File
@@ -127,10 +127,10 @@ pub fn validate_cert_files(config: &MtlsConfig) -> Result<()> {
} }
// Check CRL if specified // Check CRL if specified
if let Some(ref crl_file) = config.crl_file { if let Some(ref crl_file) = config.crl_file
if !std::path::Path::new(crl_file).exists() { && !std::path::Path::new(crl_file).exists()
anyhow::bail!("CRL file not found: {}", crl_file); {
} anyhow::bail!("CRL file not found: {}", crl_file);
} }
debug!("All certificate files validated successfully"); debug!("All certificate files validated successfully");
+27 -27
View File
@@ -76,20 +76,20 @@ fn init_tracer(
.with_resource(resource); .with_resource(resource);
// Add OTLP exporter if enabled // Add OTLP exporter if enabled
if config.enable_otlp { if config.enable_otlp
if let Some(endpoint) = &config.otlp_endpoint { && let Some(endpoint) = &config.otlp_endpoint
info!("Configuring OTLP trace exporter: {}", endpoint); {
info!("Configuring OTLP trace exporter: {}", endpoint);
// In OpenTelemetry 0.31+, use SpanExporter::builder() // In OpenTelemetry 0.31+, use SpanExporter::builder()
let exporter = opentelemetry_otlp::SpanExporter::builder() let exporter = opentelemetry_otlp::SpanExporter::builder()
.with_tonic() .with_tonic()
.with_endpoint(endpoint.clone()) .with_endpoint(endpoint.clone())
.with_timeout(Duration::from_secs(10)) .with_timeout(Duration::from_secs(10))
.build()?; .build()?;
// In OpenTelemetry 0.31+, runtime parameter is no longer needed // In OpenTelemetry 0.31+, runtime parameter is no longer needed
builder = builder.with_batch_exporter(exporter); builder = builder.with_batch_exporter(exporter);
}
} }
let provider = builder.build(); let provider = builder.build();
@@ -108,24 +108,24 @@ fn init_metrics(
let mut meter_provider_builder = SdkMeterProvider::builder().with_resource(resource); let mut meter_provider_builder = SdkMeterProvider::builder().with_resource(resource);
// Add OTLP exporter if enabled // Add OTLP exporter if enabled
if config.enable_otlp { if config.enable_otlp
if let Some(endpoint) = &config.otlp_endpoint { && let Some(endpoint) = &config.otlp_endpoint
info!("Configuring OTLP metrics exporter: {}", endpoint); {
info!("Configuring OTLP metrics exporter: {}", endpoint);
// In OpenTelemetry 0.31+, use MetricExporter::builder() (singular, not plural) // In OpenTelemetry 0.31+, use MetricExporter::builder() (singular, not plural)
let exporter = opentelemetry_otlp::MetricExporter::builder() let exporter = opentelemetry_otlp::MetricExporter::builder()
.with_tonic() .with_tonic()
.with_endpoint(endpoint.clone()) .with_endpoint(endpoint.clone())
.with_timeout(Duration::from_secs(10)) .with_timeout(Duration::from_secs(10))
.build()?; .build()?;
// PeriodicReader::builder() now takes only exporter, runtime is handled internally // PeriodicReader::builder() now takes only exporter, runtime is handled internally
let reader = PeriodicReader::builder(exporter) let reader = PeriodicReader::builder(exporter)
.with_interval(Duration::from_secs(30)) .with_interval(Duration::from_secs(30))
.build(); .build();
meter_provider_builder = meter_provider_builder.with_reader(reader); meter_provider_builder = meter_provider_builder.with_reader(reader);
}
} }
let meter_provider = meter_provider_builder.build(); let meter_provider = meter_provider_builder.build();
+36 -8
View File
@@ -28,7 +28,11 @@ fn test_component(name: &str) -> Entity {
async fn create_and_get_entity() { async fn create_and_get_entity() {
let db = TestDb::new().await; let db = TestDb::new().await;
let created = db.repository.create(&test_component("payment-api")).await.unwrap(); let created = db
.repository
.create(&test_component("payment-api"))
.await
.unwrap();
assert!(!created.id.is_empty()); assert!(!created.id.is_empty());
assert_eq!(created.kind, "Component"); assert_eq!(created.kind, "Component");
assert!(created.created_at.is_some()); assert!(created.created_at.is_some());
@@ -55,7 +59,11 @@ async fn get_nonexistent_returns_none() {
async fn update_entity() { async fn update_entity() {
let db = TestDb::new().await; let db = TestDb::new().await;
let created = db.repository.create(&test_component("auth-svc")).await.unwrap(); let created = db
.repository
.create(&test_component("auth-svc"))
.await
.unwrap();
let mut updated_data = test_component("auth-svc"); let mut updated_data = test_component("auth-svc");
if let Some(Metadata::ComponentMetadata(ref mut m)) = updated_data.metadata { if let Some(Metadata::ComponentMetadata(ref mut m)) = updated_data.metadata {
@@ -84,7 +92,11 @@ async fn update_entity() {
async fn delete_entity() { async fn delete_entity() {
let db = TestDb::new().await; let db = TestDb::new().await;
let created = db.repository.create(&test_component("to-delete")).await.unwrap(); let created = db
.repository
.create(&test_component("to-delete"))
.await
.unwrap();
let deleted = db.repository.delete(&created.id).await.unwrap(); let deleted = db.repository.delete(&created.id).await.unwrap();
assert!(deleted); assert!(deleted);
@@ -151,8 +163,14 @@ async fn list_paginated() {
async fn get_by_kind_and_name() { async fn get_by_kind_and_name() {
let db = TestDb::new().await; let db = TestDb::new().await;
db.repository.create(&test_component("unique-svc")).await.unwrap(); db.repository
db.repository.create(&test_component("other-svc")).await.unwrap(); .create(&test_component("unique-svc"))
.await
.unwrap();
db.repository
.create(&test_component("other-svc"))
.await
.unwrap();
let found = db let found = db
.repository .repository
@@ -176,7 +194,11 @@ async fn get_by_kind_and_name() {
async fn atomic_annotation_update() { async fn atomic_annotation_update() {
let db = TestDb::new().await; let db = TestDb::new().await;
let created = db.repository.create(&test_component("annotated-svc")).await.unwrap(); let created = db
.repository
.create(&test_component("annotated-svc"))
.await
.unwrap();
// First annotation update // First annotation update
let mut annotations1 = std::collections::HashMap::new(); let mut annotations1 = std::collections::HashMap::new();
@@ -198,6 +220,12 @@ async fn atomic_annotation_update() {
// Verify both annotations exist // Verify both annotations exist
let entity = db.repository.get_by_id(&created.id).await.unwrap().unwrap(); let entity = db.repository.get_by_id(&created.id).await.unwrap().unwrap();
assert_eq!(entity.annotations.get("defectdojo.com/product-id").unwrap(), "123"); assert_eq!(
assert_eq!(entity.annotations.get("github.com/repo").unwrap(), "org/repo"); entity.annotations.get("defectdojo.com/product-id").unwrap(),
"123"
);
assert_eq!(
entity.annotations.get("github.com/repo").unwrap(),
"org/repo"
);
} }