1use crate::malware_db::MalwareSignature;
2use crate::rules::RuleSeverity;
3use crate::rules::custom::YamlRule;
4use serde::{Deserialize, Serialize};
5use std::collections::HashSet;
6use std::fs;
7use std::path::Path;
8
9#[derive(Debug, Clone, Default, Serialize, Deserialize)]
11#[serde(default)]
12pub struct Config {
13 pub scan: ScanConfig,
15 pub watch: WatchConfig,
17 pub text_files: TextFilesConfig,
19 pub ignore: IgnoreConfig,
21 #[serde(default)]
23 pub baseline: BaselineConfig,
24 #[serde(default)]
26 pub severity: SeverityConfig,
27 #[serde(default)]
29 pub disabled_rules: HashSet<String>,
30 #[serde(default)]
32 pub rules: Vec<YamlRule>,
33 #[serde(default)]
35 pub malware_signatures: Vec<MalwareSignature>,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
53#[serde(default)]
54pub struct SeverityConfig {
55 pub default: RuleSeverity,
57 #[serde(default)]
59 pub warn: HashSet<String>,
60 #[serde(default)]
63 pub ignore: HashSet<String>,
64}
65
66impl Default for SeverityConfig {
67 fn default() -> Self {
68 Self {
69 default: RuleSeverity::Error,
70 warn: HashSet::new(),
71 ignore: HashSet::new(),
72 }
73 }
74}
75
76impl SeverityConfig {
77 pub fn get_rule_severity(&self, rule_id: &str) -> Option<RuleSeverity> {
80 if self.ignore.contains(rule_id) {
82 return None; }
84 if self.warn.contains(rule_id) {
85 return Some(RuleSeverity::Warn);
86 }
87 Some(self.default)
88 }
89}
90
91#[derive(Debug, Clone, Default, Serialize, Deserialize)]
93#[serde(default)]
94pub struct ScanConfig {
95 pub format: Option<String>,
97 pub strict: bool,
99 pub scan_type: Option<String>,
101 pub recursive: bool,
103 pub ci: bool,
105 pub verbose: bool,
107 pub min_confidence: Option<String>,
109 pub skip_comments: bool,
111 pub fix_hint: bool,
113 pub no_malware_scan: bool,
115 pub watch: bool,
117 pub malware_db: Option<String>,
119 pub custom_rules: Option<String>,
121 pub output: Option<String>,
123 pub deep_scan: bool,
125 pub fix: bool,
127 pub fix_dry_run: bool,
129}
130
131impl Config {
132 pub fn from_file(path: &Path) -> Result<Self, ConfigError> {
134 let content = fs::read_to_string(path).map_err(|e| ConfigError::ReadFile {
135 path: path.display().to_string(),
136 source: e,
137 })?;
138
139 let ext = path
140 .extension()
141 .and_then(|e| e.to_str())
142 .unwrap_or("")
143 .to_lowercase();
144
145 match ext.as_str() {
146 "yaml" | "yml" => serde_yaml::from_str(&content).map_err(|e| ConfigError::ParseYaml {
147 path: path.display().to_string(),
148 source: e,
149 }),
150 "json" => serde_json::from_str(&content).map_err(|e| ConfigError::ParseJson {
151 path: path.display().to_string(),
152 source: e,
153 }),
154 "toml" => toml::from_str(&content).map_err(|e| ConfigError::ParseToml {
155 path: path.display().to_string(),
156 source: e,
157 }),
158 _ => Err(ConfigError::UnsupportedFormat(
159 path.display().to_string(),
160 ext,
161 )),
162 }
163 }
164
165 pub fn load(project_root: Option<&Path>) -> Self {
174 if let Some(root) = project_root {
176 for filename in &[
177 ".cc-audit.yaml",
178 ".cc-audit.yml",
179 ".cc-audit.json",
180 ".cc-audit.toml",
181 ] {
182 let path = root.join(filename);
183 if path.exists()
184 && let Ok(config) = Self::from_file(&path)
185 {
186 return config;
187 }
188 }
189 }
190
191 if let Some(config_dir) = dirs::config_dir() {
193 let global_config = config_dir.join("cc-audit").join("config.yaml");
194 if global_config.exists()
195 && let Ok(config) = Self::from_file(&global_config)
196 {
197 return config;
198 }
199 }
200
201 Self::default()
203 }
204
205 pub fn effective_disabled_rules(&self) -> HashSet<String> {
207 let mut disabled = self.disabled_rules.clone();
208 disabled.extend(self.severity.ignore.iter().cloned());
209 disabled
210 }
211
212 pub fn is_rule_disabled(&self, rule_id: &str) -> bool {
214 self.disabled_rules.contains(rule_id) || self.severity.ignore.contains(rule_id)
215 }
216
217 pub fn get_rule_severity(&self, rule_id: &str) -> Option<crate::rules::RuleSeverity> {
219 if self.is_rule_disabled(rule_id) {
220 return None;
221 }
222 self.severity.get_rule_severity(rule_id)
223 }
224
225 pub fn generate_template() -> String {
227 r#"# cc-audit Configuration File
228# Place this file as .cc-audit.yaml in your project root
229
230# =============================================================================
231# RULE SEVERITY CONFIGURATION (v0.5.0)
232# =============================================================================
233# Controls how findings affect CI exit code.
234# - error: Causes CI failure (exit 1) - DEFAULT for all rules
235# - warn: Report only, does not cause CI failure (exit 0)
236# - ignore: Completely skip the rule (no report)
237#
238# Priority: ignore > warn > default
239
240severity:
241 # Default severity for all rules
242 default: error
243
244 # Rules to treat as warnings only (report but don't fail CI)
245 # warn:
246 # - PI-001 # Prompt injection patterns
247 # - PI-002
248 # - OB-001 # Obfuscation patterns
249
250 # Rules to completely ignore (no report)
251 # ignore:
252 # - OP-001 # Overpermission
253
254# =============================================================================
255# SCAN CONFIGURATION
256# =============================================================================
257scan:
258 # Output format: terminal, json, sarif, html
259 # format: terminal
260
261 # Strict mode: show medium/low severity findings and treat warnings as errors
262 strict: false
263
264 # Scan type: skill, hook, mcp, command, rules, docker, dependency, subagent, plugin
265 # scan_type: skill
266
267 # Recursive scan
268 recursive: false
269
270 # CI mode: non-interactive output
271 ci: false
272
273 # Verbose output
274 verbose: false
275
276 # Minimum confidence level: tentative, firm, certain
277 # min_confidence: tentative
278
279 # Skip comment lines when scanning
280 skip_comments: false
281
282 # Show fix hints in terminal output
283 fix_hint: false
284
285 # Disable malware signature scanning
286 no_malware_scan: false
287
288 # Watch mode: continuously monitor files for changes
289 watch: false
290
291 # Path to a custom malware signatures database (JSON)
292 # malware_db: ./custom-malware.json
293
294 # Path to a custom rules file (YAML format)
295 # custom_rules: ./custom-rules.yaml
296
297 # Output file path (for HTML/JSON/SARIF output)
298 # output: ./report.html
299
300 # Enable deep scan with deobfuscation
301 deep_scan: false
302
303 # Auto-fix issues (where possible)
304 fix: false
305
306 # Preview auto-fix changes without applying them
307 fix_dry_run: false
308
309# =============================================================================
310# BASELINE CONFIGURATION (Drift Detection / Rug Pull Prevention)
311# =============================================================================
312baseline:
313 # Create a baseline snapshot when scanning
314 enabled: false
315
316 # Check for drift against saved baseline
317 check_drift: false
318
319 # Path to save baseline to
320 # save_to: ./.cc-audit-baseline.json
321
322 # Path to baseline file to compare against
323 # compare_with: ./.cc-audit-baseline.json
324
325# =============================================================================
326# WATCH MODE CONFIGURATION
327# =============================================================================
328watch:
329 # Debounce duration in milliseconds
330 debounce_ms: 300
331
332 # Poll interval in milliseconds
333 poll_interval_ms: 500
334
335# =============================================================================
336# IGNORE CONFIGURATION
337# =============================================================================
338ignore:
339 # Directories to ignore (overwrites defaults if specified)
340 # directories:
341 # - node_modules
342 # - target
343 # - .git
344 # - dist
345 # - build
346
347 # Glob patterns to ignore
348 # patterns:
349 # - "*.log"
350 # - "temp/**"
351
352 # Include test directories in scan
353 include_tests: false
354
355 # Include node_modules in scan
356 include_node_modules: false
357
358 # Include vendor directories in scan
359 include_vendor: false
360
361# =============================================================================
362# RULE CONFIGURATION
363# =============================================================================
364
365# Rule IDs to disable
366# disabled_rules:
367# - "PE-001"
368# - "EX-002"
369
370# Text file detection configuration
371# text_files:
372# # Additional file extensions to treat as text
373# extensions:
374# - custom
375# - special
376#
377# # Additional special file names
378# special_names:
379# - CUSTOMFILE
380
381# Custom rules (YAML format)
382# rules:
383# - id: "CUSTOM-001"
384# name: "Custom Rule Name"
385# severity: "high" # critical, high, medium, low, info
386# category: "exfiltration" # exfiltration, privilege_escalation, persistence, etc.
387# patterns:
388# - 'pattern_to_match'
389# message: "Description of the issue"
390# confidence: "firm" # tentative, firm, certain
391# fix_hint: "How to fix this issue"
392
393# Custom malware signatures
394# malware_signatures:
395# - id: "MW-CUSTOM-001"
396# name: "Custom Malware Signature"
397# description: "Description of what this detects"
398# pattern: "malware_pattern"
399# severity: "critical"
400# category: "exfiltration"
401# confidence: "firm"
402"#
403 .to_string()
404 }
405}
406
407#[derive(Debug, Clone, Serialize, Deserialize)]
409#[serde(default)]
410pub struct WatchConfig {
411 pub debounce_ms: u64,
413 pub poll_interval_ms: u64,
415}
416
417impl Default for WatchConfig {
418 fn default() -> Self {
419 Self {
420 debounce_ms: 300,
421 poll_interval_ms: 500,
422 }
423 }
424}
425
426#[derive(Debug, Clone, Default, Serialize, Deserialize)]
428#[serde(default)]
429pub struct BaselineConfig {
430 pub enabled: bool,
432 pub check_drift: bool,
434 pub save_to: Option<String>,
436 pub compare_with: Option<String>,
438}
439
440#[derive(Debug, Clone, Serialize, Deserialize)]
442#[serde(default)]
443pub struct TextFilesConfig {
444 pub extensions: HashSet<String>,
446 pub special_names: HashSet<String>,
448}
449
450impl Default for TextFilesConfig {
451 fn default() -> Self {
452 let extensions: HashSet<String> = [
453 "md",
455 "txt",
456 "rst",
457 "json",
459 "yaml",
460 "yml",
461 "toml",
462 "xml",
463 "ini",
464 "conf",
465 "cfg",
466 "env",
467 "sh",
469 "bash",
470 "zsh",
471 "fish",
472 "py",
474 "rb",
475 "pl",
476 "pm",
477 "lua",
478 "r",
479 "js",
481 "ts",
482 "jsx",
483 "tsx",
484 "html",
485 "css",
486 "scss",
487 "sass",
488 "less",
489 "rs",
491 "go",
492 "c",
493 "cpp",
494 "h",
495 "hpp",
496 "cc",
497 "cxx",
498 "java",
500 "kt",
501 "kts",
502 "scala",
503 "clj",
504 "groovy",
505 "cs",
507 "fs",
508 "vb",
509 "swift",
511 "m",
512 "mm",
513 "php",
515 "ex",
516 "exs",
517 "hs",
518 "ml",
519 "vim",
520 "el",
521 "lisp",
522 "dockerfile",
524 "makefile",
526 "cmake",
527 "gradle",
528 ]
529 .into_iter()
530 .map(String::from)
531 .collect();
532
533 let special_names: HashSet<String> = [
534 "Dockerfile",
535 "Makefile",
536 "Rakefile",
537 "Gemfile",
538 "Podfile",
539 "Vagrantfile",
540 "Procfile",
541 "LICENSE",
542 "README",
543 "CHANGELOG",
544 "CONTRIBUTING",
545 "AUTHORS",
546 "CMakeLists.txt",
547 "Justfile",
548 ]
549 .into_iter()
550 .map(String::from)
551 .collect();
552
553 Self {
554 extensions,
555 special_names,
556 }
557 }
558}
559
560#[derive(Debug, Clone, Serialize, Deserialize)]
562#[serde(default)]
563pub struct IgnoreConfig {
564 pub directories: HashSet<String>,
566 pub patterns: Vec<String>,
568 pub include_tests: bool,
570 pub include_node_modules: bool,
572 pub include_vendor: bool,
574}
575
576impl Default for IgnoreConfig {
577 fn default() -> Self {
578 let directories: HashSet<String> = [
579 "target",
581 "dist",
582 "build",
583 "out",
584 "node_modules",
586 ".pnpm",
587 ".yarn",
588 ".git",
590 ".svn",
591 ".hg",
592 ".idea",
594 ".vscode",
595 ".cache",
597 "__pycache__",
598 ".pytest_cache",
599 ".mypy_cache",
600 "coverage",
602 ".nyc_output",
603 ]
604 .into_iter()
605 .map(String::from)
606 .collect();
607
608 Self {
609 directories,
610 patterns: Vec::new(),
611 include_tests: false,
612 include_node_modules: false,
613 include_vendor: false,
614 }
615 }
616}
617
618impl TextFilesConfig {
619 pub fn is_text_file(&self, path: &Path) -> bool {
621 if let Some(ext) = path.extension().and_then(|e| e.to_str())
623 && self.extensions.contains(&ext.to_lowercase())
624 {
625 return true;
626 }
627
628 if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
630 if self.special_names.contains(name) {
632 return true;
633 }
634 let name_lower = name.to_lowercase();
636 if self
637 .special_names
638 .iter()
639 .any(|n| n.to_lowercase() == name_lower)
640 {
641 return true;
642 }
643 }
644
645 false
646 }
647}
648
649#[derive(Debug, thiserror::Error)]
651pub enum ConfigError {
652 #[error("Failed to read config file {path}: {source}")]
653 ReadFile {
654 path: String,
655 #[source]
656 source: std::io::Error,
657 },
658
659 #[error("Failed to parse YAML config {path}: {source}")]
660 ParseYaml {
661 path: String,
662 #[source]
663 source: serde_yaml::Error,
664 },
665
666 #[error("Failed to parse JSON config {path}: {source}")]
667 ParseJson {
668 path: String,
669 #[source]
670 source: serde_json::Error,
671 },
672
673 #[error("Failed to parse TOML config {path}: {source}")]
674 ParseToml {
675 path: String,
676 #[source]
677 source: toml::de::Error,
678 },
679
680 #[error("Unsupported config format for {0}: .{1}")]
681 UnsupportedFormat(String, String),
682}
683
684#[cfg(test)]
685mod tests {
686 use super::*;
687 use tempfile::TempDir;
688
689 #[test]
690 fn test_default_config() {
691 let config = Config::default();
692 assert_eq!(config.watch.debounce_ms, 300);
693 assert_eq!(config.watch.poll_interval_ms, 500);
694 assert!(config.text_files.extensions.contains("md"));
695 assert!(config.text_files.extensions.contains("py"));
696 }
697
698 #[test]
699 fn test_is_text_file_by_extension() {
700 let config = TextFilesConfig::default();
701 assert!(config.is_text_file(Path::new("test.md")));
702 assert!(config.is_text_file(Path::new("test.py")));
703 assert!(config.is_text_file(Path::new("test.rs")));
704 assert!(config.is_text_file(Path::new("test.json")));
705 assert!(!config.is_text_file(Path::new("test.exe")));
706 assert!(!config.is_text_file(Path::new("test.bin")));
707 }
708
709 #[test]
710 fn test_is_text_file_by_name() {
711 let config = TextFilesConfig::default();
712 assert!(config.is_text_file(Path::new("Dockerfile")));
713 assert!(config.is_text_file(Path::new("Makefile")));
714 assert!(config.is_text_file(Path::new("LICENSE")));
715 assert!(!config.is_text_file(Path::new("unknown_file")));
716 }
717
718 #[test]
719 fn test_is_text_file_case_insensitive_extension() {
720 let config = TextFilesConfig::default();
721 assert!(config.is_text_file(Path::new("test.MD")));
722 assert!(config.is_text_file(Path::new("test.PY")));
723 assert!(config.is_text_file(Path::new("test.Json")));
724 }
725
726 #[test]
727 fn test_load_yaml_config() {
728 let dir = TempDir::new().unwrap();
729 let config_path = dir.path().join(".cc-audit.yaml");
730 fs::write(
731 &config_path,
732 r#"
733watch:
734 debounce_ms: 500
735 poll_interval_ms: 1000
736"#,
737 )
738 .unwrap();
739
740 let config = Config::from_file(&config_path).unwrap();
741 assert_eq!(config.watch.debounce_ms, 500);
742 assert_eq!(config.watch.poll_interval_ms, 1000);
743 }
744
745 #[test]
746 fn test_load_json_config() {
747 let dir = TempDir::new().unwrap();
748 let config_path = dir.path().join(".cc-audit.json");
749 fs::write(
750 &config_path,
751 r#"{"watch": {"debounce_ms": 200, "poll_interval_ms": 400}}"#,
752 )
753 .unwrap();
754
755 let config = Config::from_file(&config_path).unwrap();
756 assert_eq!(config.watch.debounce_ms, 200);
757 assert_eq!(config.watch.poll_interval_ms, 400);
758 }
759
760 #[test]
761 fn test_load_toml_config() {
762 let dir = TempDir::new().unwrap();
763 let config_path = dir.path().join(".cc-audit.toml");
764 fs::write(
765 &config_path,
766 r#"
767[watch]
768debounce_ms = 600
769poll_interval_ms = 800
770"#,
771 )
772 .unwrap();
773
774 let config = Config::from_file(&config_path).unwrap();
775 assert_eq!(config.watch.debounce_ms, 600);
776 assert_eq!(config.watch.poll_interval_ms, 800);
777 }
778
779 #[test]
780 fn test_load_with_project_config() {
781 let dir = TempDir::new().unwrap();
782 let config_path = dir.path().join(".cc-audit.yaml");
783 fs::write(
784 &config_path,
785 r#"
786watch:
787 debounce_ms: 100
788"#,
789 )
790 .unwrap();
791
792 let config = Config::load(Some(dir.path()));
793 assert_eq!(config.watch.debounce_ms, 100);
794 }
795
796 #[test]
797 fn test_load_fallback_to_default() {
798 let dir = TempDir::new().unwrap();
799 let config = Config::load(Some(dir.path()));
800 assert_eq!(config.watch.debounce_ms, 300); }
802
803 #[test]
804 fn test_unsupported_format_error() {
805 let dir = TempDir::new().unwrap();
806 let config_path = dir.path().join(".cc-audit.xml");
807 fs::write(&config_path, "<config></config>").unwrap();
808
809 let result = Config::from_file(&config_path);
810 assert!(matches!(result, Err(ConfigError::UnsupportedFormat(_, _))));
811 }
812
813 #[test]
814 fn test_partial_config_with_defaults() {
815 let dir = TempDir::new().unwrap();
816 let config_path = dir.path().join(".cc-audit.yaml");
817 fs::write(
818 &config_path,
819 r#"
820watch:
821 debounce_ms: 999
822"#,
823 )
824 .unwrap();
825
826 let config = Config::from_file(&config_path).unwrap();
827 assert_eq!(config.watch.debounce_ms, 999);
828 assert_eq!(config.watch.poll_interval_ms, 500);
830 }
831
832 #[test]
833 fn test_config_error_read_file() {
834 let result = Config::from_file(Path::new("/nonexistent/config.yaml"));
835 assert!(matches!(result, Err(ConfigError::ReadFile { .. })));
836 }
837
838 #[test]
839 fn test_custom_text_extensions() {
840 let dir = TempDir::new().unwrap();
841 let config_path = dir.path().join(".cc-audit.yaml");
842 fs::write(
843 &config_path,
844 r#"
845text_files:
846 extensions:
847 - custom
848 - special
849"#,
850 )
851 .unwrap();
852
853 let config = Config::from_file(&config_path).unwrap();
854 assert!(config.text_files.extensions.contains("custom"));
855 assert!(config.text_files.extensions.contains("special"));
856 }
857
858 #[test]
859 fn test_config_with_rules() {
860 let dir = TempDir::new().unwrap();
861 let config_path = dir.path().join(".cc-audit.yaml");
862 fs::write(
863 &config_path,
864 r#"
865rules:
866 - id: "CUSTOM-001"
867 name: "Test Rule"
868 severity: "high"
869 category: "exfiltration"
870 patterns:
871 - 'test_pattern'
872 message: "Test message"
873"#,
874 )
875 .unwrap();
876
877 let config = Config::from_file(&config_path).unwrap();
878 assert_eq!(config.rules.len(), 1);
879 assert_eq!(config.rules[0].id, "CUSTOM-001");
880 assert_eq!(config.rules[0].name, "Test Rule");
881 assert_eq!(config.rules[0].severity, "high");
882 }
883
884 #[test]
885 fn test_config_with_malware_signatures() {
886 let dir = TempDir::new().unwrap();
887 let config_path = dir.path().join(".cc-audit.yaml");
888 fs::write(
889 &config_path,
890 r#"
891malware_signatures:
892 - id: "MW-CUSTOM-001"
893 name: "Custom Malware"
894 description: "Test malware pattern"
895 pattern: "evil_pattern"
896 severity: "critical"
897 category: "exfiltration"
898 confidence: "firm"
899"#,
900 )
901 .unwrap();
902
903 let config = Config::from_file(&config_path).unwrap();
904 assert_eq!(config.malware_signatures.len(), 1);
905 assert_eq!(config.malware_signatures[0].id, "MW-CUSTOM-001");
906 assert_eq!(config.malware_signatures[0].name, "Custom Malware");
907 assert_eq!(config.malware_signatures[0].severity, "critical");
908 }
909
910 #[test]
911 fn test_config_with_rules_and_malware_signatures() {
912 let dir = TempDir::new().unwrap();
913 let config_path = dir.path().join(".cc-audit.yaml");
914 fs::write(
915 &config_path,
916 r#"
917watch:
918 debounce_ms: 100
919
920rules:
921 - id: "CUSTOM-001"
922 name: "Test Rule"
923 severity: "high"
924 category: "exfiltration"
925 patterns:
926 - 'test_pattern'
927 message: "Test message"
928
929malware_signatures:
930 - id: "MW-CUSTOM-001"
931 name: "Custom Malware"
932 description: "Test malware pattern"
933 pattern: "evil_pattern"
934 severity: "critical"
935 category: "exfiltration"
936 confidence: "firm"
937"#,
938 )
939 .unwrap();
940
941 let config = Config::from_file(&config_path).unwrap();
942 assert_eq!(config.watch.debounce_ms, 100);
943 assert_eq!(config.rules.len(), 1);
944 assert_eq!(config.malware_signatures.len(), 1);
945 }
946
947 #[test]
948 fn test_default_config_has_empty_rules() {
949 let config = Config::default();
950 assert!(config.rules.is_empty());
951 assert!(config.malware_signatures.is_empty());
952 }
953
954 #[test]
955 fn test_parse_yaml_error() {
956 let dir = TempDir::new().unwrap();
957 let config_path = dir.path().join(".cc-audit.yaml");
958 fs::write(&config_path, "invalid: yaml: content: [").unwrap();
959
960 let result = Config::from_file(&config_path);
961 assert!(matches!(result, Err(ConfigError::ParseYaml { .. })));
962 }
963
964 #[test]
965 fn test_parse_json_error() {
966 let dir = TempDir::new().unwrap();
967 let config_path = dir.path().join(".cc-audit.json");
968 fs::write(&config_path, "{invalid json}").unwrap();
969
970 let result = Config::from_file(&config_path);
971 assert!(matches!(result, Err(ConfigError::ParseJson { .. })));
972 }
973
974 #[test]
975 fn test_parse_toml_error() {
976 let dir = TempDir::new().unwrap();
977 let config_path = dir.path().join(".cc-audit.toml");
978 fs::write(&config_path, "[invalid toml\nkey = ").unwrap();
979
980 let result = Config::from_file(&config_path);
981 assert!(matches!(result, Err(ConfigError::ParseToml { .. })));
982 }
983
984 #[test]
985 fn test_load_with_invalid_config_falls_back() {
986 let dir = TempDir::new().unwrap();
987 let config_path = dir.path().join(".cc-audit.yaml");
988 fs::write(&config_path, "invalid: yaml: [").unwrap();
990
991 let config = Config::load(Some(dir.path()));
993 assert_eq!(config.watch.debounce_ms, 300);
994 }
995
996 #[test]
997 fn test_is_text_file_returns_false_for_unknown() {
998 let config = TextFilesConfig::default();
999 assert!(!config.is_text_file(Path::new("somefile")));
1001 assert!(!config.is_text_file(Path::new("random_binary")));
1002 }
1003
1004 #[test]
1005 fn test_ignore_config_default() {
1006 let config = IgnoreConfig::default();
1007 assert!(config.directories.contains("node_modules"));
1009 assert!(config.directories.contains("target"));
1010 assert!(config.directories.contains(".git"));
1011 assert!(config.directories.contains("dist"));
1012 assert!(config.directories.contains("build"));
1013 assert!(!config.include_tests);
1015 assert!(!config.include_node_modules);
1016 assert!(!config.include_vendor);
1017 assert!(config.patterns.is_empty());
1019 }
1020
1021 #[test]
1022 fn test_config_with_ignore_settings() {
1023 let dir = TempDir::new().unwrap();
1024 let config_path = dir.path().join(".cc-audit.yaml");
1025 fs::write(
1026 &config_path,
1027 r#"
1028ignore:
1029 directories:
1030 - custom_dir
1031 - my_cache
1032 patterns:
1033 - "*.log"
1034 - "temp/**"
1035 include_tests: true
1036 include_node_modules: false
1037 include_vendor: true
1038"#,
1039 )
1040 .unwrap();
1041
1042 let config = Config::from_file(&config_path).unwrap();
1043 assert!(config.ignore.directories.contains("custom_dir"));
1044 assert!(config.ignore.directories.contains("my_cache"));
1045 assert_eq!(config.ignore.patterns.len(), 2);
1046 assert!(config.ignore.patterns.contains(&"*.log".to_string()));
1047 assert!(config.ignore.patterns.contains(&"temp/**".to_string()));
1048 assert!(config.ignore.include_tests);
1049 assert!(!config.ignore.include_node_modules);
1050 assert!(config.ignore.include_vendor);
1051 }
1052
1053 #[test]
1054 fn test_config_with_disabled_rules() {
1055 let dir = TempDir::new().unwrap();
1056 let config_path = dir.path().join(".cc-audit.yaml");
1057 fs::write(
1058 &config_path,
1059 r#"
1060disabled_rules:
1061 - "PE-001"
1062 - "EX-002"
1063 - "CUSTOM-RULE"
1064"#,
1065 )
1066 .unwrap();
1067
1068 let config = Config::from_file(&config_path).unwrap();
1069 assert_eq!(config.disabled_rules.len(), 3);
1070 assert!(config.disabled_rules.contains("PE-001"));
1071 assert!(config.disabled_rules.contains("EX-002"));
1072 assert!(config.disabled_rules.contains("CUSTOM-RULE"));
1073 }
1074
1075 #[test]
1076 fn test_default_config_has_empty_disabled_rules() {
1077 let config = Config::default();
1078 assert!(config.disabled_rules.is_empty());
1079 }
1080
1081 #[test]
1082 fn test_config_ignore_default_when_partial() {
1083 let dir = TempDir::new().unwrap();
1084 let config_path = dir.path().join(".cc-audit.yaml");
1085 fs::write(
1086 &config_path,
1087 r#"
1088ignore:
1089 include_tests: true
1090"#,
1091 )
1092 .unwrap();
1093
1094 let config = Config::from_file(&config_path).unwrap();
1095 assert!(config.ignore.include_tests);
1097 assert!(!config.ignore.include_node_modules);
1099 assert!(!config.ignore.include_vendor);
1100 }
1101
1102 #[test]
1103 fn test_scan_config_default() {
1104 let config = ScanConfig::default();
1105 assert!(config.format.is_none());
1106 assert!(!config.strict);
1107 assert!(config.scan_type.is_none());
1108 assert!(!config.recursive);
1109 assert!(!config.ci);
1110 assert!(!config.verbose);
1111 assert!(config.min_confidence.is_none());
1112 assert!(!config.skip_comments);
1113 assert!(!config.fix_hint);
1114 assert!(!config.no_malware_scan);
1115 assert!(!config.watch);
1117 assert!(config.malware_db.is_none());
1118 assert!(config.custom_rules.is_none());
1119 assert!(config.output.is_none());
1120 assert!(!config.deep_scan);
1121 assert!(!config.fix);
1122 assert!(!config.fix_dry_run);
1123 }
1124
1125 #[test]
1126 fn test_config_with_scan_settings() {
1127 let dir = TempDir::new().unwrap();
1128 let config_path = dir.path().join(".cc-audit.yaml");
1129 fs::write(
1130 &config_path,
1131 r#"
1132scan:
1133 format: json
1134 strict: true
1135 scan_type: docker
1136 recursive: true
1137 ci: true
1138 verbose: true
1139 min_confidence: firm
1140 skip_comments: true
1141 fix_hint: true
1142 no_malware_scan: true
1143 watch: true
1144 malware_db: ./custom-malware.json
1145 custom_rules: ./custom-rules.yaml
1146 output: ./report.html
1147 deep_scan: true
1148 fix: true
1149 fix_dry_run: true
1150"#,
1151 )
1152 .unwrap();
1153
1154 let config = Config::from_file(&config_path).unwrap();
1155 assert_eq!(config.scan.format, Some("json".to_string()));
1156 assert!(config.scan.strict);
1157 assert_eq!(config.scan.scan_type, Some("docker".to_string()));
1158 assert!(config.scan.recursive);
1159 assert!(config.scan.ci);
1160 assert!(config.scan.verbose);
1161 assert_eq!(config.scan.min_confidence, Some("firm".to_string()));
1162 assert!(config.scan.skip_comments);
1163 assert!(config.scan.fix_hint);
1164 assert!(config.scan.no_malware_scan);
1165 assert!(config.scan.watch);
1167 assert_eq!(
1168 config.scan.malware_db,
1169 Some("./custom-malware.json".to_string())
1170 );
1171 assert_eq!(
1172 config.scan.custom_rules,
1173 Some("./custom-rules.yaml".to_string())
1174 );
1175 assert_eq!(config.scan.output, Some("./report.html".to_string()));
1176 assert!(config.scan.deep_scan);
1177 assert!(config.scan.fix);
1178 assert!(config.scan.fix_dry_run);
1179 }
1180
1181 #[test]
1182 fn test_config_with_partial_scan_settings() {
1183 let dir = TempDir::new().unwrap();
1184 let config_path = dir.path().join(".cc-audit.yaml");
1185 fs::write(
1186 &config_path,
1187 r#"
1188scan:
1189 strict: true
1190 verbose: true
1191"#,
1192 )
1193 .unwrap();
1194
1195 let config = Config::from_file(&config_path).unwrap();
1196 assert!(config.scan.strict);
1198 assert!(config.scan.verbose);
1199 assert!(config.scan.format.is_none());
1201 assert!(config.scan.scan_type.is_none());
1202 assert!(!config.scan.recursive);
1203 assert!(!config.scan.ci);
1204 assert!(config.scan.min_confidence.is_none());
1205 assert!(!config.scan.skip_comments);
1206 assert!(!config.scan.fix_hint);
1207 assert!(!config.scan.no_malware_scan);
1208 }
1209
1210 #[test]
1211 fn test_default_config_has_default_scan() {
1212 let config = Config::default();
1213 assert!(!config.scan.strict);
1214 assert!(!config.scan.verbose);
1215 assert!(config.scan.format.is_none());
1216 }
1217
1218 #[test]
1219 fn test_generate_template() {
1220 let template = Config::generate_template();
1221 assert!(template.contains("# cc-audit Configuration File"));
1223 assert!(template.contains("severity:"));
1224 assert!(template.contains("default: error"));
1225 assert!(template.contains("scan:"));
1226 assert!(template.contains("watch:"));
1227 assert!(template.contains("ignore:"));
1228 assert!(template.contains("# disabled_rules:"));
1229 assert!(template.contains("# rules:"));
1230 assert!(template.contains("# malware_signatures:"));
1231 }
1232
1233 #[test]
1234 fn test_generate_template_is_valid_yaml() {
1235 let template = Config::generate_template();
1236 let result: Result<Config, _> = serde_yaml::from_str(&template);
1238 assert!(result.is_ok(), "Template should be valid YAML");
1239 }
1240
1241 #[test]
1244 fn test_severity_config_default() {
1245 let config = SeverityConfig::default();
1246 assert_eq!(config.default, crate::rules::RuleSeverity::Error);
1247 assert!(config.warn.is_empty());
1248 assert!(config.ignore.is_empty());
1249 }
1250
1251 #[test]
1252 fn test_severity_config_get_rule_severity_default() {
1253 let config = SeverityConfig::default();
1254 assert_eq!(
1255 config.get_rule_severity("EX-001"),
1256 Some(crate::rules::RuleSeverity::Error)
1257 );
1258 }
1259
1260 #[test]
1261 fn test_severity_config_get_rule_severity_warn() {
1262 let mut config = SeverityConfig::default();
1263 config.warn.insert("PI-001".to_string());
1264
1265 assert_eq!(
1266 config.get_rule_severity("PI-001"),
1267 Some(crate::rules::RuleSeverity::Warn)
1268 );
1269 assert_eq!(
1270 config.get_rule_severity("EX-001"),
1271 Some(crate::rules::RuleSeverity::Error)
1272 );
1273 }
1274
1275 #[test]
1276 fn test_severity_config_get_rule_severity_ignore() {
1277 let mut config = SeverityConfig::default();
1278 config.ignore.insert("OP-001".to_string());
1279
1280 assert_eq!(config.get_rule_severity("OP-001"), None);
1281 assert_eq!(
1282 config.get_rule_severity("EX-001"),
1283 Some(crate::rules::RuleSeverity::Error)
1284 );
1285 }
1286
1287 #[test]
1288 fn test_severity_config_priority_ignore_over_warn() {
1289 let mut config = SeverityConfig::default();
1290 config.warn.insert("RULE-001".to_string());
1291 config.ignore.insert("RULE-001".to_string());
1292
1293 assert_eq!(config.get_rule_severity("RULE-001"), None);
1295 }
1296
1297 #[test]
1298 fn test_config_severity_parsing() {
1299 let dir = TempDir::new().unwrap();
1300 let config_path = dir.path().join(".cc-audit.yaml");
1301 fs::write(
1302 &config_path,
1303 r#"
1304severity:
1305 default: warn
1306 warn:
1307 - PI-001
1308 - PI-002
1309 ignore:
1310 - OP-001
1311"#,
1312 )
1313 .unwrap();
1314
1315 let config = Config::from_file(&config_path).unwrap();
1316 assert_eq!(config.severity.default, crate::rules::RuleSeverity::Warn);
1317 assert!(config.severity.warn.contains("PI-001"));
1318 assert!(config.severity.warn.contains("PI-002"));
1319 assert!(config.severity.ignore.contains("OP-001"));
1320 }
1321
1322 #[test]
1323 fn test_config_effective_disabled_rules() {
1324 let dir = TempDir::new().unwrap();
1325 let config_path = dir.path().join(".cc-audit.yaml");
1326 fs::write(
1327 &config_path,
1328 r#"
1329disabled_rules:
1330 - RULE-A
1331 - RULE-B
1332severity:
1333 ignore:
1334 - RULE-C
1335 - RULE-D
1336"#,
1337 )
1338 .unwrap();
1339
1340 let config = Config::from_file(&config_path).unwrap();
1341 let effective = config.effective_disabled_rules();
1342
1343 assert!(effective.contains("RULE-A"));
1345 assert!(effective.contains("RULE-B"));
1346 assert!(effective.contains("RULE-C"));
1347 assert!(effective.contains("RULE-D"));
1348 assert_eq!(effective.len(), 4);
1349 }
1350
1351 #[test]
1352 fn test_config_is_rule_disabled() {
1353 let dir = TempDir::new().unwrap();
1354 let config_path = dir.path().join(".cc-audit.yaml");
1355 fs::write(
1356 &config_path,
1357 r#"
1358disabled_rules:
1359 - RULE-A
1360severity:
1361 ignore:
1362 - RULE-B
1363"#,
1364 )
1365 .unwrap();
1366
1367 let config = Config::from_file(&config_path).unwrap();
1368 assert!(config.is_rule_disabled("RULE-A"));
1369 assert!(config.is_rule_disabled("RULE-B"));
1370 assert!(!config.is_rule_disabled("RULE-C"));
1371 }
1372
1373 #[test]
1374 fn test_config_get_rule_severity() {
1375 let dir = TempDir::new().unwrap();
1376 let config_path = dir.path().join(".cc-audit.yaml");
1377 fs::write(
1378 &config_path,
1379 r#"
1380disabled_rules:
1381 - RULE-A
1382severity:
1383 default: error
1384 warn:
1385 - RULE-B
1386 ignore:
1387 - RULE-C
1388"#,
1389 )
1390 .unwrap();
1391
1392 let config = Config::from_file(&config_path).unwrap();
1393
1394 assert_eq!(config.get_rule_severity("RULE-A"), None);
1396
1397 assert_eq!(
1399 config.get_rule_severity("RULE-B"),
1400 Some(crate::rules::RuleSeverity::Warn)
1401 );
1402
1403 assert_eq!(config.get_rule_severity("RULE-C"), None);
1405
1406 assert_eq!(
1408 config.get_rule_severity("RULE-D"),
1409 Some(crate::rules::RuleSeverity::Error)
1410 );
1411 }
1412
1413 #[test]
1416 fn test_baseline_config_default() {
1417 let config = BaselineConfig::default();
1418 assert!(!config.enabled);
1419 assert!(!config.check_drift);
1420 assert!(config.save_to.is_none());
1421 assert!(config.compare_with.is_none());
1422 }
1423
1424 #[test]
1425 fn test_config_with_baseline_settings() {
1426 let dir = TempDir::new().unwrap();
1427 let config_path = dir.path().join(".cc-audit.yaml");
1428 fs::write(
1429 &config_path,
1430 r#"
1431baseline:
1432 enabled: true
1433 check_drift: true
1434 save_to: ./.cc-audit-baseline.json
1435 compare_with: ./previous-baseline.json
1436"#,
1437 )
1438 .unwrap();
1439
1440 let config = Config::from_file(&config_path).unwrap();
1441 assert!(config.baseline.enabled);
1442 assert!(config.baseline.check_drift);
1443 assert_eq!(
1444 config.baseline.save_to,
1445 Some("./.cc-audit-baseline.json".to_string())
1446 );
1447 assert_eq!(
1448 config.baseline.compare_with,
1449 Some("./previous-baseline.json".to_string())
1450 );
1451 }
1452
1453 #[test]
1454 fn test_default_config_has_default_baseline() {
1455 let config = Config::default();
1456 assert!(!config.baseline.enabled);
1457 assert!(!config.baseline.check_drift);
1458 assert!(config.baseline.save_to.is_none());
1459 assert!(config.baseline.compare_with.is_none());
1460 }
1461
1462 #[test]
1463 fn test_generate_template_contains_new_sections() {
1464 let template = Config::generate_template();
1465 assert!(template.contains("baseline:"));
1467 assert!(template.contains("deep_scan:"));
1468 assert!(template.contains("fix:"));
1469 assert!(template.contains("fix_dry_run:"));
1470 assert!(template.contains("malware_db:"));
1471 assert!(template.contains("custom_rules:"));
1472 assert!(template.contains("output:"));
1473 assert!(template.contains("subagent"));
1474 assert!(template.contains("plugin"));
1475 }
1476}