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