Public Access
initial-commit
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "charybdis-server"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[[bin]]
|
||||
name = "charybdis"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
charybdis = { path = ".." }
|
||||
charybdis_defectdojo = { package = "charybdis-defectdojo-plugin", path = "../plugins/defectdojo" }
|
||||
charybdis_dependencytrack = { package = "charybdis-dependencytrack-plugin", path = "../plugins/dependencytrack" }
|
||||
charybdis_keycloak = { package = "charybdis-keycloak-plugin", path = "../plugins/keycloak" }
|
||||
|
||||
# Re-export core dependencies needed by main
|
||||
tonic = { version = "0.14.2", features = ["tls-ring", "tls-connect-info"] }
|
||||
tonic-reflection = "0.14"
|
||||
tokio = { version = "1.48", features = ["full"] }
|
||||
tracing = "0.1"
|
||||
anyhow = "1.0"
|
||||
rustls = "0.23"
|
||||
@@ -0,0 +1,7 @@
|
||||
// This is a stub build.rs that just ensures OUT_DIR is available
|
||||
// The actual protobuf compilation happens in the main charybdis crate
|
||||
|
||||
fn main() {
|
||||
// Nothing to do - we just need this to exist so OUT_DIR is defined
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
}
|
||||
@@ -0,0 +1,419 @@
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tonic::transport::{Certificate, Identity, Server, ServerTlsConfig};
|
||||
use tonic_reflection::server::Builder as ReflectionBuilder;
|
||||
use tracing::{info, warn};
|
||||
|
||||
// Plugin imports
|
||||
extern crate charybdis_defectdojo;
|
||||
extern crate charybdis_keycloak;
|
||||
extern crate charybdis_dependencytrack;
|
||||
|
||||
// Import MyEntityService and EntityServiceServer from the library crate
|
||||
use charybdis::{
|
||||
MyEntityService,
|
||||
charybdis::entities::entity_service_server::EntityServiceServer,
|
||||
charybdis::ingestion::ingestion_service_server::IngestionServiceServer,
|
||||
config::Config,
|
||||
database,
|
||||
events::{EventBus, backends::MemoryEventBus},
|
||||
findings::ingestion::MyIngestionService,
|
||||
scanners::ParserRegistry,
|
||||
security::{
|
||||
audit::AuditLogger,
|
||||
config::{RoleMapping, RoleRule, SecurityConfig, SubjectMatch},
|
||||
interceptor::AuthInterceptor,
|
||||
rbac::RbacEngine,
|
||||
tls,
|
||||
},
|
||||
telemetry::{init_telemetry, shutdown_telemetry},
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
|
||||
// Use the descriptor from the charybdis library crate
|
||||
// The descriptor is compiled by the main charybdis crate's build.rs
|
||||
use charybdis::ENTITY_DESCRIPTOR_SET;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Install default crypto provider for rustls (required in rustls 0.23+)
|
||||
// Using ring as the crypto backend since we have tls-ring feature enabled
|
||||
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||
|
||||
// Load configuration from file or environment
|
||||
info!("Loading configuration...");
|
||||
let config = Config::load()?;
|
||||
info!("Configuration loaded successfully");
|
||||
|
||||
// Initialize OpenTelemetry (traces, metrics, logs)
|
||||
let metrics = init_telemetry(config.telemetry.clone())?;
|
||||
|
||||
let addr = format!("{}:{}", config.server.grpc_host, config.server.grpc_port).parse()?;
|
||||
|
||||
info!("Connecting to database...");
|
||||
let pool = database::create_connection_pool(&config.database.url).await?;
|
||||
|
||||
info!("Ensuring database schema exists...");
|
||||
database::ensure_schema(&pool).await?;
|
||||
info!("Database ready");
|
||||
|
||||
// Initialize event bus
|
||||
info!("Initializing event bus...");
|
||||
let event_bus: Arc<dyn EventBus> = Arc::new(MemoryEventBus::new());
|
||||
event_bus.start().await?;
|
||||
info!("Event bus started successfully");
|
||||
|
||||
// Initialize plugin system
|
||||
info!("Initializing plugin system...");
|
||||
let entity_repository = Arc::new(database::EntityRepository::new(pool.clone()));
|
||||
let mut plugin_manager = charybdis::plugins::manager::PluginManager::new();
|
||||
|
||||
// Load DefectDojo plugin if configured
|
||||
if let Some(dd_config) = &config.plugins.defectdojo {
|
||||
let enabled = dd_config.get("enabled")
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false);
|
||||
if !enabled {
|
||||
info!("DefectDojo plugin is disabled");
|
||||
} else {
|
||||
info!("Loading DefectDojo plugin...");
|
||||
match charybdis_defectdojo::DefectDojoConfig::from_toml(dd_config) {
|
||||
Ok(dd_config) => {
|
||||
match charybdis_defectdojo::DefectDojoPlugin::new(
|
||||
dd_config,
|
||||
entity_repository.clone(),
|
||||
) {
|
||||
Ok(plugin) => {
|
||||
plugin_manager.register_event_driven(Arc::new(plugin));
|
||||
info!("DefectDojo plugin registered successfully");
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to initialize DefectDojo plugin: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to parse DefectDojo configuration: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load Keycloak plugin if configured
|
||||
if let Some(kc_config) = &config.plugins.keycloak {
|
||||
let enabled = kc_config.get("enabled")
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false);
|
||||
if !enabled {
|
||||
info!("Keycloak plugin is disabled");
|
||||
} else {
|
||||
info!("Loading Keycloak plugin...");
|
||||
match charybdis_keycloak::KeycloakConfig::from_toml(kc_config) {
|
||||
Ok(kc_config) => {
|
||||
match charybdis_keycloak::KeycloakPlugin::new(
|
||||
kc_config,
|
||||
entity_repository.clone(),
|
||||
) {
|
||||
Ok(plugin) => {
|
||||
plugin_manager.register_sync(Arc::new(plugin));
|
||||
info!("Keycloak plugin registered successfully");
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to initialize Keycloak plugin: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to parse Keycloak configuration: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load Dependency-Track plugin if configured
|
||||
if let Some(dt_config) = &config.plugins.dependencytrack {
|
||||
let enabled = dt_config.get("enabled")
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false);
|
||||
if !enabled {
|
||||
info!("Dependency-Track plugin is disabled");
|
||||
} else {
|
||||
info!("Loading Dependency-Track plugin...");
|
||||
match charybdis_dependencytrack::DependencyTrackConfig::from_toml(dt_config) {
|
||||
Ok(dt_config) => {
|
||||
match charybdis_dependencytrack::DependencyTrackPlugin::new(
|
||||
dt_config,
|
||||
entity_repository.clone(),
|
||||
) {
|
||||
Ok(plugin) => {
|
||||
plugin_manager.register_event_driven(Arc::new(plugin));
|
||||
info!("Dependency-Track plugin registered successfully");
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to initialize Dependency-Track plugin: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to parse Dependency-Track configuration: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create event dispatcher and subscribe to event bus
|
||||
let event_plugins = plugin_manager.event_driven_plugins().to_vec();
|
||||
if !event_plugins.is_empty() {
|
||||
let dispatcher = Arc::new(charybdis::plugins::dispatcher::EventDispatcher::new(
|
||||
event_plugins,
|
||||
entity_repository.clone(),
|
||||
));
|
||||
|
||||
// Subscribe dispatcher to all entity events
|
||||
event_bus.subscribe(dispatcher).await?;
|
||||
|
||||
info!("Event dispatcher subscribed to event bus");
|
||||
}
|
||||
|
||||
info!(
|
||||
"Plugin system initialized with {} event-driven plugins",
|
||||
plugin_manager.event_driven_plugins().len()
|
||||
);
|
||||
|
||||
// Load security configuration
|
||||
let mut security_config = config.security.clone();
|
||||
|
||||
// Initialize security components if RBAC is enabled
|
||||
let auth_interceptor = if security_config.rbac.enabled {
|
||||
info!("Security (RBAC) is enabled");
|
||||
|
||||
// Configure default roles if none are defined
|
||||
if security_config.rbac.role_mappings.is_empty() {
|
||||
info!("No role mappings configured, setting up defaults");
|
||||
security_config = setup_default_rbac_config();
|
||||
}
|
||||
|
||||
let rbac_engine = Arc::new(RbacEngine::new(security_config.rbac.clone()));
|
||||
let audit_logger = Arc::new(AuditLogger::new(security_config.rbac.audit.enabled));
|
||||
Some(AuthInterceptor::new(rbac_engine, audit_logger))
|
||||
} else {
|
||||
warn!("Security (RBAC) is disabled - running in insecure mode");
|
||||
None
|
||||
};
|
||||
|
||||
// Clone repository for YAML adapter (EntityRepository is Clone)
|
||||
let yaml_repository = entity_repository.clone();
|
||||
|
||||
let my_entity_service = MyEntityService::new(
|
||||
(*entity_repository).clone(),
|
||||
event_bus,
|
||||
metrics,
|
||||
auth_interceptor,
|
||||
);
|
||||
|
||||
// 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);
|
||||
|
||||
// Configure and build the reflection service using the embedded descriptor set.
|
||||
// In tonic-reflection 0.14+, use build_v1() instead of build()
|
||||
let reflection_service = ReflectionBuilder::configure()
|
||||
.register_encoded_file_descriptor_set(ENTITY_DESCRIPTOR_SET)
|
||||
.build_v1()?;
|
||||
|
||||
// Configure server with optional mTLS
|
||||
let mut server_builder = Server::builder()
|
||||
.http2_keepalive_interval(Some(Duration::from_secs(60)))
|
||||
.initial_connection_window_size(1048576)
|
||||
.initial_stream_window_size(1048576)
|
||||
.http2_max_header_list_size(64 * 1024); // 64KB, generous for gRPC metadata
|
||||
|
||||
if security_config.mtls.enabled {
|
||||
info!("mTLS is enabled - configuring TLS");
|
||||
|
||||
// Validate certificate files exist
|
||||
tls::validate_cert_files(&security_config.mtls)?;
|
||||
|
||||
// Load certificate files
|
||||
let (server_cert, server_key, ca_cert) = tls::load_tls_files(&security_config.mtls)?;
|
||||
|
||||
// Create TLS identity and CA certificate
|
||||
let identity = Identity::from_pem(&server_cert, &server_key);
|
||||
let client_ca = Certificate::from_pem(&ca_cert);
|
||||
|
||||
// Configure TLS with client certificate verification
|
||||
let tls_config = ServerTlsConfig::new()
|
||||
.identity(identity)
|
||||
.client_ca_root(client_ca);
|
||||
|
||||
server_builder = server_builder.tls_config(tls_config)?;
|
||||
info!("mTLS configuration complete");
|
||||
} else {
|
||||
warn!("mTLS is disabled - running without transport security");
|
||||
}
|
||||
|
||||
info!("EntityService server listening on {}", addr);
|
||||
|
||||
// Start YAML adapter if enabled
|
||||
let yaml_adapter_handle = if config.server.yaml_adapter.enabled {
|
||||
let yaml_host = config.server.yaml_adapter.host.clone();
|
||||
let yaml_port = config.server.yaml_adapter.port;
|
||||
let yaml_repo = yaml_repository.clone();
|
||||
let base_url = format!("http://{}:{}", yaml_host, yaml_port);
|
||||
|
||||
info!("Starting YAML adapter on {}:{}", yaml_host, yaml_port);
|
||||
|
||||
Some(tokio::spawn(async move {
|
||||
if let Err(e) = charybdis::adapters::yaml::start_yaml_adapter(
|
||||
yaml_host, yaml_port, yaml_repo, base_url,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!("YAML adapter error: {}", e);
|
||||
}
|
||||
}))
|
||||
} else {
|
||||
info!("YAML adapter is disabled");
|
||||
None
|
||||
};
|
||||
|
||||
// Run the server with graceful shutdown
|
||||
let server = server_builder
|
||||
.add_service(EntityServiceServer::new(my_entity_service))
|
||||
.add_service(IngestionServiceServer::new(my_ingestion_service))
|
||||
.add_service(reflection_service)
|
||||
.serve(addr);
|
||||
|
||||
// Handle shutdown
|
||||
tokio::select! {
|
||||
result = server => {
|
||||
if let Err(e) = result {
|
||||
tracing::error!("Server error: {}", e);
|
||||
}
|
||||
}
|
||||
_ = tokio::signal::ctrl_c() => {
|
||||
info!("Received shutdown signal");
|
||||
}
|
||||
}
|
||||
|
||||
// Abort YAML adapter if it's running
|
||||
if let Some(handle) = yaml_adapter_handle {
|
||||
handle.abort();
|
||||
}
|
||||
|
||||
// Gracefully shutdown telemetry
|
||||
shutdown_telemetry().await;
|
||||
info!("Shutdown complete");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Setup default RBAC configuration for testing/development
|
||||
/// Maps certificate attributes to roles based on OU (Organizational Unit)
|
||||
fn setup_default_rbac_config() -> SecurityConfig {
|
||||
use charybdis::security::config::{AuditConfig, RbacConfig};
|
||||
|
||||
let mut permissions = HashMap::new();
|
||||
|
||||
// Admin role: full access
|
||||
permissions.insert(
|
||||
"admin".to_string(),
|
||||
vec![
|
||||
"entity:create".to_string(),
|
||||
"entity:read".to_string(),
|
||||
"entity:update".to_string(),
|
||||
"entity:delete".to_string(),
|
||||
"entity:list".to_string(),
|
||||
],
|
||||
);
|
||||
|
||||
// Platform team: full access (mapped from platform-team OU)
|
||||
permissions.insert(
|
||||
"platform".to_string(),
|
||||
vec![
|
||||
"entity:create".to_string(),
|
||||
"entity:read".to_string(),
|
||||
"entity:update".to_string(),
|
||||
"entity:delete".to_string(),
|
||||
"entity:list".to_string(),
|
||||
],
|
||||
);
|
||||
|
||||
// Automation/CI: create, read, update, list
|
||||
permissions.insert(
|
||||
"automation".to_string(),
|
||||
vec![
|
||||
"entity:create".to_string(),
|
||||
"entity:read".to_string(),
|
||||
"entity:update".to_string(),
|
||||
"entity:list".to_string(),
|
||||
],
|
||||
);
|
||||
|
||||
// Plugins: read and list only
|
||||
permissions.insert(
|
||||
"plugin".to_string(),
|
||||
vec!["entity:read".to_string(), "entity:list".to_string()],
|
||||
);
|
||||
|
||||
let role_mappings = vec![
|
||||
// Map platform-team OU to platform role
|
||||
RoleMapping {
|
||||
role: "platform".to_string(),
|
||||
rules: vec![RoleRule {
|
||||
subject: SubjectMatch {
|
||||
cn: None,
|
||||
ou: Some("platform-team".to_string()),
|
||||
o: None,
|
||||
},
|
||||
}],
|
||||
},
|
||||
// Map automation OU to automation role
|
||||
RoleMapping {
|
||||
role: "automation".to_string(),
|
||||
rules: vec![RoleRule {
|
||||
subject: SubjectMatch {
|
||||
cn: None,
|
||||
ou: Some("automation".to_string()),
|
||||
o: None,
|
||||
},
|
||||
}],
|
||||
},
|
||||
// Map plugins OU to plugin role
|
||||
RoleMapping {
|
||||
role: "plugin".to_string(),
|
||||
rules: vec![RoleRule {
|
||||
subject: SubjectMatch {
|
||||
cn: None,
|
||||
ou: Some("plugins".to_string()),
|
||||
o: None,
|
||||
},
|
||||
}],
|
||||
},
|
||||
];
|
||||
|
||||
SecurityConfig {
|
||||
mtls: charybdis::security::config::MtlsConfig {
|
||||
enabled: std::env::var("SECURITY_MTLS_ENABLED")
|
||||
.unwrap_or_else(|_| "false".to_string())
|
||||
.parse()
|
||||
.unwrap_or(false),
|
||||
server_cert: std::env::var("SECURITY_MTLS_SERVER_CERT").unwrap_or_default(),
|
||||
server_key: std::env::var("SECURITY_MTLS_SERVER_KEY").unwrap_or_default(),
|
||||
client_ca_cert: std::env::var("SECURITY_MTLS_CLIENT_CA").unwrap_or_default(),
|
||||
require_client_cert: true,
|
||||
crl_file: None,
|
||||
},
|
||||
rbac: RbacConfig {
|
||||
enabled: true,
|
||||
role_mappings,
|
||||
permissions,
|
||||
audit: AuditConfig {
|
||||
enabled: true,
|
||||
log_all_requests: true,
|
||||
log_denied_requests: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user