1mod baseline;
7mod compare;
8mod config;
9mod fix;
10mod hook;
11mod mcp;
12mod remote;
13mod scan;
14
15use std::process::ExitCode;
16
17pub use baseline::{
19 filter_against_baseline, handle_baseline, handle_check_drift, handle_save_baseline,
20};
21pub use compare::handle_compare;
22pub use config::{handle_init_config, handle_save_profile, handle_show_profile};
23pub use fix::handle_fix;
24pub use hook::{handle_init_hook, handle_remove_hook};
25pub use mcp::handle_mcp_server;
26pub use remote::{handle_awesome_claude_code_scan, handle_remote_list_scan, handle_remote_scan};
27pub use scan::{run_normal_mode, run_watch_mode};
28
29#[derive(Debug, Clone, PartialEq)]
31pub enum HandlerResult {
32 Success,
33 Error(u8),
34}
35
36impl From<HandlerResult> for ExitCode {
37 fn from(result: HandlerResult) -> Self {
38 match result {
39 HandlerResult::Success => ExitCode::SUCCESS,
40 HandlerResult::Error(code) => ExitCode::from(code),
41 }
42 }
43}
44
45#[cfg(test)]
46mod tests {
47 use super::*;
48 use crate::Cli;
49 use clap::Parser;
50 use std::fs;
51 use tempfile::TempDir;
52
53 fn create_test_cli(args: &[&str]) -> Cli {
54 let mut full_args = vec!["cc-audit"];
55 full_args.extend(args);
56 Cli::parse_from(full_args)
57 }
58
59 #[test]
60 fn test_handler_result_success() {
61 let result = HandlerResult::Success;
62 let exit_code: ExitCode = result.into();
63 assert_eq!(exit_code, ExitCode::SUCCESS);
64 }
65
66 #[test]
67 fn test_handler_result_error() {
68 let result = HandlerResult::Error(2);
69 let exit_code: ExitCode = result.into();
70 assert_eq!(exit_code, ExitCode::from(2));
71 }
72
73 #[test]
74 fn test_handle_init_config_creates_file() {
75 let temp_dir = TempDir::new().unwrap();
76 let cli = create_test_cli(&[temp_dir.path().to_str().unwrap()]);
77
78 let result = handle_init_config(&cli);
79 assert_eq!(result, ExitCode::SUCCESS);
80
81 let config_path = temp_dir.path().join(".cc-audit.yaml");
82 assert!(config_path.exists());
83 }
84
85 #[test]
86 fn test_handle_init_config_file_exists() {
87 let temp_dir = TempDir::new().unwrap();
88 let config_path = temp_dir.path().join(".cc-audit.yaml");
89 fs::write(&config_path, "existing content").unwrap();
90
91 let cli = create_test_cli(&[temp_dir.path().to_str().unwrap()]);
92 let result = handle_init_config(&cli);
93 assert_eq!(result, ExitCode::from(2));
94 }
95
96 #[test]
97 fn test_handle_init_hook_not_git_repo() {
98 let temp_dir = TempDir::new().unwrap();
99 let cli = create_test_cli(&[temp_dir.path().to_str().unwrap()]);
100
101 let result = handle_init_hook(&cli);
102 assert_eq!(result, ExitCode::from(2));
103 }
104
105 #[test]
106 fn test_handle_remove_hook_not_git_repo() {
107 let temp_dir = TempDir::new().unwrap();
108 let cli = create_test_cli(&[temp_dir.path().to_str().unwrap()]);
109
110 let result = handle_remove_hook(&cli);
111 assert_eq!(result, ExitCode::from(2));
112 }
113
114 #[test]
115 fn test_handle_baseline_empty_dir() {
116 let temp_dir = TempDir::new().unwrap();
117 let cli = create_test_cli(&[temp_dir.path().to_str().unwrap()]);
118
119 let result = handle_baseline(&cli);
120 assert_eq!(result, ExitCode::SUCCESS);
121
122 let baseline_path = temp_dir.path().join(".cc-audit-baseline.json");
123 assert!(baseline_path.exists());
124 }
125
126 #[test]
127 fn test_handle_save_baseline() {
128 let temp_dir = TempDir::new().unwrap();
129 let baseline_file = temp_dir.path().join("baseline.json");
130
131 fs::write(temp_dir.path().join("test.md"), "# Test").unwrap();
133
134 let cli = create_test_cli(&[temp_dir.path().to_str().unwrap()]);
135 let result = handle_save_baseline(&cli, &baseline_file);
136 assert_eq!(result, ExitCode::SUCCESS);
137
138 assert!(baseline_file.exists());
139 }
140
141 #[test]
142 fn test_handle_check_drift_no_baseline() {
143 let temp_dir = TempDir::new().unwrap();
144 let cli = create_test_cli(&[temp_dir.path().to_str().unwrap()]);
145
146 let result = handle_check_drift(&cli);
147 assert_eq!(result, ExitCode::from(2));
148 }
149
150 #[test]
151 fn test_handle_check_drift_with_baseline() {
152 let temp_dir = TempDir::new().unwrap();
153 fs::write(temp_dir.path().join("test.md"), "# Test").unwrap();
154
155 let cli = create_test_cli(&[temp_dir.path().to_str().unwrap()]);
157 handle_baseline(&cli);
158
159 let result = handle_check_drift(&cli);
161 assert!(result == ExitCode::SUCCESS || result == ExitCode::from(1));
164 }
165
166 #[test]
167 fn test_handle_show_profile_builtin() {
168 let result = handle_show_profile("default");
169 assert_eq!(result, ExitCode::SUCCESS);
170 }
171
172 #[test]
173 fn test_handle_show_profile_not_found() {
174 let result = handle_show_profile("nonexistent_profile_12345");
175 assert_eq!(result, ExitCode::from(2));
176 }
177
178 #[test]
179 fn test_handle_compare_wrong_args() {
180 use std::path::PathBuf;
181 let cli = create_test_cli(&["."]);
182 let result = handle_compare(&cli, &[PathBuf::from(".")]);
183 assert_eq!(result, ExitCode::from(2));
184 }
185
186 #[test]
187 fn test_handle_compare_same_dirs() {
188 let temp_dir = TempDir::new().unwrap();
189 let cli = create_test_cli(&["."]);
190 let result = handle_compare(
191 &cli,
192 &[temp_dir.path().to_path_buf(), temp_dir.path().to_path_buf()],
193 );
194 assert_eq!(result, ExitCode::SUCCESS);
195 }
196
197 #[test]
198 fn test_filter_against_baseline_file_not_found() {
199 use crate::test_utils::fixtures::create_test_result;
200 use std::path::Path;
201
202 let result = create_test_result(vec![]);
203 let filtered = filter_against_baseline(result.clone(), Path::new("/nonexistent/path.json"));
204
205 assert_eq!(filtered.findings.len(), result.findings.len());
207 }
208
209 #[test]
210 fn test_filter_against_baseline_invalid_json() {
211 use crate::test_utils::fixtures::create_test_result;
212
213 let temp_dir = TempDir::new().unwrap();
214 let baseline_path = temp_dir.path().join("baseline.json");
215 fs::write(&baseline_path, "{ invalid json }").unwrap();
216
217 let result = create_test_result(vec![]);
218 let filtered = filter_against_baseline(result.clone(), &baseline_path);
219
220 assert_eq!(filtered.findings.len(), result.findings.len());
222 }
223
224 #[test]
225 fn test_filter_against_baseline_filters_findings() {
226 use crate::rules::{Category, Severity};
227 use crate::test_utils::fixtures::{create_finding, create_test_result};
228
229 let temp_dir = TempDir::new().unwrap();
230 let baseline_path = temp_dir.path().join("baseline.json");
231
232 let finding = create_finding(
234 "EX-001",
235 Severity::High,
236 Category::Exfiltration,
237 "Test finding",
238 "test.md",
239 1,
240 );
241
242 let baseline_result = create_test_result(vec![finding.clone()]);
244 let json = serde_json::to_string(&baseline_result).unwrap();
245 fs::write(&baseline_path, &json).unwrap();
246
247 let result = create_test_result(vec![finding]);
249 let filtered = filter_against_baseline(result, &baseline_path);
250
251 assert_eq!(filtered.findings.len(), 0);
252 }
253
254 #[test]
255 fn test_run_normal_mode_with_empty_dir() {
256 let temp_dir = TempDir::new().unwrap();
257 let cli = create_test_cli(&[temp_dir.path().to_str().unwrap()]);
258
259 let result = run_normal_mode(&cli);
260 assert_eq!(result, ExitCode::SUCCESS);
261 }
262
263 #[test]
264 fn test_run_normal_mode_warn_only() {
265 let temp_dir = TempDir::new().unwrap();
266 fs::write(temp_dir.path().join("test.md"), "sudo rm -rf /").unwrap();
267
268 let cli = create_test_cli(&["--warn-only", temp_dir.path().to_str().unwrap()]);
269 let result = run_normal_mode(&cli);
270 assert_eq!(result, ExitCode::SUCCESS);
271 }
272
273 #[test]
274 fn test_handle_fix_no_findings() {
275 let temp_dir = TempDir::new().unwrap();
276 let cli = create_test_cli(&["--fix-dry-run", temp_dir.path().to_str().unwrap()]);
277
278 let result = handle_fix(&cli);
279 assert_eq!(result, ExitCode::SUCCESS);
280 }
281
282 #[test]
283 fn test_handle_init_hook_in_git_repo() {
284 let temp_dir = TempDir::new().unwrap();
285 std::process::Command::new("git")
287 .args(["init"])
288 .current_dir(temp_dir.path())
289 .output()
290 .unwrap();
291
292 let cli = create_test_cli(&[temp_dir.path().to_str().unwrap()]);
293 let result = handle_init_hook(&cli);
294 assert_eq!(result, ExitCode::SUCCESS);
295 }
296
297 #[test]
298 fn test_handle_remove_hook_in_git_repo_not_installed() {
299 let temp_dir = TempDir::new().unwrap();
300 std::process::Command::new("git")
302 .args(["init"])
303 .current_dir(temp_dir.path())
304 .output()
305 .unwrap();
306
307 let cli = create_test_cli(&[temp_dir.path().to_str().unwrap()]);
308 let result = handle_remove_hook(&cli);
309 assert_eq!(result, ExitCode::from(2));
311 }
312
313 #[test]
314 fn test_handle_remove_hook_in_git_repo_installed() {
315 let temp_dir = TempDir::new().unwrap();
316 std::process::Command::new("git")
318 .args(["init"])
319 .current_dir(temp_dir.path())
320 .output()
321 .unwrap();
322
323 let cli = create_test_cli(&[temp_dir.path().to_str().unwrap()]);
325 handle_init_hook(&cli);
326
327 let result = handle_remove_hook(&cli);
329 assert_eq!(result, ExitCode::SUCCESS);
330 }
331
332 #[test]
333 fn test_handle_init_config_with_specific_path() {
334 let temp_dir = TempDir::new().unwrap();
335 let config_path = temp_dir.path().join("custom-config.yaml");
336
337 let cli = create_test_cli(&[config_path.to_str().unwrap()]);
338 let result = handle_init_config(&cli);
339 assert_eq!(result, ExitCode::SUCCESS);
340
341 assert!(config_path.exists());
342 }
343
344 #[test]
345 fn test_run_normal_mode_strict() {
346 let temp_dir = TempDir::new().unwrap();
347 let cli = create_test_cli(&["--strict", temp_dir.path().to_str().unwrap()]);
348
349 let result = run_normal_mode(&cli);
350 assert_eq!(result, ExitCode::SUCCESS);
351 }
352
353 #[test]
354 fn test_run_normal_mode_with_output_file() {
355 let temp_dir = TempDir::new().unwrap();
356 let output_file = temp_dir.path().join("output.txt");
357
358 let cli = Cli::parse_from([
359 "cc-audit",
360 "--output",
361 output_file.to_str().unwrap(),
362 temp_dir.path().to_str().unwrap(),
363 ]);
364
365 let result = run_normal_mode(&cli);
366 assert_eq!(result, ExitCode::SUCCESS);
367 assert!(output_file.exists());
368 }
369
370 #[test]
371 fn test_handle_save_profile_and_load() {
372 let cli = create_test_cli(&["--strict", "--verbose", "."]);
373 let result = handle_save_profile(&cli, "test_profile_handlers_123");
374 assert_eq!(result, ExitCode::SUCCESS);
375
376 if let Ok(profile_path) = crate::Profile::load("test_profile_handlers_123") {
378 let _ = profile_path;
379 }
380 }
381
382 #[test]
383 fn test_handle_fix_with_findings() {
384 let temp_dir = TempDir::new().unwrap();
385 fs::write(temp_dir.path().join("test.md"), "permissions: \"*\"").unwrap();
387
388 let cli = create_test_cli(&["--fix-dry-run", temp_dir.path().to_str().unwrap()]);
389 let result = handle_fix(&cli);
390 let _ = result;
393 }
394
395 #[test]
396 fn test_handle_compare_with_different_findings() {
397 let temp_dir1 = TempDir::new().unwrap();
398 let temp_dir2 = TempDir::new().unwrap();
399
400 fs::write(temp_dir1.path().join("test.md"), "# Clean").unwrap();
402 fs::write(temp_dir2.path().join("test.md"), "sudo rm -rf /").unwrap();
403
404 let cli = create_test_cli(&["."]);
405 let result = handle_compare(
406 &cli,
407 &[
408 temp_dir1.path().to_path_buf(),
409 temp_dir2.path().to_path_buf(),
410 ],
411 );
412 assert!(result == ExitCode::SUCCESS || result == ExitCode::from(1));
414 }
415
416 #[test]
417 fn test_run_normal_mode_with_baseline_file() {
418 use crate::test_utils::fixtures::create_test_result;
419
420 let temp_dir = TempDir::new().unwrap();
421 let baseline_path = temp_dir.path().join("baseline.json");
422
423 let baseline_result = create_test_result(vec![]);
425 let json = serde_json::to_string(&baseline_result).unwrap();
426 fs::write(&baseline_path, &json).unwrap();
427
428 let cli = Cli::parse_from([
429 "cc-audit",
430 "--baseline-file",
431 baseline_path.to_str().unwrap(),
432 temp_dir.path().to_str().unwrap(),
433 ]);
434
435 let result = run_normal_mode(&cli);
436 assert_eq!(result, ExitCode::SUCCESS);
437 }
438
439 #[test]
440 fn test_filter_against_baseline_with_filtering() {
441 use crate::rules::{Category, Severity};
442 use crate::test_utils::fixtures::{create_finding, create_test_result};
443
444 let temp_dir = TempDir::new().unwrap();
445 let baseline_path = temp_dir.path().join("baseline.json");
446
447 let finding1 = create_finding(
449 "EX-001",
450 Severity::High,
451 Category::Exfiltration,
452 "Finding 1",
453 "file1.md",
454 1,
455 );
456 let baseline_result = create_test_result(vec![finding1.clone()]);
457 let json = serde_json::to_string(&baseline_result).unwrap();
458 fs::write(&baseline_path, &json).unwrap();
459
460 let finding2 = create_finding(
462 "EX-002",
463 Severity::High,
464 Category::Exfiltration,
465 "Finding 2",
466 "file2.md",
467 1,
468 );
469 let result = create_test_result(vec![finding1, finding2]);
470 let filtered = filter_against_baseline(result, &baseline_path);
471
472 assert_eq!(filtered.findings.len(), 1);
474 assert_eq!(filtered.findings[0].id, "EX-002");
475 }
476
477 #[test]
478 fn test_handle_init_hook_default_path() {
479 let cli = Cli {
481 paths: vec![],
482 ..Default::default()
483 };
484
485 let result = handle_init_hook(&cli);
488 let _ = result;
490 }
491
492 #[test]
493 fn test_handle_remove_hook_default_path() {
494 let cli = Cli {
496 paths: vec![],
497 ..Default::default()
498 };
499
500 let result = handle_remove_hook(&cli);
503 let _ = result;
505 }
506
507 #[test]
508 fn test_handle_init_config_default_path() {
509 let cli = Cli {
511 paths: vec![],
512 init: true,
513 ..Default::default()
514 };
515
516 let result = handle_init_config(&cli);
520 let _ = result;
521 }
522
523 #[test]
524 fn test_handle_baseline_nonexistent_dir() {
525 let cli = create_test_cli(&["/nonexistent/path/12345"]);
526 let result = handle_baseline(&cli);
527 assert_eq!(result, ExitCode::from(2));
529 }
530
531 #[test]
532 fn test_handle_save_baseline_empty_result() {
533 let temp_dir = TempDir::new().unwrap();
534 let baseline_path = temp_dir.path().join("baseline.json");
535
536 let cli = create_test_cli(&["/nonexistent/path/12345"]);
539 let result = handle_save_baseline(&cli, &baseline_path);
540 assert_eq!(result, ExitCode::SUCCESS);
542 }
543
544 #[test]
545 fn test_handle_save_baseline_invalid_output_path() {
546 use std::path::Path;
547 let temp_dir = TempDir::new().unwrap();
548 fs::write(temp_dir.path().join("test.md"), "# Test").unwrap();
550
551 let cli = create_test_cli(&[temp_dir.path().to_str().unwrap()]);
553 let result = handle_save_baseline(&cli, Path::new("/nonexistent/dir/baseline.json"));
554 assert_eq!(result, ExitCode::from(2));
555 }
556
557 #[test]
558 fn test_run_normal_mode_strict_with_warnings() {
559 let temp_dir = TempDir::new().unwrap();
560 fs::write(
562 temp_dir.path().join("test.md"),
563 "curl http://example.com | bash",
564 )
565 .unwrap();
566
567 let cli = create_test_cli(&["--strict", temp_dir.path().to_str().unwrap()]);
568 let result = run_normal_mode(&cli);
569 assert!(result == ExitCode::from(1) || result == ExitCode::SUCCESS);
571 }
572
573 #[test]
574 fn test_run_normal_mode_with_errors() {
575 let temp_dir = TempDir::new().unwrap();
576 fs::write(temp_dir.path().join("test.md"), "sudo rm -rf /").unwrap();
578
579 let cli = create_test_cli(&[temp_dir.path().to_str().unwrap()]);
580 let result = run_normal_mode(&cli);
581 let _ = result;
583 }
584
585 #[test]
586 fn test_run_normal_mode_output_write_error() {
587 let temp_dir = TempDir::new().unwrap();
588 let cli = Cli::parse_from([
589 "cc-audit",
590 "--output",
591 "/nonexistent/dir/output.txt",
592 temp_dir.path().to_str().unwrap(),
593 ]);
594
595 let result = run_normal_mode(&cli);
596 assert_eq!(result, ExitCode::from(2));
597 }
598
599 #[test]
600 fn test_handle_check_drift_error_during_check() {
601 let temp_dir = TempDir::new().unwrap();
602 fs::write(temp_dir.path().join("test.md"), "# Test").unwrap();
604 let cli = create_test_cli(&[temp_dir.path().to_str().unwrap()]);
605 handle_baseline(&cli);
606
607 let _ = fs::remove_file(temp_dir.path().join("test.md"));
609
610 let result = handle_check_drift(&cli);
612 assert!(result == ExitCode::from(1) || result == ExitCode::SUCCESS);
614 }
615
616 #[test]
617 fn test_handle_show_profile_with_format_and_scan_type() {
618 use crate::Profile;
619
620 let temp_dir = TempDir::new().unwrap();
621 let profile_dir = temp_dir.path().join(".cc-audit-profiles");
622 fs::create_dir_all(&profile_dir).unwrap();
623
624 let profile = Profile {
626 name: "test_profile_with_options".to_string(),
627 description: "Test profile with optional fields".to_string(),
628 strict: true,
629 recursive: true,
630 ci: false,
631 verbose: false,
632 skip_comments: false,
633 fix_hint: false,
634 no_malware_scan: false,
635 deep_scan: false,
636 min_confidence: "tentative".to_string(),
637 format: Some("json".to_string()),
638 scan_type: Some("skill".to_string()),
639 disabled_rules: vec![],
640 };
641
642 let profile_path = profile_dir.join("test_profile_with_options.yaml");
643 let yaml = serde_yaml::to_string(&profile).unwrap();
644 fs::write(&profile_path, &yaml).unwrap();
645
646 let _ = handle_show_profile("strict"); }
650
651 #[test]
652 fn test_handle_fix_with_unfixable_findings() {
653 let temp_dir = TempDir::new().unwrap();
654 fs::write(
657 temp_dir.path().join("test.md"),
658 "eval(atob('c29tZUJhc2U2NA=='))",
659 )
660 .unwrap();
661
662 let cli = create_test_cli(&["--fix-dry-run", temp_dir.path().to_str().unwrap()]);
663 let result = handle_fix(&cli);
664 let _ = result;
667 }
668
669 #[test]
670 fn test_handle_compare_with_findings_in_first() {
671 let temp_dir1 = TempDir::new().unwrap();
672 let temp_dir2 = TempDir::new().unwrap();
673
674 fs::write(temp_dir1.path().join("bad.md"), "sudo rm -rf /").unwrap();
676 fs::write(temp_dir2.path().join("clean.md"), "# Nothing here").unwrap();
677
678 let cli = create_test_cli(&["."]);
679 let result = handle_compare(
680 &cli,
681 &[
682 temp_dir1.path().to_path_buf(),
683 temp_dir2.path().to_path_buf(),
684 ],
685 );
686 let _ = result;
688 }
689
690 #[test]
691 fn test_handle_compare_with_findings_in_second() {
692 let temp_dir1 = TempDir::new().unwrap();
693 let temp_dir2 = TempDir::new().unwrap();
694
695 fs::write(temp_dir1.path().join("clean.md"), "# Nothing here").unwrap();
697 fs::write(temp_dir2.path().join("bad.md"), "sudo rm -rf /").unwrap();
698
699 let cli = create_test_cli(&["."]);
700 let result = handle_compare(
701 &cli,
702 &[
703 temp_dir1.path().to_path_buf(),
704 temp_dir2.path().to_path_buf(),
705 ],
706 );
707 let _ = result;
709 }
710
711 #[test]
712 fn test_filter_against_baseline_with_risk_score() {
713 use crate::rules::{Category, Severity};
714 use crate::test_utils::fixtures::{create_finding, create_test_result};
715
716 let temp_dir = TempDir::new().unwrap();
717 let baseline_path = temp_dir.path().join("baseline.json");
718
719 let baseline_result = create_test_result(vec![]);
721 let json = serde_json::to_string(&baseline_result).unwrap();
722 fs::write(&baseline_path, &json).unwrap();
723
724 let finding = create_finding(
726 "EX-001",
727 Severity::High,
728 Category::Exfiltration,
729 "Test finding",
730 "test.md",
731 1,
732 );
733 let mut result = create_test_result(vec![finding]);
734 result.risk_score = Some(crate::RiskScore::from_findings(&result.findings));
735
736 let filtered = filter_against_baseline(result, &baseline_path);
737
738 assert!(filtered.risk_score.is_some());
740 }
741
742 #[test]
743 fn test_handle_baseline_save_error() {
744 let temp_dir = TempDir::new().unwrap();
746 let readonly_dir = temp_dir.path().join("readonly");
747 fs::create_dir(&readonly_dir).unwrap();
748 fs::write(readonly_dir.join("test.md"), "# Test").unwrap();
749
750 #[cfg(unix)]
752 {
753 use std::os::unix::fs::PermissionsExt;
754 let metadata = fs::metadata(&readonly_dir).unwrap();
755 let mut perms = metadata.permissions();
756 perms.set_mode(0o444);
757 let _ = fs::set_permissions(&readonly_dir, perms);
758
759 let cli = create_test_cli(&[readonly_dir.to_str().unwrap()]);
760 let result = handle_baseline(&cli);
761 let mut perms = metadata.permissions();
764 perms.set_mode(0o755);
765 let _ = fs::set_permissions(&readonly_dir, perms);
766 let _ = result;
767 }
768 }
769
770 #[test]
771 fn test_run_normal_mode_passed_false() {
772 let temp_dir = TempDir::new().unwrap();
773 fs::write(temp_dir.path().join("test.md"), "allowed_tools:\n - \"*\"").unwrap();
775
776 let cli = create_test_cli(&[temp_dir.path().to_str().unwrap()]);
777 let result = run_normal_mode(&cli);
778 let _ = result;
780 }
781
782 #[test]
783 fn test_handle_check_drift_with_drift() {
784 let temp_dir = TempDir::new().unwrap();
785 fs::write(temp_dir.path().join("test.md"), "# Original").unwrap();
786
787 let cli = create_test_cli(&[temp_dir.path().to_str().unwrap()]);
789 handle_baseline(&cli);
790
791 fs::write(temp_dir.path().join("test.md"), "# Modified content").unwrap();
793
794 let result = handle_check_drift(&cli);
796 assert!(result == ExitCode::from(1) || result == ExitCode::SUCCESS);
798 }
799}