pub mod core;
pub mod loader;
pub mod plugins;
pub mod profiles;
pub mod secrets;
pub mod targets;
pub mod validation;
pub use core::{
AppConfig, CacheConfig, DatabaseConfig, Environment, FeatureFlags, HttpConfig,
ObservabilityConfig, RateLimitConfig, RedisConfig, RetryStrategy, ScannerConfig,
SecretsBackend, SecurityConfig, ServerConfig, SslMode, VaultConfig,
};
pub use loader::{load_config_with_overrides, ConfigFormat, ConfigLoader, HotReloadManager};
pub use plugins::{
FindingSeverity, Plugin, PluginCapability, PluginConfig, PluginDefinition, PluginFinding,
PluginInput, PluginManager, PluginMetadata, PluginOutput, PluginType,
};
pub use profiles::{
ComplianceConfig, ComplianceFramework, PayloadConfig, PayloadSet, ProfileRegistry,
ProfileSettings, ScanProfile,
};
pub use secrets::{EnvVarsProvider, Secret, SecretsManager, SecretsProvider, VaultProvider};
pub use targets::{
ApiKeyConfig, AuthConfig, AuthType, Credentials, CustomAuthConfig, ExclusionConfig,
OAuthConfig, ProxyConfig, ScopeConfig, TargetConfig, TargetRateLimit,
};
pub use validation::{ConfigValidator, ValidationReport};
use anyhow::Result;
use std::path::Path;
use std::sync::Arc;
pub struct ConfigManager {
app_config: Arc<parking_lot::RwLock<AppConfig>>,
profile_registry: Arc<parking_lot::RwLock<ProfileRegistry>>,
target_config: Arc<parking_lot::RwLock<TargetConfig>>,
secrets_manager: Arc<SecretsManager>,
plugin_manager: Arc<parking_lot::RwLock<PluginManager>>,
}
impl ConfigManager {
pub async fn new(config_path: &Path) -> Result<Self> {
let loader = ConfigLoader::new(config_path)?;
let app_config = loader.load_config()?;
let secrets_manager = SecretsManager::from_backend(
app_config.security.secrets_backend,
app_config.security.vault.clone(),
)
.await?;
let profile_registry = ProfileRegistry::new();
let target_config = TargetConfig::default();
let plugin_manager = PluginManager::new(PluginConfig::default());
Ok(Self {
app_config: Arc::new(parking_lot::RwLock::new(app_config)),
profile_registry: Arc::new(parking_lot::RwLock::new(profile_registry)),
target_config: Arc::new(parking_lot::RwLock::new(target_config)),
secrets_manager: Arc::new(secrets_manager),
plugin_manager: Arc::new(parking_lot::RwLock::new(plugin_manager)),
})
}
pub fn get_app_config(&self) -> AppConfig {
self.app_config.read().clone()
}
pub fn update_app_config<F>(&self, updater: F) -> Result<()>
where
F: FnOnce(&mut AppConfig),
{
let mut config = self.app_config.write();
updater(&mut *config);
ConfigValidator::validate_app_config(&*config)?;
Ok(())
}
pub fn get_profile(&self, name: &str) -> Option<ScanProfile> {
self.profile_registry.read().get(name).cloned()
}
pub fn register_profile(&self, profile: ScanProfile) -> Result<()> {
ConfigValidator::validate_scan_profile(&profile)?;
self.profile_registry.write().register(profile);
Ok(())
}
pub fn list_profiles(&self) -> Vec<ScanProfile> {
self.profile_registry
.read()
.list()
.iter()
.map(|p| (*p).clone())
.collect()
}
pub fn get_target_config(&self) -> TargetConfig {
self.target_config.read().clone()
}
pub fn update_target_config<F>(&self, updater: F) -> Result<()>
where
F: FnOnce(&mut TargetConfig),
{
let mut config = self.target_config.write();
updater(&mut *config);
ConfigValidator::validate_target_config(&*config)?;
Ok(())
}
pub fn get_secret(&self, key: &str) -> Result<String> {
self.secrets_manager.get_secret(key)
}
pub fn set_secret(&self, key: &str, value: &str) -> Result<()> {
self.secrets_manager.set_secret(key, value)
}
pub fn get_plugin_manager(&self) -> Arc<parking_lot::RwLock<PluginManager>> {
Arc::clone(&self.plugin_manager)
}
pub fn validate_config(&self) -> ValidationReport {
let config = self.get_app_config();
ConfigValidator::generate_validation_report(&config)
}
pub async fn reload_from_file(&self, config_path: &Path) -> Result<()> {
let loader = ConfigLoader::new(config_path)?;
let new_config = loader.load_config()?;
ConfigValidator::validate_app_config(&new_config)?;
{
let mut config = self.app_config.write();
*config = new_config;
}
tracing::info!("Configuration reloaded successfully from {:?}", config_path);
Ok(())
}
pub fn load_profiles_from_directory(&self, dir_path: &Path) -> Result<()> {
let loader = ConfigLoader::new(dir_path.join("dummy.yaml"))?;
let new_registry = loader.load_profiles_from_directory(dir_path)?;
{
let mut registry = self.profile_registry.write();
*registry = new_registry;
}
tracing::info!("Profiles loaded from directory: {:?}", dir_path);
Ok(())
}
pub fn load_target_config_from_file(&self, target_path: &Path) -> Result<()> {
let loader = ConfigLoader::new(target_path)?;
let new_target_config = loader.load_target_config(target_path)?;
ConfigValidator::validate_target_config(&new_target_config)?;
{
let mut config = self.target_config.write();
*config = new_target_config;
}
tracing::info!("Target configuration loaded from {:?}", target_path);
Ok(())
}
}
pub fn create_default_config() -> AppConfig {
AppConfig {
server: ServerConfig {
port: 8080,
host: "0.0.0.0".to_string(),
environment: Environment::Development,
workers: num_cpus::get(),
graceful_shutdown: true,
shutdown_timeout_secs: 30,
},
redis: RedisConfig {
url: "redis://127.0.0.1:6379".to_string(),
pool_size: 20,
cluster_mode: false,
connection_timeout_secs: 5,
max_retries: 3,
enable_tls: false,
username: None,
password: None,
},
database: DatabaseConfig {
enabled: false,
url: "postgresql://lonkero:lonkero@localhost:5432/lonkero".to_string(),
pool_size: 20,
batch_size: 250,
connection_timeout_secs: 10,
auto_migrate: true,
ssl_mode: SslMode::Prefer,
},
scanner: ScannerConfig {
max_concurrency: 100,
request_timeout_secs: 30,
max_retries: 2,
retry_strategy: RetryStrategy::Exponential,
rate_limiting: RateLimitConfig {
enabled: true,
requests_per_second: 100,
adaptive: true,
per_target: false,
burst_size: None,
},
http: HttpConfig {
http2_enabled: true,
http3_enabled: false,
http2_adaptive_window: true,
http2_max_concurrent_streams: 100,
pool_max_idle_per_host: 20,
pool_idle_timeout_enabled: true,
pool_idle_timeout_secs: 90,
tcp_keepalive: true,
tcp_nodelay: false,
},
cache: CacheConfig {
enabled: true,
max_capacity: 10000,
ttl_secs: 300,
dns_cache_enabled: true,
response_cache_enabled: false,
},
custom_headers: std::collections::HashMap::new(),
user_agent: None,
follow_redirects: true,
max_redirects: 5,
subdomain_enum_enabled: false,
subdomain_enum_thorough: false,
cdn_detection_enabled: true,
early_termination_enabled: false,
http2_enabled: true,
http2_adaptive_window: true,
http2_max_concurrent_streams: 100,
pool_max_idle_per_host: 10,
cache_enabled: true,
cache_max_capacity: 10000,
cache_ttl_secs: 300,
dns_cache_enabled: true,
rate_limit_enabled: true,
rate_limit_rps: 100,
rate_limit_adaptive: true,
request_batching_enabled: false,
batch_size: 50,
adaptive_concurrency_enabled: true,
initial_concurrency: 10,
max_concurrency_per_target: 50,
},
security: SecurityConfig {
secrets_backend: SecretsBackend::EnvVars,
vault: None,
tls_verify: true,
tls_cert_path: None,
tls_key_path: None,
allowed_cipher_suites: Vec::new(),
},
features: FeatureFlags::default(),
observability: ObservabilityConfig::default(),
}
}
impl AppConfig {
pub fn from_env() -> Result<Self> {
let mut config = create_default_config();
if let Ok(redis_url) = std::env::var("REDIS_URL") {
config.redis.url = redis_url;
}
if let Ok(db_url) = std::env::var("DATABASE_URL") {
config.database.url = db_url;
config.database.enabled = true;
}
if let Ok(concurrency) = std::env::var("MAX_CONCURRENCY") {
config.scanner.max_concurrency = concurrency
.parse()
.map_err(|_| anyhow::anyhow!("Invalid MAX_CONCURRENCY value"))?;
}
if let Ok(workers) = std::env::var("WORKERS") {
config.server.workers = workers
.parse()
.map_err(|_| anyhow::anyhow!("Invalid WORKERS value"))?;
}
if let Ok(port) = std::env::var("SERVER_PORT") {
config.server.port = port
.parse()
.map_err(|_| anyhow::anyhow!("Invalid SERVER_PORT value"))?;
}
if let Ok(log_level) = std::env::var("LOG_LEVEL") {
config.observability.log_level = log_level;
}
if let Ok(env_str) = std::env::var("ENVIRONMENT") {
config.server.environment = env_str.parse().unwrap_or(Environment::Development);
}
Ok(config)
}
}