1use crate::malware_db::MalwareSignature;
2use crate::rules::custom::YamlRule;
3use serde::{Deserialize, Serialize};
4use std::collections::HashSet;
5use std::fs;
6use std::path::Path;
7
8#[derive(Debug, Clone, Default, Serialize, Deserialize)]
10#[serde(default)]
11pub struct Config {
12 pub scan: ScanConfig,
14 pub watch: WatchConfig,
16 pub text_files: TextFilesConfig,
18 pub ignore: IgnoreConfig,
20 #[serde(default)]
22 pub disabled_rules: HashSet<String>,
23 #[serde(default)]
25 pub rules: Vec<YamlRule>,
26 #[serde(default)]
28 pub malware_signatures: Vec<MalwareSignature>,
29}
30
31#[derive(Debug, Clone, Default, Serialize, Deserialize)]
33#[serde(default)]
34pub struct ScanConfig {
35 pub format: Option<String>,
37 pub strict: bool,
39 pub scan_type: Option<String>,
41 pub recursive: bool,
43 pub ci: bool,
45 pub verbose: bool,
47 pub min_confidence: Option<String>,
49 pub skip_comments: bool,
51 pub fix_hint: bool,
53 pub no_malware_scan: bool,
55}
56
57impl Config {
58 pub fn from_file(path: &Path) -> Result<Self, ConfigError> {
60 let content = fs::read_to_string(path).map_err(|e| ConfigError::ReadFile {
61 path: path.display().to_string(),
62 source: e,
63 })?;
64
65 let ext = path
66 .extension()
67 .and_then(|e| e.to_str())
68 .unwrap_or("")
69 .to_lowercase();
70
71 match ext.as_str() {
72 "yaml" | "yml" => serde_yaml::from_str(&content).map_err(|e| ConfigError::ParseYaml {
73 path: path.display().to_string(),
74 source: e,
75 }),
76 "json" => serde_json::from_str(&content).map_err(|e| ConfigError::ParseJson {
77 path: path.display().to_string(),
78 source: e,
79 }),
80 "toml" => toml::from_str(&content).map_err(|e| ConfigError::ParseToml {
81 path: path.display().to_string(),
82 source: e,
83 }),
84 _ => Err(ConfigError::UnsupportedFormat(
85 path.display().to_string(),
86 ext,
87 )),
88 }
89 }
90
91 pub fn load(project_root: Option<&Path>) -> Self {
100 if let Some(root) = project_root {
102 for filename in &[
103 ".cc-audit.yaml",
104 ".cc-audit.yml",
105 ".cc-audit.json",
106 ".cc-audit.toml",
107 ] {
108 let path = root.join(filename);
109 if path.exists()
110 && let Ok(config) = Self::from_file(&path)
111 {
112 return config;
113 }
114 }
115 }
116
117 if let Some(config_dir) = dirs::config_dir() {
119 let global_config = config_dir.join("cc-audit").join("config.yaml");
120 if global_config.exists()
121 && let Ok(config) = Self::from_file(&global_config)
122 {
123 return config;
124 }
125 }
126
127 Self::default()
129 }
130
131 pub fn generate_template() -> String {
133 r#"# cc-audit Configuration File
134# Place this file as .cc-audit.yaml in your project root
135
136# Scan configuration (CLI options can be set here as defaults)
137scan:
138 # Output format: terminal, json, sarif, html
139 # format: terminal
140
141 # Strict mode: show medium/low severity findings and treat warnings as errors
142 strict: false
143
144 # Scan type: skill, hook, mcp, command, rules, docker, dependency
145 # scan_type: skill
146
147 # Recursive scan
148 recursive: false
149
150 # CI mode: non-interactive output
151 ci: false
152
153 # Verbose output
154 verbose: false
155
156 # Minimum confidence level: tentative, firm, certain
157 # min_confidence: tentative
158
159 # Skip comment lines when scanning
160 skip_comments: false
161
162 # Show fix hints in terminal output
163 fix_hint: false
164
165 # Disable malware signature scanning
166 no_malware_scan: false
167
168# Watch mode configuration
169watch:
170 # Debounce duration in milliseconds
171 debounce_ms: 300
172
173 # Poll interval in milliseconds
174 poll_interval_ms: 500
175
176# Ignore configuration for scanning
177ignore:
178 # Directories to ignore (overwrites defaults if specified)
179 # directories:
180 # - node_modules
181 # - target
182 # - .git
183 # - dist
184 # - build
185
186 # Glob patterns to ignore
187 # patterns:
188 # - "*.log"
189 # - "temp/**"
190
191 # Include test directories in scan
192 include_tests: false
193
194 # Include node_modules in scan
195 include_node_modules: false
196
197 # Include vendor directories in scan
198 include_vendor: false
199
200# Rule IDs to disable
201# disabled_rules:
202# - "PE-001"
203# - "EX-002"
204
205# Text file detection configuration
206# text_files:
207# # Additional file extensions to treat as text
208# extensions:
209# - custom
210# - special
211#
212# # Additional special file names
213# special_names:
214# - CUSTOMFILE
215
216# Custom rules (YAML format)
217# rules:
218# - id: "CUSTOM-001"
219# name: "Custom Rule Name"
220# severity: "high" # critical, high, medium, low, info
221# category: "exfiltration" # exfiltration, privilege_escalation, persistence, etc.
222# patterns:
223# - 'pattern_to_match'
224# message: "Description of the issue"
225# confidence: "firm" # tentative, firm, certain
226# fix_hint: "How to fix this issue"
227
228# Custom malware signatures
229# malware_signatures:
230# - id: "MW-CUSTOM-001"
231# name: "Custom Malware Signature"
232# description: "Description of what this detects"
233# pattern: "malware_pattern"
234# severity: "critical"
235# category: "exfiltration"
236# confidence: "firm"
237"#
238 .to_string()
239 }
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize)]
244#[serde(default)]
245pub struct WatchConfig {
246 pub debounce_ms: u64,
248 pub poll_interval_ms: u64,
250}
251
252impl Default for WatchConfig {
253 fn default() -> Self {
254 Self {
255 debounce_ms: 300,
256 poll_interval_ms: 500,
257 }
258 }
259}
260
261#[derive(Debug, Clone, Serialize, Deserialize)]
263#[serde(default)]
264pub struct TextFilesConfig {
265 pub extensions: HashSet<String>,
267 pub special_names: HashSet<String>,
269}
270
271impl Default for TextFilesConfig {
272 fn default() -> Self {
273 let extensions: HashSet<String> = [
274 "md",
276 "txt",
277 "rst",
278 "json",
280 "yaml",
281 "yml",
282 "toml",
283 "xml",
284 "ini",
285 "conf",
286 "cfg",
287 "env",
288 "sh",
290 "bash",
291 "zsh",
292 "fish",
293 "py",
295 "rb",
296 "pl",
297 "pm",
298 "lua",
299 "r",
300 "js",
302 "ts",
303 "jsx",
304 "tsx",
305 "html",
306 "css",
307 "scss",
308 "sass",
309 "less",
310 "rs",
312 "go",
313 "c",
314 "cpp",
315 "h",
316 "hpp",
317 "cc",
318 "cxx",
319 "java",
321 "kt",
322 "kts",
323 "scala",
324 "clj",
325 "groovy",
326 "cs",
328 "fs",
329 "vb",
330 "swift",
332 "m",
333 "mm",
334 "php",
336 "ex",
337 "exs",
338 "hs",
339 "ml",
340 "vim",
341 "el",
342 "lisp",
343 "dockerfile",
345 "makefile",
347 "cmake",
348 "gradle",
349 ]
350 .into_iter()
351 .map(String::from)
352 .collect();
353
354 let special_names: HashSet<String> = [
355 "Dockerfile",
356 "Makefile",
357 "Rakefile",
358 "Gemfile",
359 "Podfile",
360 "Vagrantfile",
361 "Procfile",
362 "LICENSE",
363 "README",
364 "CHANGELOG",
365 "CONTRIBUTING",
366 "AUTHORS",
367 "CMakeLists.txt",
368 "Justfile",
369 ]
370 .into_iter()
371 .map(String::from)
372 .collect();
373
374 Self {
375 extensions,
376 special_names,
377 }
378 }
379}
380
381#[derive(Debug, Clone, Serialize, Deserialize)]
383#[serde(default)]
384pub struct IgnoreConfig {
385 pub directories: HashSet<String>,
387 pub patterns: Vec<String>,
389 pub include_tests: bool,
391 pub include_node_modules: bool,
393 pub include_vendor: bool,
395}
396
397impl Default for IgnoreConfig {
398 fn default() -> Self {
399 let directories: HashSet<String> = [
400 "target",
402 "dist",
403 "build",
404 "out",
405 "node_modules",
407 ".pnpm",
408 ".yarn",
409 ".git",
411 ".svn",
412 ".hg",
413 ".idea",
415 ".vscode",
416 ".cache",
418 "__pycache__",
419 ".pytest_cache",
420 ".mypy_cache",
421 "coverage",
423 ".nyc_output",
424 ]
425 .into_iter()
426 .map(String::from)
427 .collect();
428
429 Self {
430 directories,
431 patterns: Vec::new(),
432 include_tests: false,
433 include_node_modules: false,
434 include_vendor: false,
435 }
436 }
437}
438
439impl TextFilesConfig {
440 pub fn is_text_file(&self, path: &Path) -> bool {
442 if let Some(ext) = path.extension().and_then(|e| e.to_str())
444 && self.extensions.contains(&ext.to_lowercase())
445 {
446 return true;
447 }
448
449 if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
451 if self.special_names.contains(name) {
453 return true;
454 }
455 let name_lower = name.to_lowercase();
457 if self
458 .special_names
459 .iter()
460 .any(|n| n.to_lowercase() == name_lower)
461 {
462 return true;
463 }
464 }
465
466 false
467 }
468}
469
470#[derive(Debug, thiserror::Error)]
472pub enum ConfigError {
473 #[error("Failed to read config file {path}: {source}")]
474 ReadFile {
475 path: String,
476 #[source]
477 source: std::io::Error,
478 },
479
480 #[error("Failed to parse YAML config {path}: {source}")]
481 ParseYaml {
482 path: String,
483 #[source]
484 source: serde_yaml::Error,
485 },
486
487 #[error("Failed to parse JSON config {path}: {source}")]
488 ParseJson {
489 path: String,
490 #[source]
491 source: serde_json::Error,
492 },
493
494 #[error("Failed to parse TOML config {path}: {source}")]
495 ParseToml {
496 path: String,
497 #[source]
498 source: toml::de::Error,
499 },
500
501 #[error("Unsupported config format for {0}: .{1}")]
502 UnsupportedFormat(String, String),
503}
504
505#[cfg(test)]
506mod tests {
507 use super::*;
508 use tempfile::TempDir;
509
510 #[test]
511 fn test_default_config() {
512 let config = Config::default();
513 assert_eq!(config.watch.debounce_ms, 300);
514 assert_eq!(config.watch.poll_interval_ms, 500);
515 assert!(config.text_files.extensions.contains("md"));
516 assert!(config.text_files.extensions.contains("py"));
517 }
518
519 #[test]
520 fn test_is_text_file_by_extension() {
521 let config = TextFilesConfig::default();
522 assert!(config.is_text_file(Path::new("test.md")));
523 assert!(config.is_text_file(Path::new("test.py")));
524 assert!(config.is_text_file(Path::new("test.rs")));
525 assert!(config.is_text_file(Path::new("test.json")));
526 assert!(!config.is_text_file(Path::new("test.exe")));
527 assert!(!config.is_text_file(Path::new("test.bin")));
528 }
529
530 #[test]
531 fn test_is_text_file_by_name() {
532 let config = TextFilesConfig::default();
533 assert!(config.is_text_file(Path::new("Dockerfile")));
534 assert!(config.is_text_file(Path::new("Makefile")));
535 assert!(config.is_text_file(Path::new("LICENSE")));
536 assert!(!config.is_text_file(Path::new("unknown_file")));
537 }
538
539 #[test]
540 fn test_is_text_file_case_insensitive_extension() {
541 let config = TextFilesConfig::default();
542 assert!(config.is_text_file(Path::new("test.MD")));
543 assert!(config.is_text_file(Path::new("test.PY")));
544 assert!(config.is_text_file(Path::new("test.Json")));
545 }
546
547 #[test]
548 fn test_load_yaml_config() {
549 let dir = TempDir::new().unwrap();
550 let config_path = dir.path().join(".cc-audit.yaml");
551 fs::write(
552 &config_path,
553 r#"
554watch:
555 debounce_ms: 500
556 poll_interval_ms: 1000
557"#,
558 )
559 .unwrap();
560
561 let config = Config::from_file(&config_path).unwrap();
562 assert_eq!(config.watch.debounce_ms, 500);
563 assert_eq!(config.watch.poll_interval_ms, 1000);
564 }
565
566 #[test]
567 fn test_load_json_config() {
568 let dir = TempDir::new().unwrap();
569 let config_path = dir.path().join(".cc-audit.json");
570 fs::write(
571 &config_path,
572 r#"{"watch": {"debounce_ms": 200, "poll_interval_ms": 400}}"#,
573 )
574 .unwrap();
575
576 let config = Config::from_file(&config_path).unwrap();
577 assert_eq!(config.watch.debounce_ms, 200);
578 assert_eq!(config.watch.poll_interval_ms, 400);
579 }
580
581 #[test]
582 fn test_load_toml_config() {
583 let dir = TempDir::new().unwrap();
584 let config_path = dir.path().join(".cc-audit.toml");
585 fs::write(
586 &config_path,
587 r#"
588[watch]
589debounce_ms = 600
590poll_interval_ms = 800
591"#,
592 )
593 .unwrap();
594
595 let config = Config::from_file(&config_path).unwrap();
596 assert_eq!(config.watch.debounce_ms, 600);
597 assert_eq!(config.watch.poll_interval_ms, 800);
598 }
599
600 #[test]
601 fn test_load_with_project_config() {
602 let dir = TempDir::new().unwrap();
603 let config_path = dir.path().join(".cc-audit.yaml");
604 fs::write(
605 &config_path,
606 r#"
607watch:
608 debounce_ms: 100
609"#,
610 )
611 .unwrap();
612
613 let config = Config::load(Some(dir.path()));
614 assert_eq!(config.watch.debounce_ms, 100);
615 }
616
617 #[test]
618 fn test_load_fallback_to_default() {
619 let dir = TempDir::new().unwrap();
620 let config = Config::load(Some(dir.path()));
621 assert_eq!(config.watch.debounce_ms, 300); }
623
624 #[test]
625 fn test_unsupported_format_error() {
626 let dir = TempDir::new().unwrap();
627 let config_path = dir.path().join(".cc-audit.xml");
628 fs::write(&config_path, "<config></config>").unwrap();
629
630 let result = Config::from_file(&config_path);
631 assert!(matches!(result, Err(ConfigError::UnsupportedFormat(_, _))));
632 }
633
634 #[test]
635 fn test_partial_config_with_defaults() {
636 let dir = TempDir::new().unwrap();
637 let config_path = dir.path().join(".cc-audit.yaml");
638 fs::write(
639 &config_path,
640 r#"
641watch:
642 debounce_ms: 999
643"#,
644 )
645 .unwrap();
646
647 let config = Config::from_file(&config_path).unwrap();
648 assert_eq!(config.watch.debounce_ms, 999);
649 assert_eq!(config.watch.poll_interval_ms, 500);
651 }
652
653 #[test]
654 fn test_config_error_read_file() {
655 let result = Config::from_file(Path::new("/nonexistent/config.yaml"));
656 assert!(matches!(result, Err(ConfigError::ReadFile { .. })));
657 }
658
659 #[test]
660 fn test_custom_text_extensions() {
661 let dir = TempDir::new().unwrap();
662 let config_path = dir.path().join(".cc-audit.yaml");
663 fs::write(
664 &config_path,
665 r#"
666text_files:
667 extensions:
668 - custom
669 - special
670"#,
671 )
672 .unwrap();
673
674 let config = Config::from_file(&config_path).unwrap();
675 assert!(config.text_files.extensions.contains("custom"));
676 assert!(config.text_files.extensions.contains("special"));
677 }
678
679 #[test]
680 fn test_config_with_rules() {
681 let dir = TempDir::new().unwrap();
682 let config_path = dir.path().join(".cc-audit.yaml");
683 fs::write(
684 &config_path,
685 r#"
686rules:
687 - id: "CUSTOM-001"
688 name: "Test Rule"
689 severity: "high"
690 category: "exfiltration"
691 patterns:
692 - 'test_pattern'
693 message: "Test message"
694"#,
695 )
696 .unwrap();
697
698 let config = Config::from_file(&config_path).unwrap();
699 assert_eq!(config.rules.len(), 1);
700 assert_eq!(config.rules[0].id, "CUSTOM-001");
701 assert_eq!(config.rules[0].name, "Test Rule");
702 assert_eq!(config.rules[0].severity, "high");
703 }
704
705 #[test]
706 fn test_config_with_malware_signatures() {
707 let dir = TempDir::new().unwrap();
708 let config_path = dir.path().join(".cc-audit.yaml");
709 fs::write(
710 &config_path,
711 r#"
712malware_signatures:
713 - id: "MW-CUSTOM-001"
714 name: "Custom Malware"
715 description: "Test malware pattern"
716 pattern: "evil_pattern"
717 severity: "critical"
718 category: "exfiltration"
719 confidence: "firm"
720"#,
721 )
722 .unwrap();
723
724 let config = Config::from_file(&config_path).unwrap();
725 assert_eq!(config.malware_signatures.len(), 1);
726 assert_eq!(config.malware_signatures[0].id, "MW-CUSTOM-001");
727 assert_eq!(config.malware_signatures[0].name, "Custom Malware");
728 assert_eq!(config.malware_signatures[0].severity, "critical");
729 }
730
731 #[test]
732 fn test_config_with_rules_and_malware_signatures() {
733 let dir = TempDir::new().unwrap();
734 let config_path = dir.path().join(".cc-audit.yaml");
735 fs::write(
736 &config_path,
737 r#"
738watch:
739 debounce_ms: 100
740
741rules:
742 - id: "CUSTOM-001"
743 name: "Test Rule"
744 severity: "high"
745 category: "exfiltration"
746 patterns:
747 - 'test_pattern'
748 message: "Test message"
749
750malware_signatures:
751 - id: "MW-CUSTOM-001"
752 name: "Custom Malware"
753 description: "Test malware pattern"
754 pattern: "evil_pattern"
755 severity: "critical"
756 category: "exfiltration"
757 confidence: "firm"
758"#,
759 )
760 .unwrap();
761
762 let config = Config::from_file(&config_path).unwrap();
763 assert_eq!(config.watch.debounce_ms, 100);
764 assert_eq!(config.rules.len(), 1);
765 assert_eq!(config.malware_signatures.len(), 1);
766 }
767
768 #[test]
769 fn test_default_config_has_empty_rules() {
770 let config = Config::default();
771 assert!(config.rules.is_empty());
772 assert!(config.malware_signatures.is_empty());
773 }
774
775 #[test]
776 fn test_parse_yaml_error() {
777 let dir = TempDir::new().unwrap();
778 let config_path = dir.path().join(".cc-audit.yaml");
779 fs::write(&config_path, "invalid: yaml: content: [").unwrap();
780
781 let result = Config::from_file(&config_path);
782 assert!(matches!(result, Err(ConfigError::ParseYaml { .. })));
783 }
784
785 #[test]
786 fn test_parse_json_error() {
787 let dir = TempDir::new().unwrap();
788 let config_path = dir.path().join(".cc-audit.json");
789 fs::write(&config_path, "{invalid json}").unwrap();
790
791 let result = Config::from_file(&config_path);
792 assert!(matches!(result, Err(ConfigError::ParseJson { .. })));
793 }
794
795 #[test]
796 fn test_parse_toml_error() {
797 let dir = TempDir::new().unwrap();
798 let config_path = dir.path().join(".cc-audit.toml");
799 fs::write(&config_path, "[invalid toml\nkey = ").unwrap();
800
801 let result = Config::from_file(&config_path);
802 assert!(matches!(result, Err(ConfigError::ParseToml { .. })));
803 }
804
805 #[test]
806 fn test_load_with_invalid_config_falls_back() {
807 let dir = TempDir::new().unwrap();
808 let config_path = dir.path().join(".cc-audit.yaml");
809 fs::write(&config_path, "invalid: yaml: [").unwrap();
811
812 let config = Config::load(Some(dir.path()));
814 assert_eq!(config.watch.debounce_ms, 300);
815 }
816
817 #[test]
818 fn test_is_text_file_returns_false_for_unknown() {
819 let config = TextFilesConfig::default();
820 assert!(!config.is_text_file(Path::new("somefile")));
822 assert!(!config.is_text_file(Path::new("random_binary")));
823 }
824
825 #[test]
826 fn test_ignore_config_default() {
827 let config = IgnoreConfig::default();
828 assert!(config.directories.contains("node_modules"));
830 assert!(config.directories.contains("target"));
831 assert!(config.directories.contains(".git"));
832 assert!(config.directories.contains("dist"));
833 assert!(config.directories.contains("build"));
834 assert!(!config.include_tests);
836 assert!(!config.include_node_modules);
837 assert!(!config.include_vendor);
838 assert!(config.patterns.is_empty());
840 }
841
842 #[test]
843 fn test_config_with_ignore_settings() {
844 let dir = TempDir::new().unwrap();
845 let config_path = dir.path().join(".cc-audit.yaml");
846 fs::write(
847 &config_path,
848 r#"
849ignore:
850 directories:
851 - custom_dir
852 - my_cache
853 patterns:
854 - "*.log"
855 - "temp/**"
856 include_tests: true
857 include_node_modules: false
858 include_vendor: true
859"#,
860 )
861 .unwrap();
862
863 let config = Config::from_file(&config_path).unwrap();
864 assert!(config.ignore.directories.contains("custom_dir"));
865 assert!(config.ignore.directories.contains("my_cache"));
866 assert_eq!(config.ignore.patterns.len(), 2);
867 assert!(config.ignore.patterns.contains(&"*.log".to_string()));
868 assert!(config.ignore.patterns.contains(&"temp/**".to_string()));
869 assert!(config.ignore.include_tests);
870 assert!(!config.ignore.include_node_modules);
871 assert!(config.ignore.include_vendor);
872 }
873
874 #[test]
875 fn test_config_with_disabled_rules() {
876 let dir = TempDir::new().unwrap();
877 let config_path = dir.path().join(".cc-audit.yaml");
878 fs::write(
879 &config_path,
880 r#"
881disabled_rules:
882 - "PE-001"
883 - "EX-002"
884 - "CUSTOM-RULE"
885"#,
886 )
887 .unwrap();
888
889 let config = Config::from_file(&config_path).unwrap();
890 assert_eq!(config.disabled_rules.len(), 3);
891 assert!(config.disabled_rules.contains("PE-001"));
892 assert!(config.disabled_rules.contains("EX-002"));
893 assert!(config.disabled_rules.contains("CUSTOM-RULE"));
894 }
895
896 #[test]
897 fn test_default_config_has_empty_disabled_rules() {
898 let config = Config::default();
899 assert!(config.disabled_rules.is_empty());
900 }
901
902 #[test]
903 fn test_config_ignore_default_when_partial() {
904 let dir = TempDir::new().unwrap();
905 let config_path = dir.path().join(".cc-audit.yaml");
906 fs::write(
907 &config_path,
908 r#"
909ignore:
910 include_tests: true
911"#,
912 )
913 .unwrap();
914
915 let config = Config::from_file(&config_path).unwrap();
916 assert!(config.ignore.include_tests);
918 assert!(!config.ignore.include_node_modules);
920 assert!(!config.ignore.include_vendor);
921 }
922
923 #[test]
924 fn test_scan_config_default() {
925 let config = ScanConfig::default();
926 assert!(config.format.is_none());
927 assert!(!config.strict);
928 assert!(config.scan_type.is_none());
929 assert!(!config.recursive);
930 assert!(!config.ci);
931 assert!(!config.verbose);
932 assert!(config.min_confidence.is_none());
933 assert!(!config.skip_comments);
934 assert!(!config.fix_hint);
935 assert!(!config.no_malware_scan);
936 }
937
938 #[test]
939 fn test_config_with_scan_settings() {
940 let dir = TempDir::new().unwrap();
941 let config_path = dir.path().join(".cc-audit.yaml");
942 fs::write(
943 &config_path,
944 r#"
945scan:
946 format: json
947 strict: true
948 scan_type: docker
949 recursive: true
950 ci: true
951 verbose: true
952 min_confidence: firm
953 skip_comments: true
954 fix_hint: true
955 no_malware_scan: true
956"#,
957 )
958 .unwrap();
959
960 let config = Config::from_file(&config_path).unwrap();
961 assert_eq!(config.scan.format, Some("json".to_string()));
962 assert!(config.scan.strict);
963 assert_eq!(config.scan.scan_type, Some("docker".to_string()));
964 assert!(config.scan.recursive);
965 assert!(config.scan.ci);
966 assert!(config.scan.verbose);
967 assert_eq!(config.scan.min_confidence, Some("firm".to_string()));
968 assert!(config.scan.skip_comments);
969 assert!(config.scan.fix_hint);
970 assert!(config.scan.no_malware_scan);
971 }
972
973 #[test]
974 fn test_config_with_partial_scan_settings() {
975 let dir = TempDir::new().unwrap();
976 let config_path = dir.path().join(".cc-audit.yaml");
977 fs::write(
978 &config_path,
979 r#"
980scan:
981 strict: true
982 verbose: true
983"#,
984 )
985 .unwrap();
986
987 let config = Config::from_file(&config_path).unwrap();
988 assert!(config.scan.strict);
990 assert!(config.scan.verbose);
991 assert!(config.scan.format.is_none());
993 assert!(config.scan.scan_type.is_none());
994 assert!(!config.scan.recursive);
995 assert!(!config.scan.ci);
996 assert!(config.scan.min_confidence.is_none());
997 assert!(!config.scan.skip_comments);
998 assert!(!config.scan.fix_hint);
999 assert!(!config.scan.no_malware_scan);
1000 }
1001
1002 #[test]
1003 fn test_default_config_has_default_scan() {
1004 let config = Config::default();
1005 assert!(!config.scan.strict);
1006 assert!(!config.scan.verbose);
1007 assert!(config.scan.format.is_none());
1008 }
1009
1010 #[test]
1011 fn test_generate_template() {
1012 let template = Config::generate_template();
1013 assert!(template.contains("# cc-audit Configuration File"));
1015 assert!(template.contains("scan:"));
1016 assert!(template.contains("watch:"));
1017 assert!(template.contains("ignore:"));
1018 assert!(template.contains("# disabled_rules:"));
1019 assert!(template.contains("# rules:"));
1020 assert!(template.contains("# malware_signatures:"));
1021 }
1022
1023 #[test]
1024 fn test_generate_template_is_valid_yaml() {
1025 let template = Config::generate_template();
1026 let result: Result<Config, _> = serde_yaml::from_str(&template);
1028 assert!(result.is_ok(), "Template should be valid YAML");
1029 }
1030}