use crate::error::{AdvisoryError, Result};
use dotenvy::dotenv;
use serde::Deserialize;
use std::env;
#[derive(Debug, Clone, Deserialize)]
pub struct Config {
pub ghsa_token: Option<String>,
pub nvd_api_key: Option<String>,
pub redis_url: String,
#[serde(default)]
pub ossindex: Option<OssIndexConfig>,
#[serde(default)]
pub nvd: NvdConfig,
#[serde(default)]
pub store: StoreConfig,
#[serde(default)]
pub log_to_file: bool,
#[serde(default = "default_log_dir")]
pub log_dir: String,
}
fn default_log_dir() -> String {
"logs".to_string()
}
#[derive(Debug, Clone, Deserialize)]
pub struct NvdConfig {
pub requests_per_window: Option<u32>,
pub window_seconds: Option<u64>,
pub max_results: Option<u32>,
pub max_days_range: Option<i64>,
}
impl Default for NvdConfig {
fn default() -> Self {
Self {
requests_per_window: None, window_seconds: Some(30),
max_results: None,
max_days_range: Some(120),
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct OssIndexConfig {
pub user: Option<String>,
pub token: Option<String>,
#[serde(default = "default_ossindex_batch_size")]
pub batch_size: usize,
}
fn default_ossindex_batch_size() -> usize {
128
}
impl Default for OssIndexConfig {
fn default() -> Self {
Self {
user: None,
token: None,
batch_size: 128,
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct StoreConfig {
pub ttl_seconds: Option<u64>,
#[serde(default = "default_compression_level")]
pub compression_level: i32,
#[serde(default = "default_key_prefix")]
pub key_prefix: String,
}
fn default_compression_level() -> i32 {
3
}
fn default_key_prefix() -> String {
"vuln".to_string()
}
impl Default for StoreConfig {
fn default() -> Self {
Self {
ttl_seconds: None,
compression_level: 3,
key_prefix: "vuln".to_string(),
}
}
}
impl Config {
pub fn from_env() -> Result<Self> {
dotenv().ok();
let ghsa_token = env::var("VULNERA__APIS__GHSA__TOKEN").ok();
let nvd_api_key = env::var("VULNERA__APIS__NVD__API_KEY").ok();
let redis_url =
env::var("REDIS_URL").unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string());
let ossindex = {
let user = env::var("OSSINDEX_USER").ok();
let token = env::var("OSSINDEX_TOKEN").ok();
if user.is_some() || token.is_some() {
Some(OssIndexConfig {
user,
token,
batch_size: 128,
})
} else {
None
}
};
let ttl_seconds = env::var("VULNERA__STORE__TTL_SECONDS")
.ok()
.and_then(|s| s.parse().ok());
let nvd = NvdConfig {
requests_per_window: env::var("VULNERA__NVD__REQUESTS_PER_WINDOW")
.ok()
.and_then(|s| s.parse().ok()),
window_seconds: env::var("VULNERA__NVD__WINDOW_SECONDS")
.ok()
.and_then(|s| s.parse().ok()),
max_results: env::var("VULNERA__NVD__MAX_RESULTS")
.ok()
.and_then(|s| s.parse().ok()),
max_days_range: Some(120),
};
let store = StoreConfig {
ttl_seconds,
compression_level: env::var("VULNERA__STORE__COMPRESSION_LEVEL")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(3),
key_prefix: env::var("VULNERA__STORE__KEY_PREFIX")
.unwrap_or_else(|_| "vuln".to_string()),
};
let log_to_file = env::var("VULNERA_LOG_TO_FILE")
.map(|v| v.to_lowercase() == "true")
.unwrap_or(false);
let log_dir = env::var("VULNERA_LOG_DIR").unwrap_or_else(|_| "logs".to_string());
Ok(Self {
ghsa_token,
nvd_api_key,
redis_url,
ossindex,
nvd,
store,
log_to_file,
log_dir,
})
}
pub fn for_testing(redis_url: &str) -> Self {
Self {
ghsa_token: None,
nvd_api_key: None,
redis_url: redis_url.to_string(),
ossindex: None,
nvd: NvdConfig::default(),
store: StoreConfig::default(),
log_to_file: false,
log_dir: "logs".to_string(),
}
}
pub fn validate_for_ghsa(&self) -> Result<&str> {
self.ghsa_token.as_deref().ok_or_else(|| {
AdvisoryError::config("GHSA token is required (set VULNERA__APIS__GHSA__TOKEN)")
})
}
}