1use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
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_true")]
52 pub sidecar_blake3_512: bool,
53
54 #[serde(default = "default_true")]
57 pub sidecar_shake256_512: bool,
58
59 #[serde(default = "default_naming_convention")]
61 pub naming_convention: String,
62
63 #[serde(default = "default_publisher_name")]
68 pub publisher_name: String,
69
70 #[serde(default = "default_publisher_namespace")]
72 pub publisher_namespace: String,
73
74 #[serde(default = "default_publisher_category")]
77 pub publisher_category: String,
78
79 #[serde(default = "default_publisher_contact")]
81 pub publisher_contact_details: String,
82
83 #[serde(default = "default_tlp")]
91 pub tlp_default: String,
92
93 #[serde(default = "default_true")]
96 pub verschlusssache_enabled: bool,
97
98 #[serde(default = "default_verschlusssache")]
100 pub verschlusssache_default: String,
101
102 #[serde(default = "default_true")]
104 pub nato_enabled: bool,
105
106 #[serde(default = "default_nato")]
108 pub nato_default: String,
109
110 #[serde(default = "default_classification_storage_mode")]
114 pub classification_storage_mode: String,
115}
116
117impl Default for Settings {
118 fn default() -> Self {
119 Self {
120 csaf_mode: default_csaf_mode(),
121 theme: default_theme(),
122 import_directory: default_import_directory(),
123 export_directory: default_export_directory(),
124 dump_directory: default_dump_directory(),
125 log_directory: default_log_directory(),
126 sidecar_sha256: true,
127 sidecar_sha512: true,
128 sidecar_sha3_512: true,
129 sidecar_blake3_512: true,
130 sidecar_shake256_512: true,
131 naming_convention: default_naming_convention(),
132 publisher_name: default_publisher_name(),
133 publisher_namespace: default_publisher_namespace(),
134 publisher_category: default_publisher_category(),
135 publisher_contact_details: default_publisher_contact(),
136 tlp_default: default_tlp(),
137 verschlusssache_enabled: true,
138 verschlusssache_default: default_verschlusssache(),
139 nato_enabled: true,
140 nato_default: default_nato(),
141 classification_storage_mode: default_classification_storage_mode(),
142 }
143 }
144}
145
146pub const PUBLISHER_CATEGORIES: &[&str] = &[
148 "vendor",
149 "discoverer",
150 "coordinator",
151 "user",
152 "translator",
153 "other",
154];
155
156#[must_use]
158pub fn is_valid_publisher_category(value: &str) -> bool {
159 PUBLISHER_CATEGORIES.contains(&value)
160}
161
162pub const TLP_LABELS: &[&str] = &["CLEAR", "GREEN", "AMBER", "AMBER+STRICT", "RED"];
164
165pub const VERSCHLUSSSACHE_LABELS: &[&str] = &[
168 "VS-NfD (VS-NUR FÜR DEN DIENSTGEBRAUCH)",
169 "VS-Vertr. (VS-VERTRAULICH)",
170 "Geh. (GEHEIM)",
171 "Str. Geh. (STRENG GEHEIM)",
172];
173
174pub const NATO_LABELS: &[&str] = &[
177 "NR (NATO RESTRICTED)",
178 "NC (NATO CONFIDENTIAL)",
179 "NS (NATO SECRET)",
180 "CTS (COSMIC TOP SECRET)",
181];
182
183pub const CLASSIFICATION_STORAGE_MODES: &[&str] = &["distribution_text", "notes", "both"];
191
192#[must_use]
194pub fn is_valid_tlp(value: &str) -> bool {
195 TLP_LABELS.contains(&value)
196}
197
198#[must_use]
200pub fn is_valid_verschlusssache(value: &str) -> bool {
201 VERSCHLUSSSACHE_LABELS.contains(&value)
202}
203
204#[must_use]
207pub fn is_valid_nato(value: &str) -> bool {
208 NATO_LABELS.contains(&value)
209}
210
211#[must_use]
214pub fn is_valid_classification_storage_mode(value: &str) -> bool {
215 CLASSIFICATION_STORAGE_MODES.contains(&value)
216}
217
218pub const STORAGE_PATH_MAX_LEN: usize = 4096;
220
221#[must_use]
234pub fn is_valid_storage_path(value: &str) -> bool {
235 let trimmed = value.trim();
236 if trimmed.is_empty() || trimmed.len() > STORAGE_PATH_MAX_LEN {
237 return false;
238 }
239 if trimmed.contains('\0') {
240 return false;
241 }
242 if trimmed.starts_with('~') {
243 return false;
244 }
245 for component in std::path::Path::new(trimmed).components() {
248 if matches!(component, std::path::Component::ParentDir) {
249 return false;
250 }
251 }
252 true
253}
254
255fn default_csaf_mode() -> String {
256 "2.1".to_owned()
257}
258
259fn default_theme() -> String {
260 "light".to_owned()
261}
262
263fn default_import_directory() -> String {
264 "./data_import".to_owned()
265}
266
267fn default_export_directory() -> String {
268 "./data_export".to_owned()
269}
270
271fn default_dump_directory() -> String {
272 "./data_dump".to_owned()
273}
274
275fn default_log_directory() -> String {
276 "./data_log".to_owned()
277}
278
279fn default_naming_convention() -> String {
280 "ndaal-sa-".to_owned()
281}
282
283fn default_publisher_name() -> String {
284 "ndaal Gesellschaft für Sicherheit in der Informationstechnik mbH & Co KG".to_owned()
285}
286
287fn default_publisher_namespace() -> String {
288 "https://ndaal.eu/csaf".to_owned()
289}
290
291fn default_publisher_category() -> String {
292 "vendor".to_owned()
293}
294
295fn default_publisher_contact() -> String {
296 "security@ndaal.eu".to_owned()
297}
298
299fn default_tlp() -> String {
300 "AMBER".to_owned()
301}
302
303fn default_verschlusssache() -> String {
304 "VS-NfD (VS-NUR FÜR DEN DIENSTGEBRAUCH)".to_owned()
305}
306
307fn default_nato() -> String {
308 "NR (NATO RESTRICTED)".to_owned()
309}
310
311fn default_classification_storage_mode() -> String {
312 "both".to_owned()
313}
314
315const fn default_true() -> bool {
316 true
317}
318
319#[cfg(test)]
320#[allow(clippy::cognitive_complexity)]
323mod tests {
324 use super::*;
325
326 #[test]
327 fn test_default_settings() {
328 let settings = Settings::default();
329 assert_eq!(settings.csaf_mode, "2.1");
330 assert_eq!(settings.theme, "light");
331 assert!(settings.sidecar_sha256);
332 assert!(settings.sidecar_sha512);
333 assert!(settings.sidecar_sha3_512);
334 assert!(settings.sidecar_blake3_512);
335 assert!(settings.sidecar_shake256_512);
336 assert_eq!(settings.naming_convention, "ndaal-sa-");
337 assert_eq!(settings.import_directory, "./data_import");
338 assert_eq!(settings.export_directory, "./data_export");
339 assert_eq!(settings.dump_directory, "./data_dump");
340 assert_eq!(settings.log_directory, "./data_log");
341 assert_eq!(
342 settings.publisher_name,
343 "ndaal Gesellschaft für Sicherheit in der Informationstechnik mbH & Co KG"
344 );
345 assert_eq!(settings.publisher_namespace, "https://ndaal.eu/csaf");
346 assert_eq!(settings.publisher_category, "vendor");
347 assert_eq!(settings.publisher_contact_details, "security@ndaal.eu");
348 assert_eq!(settings.tlp_default, "AMBER");
349 assert!(settings.verschlusssache_enabled);
350 assert_eq!(
351 settings.verschlusssache_default,
352 "VS-NfD (VS-NUR FÜR DEN DIENSTGEBRAUCH)"
353 );
354 assert!(settings.nato_enabled);
355 assert_eq!(settings.nato_default, "NR (NATO RESTRICTED)");
356 assert_eq!(settings.classification_storage_mode, "both");
357 }
358
359 #[test]
360 fn test_settings_roundtrip() {
361 let settings = Settings::default();
362 let json = serde_json::to_string(&settings).expect("serialize failed");
363 let parsed: Settings = serde_json::from_str(&json).expect("deserialize failed");
364 assert_eq!(settings, parsed);
365 }
366
367 #[test]
368 fn test_partial_deserialization() {
369 let json = r#"{"csaf_mode": "2.0", "theme": "dark"}"#;
370 let settings: Settings = serde_json::from_str(json).expect("deserialize failed");
371 assert_eq!(settings.csaf_mode, "2.0");
372 assert_eq!(settings.theme, "dark");
373 assert_eq!(settings.dump_directory, "./data_dump");
375 assert_eq!(settings.log_directory, "./data_log");
376 assert!(settings.sidecar_sha256);
377 assert!(settings.sidecar_sha512);
378 assert_eq!(settings.naming_convention, "ndaal-sa-");
379 assert_eq!(settings.publisher_category, "vendor");
380 assert_eq!(settings.publisher_namespace, "https://ndaal.eu/csaf");
381 }
382
383 #[test]
384 fn test_publisher_category_whitelist() {
385 for cat in PUBLISHER_CATEGORIES {
386 assert!(is_valid_publisher_category(cat), "{cat} should be valid");
387 }
388 assert!(!is_valid_publisher_category("VENDOR"));
389 assert!(!is_valid_publisher_category("bogus"));
390 assert!(!is_valid_publisher_category(""));
391 }
392
393 #[test]
394 fn test_tlp_whitelist() {
395 for label in TLP_LABELS {
396 assert!(is_valid_tlp(label), "{label} should be a valid TLP label");
397 }
398 assert!(!is_valid_tlp(""));
399 assert!(!is_valid_tlp("clear"));
400 assert!(!is_valid_tlp("AMBER "));
401 assert!(!is_valid_tlp("WHITE"));
402 }
403
404 #[test]
405 fn test_verschlusssache_whitelist() {
406 for label in VERSCHLUSSSACHE_LABELS {
407 assert!(is_valid_verschlusssache(label));
408 }
409 assert!(!is_valid_verschlusssache(""));
410 assert!(!is_valid_verschlusssache("VS-NfD"));
411 assert!(!is_valid_verschlusssache("VS-NUR FÜR DEN DIENSTGEBRAUCH"));
412 }
413
414 #[test]
415 fn test_nato_whitelist() {
416 for label in NATO_LABELS {
417 assert!(is_valid_nato(label));
418 }
419 assert!(!is_valid_nato(""));
420 assert!(!is_valid_nato("NR"));
421 assert!(!is_valid_nato("nr (nato restricted)"));
422 }
423
424 #[test]
425 fn test_is_valid_storage_path_accepts() {
426 for good in [
427 "./data_dump",
428 "./data_import",
429 "/var/lib/csaf",
430 "data/dump",
431 "C:/dev/csaf/dump",
432 ] {
433 assert!(is_valid_storage_path(good), "{good} should be accepted");
434 }
435 }
436
437 #[test]
438 fn test_is_valid_storage_path_rejects() {
439 for bad in [
440 "",
441 " ",
442 "\t",
443 "~/dumps",
444 "~\\dumps",
445 "../etc/passwd",
446 "./../a",
447 "foo/../bar",
448 "a\0b",
449 ] {
450 assert!(!is_valid_storage_path(bad), "{bad:?} should be rejected");
451 }
452 }
453
454 #[test]
455 fn test_is_valid_storage_path_max_length() {
456 let huge = "a".repeat(STORAGE_PATH_MAX_LEN + 1);
457 assert!(!is_valid_storage_path(&huge));
458 let ok = "a".repeat(STORAGE_PATH_MAX_LEN);
459 assert!(is_valid_storage_path(&ok));
460 }
461
462 #[test]
463 fn test_classification_storage_mode_whitelist() {
464 for mode in CLASSIFICATION_STORAGE_MODES {
465 assert!(is_valid_classification_storage_mode(mode));
466 }
467 assert!(!is_valid_classification_storage_mode(""));
468 assert!(!is_valid_classification_storage_mode("BOTH"));
469 assert!(!is_valid_classification_storage_mode("distribution"));
470 assert!(!is_valid_classification_storage_mode("note"));
471 }
472
473 #[test]
474 fn test_cartesian_combinations_roundtrip() {
475 for tlp in TLP_LABELS {
478 for vs in VERSCHLUSSSACHE_LABELS {
479 for nato in NATO_LABELS {
480 for mode in CLASSIFICATION_STORAGE_MODES {
481 let settings = Settings {
482 tlp_default: (*tlp).to_owned(),
483 verschlusssache_default: (*vs).to_owned(),
484 nato_default: (*nato).to_owned(),
485 classification_storage_mode: (*mode).to_owned(),
486 ..Settings::default()
487 };
488 let json = serde_json::to_string(&settings).expect("serialize failed");
489 let parsed: Settings =
490 serde_json::from_str(&json).expect("deserialize failed");
491 assert_eq!(settings, parsed);
492 }
493 }
494 }
495 }
496 }
497
498 #[test]
499 fn test_old_settings_json_still_loads() {
500 let json = r#"{
503 "csaf_mode": "2.1",
504 "theme": "dark",
505 "import_directory": "./data_import",
506 "export_directory": "./data_export",
507 "sidecar_sha256": true,
508 "sidecar_sha3_512": true,
509 "naming_convention": "ndaal-sa-"
510 }"#;
511 let settings: Settings = serde_json::from_str(json).expect("deserialize failed");
512 assert_eq!(settings.publisher_category, "vendor");
513 assert_eq!(settings.publisher_contact_details, "security@ndaal.eu");
514 assert_eq!(settings.dump_directory, "./data_dump");
516 assert_eq!(settings.log_directory, "./data_log");
518 assert!(settings.sidecar_sha512);
519 assert_eq!(settings.tlp_default, "AMBER");
522 assert!(settings.verschlusssache_enabled);
523 assert_eq!(
524 settings.verschlusssache_default,
525 "VS-NfD (VS-NUR FÜR DEN DIENSTGEBRAUCH)"
526 );
527 assert!(settings.nato_enabled);
528 assert_eq!(settings.nato_default, "NR (NATO RESTRICTED)");
529 assert_eq!(settings.classification_storage_mode, "both");
530 }
531}