Public Access
fix: format and lint
This commit is contained in:
@@ -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(¤t_section, plugin_data) {
|
&& let Some(plugin) = build_plugin_config(¤t_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(¤t_section, plugin_data) {
|
&& let Some(plugin) = build_plugin_config(¤t_section, plugin_data)
|
||||||
plugins.push(plugin);
|
{
|
||||||
}
|
plugins.push(plugin);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(plugins)
|
Ok(plugins)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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(())
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||||
|
|||||||
@@ -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
@@ -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::*;
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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()
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -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(", "))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user