1use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
10#[allow(clippy::struct_excessive_bools)]
11pub struct Settings {
12 #[serde(default = "default_csaf_mode")]
14 pub csaf_mode: String,
15
16 #[serde(default = "default_theme")]
18 pub theme: String,
19
20 #[serde(default = "default_import_directory")]
22 pub import_directory: String,
23
24 #[serde(default = "default_export_directory")]
26 pub export_directory: String,
27
28 #[serde(default = "default_dump_directory")]
30 pub dump_directory: String,
31
32 #[serde(default = "default_log_directory")]
34 pub log_directory: String,
35
36 #[serde(default = "default_true")]
38 pub sidecar_sha256: bool,
39
40 #[serde(default = "default_true")]
43 pub sidecar_sha512: bool,
44
45 #[serde(default = "default_true")]
47 pub sidecar_sha3_512: bool,
48
49 #[serde(default = "default_naming_convention")]
51 pub naming_convention: String,
52
53 #[serde(default = "default_publisher_name")]
58 pub publisher_name: String,
59
60 #[serde(default = "default_publisher_namespace")]
62 pub publisher_namespace: String,
63
64 #[serde(default = "default_publisher_category")]
67 pub publisher_category: String,
68
69 #[serde(default = "default_publisher_contact")]
71 pub publisher_contact_details: String,
72
73 #[serde(default = "default_tlp")]
81 pub tlp_default: String,
82
83 #[serde(default = "default_true")]
86 pub verschlusssache_enabled: bool,
87
88 #[serde(default = "default_verschlusssache")]
90 pub verschlusssache_default: String,
91
92 #[serde(default = "default_true")]
94 pub nato_enabled: bool,
95
96 #[serde(default = "default_nato")]
98 pub nato_default: String,
99
100 #[serde(default = "default_classification_storage_mode")]
104 pub classification_storage_mode: String,
105}
106
107impl Default for Settings {
108 fn default() -> Self {
109 Self {
110 csaf_mode: default_csaf_mode(),
111 theme: default_theme(),
112 import_directory: default_import_directory(),
113 export_directory: default_export_directory(),
114 dump_directory: default_dump_directory(),
115 log_directory: default_log_directory(),
116 sidecar_sha256: true,
117 sidecar_sha512: true,
118 sidecar_sha3_512: true,
119 naming_convention: default_naming_convention(),
120 publisher_name: default_publisher_name(),
121 publisher_namespace: default_publisher_namespace(),
122 publisher_category: default_publisher_category(),
123 publisher_contact_details: default_publisher_contact(),
124 tlp_default: default_tlp(),
125 verschlusssache_enabled: true,
126 verschlusssache_default: default_verschlusssache(),
127 nato_enabled: true,
128 nato_default: default_nato(),
129 classification_storage_mode: default_classification_storage_mode(),
130 }
131 }
132}
133
134pub const PUBLISHER_CATEGORIES: &[&str] = &[
136 "vendor",
137 "discoverer",
138 "coordinator",
139 "user",
140 "translator",
141 "other",
142];
143
144#[must_use]
146pub fn is_valid_publisher_category(value: &str) -> bool {
147 PUBLISHER_CATEGORIES.contains(&value)
148}
149
150pub const TLP_LABELS: &[&str] = &["CLEAR", "GREEN", "AMBER", "AMBER+STRICT", "RED"];
152
153pub const VERSCHLUSSSACHE_LABELS: &[&str] = &[
156 "VS-NfD (VS-NUR FÜR DEN DIENSTGEBRAUCH)",
157 "VS-Vertr. (VS-VERTRAULICH)",
158 "Geh. (GEHEIM)",
159 "Str. Geh. (STRENG GEHEIM)",
160];
161
162pub const NATO_LABELS: &[&str] = &[
165 "NR (NATO RESTRICTED)",
166 "NC (NATO CONFIDENTIAL)",
167 "NS (NATO SECRET)",
168 "CTS (COSMIC TOP SECRET)",
169];
170
171pub const CLASSIFICATION_STORAGE_MODES: &[&str] = &["distribution_text", "notes", "both"];
179
180#[must_use]
182pub fn is_valid_tlp(value: &str) -> bool {
183 TLP_LABELS.contains(&value)
184}
185
186#[must_use]
188pub fn is_valid_verschlusssache(value: &str) -> bool {
189 VERSCHLUSSSACHE_LABELS.contains(&value)
190}
191
192#[must_use]
195pub fn is_valid_nato(value: &str) -> bool {
196 NATO_LABELS.contains(&value)
197}
198
199#[must_use]
202pub fn is_valid_classification_storage_mode(value: &str) -> bool {
203 CLASSIFICATION_STORAGE_MODES.contains(&value)
204}
205
206pub const STORAGE_PATH_MAX_LEN: usize = 4096;
208
209#[must_use]
222pub fn is_valid_storage_path(value: &str) -> bool {
223 let trimmed = value.trim();
224 if trimmed.is_empty() || trimmed.len() > STORAGE_PATH_MAX_LEN {
225 return false;
226 }
227 if trimmed.contains('\0') {
228 return false;
229 }
230 if trimmed.starts_with('~') {
231 return false;
232 }
233 for component in std::path::Path::new(trimmed).components() {
236 if matches!(component, std::path::Component::ParentDir) {
237 return false;
238 }
239 }
240 true
241}
242
243fn default_csaf_mode() -> String {
244 "2.1".to_owned()
245}
246
247fn default_theme() -> String {
248 "light".to_owned()
249}
250
251fn default_import_directory() -> String {
252 "./data_import".to_owned()
253}
254
255fn default_export_directory() -> String {
256 "./data_export".to_owned()
257}
258
259fn default_dump_directory() -> String {
260 "./data_dump".to_owned()
261}
262
263fn default_log_directory() -> String {
264 "./data_log".to_owned()
265}
266
267fn default_naming_convention() -> String {
268 "ndaal-sa-".to_owned()
269}
270
271fn default_publisher_name() -> String {
272 "ndaal Gesellschaft für Sicherheit in der Informationstechnik mbH & Co KG".to_owned()
273}
274
275fn default_publisher_namespace() -> String {
276 "https://ndaal.eu/csaf".to_owned()
277}
278
279fn default_publisher_category() -> String {
280 "vendor".to_owned()
281}
282
283fn default_publisher_contact() -> String {
284 "security@ndaal.eu".to_owned()
285}
286
287fn default_tlp() -> String {
288 "AMBER".to_owned()
289}
290
291fn default_verschlusssache() -> String {
292 "VS-NfD (VS-NUR FÜR DEN DIENSTGEBRAUCH)".to_owned()
293}
294
295fn default_nato() -> String {
296 "NR (NATO RESTRICTED)".to_owned()
297}
298
299fn default_classification_storage_mode() -> String {
300 "both".to_owned()
301}
302
303const fn default_true() -> bool {
304 true
305}
306
307#[cfg(test)]
308#[allow(clippy::cognitive_complexity)]
311mod tests {
312 use super::*;
313
314 #[test]
315 fn test_default_settings() {
316 let settings = Settings::default();
317 assert_eq!(settings.csaf_mode, "2.1");
318 assert_eq!(settings.theme, "light");
319 assert!(settings.sidecar_sha256);
320 assert!(settings.sidecar_sha512);
321 assert!(settings.sidecar_sha3_512);
322 assert_eq!(settings.naming_convention, "ndaal-sa-");
323 assert_eq!(settings.import_directory, "./data_import");
324 assert_eq!(settings.export_directory, "./data_export");
325 assert_eq!(settings.dump_directory, "./data_dump");
326 assert_eq!(settings.log_directory, "./data_log");
327 assert_eq!(
328 settings.publisher_name,
329 "ndaal Gesellschaft für Sicherheit in der Informationstechnik mbH & Co KG"
330 );
331 assert_eq!(settings.publisher_namespace, "https://ndaal.eu/csaf");
332 assert_eq!(settings.publisher_category, "vendor");
333 assert_eq!(settings.publisher_contact_details, "security@ndaal.eu");
334 assert_eq!(settings.tlp_default, "AMBER");
335 assert!(settings.verschlusssache_enabled);
336 assert_eq!(
337 settings.verschlusssache_default,
338 "VS-NfD (VS-NUR FÜR DEN DIENSTGEBRAUCH)"
339 );
340 assert!(settings.nato_enabled);
341 assert_eq!(settings.nato_default, "NR (NATO RESTRICTED)");
342 assert_eq!(settings.classification_storage_mode, "both");
343 }
344
345 #[test]
346 fn test_settings_roundtrip() {
347 let settings = Settings::default();
348 let json = serde_json::to_string(&settings).expect("serialize failed");
349 let parsed: Settings = serde_json::from_str(&json).expect("deserialize failed");
350 assert_eq!(settings, parsed);
351 }
352
353 #[test]
354 fn test_partial_deserialization() {
355 let json = r#"{"csaf_mode": "2.0", "theme": "dark"}"#;
356 let settings: Settings = serde_json::from_str(json).expect("deserialize failed");
357 assert_eq!(settings.csaf_mode, "2.0");
358 assert_eq!(settings.theme, "dark");
359 assert_eq!(settings.dump_directory, "./data_dump");
361 assert_eq!(settings.log_directory, "./data_log");
362 assert!(settings.sidecar_sha256);
363 assert!(settings.sidecar_sha512);
364 assert_eq!(settings.naming_convention, "ndaal-sa-");
365 assert_eq!(settings.publisher_category, "vendor");
366 assert_eq!(settings.publisher_namespace, "https://ndaal.eu/csaf");
367 }
368
369 #[test]
370 fn test_publisher_category_whitelist() {
371 for cat in PUBLISHER_CATEGORIES {
372 assert!(is_valid_publisher_category(cat), "{cat} should be valid");
373 }
374 assert!(!is_valid_publisher_category("VENDOR"));
375 assert!(!is_valid_publisher_category("bogus"));
376 assert!(!is_valid_publisher_category(""));
377 }
378
379 #[test]
380 fn test_tlp_whitelist() {
381 for label in TLP_LABELS {
382 assert!(is_valid_tlp(label), "{label} should be a valid TLP label");
383 }
384 assert!(!is_valid_tlp(""));
385 assert!(!is_valid_tlp("clear"));
386 assert!(!is_valid_tlp("AMBER "));
387 assert!(!is_valid_tlp("WHITE"));
388 }
389
390 #[test]
391 fn test_verschlusssache_whitelist() {
392 for label in VERSCHLUSSSACHE_LABELS {
393 assert!(is_valid_verschlusssache(label));
394 }
395 assert!(!is_valid_verschlusssache(""));
396 assert!(!is_valid_verschlusssache("VS-NfD"));
397 assert!(!is_valid_verschlusssache("VS-NUR FÜR DEN DIENSTGEBRAUCH"));
398 }
399
400 #[test]
401 fn test_nato_whitelist() {
402 for label in NATO_LABELS {
403 assert!(is_valid_nato(label));
404 }
405 assert!(!is_valid_nato(""));
406 assert!(!is_valid_nato("NR"));
407 assert!(!is_valid_nato("nr (nato restricted)"));
408 }
409
410 #[test]
411 fn test_is_valid_storage_path_accepts() {
412 for good in [
413 "./data_dump",
414 "./data_import",
415 "/var/lib/csaf",
416 "data/dump",
417 "C:/dev/csaf/dump",
418 ] {
419 assert!(is_valid_storage_path(good), "{good} should be accepted");
420 }
421 }
422
423 #[test]
424 fn test_is_valid_storage_path_rejects() {
425 for bad in [
426 "",
427 " ",
428 "\t",
429 "~/dumps",
430 "~\\dumps",
431 "../etc/passwd",
432 "./../a",
433 "foo/../bar",
434 "a\0b",
435 ] {
436 assert!(!is_valid_storage_path(bad), "{bad:?} should be rejected");
437 }
438 }
439
440 #[test]
441 fn test_is_valid_storage_path_max_length() {
442 let huge = "a".repeat(STORAGE_PATH_MAX_LEN + 1);
443 assert!(!is_valid_storage_path(&huge));
444 let ok = "a".repeat(STORAGE_PATH_MAX_LEN);
445 assert!(is_valid_storage_path(&ok));
446 }
447
448 #[test]
449 fn test_classification_storage_mode_whitelist() {
450 for mode in CLASSIFICATION_STORAGE_MODES {
451 assert!(is_valid_classification_storage_mode(mode));
452 }
453 assert!(!is_valid_classification_storage_mode(""));
454 assert!(!is_valid_classification_storage_mode("BOTH"));
455 assert!(!is_valid_classification_storage_mode("distribution"));
456 assert!(!is_valid_classification_storage_mode("note"));
457 }
458
459 #[test]
460 fn test_cartesian_combinations_roundtrip() {
461 for tlp in TLP_LABELS {
464 for vs in VERSCHLUSSSACHE_LABELS {
465 for nato in NATO_LABELS {
466 for mode in CLASSIFICATION_STORAGE_MODES {
467 let settings = Settings {
468 tlp_default: (*tlp).to_owned(),
469 verschlusssache_default: (*vs).to_owned(),
470 nato_default: (*nato).to_owned(),
471 classification_storage_mode: (*mode).to_owned(),
472 ..Settings::default()
473 };
474 let json = serde_json::to_string(&settings).expect("serialize failed");
475 let parsed: Settings =
476 serde_json::from_str(&json).expect("deserialize failed");
477 assert_eq!(settings, parsed);
478 }
479 }
480 }
481 }
482 }
483
484 #[test]
485 fn test_old_settings_json_still_loads() {
486 let json = r#"{
489 "csaf_mode": "2.1",
490 "theme": "dark",
491 "import_directory": "./data_import",
492 "export_directory": "./data_export",
493 "sidecar_sha256": true,
494 "sidecar_sha3_512": true,
495 "naming_convention": "ndaal-sa-"
496 }"#;
497 let settings: Settings = serde_json::from_str(json).expect("deserialize failed");
498 assert_eq!(settings.publisher_category, "vendor");
499 assert_eq!(settings.publisher_contact_details, "security@ndaal.eu");
500 assert_eq!(settings.dump_directory, "./data_dump");
502 assert_eq!(settings.log_directory, "./data_log");
504 assert!(settings.sidecar_sha512);
505 assert_eq!(settings.tlp_default, "AMBER");
508 assert!(settings.verschlusssache_enabled);
509 assert_eq!(
510 settings.verschlusssache_default,
511 "VS-NfD (VS-NUR FÜR DEN DIENSTGEBRAUCH)"
512 );
513 assert!(settings.nato_enabled);
514 assert_eq!(settings.nato_default, "NR (NATO RESTRICTED)");
515 assert_eq!(settings.classification_storage_mode, "both");
516 }
517}