diff --git a/build.rs b/build.rs index cf873b7..c6b4d1c 100644 --- a/build.rs +++ b/build.rs @@ -85,10 +85,10 @@ fn parse_plugins_config(config: &str) -> Result, Box Result, Box Result<(), Box> { // Load DefectDojo plugin if configured 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()) .unwrap_or(false); if !enabled { @@ -101,7 +102,8 @@ async fn main() -> Result<(), Box> { // Load Keycloak plugin if configured 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()) .unwrap_or(false); if !enabled { @@ -132,7 +134,8 @@ async fn main() -> Result<(), Box> { // Load Dependency-Track plugin if configured 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()) .unwrap_or(false); if !enabled { @@ -213,8 +216,7 @@ async fn main() -> Result<(), Box> { // Initialize parser registry and ingestion service let parser_registry = Arc::new(ParserRegistry::with_builtins()); - let my_ingestion_service = - MyIngestionService::new(entity_repository.clone(), parser_registry); + let my_ingestion_service = MyIngestionService::new(entity_repository.clone(), parser_registry); // Configure and build the reflection service using the embedded descriptor set. // In tonic-reflection 0.14+, use build_v1() instead of build() diff --git a/plugins/defectdojo/src/handlers/product.rs b/plugins/defectdojo/src/handlers/product.rs index 82cb02b..0238c15 100644 --- a/plugins/defectdojo/src/handlers/product.rs +++ b/plugins/defectdojo/src/handlers/product.rs @@ -95,17 +95,18 @@ impl ProductHandler { let mapped = self.field_mapper.map_all(entity).await?; // 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))); - let description = mapped.get("description").cloned() - .unwrap_or_else(|| { - let desc = self.extract_description(entity); - if desc.is_empty() { - json!(format!("Managed by Charybdis (entity: {})", entity.id)) - } else { - json!(desc) - } - }); + let description = mapped.get("description").cloned().unwrap_or_else(|| { + let desc = self.extract_description(entity); + if desc.is_empty() { + json!(format!("Managed by Charybdis (entity: {})", entity.id)) + } else { + json!(desc) + } + }); // Build DefectDojo product payload let mut payload = json!({ @@ -315,7 +316,10 @@ impl ProductHandler { match &group.spec { 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); } } @@ -382,11 +386,7 @@ impl ProductHandler { } /// Assign resolved users as product members in DefectDojo - async fn assign_owner_to_product( - &self, - product_id: i32, - owner: &str, - ) -> Result> { + async fn assign_owner_to_product(&self, product_id: i32, owner: &str) -> Result> { let resolved = self.resolve_owner_to_users(owner).await?; if resolved.is_empty() { @@ -471,9 +471,7 @@ impl ProductHandler { fn extract_owner(&self, entity: &Entity) -> Option { use charybdis::charybdis::entities::entity; match &entity.spec { - Some(entity::Spec::ComponentSpec(s)) if !s.owner.is_empty() => { - Some(s.owner.clone()) - } + Some(entity::Spec::ComponentSpec(s)) if !s.owner.is_empty() => Some(s.owner.clone()), _ => None, } } @@ -501,7 +499,10 @@ impl ResourceHandler for ProductHandler { // Build annotations to store DefectDojo product ID 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 if self.config.default_engagement.auto_create { @@ -516,7 +517,10 @@ impl ResourceHandler for ProductHandler { .await { 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!( "Auto-created engagement {} for product {} (entity: {})", engagement_id, product_id, entity.id @@ -552,7 +556,9 @@ impl ResourceHandler for ProductHandler { } // 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 Ok(None) @@ -571,8 +577,13 @@ impl ResourceHandler for ProductHandler { // Update only annotations with product ID let mut annotations = HashMap::new(); - annotations.insert("defectdojo.com/product-id".to_string(), product_id.to_string()); - self.repository.update_annotations(&entity.id, annotations).await?; + annotations.insert( + "defectdojo.com/product-id".to_string(), + product_id.to_string(), + ); + self.repository + .update_annotations(&entity.id, annotations) + .await?; } Ok(()) diff --git a/plugins/defectdojo/tests/integration_tests.rs b/plugins/defectdojo/tests/integration_tests.rs index 4576ae8..b87c3e6 100644 --- a/plugins/defectdojo/tests/integration_tests.rs +++ b/plugins/defectdojo/tests/integration_tests.rs @@ -8,7 +8,9 @@ use charybdis::charybdis::entities::entity::{Metadata, Spec}; use charybdis::charybdis::entities::Entity; use charybdis::database::{ensure_schema, EntityRepository}; use charybdis::plugins::ResourceHandler; -use charybdis_defectdojo::{DefectDojoClient, DefectDojoConfig, EngagementConfig, OwnerResolutionConfig}; +use charybdis_defectdojo::{ + DefectDojoClient, DefectDojoConfig, EngagementConfig, OwnerResolutionConfig, +}; use serde_json::json; use sqlx::PgPool; use std::collections::HashMap; @@ -17,9 +19,11 @@ use wiremock::matchers::{method, path, query_param}; use wiremock::{Mock, MockServer, ResponseTemplate}; async fn setup_db() -> PgPool { - let url = 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 url = + 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"); ensure_schema(&pool).await.expect("Failed to create schema"); // 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; let client = DefectDojoClient::new(mock_server.uri(), "test-token".to_string()).unwrap(); - let engagement_handler = Arc::new( - charybdis_defectdojo::handlers::EngagementHandler::new( - client.clone(), - repository.clone(), - HashMap::new(), - ), - ); + let engagement_handler = Arc::new(charybdis_defectdojo::handlers::EngagementHandler::new( + client.clone(), + repository.clone(), + HashMap::new(), + )); let handler = charybdis_defectdojo::handlers::ProductHandler::new( 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) 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 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 client = DefectDojoClient::new(mock_server.uri(), "test-token".to_string()).unwrap(); - let engagement_handler = Arc::new( - charybdis_defectdojo::handlers::EngagementHandler::new( - client.clone(), - repository.clone(), - HashMap::new(), - ), - ); + let engagement_handler = Arc::new(charybdis_defectdojo::handlers::EngagementHandler::new( + client.clone(), + repository.clone(), + HashMap::new(), + )); let handler = charybdis_defectdojo::handlers::ProductHandler::new( client, @@ -196,11 +199,13 @@ async fn test_product_update_with_existing_product_id() { // Create entity with existing product-id annotation let mut entity = make_component_entity("", "payment-api", "team-payments"); - entity.annotations.insert( - "defectdojo.com/product-id".to_string(), - "42".to_string(), - ); - let entity = repository.create(&entity).await.expect("Failed to create entity"); + entity + .annotations + .insert("defectdojo.com/product-id".to_string(), "42".to_string()); + let entity = repository + .create(&entity) + .await + .expect("Failed to create entity"); // Trigger update handler let result = handler.handle_update(&entity).await; @@ -228,13 +233,11 @@ async fn test_product_deletion() { let config = make_config(&mock_server.uri()); let client = DefectDojoClient::new(mock_server.uri(), "test-token".to_string()).unwrap(); - let engagement_handler = Arc::new( - charybdis_defectdojo::handlers::EngagementHandler::new( - client.clone(), - repository.clone(), - HashMap::new(), - ), - ); + let engagement_handler = Arc::new(charybdis_defectdojo::handlers::EngagementHandler::new( + client.clone(), + repository.clone(), + HashMap::new(), + )); let handler = charybdis_defectdojo::handlers::ProductHandler::new( client, @@ -246,11 +249,13 @@ async fn test_product_deletion() { // Entity with product-id annotation let mut entity = make_component_entity("", "payment-api", "team-payments"); - entity.annotations.insert( - "defectdojo.com/product-id".to_string(), - "42".to_string(), - ); - let entity = repository.create(&entity).await.expect("Failed to create entity"); + entity + .annotations + .insert("defectdojo.com/product-id".to_string(), "42".to_string()); + let entity = repository + .create(&entity) + .await + .expect("Failed to create entity"); // Trigger delete handler 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()], })), }; - 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(); // 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()], })), }; - 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(); // Create handlers let client = DefectDojoClient::new(mock_server.uri(), "test-token".to_string()).unwrap(); - let engagement_handler = Arc::new( - charybdis_defectdojo::handlers::EngagementHandler::new( - client.clone(), - repository.clone(), - HashMap::new(), - ), - ); + let engagement_handler = Arc::new(charybdis_defectdojo::handlers::EngagementHandler::new( + client.clone(), + repository.clone(), + HashMap::new(), + )); let handler = charybdis_defectdojo::handlers::ProductHandler::new( client, @@ -489,7 +498,9 @@ async fn test_owner_resolution_assigns_product_members() { ); // Owner member IDs should be set assert!( - updated.annotations.contains_key("defectdojo.com/owner-member-ids"), + updated + .annotations + .contains_key("defectdojo.com/owner-member-ids"), "Expected owner-member-ids annotation" ); } @@ -529,13 +540,11 @@ async fn test_product_creation_without_engagement() { config.default_engagement.auto_create = false; let client = DefectDojoClient::new(mock_server.uri(), "test-token".to_string()).unwrap(); - let engagement_handler = Arc::new( - charybdis_defectdojo::handlers::EngagementHandler::new( - client.clone(), - repository.clone(), - HashMap::new(), - ), - ); + let engagement_handler = Arc::new(charybdis_defectdojo::handlers::EngagementHandler::new( + client.clone(), + repository.clone(), + HashMap::new(), + )); let handler = charybdis_defectdojo::handlers::ProductHandler::new( client, @@ -557,7 +566,9 @@ async fn test_product_creation_without_engagement() { updated.annotations.get("defectdojo.com/product-id"), 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 client = DefectDojoClient::new(mock_server.uri(), "test-token".to_string()).unwrap(); - let engagement_handler = Arc::new( - charybdis_defectdojo::handlers::EngagementHandler::new( - client.clone(), - repository.clone(), - HashMap::new(), - ), - ); + let engagement_handler = Arc::new(charybdis_defectdojo::handlers::EngagementHandler::new( + client.clone(), + repository.clone(), + HashMap::new(), + )); let handler = charybdis_defectdojo::handlers::ProductHandler::new( client, diff --git a/plugins/keycloak/src/lib.rs b/plugins/keycloak/src/lib.rs index 54e33ae..e34a28f 100644 --- a/plugins/keycloak/src/lib.rs +++ b/plugins/keycloak/src/lib.rs @@ -179,7 +179,8 @@ impl KeycloakClient { pub async fn authenticate(&self) -> Result<()> { info!("Authenticating with Keycloak (client credentials grant)"); - let response = self.raw_client + let response = self + .raw_client .post(&self.token_url) .form(&[ ("grant_type", "client_credentials"), @@ -222,7 +223,8 @@ impl KeycloakClient { let url = format!("{}/{}", self.http.base_url(), path.trim_start_matches('/')); - let response = self.raw_client + let response = self + .raw_client .get(&url) .header("Authorization", format!("Bearer {}", token)) .header("Accept", "application/json") @@ -258,7 +260,11 @@ impl KeycloakClient { 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) } @@ -266,7 +272,11 @@ impl KeycloakClient { pub async fn fetch_groups(&self) -> Result> { let response = self.get("groups?briefRepresentation=false").await?; let groups: Vec = 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) } diff --git a/plugins/keycloak/src/sync.rs b/plugins/keycloak/src/sync.rs index 710c2cd..7530e83 100644 --- a/plugins/keycloak/src/sync.rs +++ b/plugins/keycloak/src/sync.rs @@ -194,10 +194,7 @@ fn build_group_entity( let mut annotations = HashMap::new(); annotations.insert("keycloak.com/group-id".to_string(), kc_group.id.clone()); - annotations.insert( - "keycloak.com/group-path".to_string(), - kc_group.path.clone(), - ); + annotations.insert("keycloak.com/group-path".to_string(), kc_group.path.clone()); annotations.insert("keycloak.com/realm".to_string(), config.realm.clone()); annotations.insert( "keycloak.com/member-count".to_string(), diff --git a/src/adapters/yaml/backstage.rs b/src/adapters/yaml/backstage.rs index 1d27a11..63e23fc 100644 --- a/src/adapters/yaml/backstage.rs +++ b/src/adapters/yaml/backstage.rs @@ -308,7 +308,10 @@ fn extract_metadata(entity: &Entity) -> Result } let mut annotations = serde_json::Map::new(); 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() { annotations.insert("keycloak.org/realm".to_string(), json!(m.realm)); @@ -465,10 +468,10 @@ fn extract_spec(entity: &Entity) -> Option { Some(Spec::DefectdojoSpec(s)) => { // DefectDojo plugin spec - parse config_json if present let mut spec = json!({}); - if !s.config_json.is_empty() { - if let Ok(config) = serde_json::from_str::(&s.config_json) { - spec = config; - } + if !s.config_json.is_empty() + && let Ok(config) = serde_json::from_str::(&s.config_json) + { + spec = config; } if !s.sync_status.is_empty() { spec["sync_status"] = json!(s.sync_status); @@ -478,10 +481,10 @@ fn extract_spec(entity: &Entity) -> Option { Some(Spec::KeycloakSpec(s)) => { // Keycloak plugin spec - parse config_json if present let mut spec = json!({}); - if !s.config_json.is_empty() { - if let Ok(config) = serde_json::from_str::(&s.config_json) { - spec = config; - } + if !s.config_json.is_empty() + && let Ok(config) = serde_json::from_str::(&s.config_json) + { + spec = config; } if !s.sync_status.is_empty() { spec["sync_status"] = json!(s.sync_status); @@ -497,10 +500,10 @@ fn extract_spec(entity: &Entity) -> Option { Some(Spec::DependencytrackSpec(s)) => { // Dependency-Track plugin spec - parse config_json if present let mut spec = json!({}); - if !s.config_json.is_empty() { - if let Ok(config) = serde_json::from_str::(&s.config_json) { - spec = config; - } + if !s.config_json.is_empty() + && let Ok(config) = serde_json::from_str::(&s.config_json) + { + spec = config; } if !s.sync_status.is_empty() { spec["sync_status"] = json!(s.sync_status); diff --git a/src/adapters/yaml/mod.rs b/src/adapters/yaml/mod.rs index d020b8d..b8dbf7b 100644 --- a/src/adapters/yaml/mod.rs +++ b/src/adapters/yaml/mod.rs @@ -71,16 +71,16 @@ async fn get_locations(State(state): State) -> Response { // Check cache first { let cache = state.cache.read().await; - if let Some(ref cached) = *cache { - if cached.cached_at.elapsed() < state.cache_ttl { - debug!("Serving /yaml/locations from cache"); - return ( - StatusCode::OK, - [("content-type", "text/yaml; charset=utf-8")], - cached.data.clone(), - ) - .into_response(); - } + if let Some(ref cached) = *cache + && cached.cached_at.elapsed() < state.cache_ttl + { + debug!("Serving /yaml/locations from cache"); + return ( + StatusCode::OK, + [("content-type", "text/yaml; charset=utf-8")], + cached.data.clone(), + ) + .into_response(); } } diff --git a/src/config.rs b/src/config.rs index 112a99d..908fb55 100644 --- a/src/config.rs +++ b/src/config.rs @@ -231,8 +231,7 @@ impl Config { fn substitute_env_vars(content: &str) -> Result { let mut result = content.to_string(); // Match ${VAR_NAME} or ${VAR_NAME:-default_value} - let var_pattern = - regex::Regex::new(r"\$\{([A-Z_][A-Z0-9_]*)(?::-((?:[^}])*))?\}").unwrap(); + let var_pattern = regex::Regex::new(r"\$\{([A-Z_][A-Z0-9_]*)(?::-((?:[^}])*))?\}").unwrap(); for capture in var_pattern.captures_iter(content) { let full_match = &capture[0]; diff --git a/src/database.rs b/src/database.rs index 7e79770..922044c 100644 --- a/src/database.rs +++ b/src/database.rs @@ -403,10 +403,14 @@ impl EntityRepository { // Decode cursor: "timestamp|uuid" (pipe separator since RFC3339 contains colons) let cursor = page_token.and_then(|token| { 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 (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()?; Some((ts, id)) }); @@ -673,7 +677,10 @@ impl EntityRepository { Ok(Some((entity_data_bytes, merged_annotations_json))) => { // Rebuild protobuf blob with merged annotations let mut entity = Entity::decode(entity_data_bytes.as_slice())?; - if let Ok(merged) = serde_json::from_value::>(merged_annotations_json) { + if let Ok(merged) = serde_json::from_value::< + std::collections::HashMap, + >(merged_annotations_json) + { entity.annotations = merged; } entity.updated_at = Some(prost_types::Timestamp { @@ -872,56 +879,112 @@ fn apply_field_mask( // Nested fields - annotations path if path.starts_with("annotations.") => { - if let Some(key) = path.strip_prefix("annotations.") { - if let Some(value) = partial.annotations.get(key) { - existing.annotations.insert(key.to_string(), value.clone()); - } + if let Some(key) = path.strip_prefix("annotations.") + && let Some(value) = partial.annotations.get(key) + { + existing.annotations.insert(key.to_string(), value.clone()); } } // Component metadata fields - "component_metadata.name" => apply_metadata_field!(partial, existing, ComponentMetadata, name), - "component_metadata.namespace" => apply_metadata_field!(partial, existing, ComponentMetadata, namespace), - "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_metadata.name" => { + apply_metadata_field!(partial, existing, ComponentMetadata, name) + } + "component_metadata.namespace" => { + apply_metadata_field!(partial, existing, ComponentMetadata, namespace) + } + "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.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.system" => apply_spec_field!(partial, existing, ComponentSpec, system), - "component_spec.subcomponent_of" => apply_spec_field!(partial, existing, ComponentSpec, subcomponent_of), - "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), + "component_spec.subcomponent_of" => { + apply_spec_field!(partial, existing, ComponentSpec, subcomponent_of) + } + "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.name" => apply_metadata_field!(partial, existing, ServiceMetadata, name), - "service_metadata.namespace" => apply_metadata_field!(partial, existing, ServiceMetadata, namespace), - "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_metadata.name" => { + apply_metadata_field!(partial, existing, ServiceMetadata, name) + } + "service_metadata.namespace" => { + apply_metadata_field!(partial, existing, ServiceMetadata, namespace) + } + "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.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.system" => apply_spec_field!(partial, existing, ServiceSpec, system), - "service_spec.subcomponent_of" => apply_spec_field!(partial, existing, ServiceSpec, subcomponent_of), - "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), + "service_spec.subcomponent_of" => { + apply_spec_field!(partial, existing, ServiceSpec, subcomponent_of) + } + "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.name" => apply_metadata_field!(partial, existing, SystemMetadata, name), - "system_metadata.namespace" => apply_metadata_field!(partial, existing, SystemMetadata, namespace), - "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_metadata.name" => { + apply_metadata_field!(partial, existing, SystemMetadata, name) + } + "system_metadata.namespace" => { + apply_metadata_field!(partial, existing, SystemMetadata, namespace) + } + "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.owner" => apply_spec_field!(partial, existing, SystemSpec, owner), @@ -929,8 +992,12 @@ fn apply_field_mask( // API metadata fields "api_metadata.name" => apply_metadata_field!(partial, existing, ApiMetadata, name), - "api_metadata.namespace" => apply_metadata_field!(partial, existing, ApiMetadata, namespace), - "api_metadata.description" => apply_metadata_field!(partial, existing, ApiMetadata, description), + "api_metadata.namespace" => { + 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.tags" => apply_metadata_field!(partial, existing, ApiMetadata, tags), "api_metadata.links" => apply_metadata_field!(partial, existing, ApiMetadata, links), @@ -944,9 +1011,15 @@ fn apply_field_mask( // User metadata fields "user_metadata.name" => apply_metadata_field!(partial, existing, UserMetadata, name), - "user_metadata.namespace" => apply_metadata_field!(partial, existing, UserMetadata, namespace), - "user_metadata.description" => apply_metadata_field!(partial, existing, UserMetadata, description), - "user_metadata.labels" => apply_metadata_field!(partial, existing, UserMetadata, labels), + "user_metadata.namespace" => { + apply_metadata_field!(partial, existing, UserMetadata, namespace) + } + "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.links" => apply_metadata_field!(partial, existing, UserMetadata, links), @@ -956,43 +1029,48 @@ fn apply_field_mask( // User spec nested profile fields "user_spec.profile.display_name" => { - if let Some(Spec::UserSpec(ref partial_spec)) = partial.spec { - if let Some(Spec::UserSpec(ref mut existing_spec)) = existing.spec { - if 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(); - } - } + if let Some(Spec::UserSpec(ref partial_spec)) = partial.spec + && let Some(Spec::UserSpec(ref mut existing_spec)) = existing.spec + && 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(); } } "user_spec.profile.email" => { - if let Some(Spec::UserSpec(ref partial_spec)) = partial.spec { - if let Some(Spec::UserSpec(ref mut existing_spec)) = existing.spec { - if let Some(ref partial_profile) = partial_spec.profile { - let profile = existing_spec.profile.get_or_insert_default(); - profile.email = partial_profile.email.clone(); - } - } + if let Some(Spec::UserSpec(ref partial_spec)) = partial.spec + && let Some(Spec::UserSpec(ref mut existing_spec)) = existing.spec + && let Some(ref partial_profile) = partial_spec.profile + { + let profile = existing_spec.profile.get_or_insert_default(); + profile.email = partial_profile.email.clone(); } } "user_spec.profile.picture" => { - if let Some(Spec::UserSpec(ref partial_spec)) = partial.spec { - if let Some(Spec::UserSpec(ref mut existing_spec)) = existing.spec { - if let Some(ref partial_profile) = partial_spec.profile { - let profile = existing_spec.profile.get_or_insert_default(); - profile.picture = partial_profile.picture.clone(); - } - } + if let Some(Spec::UserSpec(ref partial_spec)) = partial.spec + && let Some(Spec::UserSpec(ref mut existing_spec)) = existing.spec + && let Some(ref partial_profile) = partial_spec.profile + { + let profile = existing_spec.profile.get_or_insert_default(); + profile.picture = partial_profile.picture.clone(); } } // Group metadata fields "group_metadata.name" => apply_metadata_field!(partial, existing, GroupMetadata, name), - "group_metadata.namespace" => apply_metadata_field!(partial, existing, GroupMetadata, namespace), - "group_metadata.description" => apply_metadata_field!(partial, existing, GroupMetadata, description), - "group_metadata.labels" => apply_metadata_field!(partial, existing, GroupMetadata, labels), + "group_metadata.namespace" => { + apply_metadata_field!(partial, existing, GroupMetadata, namespace) + } + "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.links" => apply_metadata_field!(partial, existing, GroupMetadata, links), + "group_metadata.links" => { + apply_metadata_field!(partial, existing, GroupMetadata, links) + } // Group spec fields "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.profile.display_name" => { - if let Some(Spec::GroupSpec(ref partial_spec)) = partial.spec { - if let Some(Spec::GroupSpec(ref mut existing_spec)) = existing.spec { - if 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(); - } - } + if let Some(Spec::GroupSpec(ref partial_spec)) = partial.spec + && let Some(Spec::GroupSpec(ref mut existing_spec)) = existing.spec + && 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(); } } "group_spec.profile.email" => { - if let Some(Spec::GroupSpec(ref partial_spec)) = partial.spec { - if let Some(Spec::GroupSpec(ref mut existing_spec)) = existing.spec { - if let Some(ref partial_profile) = partial_spec.profile { - let profile = existing_spec.profile.get_or_insert_default(); - profile.email = partial_profile.email.clone(); - } - } + if let Some(Spec::GroupSpec(ref partial_spec)) = partial.spec + && let Some(Spec::GroupSpec(ref mut existing_spec)) = existing.spec + && let Some(ref partial_profile) = partial_spec.profile + { + let profile = existing_spec.profile.get_or_insert_default(); + profile.email = partial_profile.email.clone(); } } "group_spec.profile.picture" => { - if let Some(Spec::GroupSpec(ref partial_spec)) = partial.spec { - if let Some(Spec::GroupSpec(ref mut existing_spec)) = existing.spec { - if let Some(ref partial_profile) = partial_spec.profile { - let profile = existing_spec.profile.get_or_insert_default(); - profile.picture = partial_profile.picture.clone(); - } - } + if let Some(Spec::GroupSpec(ref partial_spec)) = partial.spec + && let Some(Spec::GroupSpec(ref mut existing_spec)) = existing.spec + && let Some(ref partial_profile) = partial_spec.profile + { + let profile = existing_spec.profile.get_or_insert_default(); + profile.picture = partial_profile.picture.clone(); } } // Domain metadata fields - "domain_metadata.name" => apply_metadata_field!(partial, existing, DomainMetadata, name), - "domain_metadata.namespace" => apply_metadata_field!(partial, existing, DomainMetadata, namespace), - "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_metadata.name" => { + apply_metadata_field!(partial, existing, DomainMetadata, name) + } + "domain_metadata.namespace" => { + apply_metadata_field!(partial, existing, DomainMetadata, namespace) + } + "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.owner" => apply_spec_field!(partial, existing, DomainSpec, owner), // Resource metadata fields - "resource_metadata.name" => apply_metadata_field!(partial, existing, ResourceMetadata, name), - "resource_metadata.namespace" => apply_metadata_field!(partial, existing, ResourceMetadata, namespace), - "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_metadata.name" => { + apply_metadata_field!(partial, existing, ResourceMetadata, name) + } + "resource_metadata.namespace" => { + apply_metadata_field!(partial, existing, ResourceMetadata, namespace) + } + "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.type" => apply_spec_field!(partial, existing, ResourceSpec, r#type), "resource_spec.owner" => apply_spec_field!(partial, existing, ResourceSpec, owner), "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); diff --git a/src/events/backends/memory.rs b/src/events/backends/memory.rs index 91ab2b1..1e161a7 100644 --- a/src/events/backends/memory.rs +++ b/src/events/backends/memory.rs @@ -51,10 +51,10 @@ impl EventBus for MemoryEventBus { ); // Send through broadcast channel if we have one - if let Some(sender) = &self.sender { - if let Err(e) = sender.send(event.clone()) { - error!("Failed to send event through broadcast channel: {}", e); - } + if let Some(sender) = &self.sender + && let Err(e) = sender.send(event.clone()) + { + error!("Failed to send event through broadcast channel: {}", e); } // Always dispatch directly to handlers diff --git a/src/events/mod.rs b/src/events/mod.rs index 396607e..bbc34cc 100644 --- a/src/events/mod.rs +++ b/src/events/mod.rs @@ -4,11 +4,11 @@ //! Events are published when entities are created, updated, or deleted. //! 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 bus; +pub mod handler; +pub mod types; -pub use types::*; -pub use handler::*; pub use bus::*; +pub use handler::*; +pub use types::*; diff --git a/src/findings/fingerprint.rs b/src/findings/fingerprint.rs index 5967866..f21ec69 100644 --- a/src/findings/fingerprint.rs +++ b/src/findings/fingerprint.rs @@ -10,10 +10,7 @@ use crate::scanners::NormalizedFinding; /// 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 /// twice in the same file, they collapse into one finding. -pub fn compute_fingerprint( - scanner: &str, - finding: &NormalizedFinding, -) -> String { +pub fn compute_fingerprint(scanner: &str, finding: &NormalizedFinding) -> String { if let Some(ref fp) = finding.scanner_fingerprint { return fp.clone(); } @@ -79,7 +76,10 @@ mod tests { let fp1 = compute_fingerprint("scanner", &f1); 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] diff --git a/src/findings/ingestion.rs b/src/findings/ingestion.rs index b4fa133..b13137b 100644 --- a/src/findings/ingestion.rs +++ b/src/findings/ingestion.rs @@ -10,7 +10,7 @@ use crate::charybdis::ingestion::{ ReconciliationSummary, }; use crate::database::EntityRepository; -use crate::findings::reconciler::{ReconciliationEngine, ReconciledFinding}; +use crate::findings::reconciler::{ReconciledFinding, ReconciliationEngine}; use crate::scanners::{ParserRegistry, Severity}; pub struct MyIngestionService { @@ -61,7 +61,6 @@ impl MyIngestionService { } } - #[tonic::async_trait] impl IngestionService for MyIngestionService { #[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 parser = self - .parser_registry - .get(&req.format) - .ok_or_else(|| Status::invalid_argument(format!("Unsupported format: {}", req.format)))?; + let parser = self.parser_registry.get(&req.format).ok_or_else(|| { + Status::invalid_argument(format!("Unsupported format: {}", req.format)) + })?; let report = parser .parse(&req.data) @@ -108,7 +106,12 @@ impl IngestionService for MyIngestionService { let result = self .reconciler - .reconcile(&component_id, &req.lifecycle, &scanner_name, &report.findings) + .reconcile( + &component_id, + &req.lifecycle, + &scanner_name, + &report.findings, + ) .await .map_err(|e| { 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 parser = self - .parser_registry - .get(&req.format) - .ok_or_else(|| Status::invalid_argument(format!("Unsupported format: {}", req.format)))?; + let parser = self.parser_registry.get(&req.format).ok_or_else(|| { + Status::invalid_argument(format!("Unsupported format: {}", req.format)) + })?; let report = parser .parse(&req.data) @@ -192,7 +194,12 @@ impl IngestionService for MyIngestionService { let result = self .reconciler - .reconcile(&component_id, &req.lifecycle, &scanner_name, &report.findings) + .reconcile( + &component_id, + &req.lifecycle, + &scanner_name, + &report.findings, + ) .await .map_err(|e| { error!(error = %e, "Reconciliation failed"); diff --git a/src/findings/reconciler.rs b/src/findings/reconciler.rs index 3f62a26..9743891 100644 --- a/src/findings/reconciler.rs +++ b/src/findings/reconciler.rs @@ -5,9 +5,11 @@ use anyhow::Result; use chrono::Utc; use tracing::{info, instrument}; -use crate::charybdis::core::{FindingMetadata, FindingSpec, FindingState, Severity as ProtoSeverity}; -use crate::charybdis::entities::entity::{Metadata, Spec}; +use crate::charybdis::core::{ + FindingMetadata, FindingSpec, FindingState, Severity as ProtoSeverity, +}; use crate::charybdis::entities::Entity; +use crate::charybdis::entities::entity::{Metadata, Spec}; use crate::database::EntityRepository; use crate::findings::fingerprint::compute_fingerprint; use crate::scanners::{NormalizedFinding, Severity}; @@ -51,7 +53,9 @@ impl ReconciliationEngine { scanner: &str, incoming: &[NormalizedFinding], ) -> Result { - 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 = HashMap::new(); for entity in existing { @@ -140,8 +144,16 @@ impl ReconciliationEngine { .unwrap_or_default(), proto_severity_to_domain(spec.severity), spec.rule_id.clone(), - if spec.file_path.is_empty() { None } else { Some(spec.file_path.clone()) }, - if spec.line_start == 0 { None } else { Some(spec.line_start) }, + if spec.file_path.is_empty() { + 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), }) @@ -216,29 +228,27 @@ impl ReconciliationEngine { for reconciled in &result.unchanged { if let Some(ref entity_id) = reconciled.entity_id { let mut annotations = std::collections::HashMap::new(); - annotations.insert( - "charybdis.io/last-seen".to_string(), - now.to_rfc3339(), - ); - annotations.insert( - "charybdis.io/scan-id".to_string(), - scan_id.to_string(), - ); - self.repository.update_annotations(entity_id, annotations).await?; + annotations.insert("charybdis.io/last-seen".to_string(), now.to_rfc3339()); + annotations.insert("charybdis.io/scan-id".to_string(), scan_id.to_string()); + self.repository + .update_annotations(entity_id, annotations) + .await?; } } // Resolve findings no longer detected for reconciled in &result.resolved { 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 for reconciled in &result.reopened { 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(), fixed_version: finding.fixed_version.clone().unwrap_or_default(), details_url: finding.details_url.clone().unwrap_or_default(), - first_seen: Some(timestamp.clone()), - last_seen: Some(timestamp.clone()), + first_seen: Some(*timestamp), + last_seen: Some(*timestamp), resolved_at: None, scan_id: scan_id.to_string(), }; diff --git a/src/lib.rs b/src/lib.rs index 269bfc2..30ba80d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,6 +16,7 @@ pub mod security; pub mod telemetry; // Include the generated protobuf code with serde support +#[allow(clippy::large_enum_variant)] pub mod charybdis { pub mod core { tonic::include_proto!("charybdis.core"); @@ -106,14 +107,10 @@ impl EntityService for MyEntityService { let entity_kind = entity_data.kind.clone(); tracing::Span::current().record("entity.kind", &entity_kind); - let created_entity = self - .repository - .create(&entity_data) - .await - .map_err(|e| { - error!(error = %e, "Failed to create entity"); - Status::from(e) - })?; + let created_entity = self.repository.create(&entity_data).await.map_err(|e| { + error!(error = %e, "Failed to create entity"); + Status::from(e) + })?; let entity_id = created_entity.id.clone(); 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"); Status::from(e) })? - .ok_or_else(|| { - Status::not_found(format!("Entity not found: {}", entity_id)) - })?; + .ok_or_else(|| Status::not_found(format!("Entity not found: {}", entity_id)))?; 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"); Status::from(e) })? - .ok_or_else(|| { - Status::not_found(format!("Entity not found: {}", entity_id)) - })?; + .ok_or_else(|| Status::not_found(format!("Entity not found: {}", entity_id)))?; let entity_kind = entity.kind.clone(); tracing::Span::current().record("entity.kind", &entity_kind); @@ -278,7 +271,10 @@ impl EntityService for MyEntityService { })?; 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"); @@ -314,10 +310,26 @@ impl EntityService for MyEntityService { }; let req = request.into_inner(); - let kind = if req.kind.is_empty() { None } else { 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 kind = if req.kind.is_empty() { + None + } else { + 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 .repository diff --git a/src/main.rs b/src/main.rs index fdcec84..d574254 100644 --- a/src/main.rs +++ b/src/main.rs @@ -135,8 +135,7 @@ async fn main() -> Result<(), Box> { // Initialize parser registry and ingestion service let parser_registry = Arc::new(ParserRegistry::with_builtins()); - let my_ingestion_service = - MyIngestionService::new(entity_repository.clone(), parser_registry); + let my_ingestion_service = MyIngestionService::new(entity_repository.clone(), parser_registry); // Configure and build the reflection service using the embedded descriptor set. // In tonic-reflection 0.14+, use build_v1() instead of build() diff --git a/src/plugins/field_mapper.rs b/src/plugins/field_mapper.rs index 46c3663..377de15 100644 --- a/src/plugins/field_mapper.rs +++ b/src/plugins/field_mapper.rs @@ -275,7 +275,7 @@ impl FieldMapper { pub async fn map_all(&self, entity: &Entity) -> Result> { 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 { Ok(value) => { result.insert(field_name.clone(), value); @@ -540,7 +540,8 @@ fn entity_to_json(entity: &Entity) -> Result { if let Some(profile) = &s.profile { let mut profile_obj = serde_json::Map::new(); 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() { profile_obj.insert("email".to_string(), json!(&profile.email)); @@ -563,7 +564,8 @@ fn entity_to_json(entity: &Entity) -> Result { if let Some(profile) = &s.profile { let mut profile_obj = serde_json::Map::new(); 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() { profile_obj.insert("email".to_string(), json!(&profile.email)); diff --git a/src/plugins/http_client.rs b/src/plugins/http_client.rs index 8ed1957..3007070 100644 --- a/src/plugins/http_client.rs +++ b/src/plugins/http_client.rs @@ -285,20 +285,19 @@ impl PluginHttpClient { let request = build_request(); match request.send().await { Ok(response) => { - if response.status() == StatusCode::TOO_MANY_REQUESTS + if (response.status() == StatusCode::TOO_MANY_REQUESTS || response.status() == StatusCode::BAD_GATEWAY || 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()); - last_error = Some(anyhow!( - "HTTP {} (attempt {})", - response.status(), - attempt + 1 - )); - continue; - } + debug!("Retryable status {}, will retry", response.status()); + last_error = Some(anyhow!( + "HTTP {} (attempt {})", + response.status(), + attempt + 1 + )); + continue; } return Ok(response); } diff --git a/src/plugins/sync_scheduler.rs b/src/plugins/sync_scheduler.rs index 21e708f..d90bd60 100644 --- a/src/plugins/sync_scheduler.rs +++ b/src/plugins/sync_scheduler.rs @@ -107,10 +107,10 @@ impl SyncScheduler { info!("Sync scheduler started with tokio_cron_scheduler"); self.scheduler = Some(scheduler); - if let Some(s) = &self.scheduler { - if let Err(e) = s.start().await { - error!("Failed to start scheduler: {}", e); - } + if let Some(s) = &self.scheduler + && let Err(e) = s.start().await + { + error!("Failed to start scheduler: {}", e); } } else { info!("Sync scheduler started with manual cron implementation"); diff --git a/src/scanners/sarif.rs b/src/scanners/sarif.rs index 11ef8fb..65523fd 100644 --- a/src/scanners/sarif.rs +++ b/src/scanners/sarif.rs @@ -149,10 +149,10 @@ fn extract_scanner_fingerprint(result: &SarifResult) -> Option { return Some(v.clone()); } } - if let Some(ref fps) = result.fingerprints { - if let Some(v) = fps.values().next() { - return Some(v.clone()); - } + if let Some(ref fps) = result.fingerprints + && let Some(v) = fps.values().next() + { + return Some(v.clone()); } None } diff --git a/src/security/config.rs b/src/security/config.rs index c6f750a..20bbc3d 100644 --- a/src/security/config.rs +++ b/src/security/config.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; -#[derive(Debug, Clone, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize, Default)] pub struct SecurityConfig { #[serde(default)] pub mtls: MtlsConfig, @@ -10,15 +10,6 @@ pub struct SecurityConfig { pub rbac: RbacConfig, } -impl Default for SecurityConfig { - fn default() -> Self { - Self { - mtls: MtlsConfig::default(), - rbac: RbacConfig::default(), - } - } -} - #[derive(Debug, Clone, Deserialize, Serialize)] pub struct MtlsConfig { #[serde(default)] @@ -57,7 +48,7 @@ impl Default for MtlsConfig { } } -#[derive(Debug, Clone, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize, Default)] pub struct RbacConfig { #[serde(default)] pub enabled: bool, @@ -72,17 +63,6 @@ pub struct RbacConfig { 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)] pub struct RoleMapping { pub role: String, diff --git a/src/security/identity.rs b/src/security/identity.rs index 8800821..5d309bf 100644 --- a/src/security/identity.rs +++ b/src/security/identity.rs @@ -1,3 +1,5 @@ +use std::fmt; + use thiserror::Error; use tracing::debug; @@ -72,9 +74,10 @@ impl ClientIdentity { certificate_serial: serial, }) } +} - /// Display identity for logging - pub fn to_string(&self) -> String { +impl fmt::Display for ClientIdentity { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut parts = vec![format!("CN={}", self.common_name)]; if let Some(ref ou) = self.organizational_unit { @@ -85,7 +88,7 @@ impl ClientIdentity { parts.push(format!("O={}", o)); } - parts.join(", ") + f.write_str(&parts.join(", ")) } } diff --git a/src/security/interceptor.rs b/src/security/interceptor.rs index e10e3a9..e94dfea 100644 --- a/src/security/interceptor.rs +++ b/src/security/interceptor.rs @@ -45,7 +45,7 @@ impl AuthInterceptor { tracing::Span::current().record("client.cn", &identity.common_name); debug!( - identity = %identity.to_string(), + identity = %identity, "Extracted client identity from certificate" ); @@ -62,10 +62,7 @@ impl AuthInterceptor { "No role assigned to identity", duration, ); - Status::permission_denied(format!( - "No role assigned to identity: {}", - identity.to_string() - )) + Status::permission_denied(format!("No role assigned to identity: {}", identity)) })?; tracing::Span::current().record("role", &role); @@ -135,7 +132,7 @@ impl AuthInterceptor { } debug!( - identity = %identity.to_string(), + identity = %identity, role = %role, "Request authorized" ); @@ -150,13 +147,12 @@ impl AuthInterceptor { // Try 1: Extract from Tonic's TlsConnectInfo (direct mTLS) // This requires the tls-connect-info feature enabled in Tonic // Note: Must use TlsConnectInfo, not TlsConnectInfo - if let Some(connect_info) = request.extensions().get::>() { - if let Some(certs) = connect_info.peer_certs() { - if let Some(cert) = certs.first() { - debug!("Extracted certificate from TlsConnectInfo (direct mTLS)"); - return Ok(cert.as_ref().to_vec()); - } - } + if let Some(connect_info) = request.extensions().get::>() + && let Some(certs) = connect_info.peer_certs() + && let Some(cert) = certs.first() + { + debug!("Extracted certificate from TlsConnectInfo (direct mTLS)"); + return Ok(cert.as_ref().to_vec()); } // Try 2: Extract from headers (reverse proxy injected certificate) diff --git a/src/security/tls.rs b/src/security/tls.rs index b4468b1..ce630fc 100644 --- a/src/security/tls.rs +++ b/src/security/tls.rs @@ -127,10 +127,10 @@ pub fn validate_cert_files(config: &MtlsConfig) -> Result<()> { } // Check CRL if specified - if let Some(ref crl_file) = config.crl_file { - if !std::path::Path::new(crl_file).exists() { - anyhow::bail!("CRL file not found: {}", crl_file); - } + if let Some(ref crl_file) = config.crl_file + && !std::path::Path::new(crl_file).exists() + { + anyhow::bail!("CRL file not found: {}", crl_file); } debug!("All certificate files validated successfully"); diff --git a/src/telemetry/setup.rs b/src/telemetry/setup.rs index 0355587..fa0aa86 100644 --- a/src/telemetry/setup.rs +++ b/src/telemetry/setup.rs @@ -76,20 +76,20 @@ fn init_tracer( .with_resource(resource); // Add OTLP exporter if enabled - if config.enable_otlp { - if let Some(endpoint) = &config.otlp_endpoint { - info!("Configuring OTLP trace exporter: {}", endpoint); + if config.enable_otlp + && let Some(endpoint) = &config.otlp_endpoint + { + info!("Configuring OTLP trace exporter: {}", endpoint); - // In OpenTelemetry 0.31+, use SpanExporter::builder() - let exporter = opentelemetry_otlp::SpanExporter::builder() - .with_tonic() - .with_endpoint(endpoint.clone()) - .with_timeout(Duration::from_secs(10)) - .build()?; + // In OpenTelemetry 0.31+, use SpanExporter::builder() + let exporter = opentelemetry_otlp::SpanExporter::builder() + .with_tonic() + .with_endpoint(endpoint.clone()) + .with_timeout(Duration::from_secs(10)) + .build()?; - // In OpenTelemetry 0.31+, runtime parameter is no longer needed - builder = builder.with_batch_exporter(exporter); - } + // In OpenTelemetry 0.31+, runtime parameter is no longer needed + builder = builder.with_batch_exporter(exporter); } let provider = builder.build(); @@ -108,24 +108,24 @@ fn init_metrics( let mut meter_provider_builder = SdkMeterProvider::builder().with_resource(resource); // Add OTLP exporter if enabled - if config.enable_otlp { - if let Some(endpoint) = &config.otlp_endpoint { - info!("Configuring OTLP metrics exporter: {}", endpoint); + if config.enable_otlp + && let Some(endpoint) = &config.otlp_endpoint + { + info!("Configuring OTLP metrics exporter: {}", endpoint); - // In OpenTelemetry 0.31+, use MetricExporter::builder() (singular, not plural) - let exporter = opentelemetry_otlp::MetricExporter::builder() - .with_tonic() - .with_endpoint(endpoint.clone()) - .with_timeout(Duration::from_secs(10)) - .build()?; + // In OpenTelemetry 0.31+, use MetricExporter::builder() (singular, not plural) + let exporter = opentelemetry_otlp::MetricExporter::builder() + .with_tonic() + .with_endpoint(endpoint.clone()) + .with_timeout(Duration::from_secs(10)) + .build()?; - // PeriodicReader::builder() now takes only exporter, runtime is handled internally - let reader = PeriodicReader::builder(exporter) - .with_interval(Duration::from_secs(30)) - .build(); + // PeriodicReader::builder() now takes only exporter, runtime is handled internally + let reader = PeriodicReader::builder(exporter) + .with_interval(Duration::from_secs(30)) + .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(); diff --git a/tests/entity_crud.rs b/tests/entity_crud.rs index 1cfda41..38cf6d5 100644 --- a/tests/entity_crud.rs +++ b/tests/entity_crud.rs @@ -28,7 +28,11 @@ fn test_component(name: &str) -> Entity { async fn create_and_get_entity() { 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_eq!(created.kind, "Component"); assert!(created.created_at.is_some()); @@ -55,7 +59,11 @@ async fn get_nonexistent_returns_none() { async fn update_entity() { 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"); if let Some(Metadata::ComponentMetadata(ref mut m)) = updated_data.metadata { @@ -84,7 +92,11 @@ async fn update_entity() { async fn delete_entity() { 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(); assert!(deleted); @@ -151,8 +163,14 @@ async fn list_paginated() { async fn get_by_kind_and_name() { let db = TestDb::new().await; - db.repository.create(&test_component("unique-svc")).await.unwrap(); - db.repository.create(&test_component("other-svc")).await.unwrap(); + db.repository + .create(&test_component("unique-svc")) + .await + .unwrap(); + db.repository + .create(&test_component("other-svc")) + .await + .unwrap(); let found = db .repository @@ -176,7 +194,11 @@ async fn get_by_kind_and_name() { async fn atomic_annotation_update() { 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 let mut annotations1 = std::collections::HashMap::new(); @@ -198,6 +220,12 @@ async fn atomic_annotation_update() { // Verify both annotations exist 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!(entity.annotations.get("github.com/repo").unwrap(), "org/repo"); + assert_eq!( + entity.annotations.get("defectdojo.com/product-id").unwrap(), + "123" + ); + assert_eq!( + entity.annotations.get("github.com/repo").unwrap(), + "org/repo" + ); }