1use crate::config::ScanIntensity;
16use serde::{Deserialize, Serialize};
17use std::collections::HashMap;
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct ScanProfile {
22 pub name: String,
23 pub description: String,
24 pub scan_intensity: ScanIntensity,
25 pub timeout_secs: u32,
26 pub rate_limit_rps: u32,
27 pub concurrent_workers: u32,
28 pub plugins_enabled: Vec<String>,
29 pub lua_scripts_enabled: Vec<String>,
30 pub event_subscriptions: Vec<String>,
31 pub options: HashMap<String, String>,
32}
33
34impl Default for ScanProfile {
36 fn default() -> Self {
37 Self::enterprise()
38 }
39}
40
41impl ScanProfile {
42 pub fn enterprise() -> Self {
44 Self {
45 name: "enterprise".to_string(),
46 description: "Descriptive enterprise policy model; not runtime-wired".to_string(),
47 scan_intensity: ScanIntensity::Normal,
48 timeout_secs: 300,
49 rate_limit_rps: 10,
50 concurrent_workers: 4,
51 plugins_enabled: vec![],
52 lua_scripts_enabled: vec![],
53 event_subscriptions: vec![],
54 options: HashMap::new(),
55 }
56 }
57
58 pub fn cloud() -> Self {
60 Self {
61 name: "cloud".to_string(),
62 description: "Descriptive cloud policy model; not runtime-wired".to_string(),
63 scan_intensity: ScanIntensity::Aggressive,
64 timeout_secs: 600,
65 rate_limit_rps: 50,
66 concurrent_workers: 16,
67 plugins_enabled: vec![],
68 lua_scripts_enabled: vec![],
69 event_subscriptions: vec![],
70 options: HashMap::new(),
71 }
72 }
73
74 pub fn aggressive() -> Self {
76 Self {
77 name: "aggressive".to_string(),
78 description: "Descriptive high-intensity policy model; not runtime-wired".to_string(),
79 scan_intensity: ScanIntensity::Aggressive,
80 timeout_secs: 180,
81 rate_limit_rps: 100,
82 concurrent_workers: 32,
83 plugins_enabled: vec![],
84 lua_scripts_enabled: vec![],
85 event_subscriptions: vec![],
86 options: HashMap::new(),
87 }
88 }
89
90 pub fn passive() -> Self {
92 Self {
93 name: "passive".to_string(),
94 description: "Descriptive low-activity policy model; not runtime-wired".to_string(),
95 scan_intensity: ScanIntensity::Stealth,
96 timeout_secs: 60,
97 rate_limit_rps: 5,
98 concurrent_workers: 2,
99 plugins_enabled: vec![],
100 lua_scripts_enabled: vec![],
101 event_subscriptions: vec![],
102 options: HashMap::new(),
103 }
104 }
105
106 pub fn custom(name: impl Into<String>, intensity: ScanIntensity) -> Self {
108 Self {
109 name: name.into(),
110 description: String::new(),
111 scan_intensity: intensity,
112 timeout_secs: 300,
113 rate_limit_rps: 20,
114 concurrent_workers: 8,
115 plugins_enabled: vec![],
116 lua_scripts_enabled: vec![],
117 event_subscriptions: vec![],
118 options: HashMap::new(),
119 }
120 }
121
122 pub fn with_description(mut self, desc: impl Into<String>) -> Self {
123 self.description = desc.into();
124 self
125 }
126
127 pub fn with_timeout(mut self, secs: u32) -> Self {
128 self.timeout_secs = secs;
129 self
130 }
131
132 pub fn with_rate_limit(mut self, rps: u32) -> Self {
133 self.rate_limit_rps = rps;
134 self
135 }
136
137 pub fn with_workers(mut self, count: u32) -> Self {
138 self.concurrent_workers = count;
139 self
140 }
141
142 pub fn add_plugin(mut self, plugin_id: impl Into<String>) -> Self {
143 self.plugins_enabled.push(plugin_id.into());
144 self
145 }
146
147 pub fn add_script(mut self, script_id: impl Into<String>) -> Self {
148 self.lua_scripts_enabled.push(script_id.into());
149 self
150 }
151
152 pub fn add_subscription(mut self, event: impl Into<String>) -> Self {
153 self.event_subscriptions.push(event.into());
154 self
155 }
156
157 pub fn add_option(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
158 self.options.insert(key.into(), value.into());
159 self
160 }
161}
162
163pub struct ConfigLoader {
165 profiles: dashmap::DashMap<String, ScanProfile>,
166 active_profile: std::sync::Mutex<String>,
167}
168
169impl ConfigLoader {
170 pub fn new() -> Self {
172 let profiles = dashmap::DashMap::new();
173
174 profiles.insert("enterprise".to_string(), ScanProfile::enterprise());
176 profiles.insert("cloud".to_string(), ScanProfile::cloud());
177 profiles.insert("aggressive".to_string(), ScanProfile::aggressive());
178 profiles.insert("passive".to_string(), ScanProfile::passive());
179
180 Self {
181 profiles,
182 active_profile: std::sync::Mutex::new("enterprise".to_string()),
183 }
184 }
185
186 pub fn register_profile(&self, profile: ScanProfile) {
188 self.profiles.insert(profile.name.clone(), profile);
189 }
190
191 pub fn get_profile(&self, name: &str) -> Option<ScanProfile> {
193 self.profiles.get(name).map(|p| p.clone())
194 }
195
196 pub fn list_profiles(&self) -> Vec<String> {
198 let mut profiles = self
199 .profiles
200 .iter()
201 .map(|ref_multi| ref_multi.key().clone())
202 .collect::<Vec<_>>();
203 profiles.sort();
204 profiles
205 }
206
207 pub fn get_active_profile(&self) -> ScanProfile {
209 let name = self.active_profile.lock().unwrap().clone();
210 self.get_profile(&name)
211 .unwrap_or_else(ScanProfile::enterprise)
212 }
213
214 pub fn set_active_profile(&self, name: &str) -> Result<(), String> {
216 if self.profiles.contains_key(name) {
217 *self.active_profile.lock().unwrap() = name.to_string();
218 Ok(())
219 } else {
220 Err(format!("Profile '{name}' not found"))
221 }
222 }
223
224 pub fn profile_count(&self) -> usize {
226 self.profiles.len()
227 }
228
229 pub fn merge_profiles(&self, base: &str, overlay: &str) -> Result<ScanProfile, String> {
235 let base_profile = self
236 .get_profile(base)
237 .ok_or_else(|| format!("Base profile '{base}' not found"))?;
238 let overlay_profile = self
239 .get_profile(overlay)
240 .ok_or_else(|| format!("Overlay profile '{overlay}' not found"))?;
241
242 let mut merged = base_profile;
243 merged.name = format!("{base}_merged_with_{overlay}");
244 merged.description = overlay_profile.description;
245 merged.scan_intensity = overlay_profile.scan_intensity;
246 merged.timeout_secs = overlay_profile.timeout_secs;
247 merged.rate_limit_rps = overlay_profile.rate_limit_rps;
248 merged.concurrent_workers = overlay_profile.concurrent_workers;
249 extend_unique(&mut merged.plugins_enabled, overlay_profile.plugins_enabled);
250 extend_unique(
251 &mut merged.lua_scripts_enabled,
252 overlay_profile.lua_scripts_enabled,
253 );
254 extend_unique(
255 &mut merged.event_subscriptions,
256 overlay_profile.event_subscriptions,
257 );
258 merged.options.extend(overlay_profile.options);
259
260 Ok(merged)
261 }
262}
263
264fn extend_unique(values: &mut Vec<String>, additions: Vec<String>) {
265 for addition in additions {
266 if !values.contains(&addition) {
267 values.push(addition);
268 }
269 }
270}
271
272impl Default for ConfigLoader {
273 fn default() -> Self {
274 Self::new()
275 }
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281
282 #[test]
283 fn test_scan_intensity() {
284 assert_eq!(ScanIntensity::Light.as_str(), "light");
285 assert_eq!(ScanIntensity::Stealth.as_str(), "stealth");
286 }
287
288 #[test]
289 fn test_enterprise_profile() {
290 let profile = ScanProfile::enterprise();
291 assert_eq!(profile.name, "enterprise");
292 assert_eq!(profile.scan_intensity, ScanIntensity::Normal);
293 assert_eq!(profile.concurrent_workers, 4);
294 }
295
296 #[test]
297 fn test_cloud_profile() {
298 let profile = ScanProfile::cloud();
299 assert_eq!(profile.name, "cloud");
300 assert_eq!(profile.scan_intensity, ScanIntensity::Aggressive);
301 assert_eq!(profile.concurrent_workers, 16);
302 }
303
304 #[test]
305 fn test_aggressive_profile() {
306 let profile = ScanProfile::aggressive();
307 assert_eq!(profile.name, "aggressive");
308 assert_eq!(profile.scan_intensity, ScanIntensity::Aggressive);
309 assert!(profile.plugins_enabled.is_empty());
310 }
311
312 #[test]
313 fn built_in_profiles_do_not_claim_unwired_capabilities() {
314 for profile in [
315 ScanProfile::enterprise(),
316 ScanProfile::cloud(),
317 ScanProfile::aggressive(),
318 ScanProfile::passive(),
319 ] {
320 assert!(profile.plugins_enabled.is_empty());
321 assert!(profile.lua_scripts_enabled.is_empty());
322 assert!(profile.event_subscriptions.is_empty());
323 assert!(profile.options.is_empty());
324 assert!(profile.description.contains("not runtime-wired"));
325 }
326 }
327
328 #[test]
329 fn test_passive_profile() {
330 let profile = ScanProfile::passive();
331 assert_eq!(profile.name, "passive");
332 assert_eq!(profile.scan_intensity, ScanIntensity::Stealth);
333 assert_eq!(profile.plugins_enabled.len(), 0);
334 }
335
336 #[test]
337 fn test_custom_profile() {
338 let profile = ScanProfile::custom("test", ScanIntensity::Normal)
339 .with_description("Test Profile")
340 .with_timeout(120)
341 .with_rate_limit(50)
342 .with_workers(8);
343
344 assert_eq!(profile.name, "test");
345 assert_eq!(profile.description, "Test Profile");
346 assert_eq!(profile.timeout_secs, 120);
347 assert_eq!(profile.rate_limit_rps, 50);
348 assert_eq!(profile.concurrent_workers, 8);
349 }
350
351 #[test]
352 fn test_profile_with_plugins() {
353 let profile = ScanProfile::custom("test", ScanIntensity::Normal)
354 .add_plugin("example.marker.one")
355 .add_plugin("example.marker.two");
356
357 assert_eq!(profile.plugins_enabled.len(), 2);
358 }
359
360 #[test]
361 fn test_profile_with_scripts() {
362 let profile = ScanProfile::custom("test", ScanIntensity::Normal)
363 .add_script("script1")
364 .add_script("script2");
365
366 assert_eq!(profile.lua_scripts_enabled.len(), 2);
367 }
368
369 #[test]
370 fn test_profile_with_options() {
371 let profile = ScanProfile::custom("test", ScanIntensity::Normal)
372 .add_option("key1", "value1")
373 .add_option("key2", "value2");
374
375 assert_eq!(profile.options.get("key1"), Some(&"value1".to_string()));
376 }
377
378 #[test]
379 fn test_config_loader_creation() {
380 let loader = ConfigLoader::new();
381 assert!(loader.profile_count() >= 4);
382 }
383
384 #[test]
385 fn test_config_loader_get_profile() {
386 let loader = ConfigLoader::new();
387 let enterprise = loader.get_profile("enterprise");
388
389 assert!(enterprise.is_some());
390 assert_eq!(enterprise.unwrap().name, "enterprise");
391 }
392
393 #[test]
394 fn test_config_loader_list_profiles() {
395 let loader = ConfigLoader::new();
396 let profiles = loader.list_profiles();
397
398 assert!(profiles.contains(&"enterprise".to_string()));
399 assert!(profiles.contains(&"cloud".to_string()));
400 assert!(profiles.contains(&"aggressive".to_string()));
401 assert!(profiles.contains(&"passive".to_string()));
402 }
403
404 #[test]
405 fn test_config_loader_active_profile() {
406 let loader = ConfigLoader::new();
407 let active = loader.get_active_profile();
408
409 assert_eq!(active.name, "enterprise");
410 }
411
412 #[test]
413 fn test_config_loader_set_active() {
414 let loader = ConfigLoader::new();
415 loader.set_active_profile("cloud").unwrap();
416
417 let active = loader.get_active_profile();
418 assert_eq!(active.name, "cloud");
419
420 let missing = loader.set_active_profile("missing").unwrap_err();
421 assert_eq!(missing, "Profile 'missing' not found");
422 assert_eq!(loader.get_active_profile().name, "cloud");
423 }
424
425 #[test]
426 fn test_config_loader_register_profile() {
427 let loader = ConfigLoader::new();
428 let initial_count = loader.profile_count();
429
430 let custom = ScanProfile::custom("custom", ScanIntensity::Normal);
431 loader.register_profile(custom);
432
433 assert_eq!(loader.profile_count(), initial_count + 1);
434 assert!(loader.get_profile("custom").is_some());
435 }
436
437 #[test]
438 fn profile_merge_overlays_every_field_deterministically() {
439 let loader = ConfigLoader::new();
440
441 let mut base = ScanProfile::custom("base", ScanIntensity::Light)
442 .with_description("base description")
443 .with_timeout(10)
444 .with_rate_limit(2)
445 .with_workers(1)
446 .add_plugin("base-plugin")
447 .add_plugin("shared-plugin")
448 .add_script("base-script")
449 .add_subscription("base-event")
450 .add_option("base-only", "base")
451 .add_option("shared-option", "base");
452 base.options
453 .insert("stable-option".to_owned(), "stable".to_owned());
454
455 let overlay = ScanProfile::custom("overlay", ScanIntensity::Aggressive)
456 .with_description("overlay description")
457 .with_timeout(99)
458 .with_rate_limit(42)
459 .with_workers(8)
460 .add_plugin("shared-plugin")
461 .add_plugin("overlay-plugin")
462 .add_script("overlay-script")
463 .add_subscription("overlay-event")
464 .add_option("shared-option", "overlay")
465 .add_option("overlay-only", "overlay");
466 loader.register_profile(base);
467 loader.register_profile(overlay);
468
469 let merged = loader.merge_profiles("base", "overlay").unwrap();
470
471 assert_eq!(merged.name, "base_merged_with_overlay");
472 assert_eq!(merged.description, "overlay description");
473 assert_eq!(merged.scan_intensity, ScanIntensity::Aggressive);
474 assert_eq!(merged.timeout_secs, 99);
475 assert_eq!(merged.rate_limit_rps, 42);
476 assert_eq!(merged.concurrent_workers, 8);
477 assert_eq!(
478 merged.plugins_enabled,
479 vec![
480 "base-plugin".to_owned(),
481 "shared-plugin".to_owned(),
482 "overlay-plugin".to_owned(),
483 ]
484 );
485 assert_eq!(
486 merged.lua_scripts_enabled,
487 vec!["base-script".to_owned(), "overlay-script".to_owned()]
488 );
489 assert_eq!(
490 merged.event_subscriptions,
491 vec!["base-event".to_owned(), "overlay-event".to_owned()]
492 );
493 assert_eq!(merged.options["base-only"], "base");
494 assert_eq!(merged.options["stable-option"], "stable");
495 assert_eq!(merged.options["shared-option"], "overlay");
496 assert_eq!(merged.options["overlay-only"], "overlay");
497 }
498}