1use std::path::Path;
7use std::process::Command;
8use std::time::Instant;
9
10use super::helpers::{apply_check_outcome, CheckOutcome};
11use super::types::{CheckItem, CheckStatus, Evidence, EvidenceType, Severity};
12
13pub fn evaluate_all(project_path: &Path) -> Vec<CheckItem> {
15 vec![
16 check_unsafe_code_isolation(project_path),
17 check_memory_safety_fuzzing(project_path),
18 check_miri_validation(project_path),
19 check_formal_safety_properties(project_path),
20 check_adversarial_robustness(project_path),
21 check_thread_safety(project_path),
22 check_resource_leak_prevention(project_path),
23 check_panic_safety(project_path),
24 check_input_validation(project_path),
25 check_supply_chain_security(project_path),
26 ]
27}
28
29fn is_allowed_unsafe_location(path_str: &str, file_name: &str, content: &str) -> bool {
32 const ALLOWED_DIRS: &[&str] = &["/internal/", "/ffi/", "/simd/", "/wasm/"];
33 const ALLOWED_SUFFIXES: &[&str] = &["_internal.rs", "_ffi.rs", "_simd.rs", "_tests.rs"];
34
35 ALLOWED_DIRS.iter().any(|d| path_str.contains(d))
36 || ALLOWED_SUFFIXES.iter().any(|s| path_str.contains(s))
37 || file_name == "lib.rs"
38 || content.contains("// SAFETY:")
39 || content.contains("# Safety")
40}
41
42pub fn check_unsafe_code_isolation(project_path: &Path) -> CheckItem {
50 let start = Instant::now();
51 let mut item = CheckItem::new(
52 "SF-01",
53 "Unsafe Code Isolation",
54 "All unsafe code isolated in marked internal modules",
55 )
56 .with_severity(Severity::Major)
57 .with_tps("Jidoka — containment");
58
59 let mut unsafe_locations = Vec::new();
60 let mut total_unsafe_blocks = 0;
61
62 const UNSAFE_BLOCK_PATTERN: &str = concat!("unsafe", " {");
64 const UNSAFE_FN_PATTERN: &str = concat!("unsafe", " fn ");
65
66 if let Ok(entries) = glob::glob(&format!("{}/src/**/*.rs", project_path.display())) {
67 for entry in entries.flatten() {
68 let Ok(content) = std::fs::read_to_string(&entry) else {
69 continue;
70 };
71 let unsafe_count = content.matches(UNSAFE_BLOCK_PATTERN).count()
73 + content.matches(UNSAFE_FN_PATTERN).count();
74
75 if unsafe_count == 0 {
76 continue;
77 }
78 total_unsafe_blocks += unsafe_count;
79
80 let file_name = entry.file_name().unwrap_or_default().to_string_lossy();
81 let path_str = entry.to_string_lossy();
82 if !is_allowed_unsafe_location(&path_str, &file_name, &content) {
84 unsafe_locations.push(format!("{}: {} blocks", path_str, unsafe_count));
85 }
86 }
87 }
88
89 item = item.with_evidence(Evidence {
91 evidence_type: EvidenceType::StaticAnalysis,
92 description: format!(
93 "Found {} unsafe blocks, {} in non-designated locations",
94 total_unsafe_blocks,
95 unsafe_locations.len()
96 ),
97 data: Some(format!("locations: {:?}", unsafe_locations)),
99 files: Vec::new(),
100 });
101
102 let partial_msg =
104 format!("{} unsafe blocks outside designated modules", unsafe_locations.len());
105 let fail_msg = format!(
107 "{} unsafe blocks outside designated modules: {}",
108 unsafe_locations.len(),
109 unsafe_locations.join(", ")
110 );
111 item = apply_check_outcome(
112 item,
113 &[
114 (unsafe_locations.is_empty(), CheckOutcome::Pass),
115 (unsafe_locations.len() <= 3, CheckOutcome::Partial(&partial_msg)),
116 (true, CheckOutcome::Fail(&fail_msg)),
117 ],
118 );
119
120 item.finish_timed(start)
121}
122
123pub fn check_memory_safety_fuzzing(project_path: &Path) -> CheckItem {
130 let start = Instant::now();
131 let mut item = CheckItem::new(
132 "SF-02",
133 "Memory Safety Under Fuzzing",
134 "No memory safety violations under fuzzing",
135 )
136 .with_severity(Severity::Major)
137 .with_tps("Jidoka — defect detection");
138
139 let fuzz_dir = project_path.join("fuzz");
141 let has_fuzz_dir = fuzz_dir.exists();
142 let has_fuzz_targets = if has_fuzz_dir {
143 glob::glob(&format!("{}/fuzz_targets/**/*.rs", fuzz_dir.display()))
144 .ok()
145 .map(|entries| entries.count() > 0)
146 .unwrap_or(false)
147 || fuzz_dir.join("fuzz_targets").exists()
148 } else {
149 false
150 };
151
152 let cargo_toml = project_path.join("Cargo.toml");
154 let cargo_content =
155 cargo_toml.exists().then(|| std::fs::read_to_string(&cargo_toml).ok()).flatten();
156
157 let has_fuzz_dep = cargo_content
158 .as_ref()
159 .map(|c| c.contains("libfuzzer-sys") || c.contains("arbitrary"))
160 .unwrap_or(false);
161
162 let has_proptest = cargo_content
164 .as_ref()
165 .map(|c| c.contains("proptest") || c.contains("quickcheck"))
166 .unwrap_or(false);
167
168 item = item.with_evidence(Evidence {
169 evidence_type: EvidenceType::StaticAnalysis,
170 description: format!(
171 "Fuzzing setup: dir={}, targets={}, fuzz_deps={}, proptest={}",
172 has_fuzz_dir, has_fuzz_targets, has_fuzz_dep, has_proptest
173 ),
174 data: None,
175 files: Vec::new(),
176 });
177
178 let is_small_project = glob::glob(&format!("{}/src/**/*.rs", project_path.display()))
179 .ok()
180 .map(|entries| entries.count() < 20)
181 .unwrap_or(true);
182
183 item = apply_check_outcome(
184 item,
185 &[
186 (has_fuzz_targets && has_fuzz_dep, CheckOutcome::Pass),
187 (has_proptest, CheckOutcome::Pass),
188 (has_fuzz_dir || has_fuzz_dep, CheckOutcome::Partial("Fuzzing partially configured")),
189 (is_small_project, CheckOutcome::Partial("No fuzzing setup (small project)")),
190 (true, CheckOutcome::Fail("No fuzzing infrastructure detected")),
191 ],
192 );
193
194 item.finish_timed(start)
195}
196
197pub fn check_miri_validation(project_path: &Path) -> CheckItem {
204 let start = Instant::now();
205 let mut item = CheckItem::new(
206 "SF-03",
207 "Miri Undefined Behavior Detection",
208 "Core operations pass Miri validation",
209 )
210 .with_severity(Severity::Major)
211 .with_tps("Jidoka — automatic UB detection");
212
213 let ci_configs = [
215 project_path.join(".github/workflows/ci.yml"),
216 project_path.join(".github/workflows/test.yml"),
217 project_path.join(".github/workflows/rust.yml"),
218 ];
219
220 let mut has_miri_in_ci = false;
221 for ci_path in &ci_configs {
222 if ci_path.exists() {
223 if let Ok(content) = std::fs::read_to_string(ci_path) {
224 if content.contains("miri") {
225 has_miri_in_ci = true;
226 break;
227 }
228 }
229 }
230 }
231
232 let makefile = project_path.join("Makefile");
234 let has_miri_in_makefile = makefile
235 .exists()
236 .then(|| std::fs::read_to_string(&makefile).ok())
237 .flatten()
238 .map(|c| c.contains("miri"))
239 .unwrap_or(false);
240
241 item = item.with_evidence(Evidence {
242 evidence_type: EvidenceType::StaticAnalysis,
243 description: format!(
244 "Miri setup: ci={}, makefile={}",
245 has_miri_in_ci, has_miri_in_makefile
246 ),
247 data: None,
248 files: Vec::new(),
249 });
250
251 const UNSAFE_BLOCK: &str = concat!("unsafe", " {");
253 let unsafe_count: usize = glob::glob(&format!("{}/src/**/*.rs", project_path.display()))
254 .ok()
255 .map(|entries| {
256 entries
257 .flatten()
258 .filter_map(|p| std::fs::read_to_string(&p).ok())
259 .map(|c| c.matches(UNSAFE_BLOCK).count())
260 .sum()
261 })
262 .unwrap_or(0);
263
264 let miri_partial_msg = format!("Miri not configured ({} unsafe blocks)", unsafe_count);
266 item = apply_check_outcome(
267 item,
268 &[
269 (has_miri_in_ci, CheckOutcome::Pass),
270 (has_miri_in_makefile, CheckOutcome::Partial("Miri available but not in CI")),
271 (unsafe_count == 0, CheckOutcome::Pass),
272 (true, CheckOutcome::Partial(&miri_partial_msg)),
273 ],
274 );
275
276 item.finish_timed(start)
277}
278
279pub fn check_formal_safety_properties(project_path: &Path) -> CheckItem {
286 let start = Instant::now();
287 let mut item = CheckItem::new(
288 "SF-04",
289 "Formal Safety Properties",
290 "Safety-critical components have formal proofs",
291 )
292 .with_severity(Severity::Minor)
293 .with_tps("Formal verification requirement");
294
295 let cargo_toml = project_path.join("Cargo.toml");
297 let has_kani = cargo_toml
298 .exists()
299 .then(|| std::fs::read_to_string(&cargo_toml).ok())
300 .flatten()
301 .map(|c| c.contains("kani") || c.contains("creusot") || c.contains("prusti"))
302 .unwrap_or(false);
303
304 let has_proof_annotations = glob::glob(&format!("{}/src/**/*.rs", project_path.display()))
306 .ok()
307 .map(|entries| {
308 entries.flatten().any(|p| {
309 std::fs::read_to_string(&p)
310 .ok()
311 .map(|c| {
312 c.contains("#[kani::")
313 || c.contains("#[requires(")
314 || c.contains("#[ensures(")
315 || c.contains("// PROOF:")
316 })
317 .unwrap_or(false)
318 })
319 })
320 .unwrap_or(false);
321
322 item = item.with_evidence(Evidence {
323 evidence_type: EvidenceType::StaticAnalysis,
324 description: format!(
325 "Formal verification: tools={}, annotations={}",
326 has_kani, has_proof_annotations
327 ),
328 data: None,
329 files: Vec::new(),
330 });
331
332 item = apply_check_outcome(
333 item,
334 &[
335 (has_kani && has_proof_annotations, CheckOutcome::Pass),
336 (
337 has_kani || has_proof_annotations,
338 CheckOutcome::Partial("Partial formal verification setup"),
339 ),
340 (true, CheckOutcome::Partial("No formal verification (advanced feature)")),
341 ],
342 );
343
344 item.finish_timed(start)
345}
346
347pub fn check_adversarial_robustness(project_path: &Path) -> CheckItem {
354 let start = Instant::now();
355 let mut item = CheckItem::new(
356 "SF-05",
357 "Adversarial Robustness Verification",
358 "Models tested against adversarial examples",
359 )
360 .with_severity(Severity::Major)
361 .with_tps("AI Safety requirement");
362
363 let has_adversarial_tests = glob::glob(&format!("{}/src/**/*.rs", project_path.display()))
365 .ok()
366 .map(|entries| {
367 entries.flatten().any(|p| {
368 std::fs::read_to_string(&p)
369 .ok()
370 .map(|c| {
371 c.contains("adversarial")
372 || c.contains("perturbation")
373 || c.contains("robustness")
374 || c.contains("attack")
375 })
376 .unwrap_or(false)
377 })
378 })
379 .unwrap_or(false);
380
381 let has_robustness_verification =
383 glob::glob(&format!("{}/tests/**/*.rs", project_path.display()))
384 .ok()
385 .map(|entries| {
386 entries.flatten().any(|p| {
387 std::fs::read_to_string(&p)
388 .ok()
389 .map(|c| c.contains("adversarial") || c.contains("robustness"))
390 .unwrap_or(false)
391 })
392 })
393 .unwrap_or(false);
394
395 item = item.with_evidence(Evidence {
396 evidence_type: EvidenceType::StaticAnalysis,
397 description: format!(
398 "Adversarial robustness: testing={}, verification={}",
399 has_adversarial_tests, has_robustness_verification
400 ),
401 data: None,
402 files: Vec::new(),
403 });
404
405 let has_ml_models = glob::glob(&format!("{}/src/**/*.rs", project_path.display()))
407 .ok()
408 .map(|entries| {
409 entries.flatten().any(|p| {
410 std::fs::read_to_string(&p)
411 .ok()
412 .map(|c| {
413 c.contains("predict") || c.contains("classifier") || c.contains("neural")
414 })
415 .unwrap_or(false)
416 })
417 })
418 .unwrap_or(false);
419
420 item = apply_check_outcome(
421 item,
422 &[
423 (
424 !has_ml_models || has_adversarial_tests || has_robustness_verification,
425 CheckOutcome::Pass,
426 ),
427 (true, CheckOutcome::Partial("ML models without adversarial testing")),
428 ],
429 );
430
431 item.finish_timed(start)
432}
433
434fn find_unsafe_send_sync(project_path: &Path) -> Vec<String> {
437 let mut results = Vec::new();
438 let Ok(entries) = glob::glob(&format!("{}/src/**/*.rs", project_path.display())) else {
439 return results;
440 };
441 for entry in entries.flatten() {
442 let Ok(content) = std::fs::read_to_string(&entry) else {
443 continue;
444 };
445 let has_unsafe_impl = content.contains("unsafe impl Send")
446 || content.contains("unsafe impl Sync")
447 || content.contains("unsafe impl<") && content.contains("> Send")
448 || content.contains("unsafe impl<") && content.contains("> Sync");
449
450 if has_unsafe_impl && !content.contains("// SAFETY:") && !content.contains("# Safety") {
452 results.push(entry.to_string_lossy().to_string());
453 }
454 }
455 results
456}
457
458pub fn check_thread_safety(project_path: &Path) -> CheckItem {
465 let start = Instant::now();
466 let mut item = CheckItem::new(
467 "SF-06",
468 "Thread Safety (Send + Sync)",
469 "All Send + Sync implementations correct",
470 )
471 .with_severity(Severity::Major)
472 .with_tps("Jidoka — race detection");
473
474 let unsafe_send_sync = find_unsafe_send_sync(project_path);
475
476 let cargo_toml = project_path.join("Cargo.toml");
478 let uses_concurrent = cargo_toml
479 .exists()
480 .then(|| std::fs::read_to_string(&cargo_toml).ok())
481 .flatten()
482 .map(|c| {
483 c.contains("crossbeam")
484 || c.contains("parking_lot")
485 || c.contains("dashmap")
486 || c.contains("rayon")
487 })
488 .unwrap_or(false);
489
490 item = item.with_evidence(Evidence {
492 evidence_type: EvidenceType::StaticAnalysis,
493 description: format!(
494 "Thread safety: unsafe_impls={}, concurrent_libs={}",
495 unsafe_send_sync.len(),
496 uses_concurrent
497 ),
498 data: Some(format!("unsafe_send_sync: {:?}", unsafe_send_sync)),
499 files: Vec::new(),
500 });
501
502 let sync_partial =
504 format!("{} unsafe Send/Sync without safety comment", unsafe_send_sync.len());
505 let sync_fail = format!(
507 "{} unsafe Send/Sync implementations without documentation",
508 unsafe_send_sync.len()
509 );
510 item = apply_check_outcome(
511 item,
512 &[
513 (unsafe_send_sync.is_empty(), CheckOutcome::Pass),
514 (unsafe_send_sync.len() <= 2, CheckOutcome::Partial(&sync_partial)),
515 (true, CheckOutcome::Fail(&sync_fail)),
516 ],
517 );
518
519 item.finish_timed(start)
520}
521
522fn scan_resource_patterns(project_path: &Path) -> (usize, Vec<&'static str>) {
524 let mut drop_impls = 0;
525 let mut resource_types = Vec::new();
526 let Ok(entries) = glob::glob(&format!("{}/src/**/*.rs", project_path.display())) else {
527 return (0, resource_types);
528 };
529 for entry in entries.flatten() {
530 let Ok(content) = std::fs::read_to_string(&entry) else {
531 continue;
532 };
533 drop_impls += content.matches("impl Drop for").count();
534 drop_impls +=
535 content.matches("impl<").count() * content.matches("> Drop for").count().min(1);
536 if content.contains("File") || content.contains("TcpStream") {
537 resource_types.push("file/network handles");
538 }
539 if content.contains("Arc<") || content.contains("Rc<") {
540 resource_types.push("reference counting");
541 }
542 if content.contains("ManuallyDrop") {
543 resource_types.push("ManuallyDrop");
544 }
545 }
546 resource_types.sort_unstable();
547 resource_types.dedup();
548 (drop_impls, resource_types)
549}
550
551pub fn check_resource_leak_prevention(project_path: &Path) -> CheckItem {
558 let start = Instant::now();
559 let mut item = CheckItem::new("SF-07", "Resource Leak Prevention", "No resource leaks")
560 .with_severity(Severity::Major)
561 .with_tps("Muda (Defects)");
562
563 let (drop_impls, resource_types) = scan_resource_patterns(project_path);
564
565 let has_mem_forget =
566 super::helpers::source_contains_pattern(project_path, &["mem::forget", "std::mem::forget"]);
567
568 item = item.with_evidence(Evidence {
569 evidence_type: EvidenceType::StaticAnalysis,
570 description: format!(
571 "Resource management: drop_impls={}, mem_forget={}, resource_types={:?}",
572 drop_impls, has_mem_forget, resource_types
573 ),
574 data: None,
575 files: Vec::new(),
576 });
577
578 item = apply_check_outcome(
579 item,
580 &[
581 (!has_mem_forget, CheckOutcome::Pass),
582 (true, CheckOutcome::Partial("Uses mem::forget (verify intentional)")),
583 ],
584 );
585
586 item.finish_timed(start)
587}
588
589fn scan_panic_patterns(project_path: &Path) -> (bool, bool, Vec<String>) {
591 let mut has_catch_unwind = false;
592 let mut has_panic_hook = false;
593 let mut high_unwrap_files = Vec::new();
594 let Ok(entries) = glob::glob(&format!("{}/src/**/*.rs", project_path.display())) else {
595 return (false, false, high_unwrap_files);
596 };
597 for entry in entries.flatten() {
598 let Ok(content) = std::fs::read_to_string(&entry) else {
599 continue;
600 };
601 if content.contains("catch_unwind") {
602 has_catch_unwind = true;
603 }
604 if content.contains("set_panic_hook") || content.contains("panic::set_hook") {
605 has_panic_hook = true;
606 }
607 let unwrap_count = content.matches(".unwrap()").count();
608 if unwrap_count > 10 {
609 high_unwrap_files.push(format!(
610 "{}: {} unwraps",
611 entry.file_name().unwrap_or_default().to_string_lossy(),
612 unwrap_count
613 ));
614 }
615 }
616 (has_catch_unwind, has_panic_hook, high_unwrap_files)
617}
618
619pub fn check_panic_safety(project_path: &Path) -> CheckItem {
626 let start = Instant::now();
627 let mut item = CheckItem::new("SF-08", "Panic Safety", "Panics don't corrupt data structures")
628 .with_severity(Severity::Minor)
629 .with_tps("Graceful degradation");
630
631 let (has_catch_unwind, has_panic_hook, panic_patterns) = scan_panic_patterns(project_path);
632
633 item = item.with_evidence(Evidence {
634 evidence_type: EvidenceType::StaticAnalysis,
635 description: format!(
636 "Panic handling: catch_unwind={}, panic_hook={}, high_unwrap_files={}",
637 has_catch_unwind,
638 has_panic_hook,
639 panic_patterns.len()
640 ),
641 data: Some(format!("patterns: {:?}", panic_patterns)),
642 files: Vec::new(),
643 });
644
645 let panic_few = format!("{} files with high unwrap count", panic_patterns.len());
646 let panic_many = format!(
647 "{} files with excessive unwraps - consider expect() or ? operator",
648 panic_patterns.len()
649 );
650 item = apply_check_outcome(
651 item,
652 &[
653 (panic_patterns.is_empty(), CheckOutcome::Pass),
654 (panic_patterns.len() <= 5, CheckOutcome::Partial(&panic_few)),
655 (true, CheckOutcome::Partial(&panic_many)),
656 ],
657 );
658
659 item.finish_timed(start)
660}
661
662fn classify_validation_in_file(content: &str) -> (bool, Vec<&'static str>) {
664 let mut has_explicit = false;
665 let mut methods = Vec::new();
666
667 if content.contains("fn validate")
668 || content.contains("fn is_valid")
669 || content.contains("impl Validate")
670 || content.contains("#[validate")
671 {
672 has_explicit = true;
673 methods.push("explicit validation");
674 }
675 if content.contains("pub fn")
676 && (content.contains("-> Result<") || content.contains("-> Option<"))
677 {
678 methods.push("Result/Option returns");
679 }
680 if content.contains("assert!(") || content.contains("debug_assert!(") {
681 methods.push("assertions");
682 }
683 (has_explicit, methods)
684}
685
686fn scan_validation_patterns(project_path: &Path) -> (bool, Vec<&'static str>) {
688 let mut has_explicit = false;
689 let mut methods = Vec::new();
690
691 let Ok(entries) = glob::glob(&format!("{}/src/**/*.rs", project_path.display())) else {
692 return (false, methods);
693 };
694
695 for entry in entries.flatten() {
696 let Ok(content) = std::fs::read_to_string(&entry) else {
697 continue;
698 };
699 let (file_explicit, file_methods) = classify_validation_in_file(&content);
700 has_explicit = has_explicit || file_explicit;
701 methods.extend(file_methods);
702 }
703 (has_explicit, methods)
704}
705
706fn has_validator_crate(project_path: &Path) -> bool {
708 let cargo_toml = project_path.join("Cargo.toml");
709 std::fs::read_to_string(cargo_toml)
710 .ok()
711 .is_some_and(|c| c.contains("validator") || c.contains("garde"))
712}
713
714pub fn check_input_validation(project_path: &Path) -> CheckItem {
721 let start = Instant::now();
722 let mut item = CheckItem::new("SF-09", "Input Validation", "All public APIs validate inputs")
723 .with_severity(Severity::Major)
724 .with_tps("Poka-Yoke — error prevention");
725
726 let (has_validation, mut validation_methods) = scan_validation_patterns(project_path);
727 let has_validator = has_validator_crate(project_path);
728
729 if has_validator {
730 validation_methods.push("validator crate");
731 }
732
733 item = item.with_evidence(Evidence {
734 evidence_type: EvidenceType::StaticAnalysis,
735 description: format!(
736 "Validation: explicit={}, methods={:?}",
737 has_validation, validation_methods
738 ),
739 data: None,
740 files: Vec::new(),
741 });
742
743 item = apply_check_outcome(
744 item,
745 &[
746 (has_validation || has_validator, CheckOutcome::Pass),
747 (!validation_methods.is_empty(), CheckOutcome::Pass),
748 (true, CheckOutcome::Partial("Consider adding explicit input validation")),
749 ],
750 );
751
752 item.finish_timed(start)
753}
754
755pub fn check_supply_chain_security(project_path: &Path) -> CheckItem {
762 let start = Instant::now();
763 let mut item = CheckItem::new("SF-10", "Supply Chain Security", "All dependencies audited")
764 .with_severity(Severity::Critical)
765 .with_tps("Jidoka — supply chain circuit breaker");
766
767 let has_audit_in_ci = check_ci_for_tool(project_path, "cargo audit");
769 let has_deny_in_ci = check_ci_for_tool(project_path, "cargo deny");
770
771 let deny_toml = project_path.join("deny.toml");
773 let has_deny_config = deny_toml.exists();
774
775 let audit_result =
777 Command::new("cargo").args(["audit", "--json"]).current_dir(project_path).output().ok();
778
779 let audit_clean = audit_result
780 .as_ref()
781 .map(|o| {
782 o.status.success()
783 || String::from_utf8_lossy(&o.stdout).contains("\"vulnerabilities\":[]")
784 })
785 .unwrap_or(false);
786
787 item = item.with_evidence(Evidence {
788 evidence_type: EvidenceType::DependencyAudit,
789 description: format!(
790 "Supply chain: audit_ci={}, deny_ci={}, deny_config={}, audit_clean={}",
791 has_audit_in_ci, has_deny_in_ci, has_deny_config, audit_clean
792 ),
793 data: None,
794 files: Vec::new(),
795 });
796
797 item = apply_check_outcome(
798 item,
799 &[
800 (has_deny_config && (has_audit_in_ci || has_deny_in_ci), CheckOutcome::Pass),
801 (
802 has_deny_config || has_audit_in_ci || has_deny_in_ci,
803 CheckOutcome::Partial("Partial supply chain security setup"),
804 ),
805 (audit_clean, CheckOutcome::Partial("No vulnerabilities but no CI enforcement")),
806 (true, CheckOutcome::Fail("No supply chain security tooling configured")),
807 ],
808 );
809
810 item.finish_timed(start)
811}
812
813fn check_ci_for_tool(project_path: &Path, tool: &str) -> bool {
815 let ci_configs = [
816 project_path.join(".github/workflows/ci.yml"),
817 project_path.join(".github/workflows/test.yml"),
818 project_path.join(".github/workflows/rust.yml"),
819 project_path.join(".github/workflows/security.yml"),
820 ];
821
822 for ci_path in &ci_configs {
823 if ci_path.exists() {
824 if let Ok(content) = std::fs::read_to_string(ci_path) {
825 if content.contains(tool) {
826 return true;
827 }
828 }
829 }
830 }
831 false
832}
833
834#[cfg(test)]
835#[path = "safety_tests.rs"]
836mod tests;