vulnera_advisor/
config.rs

1//! Configuration types for the vulnera-advisors crate.
2//!
3//! This module provides configuration structures for all components including
4//! sources, storage, and rate limiting.
5
6use crate::error::{AdvisoryError, Result};
7use dotenvy::dotenv;
8use serde::Deserialize;
9use std::env;
10
11/// Main configuration for VulnerabilityManager.
12#[derive(Debug, Clone, Deserialize)]
13pub struct Config {
14    /// GitHub Personal Access Token for GHSA API.
15    pub ghsa_token: Option<String>,
16    /// NVD API key (optional, but recommended for higher rate limits).
17    pub nvd_api_key: Option<String>,
18    /// Redis/DragonflyDB connection URL.
19    pub redis_url: String,
20    /// OSS Index configuration (optional).
21    #[serde(default)]
22    pub ossindex: Option<OssIndexConfig>,
23    /// NVD source configuration.
24    #[serde(default)]
25    pub nvd: NvdConfig,
26    /// Store configuration.
27    #[serde(default)]
28    pub store: StoreConfig,
29    /// Enable logging to file instead of stdout.
30    #[serde(default)]
31    pub log_to_file: bool,
32    /// Directory for log files (default: "logs").
33    #[serde(default = "default_log_dir")]
34    pub log_dir: String,
35}
36
37fn default_log_dir() -> String {
38    "logs".to_string()
39}
40
41/// Configuration for the NVD source.
42#[derive(Debug, Clone, Deserialize)]
43pub struct NvdConfig {
44    /// Maximum number of requests per time window.
45    /// Default: 50 with API key, 5 without.
46    pub requests_per_window: Option<u32>,
47    /// Time window in seconds for rate limiting.
48    /// Default: 30 seconds.
49    pub window_seconds: Option<u64>,
50    /// Maximum results to fetch per sync (None = unlimited).
51    /// Set this to limit initial sync size.
52    pub max_results: Option<u32>,
53    /// Maximum days to look back for incremental sync.
54    /// NVD API has a 120-day limit.
55    pub max_days_range: Option<i64>,
56}
57
58impl Default for NvdConfig {
59    fn default() -> Self {
60        Self {
61            requests_per_window: None, // Will use 50/5 based on API key
62            window_seconds: Some(30),
63            max_results: None,
64            max_days_range: Some(120),
65        }
66    }
67}
68
69/// Configuration for OSS Index source.
70#[derive(Debug, Clone, Deserialize)]
71pub struct OssIndexConfig {
72    /// OSS Index username (email) for authenticated requests.
73    pub user: Option<String>,
74    /// OSS Index API token.
75    pub token: Option<String>,
76    /// Maximum components per batch request (max: 128).
77    #[serde(default = "default_ossindex_batch_size")]
78    pub batch_size: usize,
79}
80
81fn default_ossindex_batch_size() -> usize {
82    128
83}
84
85impl Default for OssIndexConfig {
86    fn default() -> Self {
87        Self {
88            user: None,
89            token: None,
90            batch_size: 128,
91        }
92    }
93}
94
95/// Configuration for the advisory store.
96#[derive(Debug, Clone, Deserialize)]
97pub struct StoreConfig {
98    /// TTL in seconds for advisory data (None = no expiration).
99    pub ttl_seconds: Option<u64>,
100    /// Compression level for zstd (1-22, default: 3).
101    #[serde(default = "default_compression_level")]
102    pub compression_level: i32,
103    /// Prefix for all Redis keys.
104    #[serde(default = "default_key_prefix")]
105    pub key_prefix: String,
106}
107
108fn default_compression_level() -> i32 {
109    3
110}
111
112fn default_key_prefix() -> String {
113    "vuln".to_string()
114}
115
116impl Default for StoreConfig {
117    fn default() -> Self {
118        Self {
119            ttl_seconds: None,
120            compression_level: 3,
121            key_prefix: "vuln".to_string(),
122        }
123    }
124}
125
126impl Config {
127    /// Load configuration from environment variables.
128    ///
129    /// # Environment Variables
130    ///
131    /// - `VULNERA__APIS__GHSA__TOKEN` - GitHub token for GHSA (required for GHSA source)
132    /// - `VULNERA__APIS__NVD__API_KEY` - NVD API key (optional)
133    /// - `REDIS_URL` - Redis connection URL (default: `redis://127.0.0.1:6379`)
134    /// - `OSSINDEX_USER` - OSS Index username (optional)
135    /// - `OSSINDEX_TOKEN` - OSS Index token (optional)
136    /// - `VULNERA__STORE__TTL_SECONDS` - Advisory TTL in seconds (optional)
137    ///
138    /// # Errors
139    ///
140    /// Returns `AdvisoryError::Config` if required variables are missing.
141    pub fn from_env() -> Result<Self> {
142        dotenv().ok();
143
144        let ghsa_token = env::var("VULNERA__APIS__GHSA__TOKEN").ok();
145        let nvd_api_key = env::var("VULNERA__APIS__NVD__API_KEY").ok();
146
147        let redis_url =
148            env::var("REDIS_URL").unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string());
149
150        let ossindex = {
151            let user = env::var("OSSINDEX_USER").ok();
152            let token = env::var("OSSINDEX_TOKEN").ok();
153            if user.is_some() || token.is_some() {
154                Some(OssIndexConfig {
155                    user,
156                    token,
157                    batch_size: 128,
158                })
159            } else {
160                None
161            }
162        };
163
164        let ttl_seconds = env::var("VULNERA__STORE__TTL_SECONDS")
165            .ok()
166            .and_then(|s| s.parse().ok());
167
168        let nvd = NvdConfig {
169            requests_per_window: env::var("VULNERA__NVD__REQUESTS_PER_WINDOW")
170                .ok()
171                .and_then(|s| s.parse().ok()),
172            window_seconds: env::var("VULNERA__NVD__WINDOW_SECONDS")
173                .ok()
174                .and_then(|s| s.parse().ok()),
175            max_results: env::var("VULNERA__NVD__MAX_RESULTS")
176                .ok()
177                .and_then(|s| s.parse().ok()),
178            max_days_range: Some(120),
179        };
180
181        let store = StoreConfig {
182            ttl_seconds,
183            compression_level: env::var("VULNERA__STORE__COMPRESSION_LEVEL")
184                .ok()
185                .and_then(|s| s.parse().ok())
186                .unwrap_or(3),
187            key_prefix: env::var("VULNERA__STORE__KEY_PREFIX")
188                .unwrap_or_else(|_| "vuln".to_string()),
189        };
190
191        let log_to_file = env::var("VULNERA_LOG_TO_FILE")
192            .map(|v| v.to_lowercase() == "true")
193            .unwrap_or(false);
194
195        let log_dir = env::var("VULNERA_LOG_DIR").unwrap_or_else(|_| "logs".to_string());
196
197        Ok(Self {
198            ghsa_token,
199            nvd_api_key,
200            redis_url,
201            ossindex,
202            nvd,
203            store,
204            log_to_file,
205            log_dir,
206        })
207    }
208
209    /// Create a minimal configuration for testing.
210    pub fn for_testing(redis_url: &str) -> Self {
211        Self {
212            ghsa_token: None,
213            nvd_api_key: None,
214            redis_url: redis_url.to_string(),
215            ossindex: None,
216            nvd: NvdConfig::default(),
217            store: StoreConfig::default(),
218            log_to_file: false,
219            log_dir: "logs".to_string(),
220        }
221    }
222
223    /// Validate that required configuration is present for specific sources.
224    pub fn validate_for_ghsa(&self) -> Result<&str> {
225        self.ghsa_token.as_deref().ok_or_else(|| {
226            AdvisoryError::config("GHSA token is required (set VULNERA__APIS__GHSA__TOKEN)")
227        })
228    }
229}