1pub mod abi;
29pub mod access;
30pub mod defi;
31pub mod external;
32pub mod proxy;
33pub mod source;
34pub mod vulnerability;
35
36use serde::{Deserialize, Serialize};
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct ContractAnalysis {
41 pub address: String,
43 pub chain: String,
45 pub is_verified: bool,
47 pub source_info: Option<source::ContractSource>,
49 pub proxy_info: Option<proxy::ProxyInfo>,
51 pub access_control: Option<access::AccessControlMap>,
53 pub vulnerabilities: Vec<vulnerability::VulnerabilityFinding>,
55 pub defi_analysis: Option<defi::DefiAnalysis>,
57 pub external_info: Option<external::ExternalInfo>,
59 pub security_score: u32,
61 pub security_summary: String,
63}
64
65pub async fn analyze_contract(
70 address: &str,
71 chain: &str,
72 client: &dyn crate::chains::ChainClient,
73 http_client: &reqwest::Client,
74) -> crate::error::Result<ContractAnalysis> {
75 let code = client.get_code(address).await?;
77 if code.is_empty() || code == "0x" {
78 return Err(crate::error::ScopeError::Chain(format!(
79 "{} is an EOA (externally owned account), not a contract",
80 address
81 )));
82 }
83
84 let source_result = source::fetch_contract_source(address, chain, http_client).await;
86 let source_info = source_result.ok();
87 let is_verified = source_info.is_some();
88
89 let proxy_info = proxy::detect_proxy(
91 address,
92 chain,
93 &code,
94 source_info.as_ref(),
95 client,
96 http_client,
97 )
98 .await
99 .ok();
100
101 let access_control = source_info.as_ref().map(access::analyze_access_control);
103
104 let vulnerabilities = if let Some(src) = source_info.as_ref() {
106 vulnerability::scan_vulnerabilities(src)
107 } else {
108 vulnerability::scan_bytecode_only(&code)
109 };
110
111 let defi_analysis = source_info.as_ref().map(defi::analyze_defi_patterns);
113
114 let external_info =
116 external::gather_external_info(address, chain, source_info.as_ref(), http_client)
117 .await
118 .ok();
119
120 let security_score = compute_security_score(
122 is_verified,
123 &proxy_info,
124 &access_control,
125 &vulnerabilities,
126 &defi_analysis,
127 &external_info,
128 );
129
130 let security_summary = generate_security_summary(
131 is_verified,
132 &proxy_info,
133 &access_control,
134 &vulnerabilities,
135 security_score,
136 );
137
138 Ok(ContractAnalysis {
139 address: address.to_string(),
140 chain: chain.to_string(),
141 is_verified,
142 source_info,
143 proxy_info,
144 access_control,
145 vulnerabilities,
146 defi_analysis,
147 external_info,
148 security_score,
149 security_summary,
150 })
151}
152
153fn compute_security_score(
155 is_verified: bool,
156 proxy_info: &Option<proxy::ProxyInfo>,
157 access_control: &Option<access::AccessControlMap>,
158 vulnerabilities: &[vulnerability::VulnerabilityFinding],
159 defi_analysis: &Option<defi::DefiAnalysis>,
160 external_info: &Option<external::ExternalInfo>,
161) -> u32 {
162 let mut score: i32 = 50; if is_verified {
166 score += 15;
167 } else {
168 score -= 20;
169 }
170
171 if let Some(proxy) = proxy_info
173 && proxy.is_proxy
174 {
175 score -= 5; if proxy.admin_address.is_some() {
177 score += 3; }
179 }
180
181 if let Some(ac) = access_control {
183 if ac.has_renounced_ownership {
184 score += 10;
185 }
186 if ac.has_role_based_access {
187 score += 5;
188 }
189 if ac.uses_tx_origin {
190 score -= 15;
191 }
192 }
193
194 for vuln in vulnerabilities {
196 match vuln.severity {
197 vulnerability::Severity::Critical => score -= 20,
198 vulnerability::Severity::High => score -= 12,
199 vulnerability::Severity::Medium => score -= 6,
200 vulnerability::Severity::Low => score -= 2,
201 vulnerability::Severity::Informational => score -= 1,
202 }
203 }
204
205 if let Some(defi) = defi_analysis {
207 if defi.has_oracle_dependency {
208 score -= 5;
209 }
210 if defi.has_flash_loan_risk {
211 score -= 8;
212 }
213 }
214
215 if let Some(ext) = external_info {
217 if !ext.audit_reports.is_empty() {
218 score += 15;
219 }
220 if ext.github_repo.is_some() {
221 score += 5;
222 }
223 }
224
225 score.clamp(0, 100) as u32
226}
227
228fn generate_security_summary(
230 is_verified: bool,
231 proxy_info: &Option<proxy::ProxyInfo>,
232 access_control: &Option<access::AccessControlMap>,
233 vulnerabilities: &[vulnerability::VulnerabilityFinding],
234 score: u32,
235) -> String {
236 let mut parts = Vec::new();
237
238 if is_verified {
240 parts.push("Source code is verified on the block explorer.".to_string());
241 } else {
242 parts.push(
243 "WARNING: Source code is NOT verified — unable to perform source-level analysis."
244 .to_string(),
245 );
246 }
247
248 if let Some(proxy) = proxy_info
250 && proxy.is_proxy
251 {
252 parts.push(format!(
253 "Contract is a {} proxy{}.",
254 proxy.proxy_type,
255 proxy
256 .implementation_address
257 .as_ref()
258 .map(|a| format!(" pointing to {}", a))
259 .unwrap_or_default()
260 ));
261 }
262
263 if let Some(ac) = access_control {
265 if ac.has_renounced_ownership {
266 parts.push("Ownership has been renounced.".to_string());
267 } else if !ac.privileged_functions.is_empty() {
268 parts.push(format!(
269 "{} privileged function(s) found with owner/admin restrictions.",
270 ac.privileged_functions.len()
271 ));
272 }
273 if ac.uses_tx_origin {
274 parts.push("DANGER: Uses tx.origin for authorization.".to_string());
275 }
276 }
277
278 let critical = vulnerabilities
280 .iter()
281 .filter(|v| v.severity == vulnerability::Severity::Critical)
282 .count();
283 let high = vulnerabilities
284 .iter()
285 .filter(|v| v.severity == vulnerability::Severity::High)
286 .count();
287 if critical > 0 || high > 0 {
288 parts.push(format!(
289 "Found {} critical and {} high severity issue(s).",
290 critical, high
291 ));
292 } else if vulnerabilities.is_empty() {
293 parts.push("No vulnerability heuristics triggered.".to_string());
294 } else {
295 parts.push(format!(
296 "{} lower-severity finding(s) detected.",
297 vulnerabilities.len()
298 ));
299 }
300
301 let rating = match score {
303 80..=100 => "GOOD",
304 60..=79 => "MODERATE",
305 40..=59 => "CAUTION",
306 20..=39 => "HIGH RISK",
307 _ => "CRITICAL RISK",
308 };
309 parts.push(format!("Security Score: {}/100 ({})", score, rating));
310
311 parts.join(" ")
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317
318 #[test]
319 fn test_compute_security_score_verified() {
320 let score = compute_security_score(true, &None, &None, &[], &None, &None);
321 assert_eq!(score, 65); }
323
324 #[test]
325 fn test_compute_security_score_unverified() {
326 let score = compute_security_score(false, &None, &None, &[], &None, &None);
327 assert_eq!(score, 30); }
329
330 #[test]
331 fn test_compute_security_score_with_critical_vuln() {
332 let vulns = vec![vulnerability::VulnerabilityFinding {
333 id: "TEST-001".to_string(),
334 title: "Test".to_string(),
335 severity: vulnerability::Severity::Critical,
336 category: vulnerability::VulnCategory::Reentrancy,
337 description: "Test finding".to_string(),
338 source_location: None,
339 recommendation: "Fix it".to_string(),
340 }];
341 let score = compute_security_score(true, &None, &None, &vulns, &None, &None);
342 assert_eq!(score, 45); }
344
345 #[test]
346 fn test_security_summary_unverified() {
347 let summary = generate_security_summary(false, &None, &None, &[], 30);
348 assert!(summary.contains("NOT verified"));
349 assert!(summary.contains("30/100"));
350 }
351
352 #[test]
353 fn test_score_clamped_to_valid_range() {
354 let many_vulns: Vec<_> = (0..10)
355 .map(|i| vulnerability::VulnerabilityFinding {
356 id: format!("V-{}", i),
357 title: "Critical".to_string(),
358 severity: vulnerability::Severity::Critical,
359 category: vulnerability::VulnCategory::Reentrancy,
360 description: "Bad".to_string(),
361 source_location: None,
362 recommendation: "Fix".to_string(),
363 })
364 .collect();
365 let score = compute_security_score(false, &None, &None, &many_vulns, &None, &None);
366 assert_eq!(score, 0); }
368
369 #[test]
370 fn test_compute_security_score_proxy_with_admin() {
371 use proxy::ProxyInfo;
372
373 let proxy = Some(ProxyInfo {
374 is_proxy: true,
375 admin_address: Some("0xadmin".to_string()),
376 proxy_type: "EIP-1967".to_string(),
377 implementation_address: None,
378 beacon_address: None,
379 details: vec![],
380 });
381 let score = compute_security_score(true, &proxy, &None, &[], &None, &None);
382 assert_eq!(score, 63);
384 }
385
386 #[test]
387 fn test_compute_security_score_access_control_renounced_and_role_based() {
388 use access::{AccessControlMap, AuthAnalysis};
389
390 let ac = Some(AccessControlMap {
391 ownership_pattern: None,
392 has_renounced_ownership: true,
393 has_role_based_access: true,
394 uses_tx_origin: false,
395 tx_origin_locations: vec![],
396 modifiers: vec![],
397 privileged_functions: vec![],
398 roles: vec![],
399 auth_analysis: AuthAnalysis {
400 msg_sender_checks: 0,
401 tx_origin_checks: 0,
402 has_origin_sender_comparison: false,
403 summary: String::new(),
404 },
405 });
406 let score = compute_security_score(true, &None, &ac, &[], &None, &None);
407 assert_eq!(score, 80);
409 }
410
411 #[test]
412 fn test_compute_security_score_tx_origin() {
413 use access::{AccessControlMap, AuthAnalysis};
414
415 let ac = Some(AccessControlMap {
416 ownership_pattern: None,
417 has_renounced_ownership: false,
418 has_role_based_access: false,
419 uses_tx_origin: true,
420 tx_origin_locations: vec![],
421 modifiers: vec![],
422 privileged_functions: vec![],
423 roles: vec![],
424 auth_analysis: AuthAnalysis {
425 msg_sender_checks: 0,
426 tx_origin_checks: 1,
427 has_origin_sender_comparison: false,
428 summary: String::new(),
429 },
430 });
431 let score = compute_security_score(true, &None, &ac, &[], &None, &None);
432 assert_eq!(score, 50);
434 }
435
436 #[test]
437 fn test_compute_security_score_defi_oracle_and_flash_loan() {
438 use defi::{DefiAnalysis, ProtocolType};
439
440 let defi = Some(DefiAnalysis {
441 protocol_type: ProtocolType::Lending,
442 has_oracle_dependency: true,
443 oracle_info: vec![],
444 has_flash_loan_risk: true,
445 flash_loan_info: vec![],
446 dex_integrations: vec![],
447 lending_patterns: vec![],
448 token_standards: vec![],
449 staking_patterns: vec![],
450 risk_factors: vec![],
451 });
452 let score = compute_security_score(true, &None, &None, &[], &defi, &None);
453 assert_eq!(score, 52);
455 }
456
457 #[test]
458 fn test_compute_security_score_external_audit_and_github() {
459 use external::{AuditReport, ExternalInfo};
460
461 let ext = Some(ExternalInfo {
462 github_repo: Some("https://github.com/org/repo".to_string()),
463 audit_reports: vec![AuditReport {
464 auditor: "Trail of Bits".to_string(),
465 url: "https://example.com/report".to_string(),
466 date: Some("2024-01-01".to_string()),
467 scope: "Full".to_string(),
468 }],
469 sourcify_verified: None,
470 deployer: None,
471 explorer_url: String::new(),
472 metadata: vec![],
473 });
474 let score = compute_security_score(true, &None, &None, &[], &None, &ext);
475 assert_eq!(score, 85);
477 }
478
479 #[test]
480 fn test_compute_security_score_clamped_to_100() {
481 use access::{AccessControlMap, AuthAnalysis};
482 use external::{AuditReport, ExternalInfo};
483
484 let ac = Some(AccessControlMap {
485 ownership_pattern: None,
486 has_renounced_ownership: true,
487 has_role_based_access: true,
488 uses_tx_origin: false,
489 tx_origin_locations: vec![],
490 modifiers: vec![],
491 privileged_functions: vec![],
492 roles: vec![],
493 auth_analysis: AuthAnalysis {
494 msg_sender_checks: 0,
495 tx_origin_checks: 0,
496 has_origin_sender_comparison: false,
497 summary: String::new(),
498 },
499 });
500 let ext = Some(ExternalInfo {
501 github_repo: Some("https://github.com/org/repo".to_string()),
502 audit_reports: vec![AuditReport {
503 auditor: "ToB".to_string(),
504 url: "https://example.com".to_string(),
505 date: None,
506 scope: "Full".to_string(),
507 }],
508 sourcify_verified: None,
509 deployer: None,
510 explorer_url: String::new(),
511 metadata: vec![],
512 });
513 let score = compute_security_score(true, &None, &ac, &[], &None, &ext);
514 assert_eq!(score, 100);
516 }
517
518 #[test]
519 fn test_generate_security_summary_proxy_implementation() {
520 use proxy::ProxyInfo;
521
522 let proxy = Some(ProxyInfo {
523 is_proxy: true,
524 proxy_type: "EIP-1967".to_string(),
525 implementation_address: Some("0x1234567890123456789012345678901234567890".to_string()),
526 admin_address: None,
527 beacon_address: None,
528 details: vec![],
529 });
530 let summary = generate_security_summary(true, &proxy, &None, &[], 70);
531 assert!(summary.contains("pointing to 0x1234567890123456789012345678901234567890"));
532 }
533
534 #[test]
535 fn test_generate_security_summary_verified_no_vulns() {
536 let summary = generate_security_summary(true, &None, &None, &[], 80);
537 assert!(summary.contains("No vulnerability"));
538 }
539
540 #[test]
541 fn test_generate_security_summary_low_severity_vulns() {
542 let vulns = vec![vulnerability::VulnerabilityFinding {
543 id: "L-001".to_string(),
544 title: "Low".to_string(),
545 severity: vulnerability::Severity::Low,
546 category: vulnerability::VulnCategory::LogicError,
547 description: "Minor".to_string(),
548 source_location: None,
549 recommendation: "Consider".to_string(),
550 }];
551 let summary = generate_security_summary(true, &None, &None, &vulns, 75);
552 assert!(summary.contains("lower-severity"));
553 }
554
555 #[test]
556 fn test_generate_security_summary_access_control_privileged_and_tx_origin() {
557 use access::{
558 AccessControlMap, AuthAnalysis, PrivilegeRisk, PrivilegedFunction, SourceLocation,
559 };
560
561 let ac = Some(AccessControlMap {
562 ownership_pattern: Some("Ownable".to_string()),
563 has_renounced_ownership: false,
564 has_role_based_access: false,
565 uses_tx_origin: true,
566 tx_origin_locations: vec![SourceLocation {
567 file: "Contract.sol".to_string(),
568 line: 10,
569 snippet: "require(tx.origin == owner)".to_string(),
570 }],
571 modifiers: vec![],
572 privileged_functions: vec![PrivilegedFunction {
573 name: "withdraw".to_string(),
574 modifiers: vec!["onlyOwner".to_string()],
575 capability: "drain funds".to_string(),
576 risk: PrivilegeRisk::Critical,
577 }],
578 roles: vec![],
579 auth_analysis: AuthAnalysis {
580 msg_sender_checks: 0,
581 tx_origin_checks: 1,
582 has_origin_sender_comparison: false,
583 summary: String::new(),
584 },
585 });
586 let summary = generate_security_summary(true, &None, &ac, &[], 50);
587 assert!(summary.contains("privileged function"));
588 assert!(summary.contains("tx.origin"));
589 }
590
591 #[test]
592 fn test_generate_security_summary_score_ratings() {
593 assert!(generate_security_summary(true, &None, &None, &[], 85).contains("GOOD"));
594 assert!(generate_security_summary(true, &None, &None, &[], 70).contains("MODERATE"));
595 assert!(generate_security_summary(true, &None, &None, &[], 50).contains("CAUTION"));
596 assert!(generate_security_summary(true, &None, &None, &[], 30).contains("HIGH RISK"));
597 assert!(generate_security_summary(true, &None, &None, &[], 15).contains("CRITICAL RISK"));
598 }
599
600 #[test]
601 fn test_contract_analysis_struct_construction() {
602 let analysis = ContractAnalysis {
603 address: "0x123".to_string(),
604 chain: "ethereum".to_string(),
605 is_verified: true,
606 source_info: None,
607 proxy_info: None,
608 access_control: None,
609 vulnerabilities: vec![],
610 defi_analysis: None,
611 external_info: None,
612 security_score: 75,
613 security_summary: "Test summary".to_string(),
614 };
615 assert_eq!(analysis.address, "0x123");
616 assert_eq!(analysis.chain, "ethereum");
617 assert!(analysis.is_verified);
618 assert_eq!(analysis.security_score, 75);
619 assert!(analysis.security_summary.contains("Test"));
620 }
621
622 #[test]
623 fn test_contract_analysis_serialization_roundtrip() {
624 let analysis = ContractAnalysis {
625 address: "0xabc".to_string(),
626 chain: "polygon".to_string(),
627 is_verified: false,
628 source_info: None,
629 proxy_info: None,
630 access_control: None,
631 vulnerabilities: vec![],
632 defi_analysis: None,
633 external_info: None,
634 security_score: 42,
635 security_summary: "Summary".to_string(),
636 };
637 let json = serde_json::to_string(&analysis).unwrap();
638 let restored: ContractAnalysis = serde_json::from_str(&json).unwrap();
639 assert_eq!(restored.address, analysis.address);
640 assert_eq!(restored.security_score, analysis.security_score);
641 }
642
643 #[test]
644 fn test_compute_security_score_proxy_without_admin() {
645 use proxy::ProxyInfo;
646
647 let proxy = Some(ProxyInfo {
648 is_proxy: true,
649 admin_address: None,
650 proxy_type: "EIP-1967".to_string(),
651 implementation_address: Some("0ximpl".to_string()),
652 beacon_address: None,
653 details: vec![],
654 });
655 let score = compute_security_score(true, &proxy, &None, &[], &None, &None);
656 assert_eq!(score, 60);
658 }
659
660 #[test]
661 fn test_compute_security_score_proxy_info_not_proxy() {
662 use proxy::ProxyInfo;
663
664 let proxy = Some(ProxyInfo {
665 is_proxy: false,
666 admin_address: None,
667 proxy_type: "None".to_string(),
668 implementation_address: None,
669 beacon_address: None,
670 details: vec![],
671 });
672 let score = compute_security_score(true, &proxy, &None, &[], &None, &None);
673 assert_eq!(score, 65); }
675
676 #[test]
677 fn test_compute_security_score_severity_high() {
678 let vulns = vec![vulnerability::VulnerabilityFinding {
679 id: "H-001".to_string(),
680 title: "High".to_string(),
681 severity: vulnerability::Severity::High,
682 category: vulnerability::VulnCategory::AccessControl,
683 description: "High finding".to_string(),
684 source_location: None,
685 recommendation: "Fix".to_string(),
686 }];
687 let score = compute_security_score(true, &None, &None, &vulns, &None, &None);
688 assert_eq!(score, 53); }
690
691 #[test]
692 fn test_compute_security_score_severity_medium() {
693 let vulns = vec![vulnerability::VulnerabilityFinding {
694 id: "M-001".to_string(),
695 title: "Medium".to_string(),
696 severity: vulnerability::Severity::Medium,
697 category: vulnerability::VulnCategory::LogicError,
698 description: "Medium finding".to_string(),
699 source_location: None,
700 recommendation: "Fix".to_string(),
701 }];
702 let score = compute_security_score(true, &None, &None, &vulns, &None, &None);
703 assert_eq!(score, 59); }
705
706 #[test]
707 fn test_compute_security_score_severity_low() {
708 let vulns = vec![vulnerability::VulnerabilityFinding {
709 id: "L-001".to_string(),
710 title: "Low".to_string(),
711 severity: vulnerability::Severity::Low,
712 category: vulnerability::VulnCategory::UncheckedCall,
713 description: "Low finding".to_string(),
714 source_location: None,
715 recommendation: "Fix".to_string(),
716 }];
717 let score = compute_security_score(true, &None, &None, &vulns, &None, &None);
718 assert_eq!(score, 63); }
720
721 #[test]
722 fn test_compute_security_score_severity_informational() {
723 let vulns = vec![vulnerability::VulnerabilityFinding {
724 id: "I-001".to_string(),
725 title: "Info".to_string(),
726 severity: vulnerability::Severity::Informational,
727 category: vulnerability::VulnCategory::Informational,
728 description: "Info finding".to_string(),
729 source_location: None,
730 recommendation: "Fix".to_string(),
731 }];
732 let score = compute_security_score(true, &None, &None, &vulns, &None, &None);
733 assert_eq!(score, 64); }
735
736 #[test]
737 fn test_compute_security_score_defi_oracle_only() {
738 use defi::{DefiAnalysis, ProtocolType};
739
740 let defi = Some(DefiAnalysis {
741 protocol_type: ProtocolType::DEX,
742 has_oracle_dependency: true,
743 oracle_info: vec![],
744 has_flash_loan_risk: false,
745 flash_loan_info: vec![],
746 dex_integrations: vec![],
747 lending_patterns: vec![],
748 token_standards: vec![],
749 staking_patterns: vec![],
750 risk_factors: vec![],
751 });
752 let score = compute_security_score(true, &None, &None, &[], &defi, &None);
753 assert_eq!(score, 60); }
755
756 #[test]
757 fn test_compute_security_score_defi_flash_loan_only() {
758 use defi::{DefiAnalysis, ProtocolType};
759
760 let defi = Some(DefiAnalysis {
761 protocol_type: ProtocolType::Lending,
762 has_oracle_dependency: false,
763 oracle_info: vec![],
764 has_flash_loan_risk: true,
765 flash_loan_info: vec![],
766 dex_integrations: vec![],
767 lending_patterns: vec![],
768 token_standards: vec![],
769 staking_patterns: vec![],
770 risk_factors: vec![],
771 });
772 let score = compute_security_score(true, &None, &None, &[], &defi, &None);
773 assert_eq!(score, 57); }
775
776 #[test]
777 fn test_compute_security_score_external_github_only() {
778 use external::ExternalInfo;
779
780 let ext = Some(ExternalInfo {
781 github_repo: Some("https://github.com/org/repo".to_string()),
782 audit_reports: vec![],
783 sourcify_verified: None,
784 deployer: None,
785 explorer_url: String::new(),
786 metadata: vec![],
787 });
788 let score = compute_security_score(true, &None, &None, &[], &None, &ext);
789 assert_eq!(score, 70); }
791
792 #[test]
793 fn test_compute_security_score_external_audit_only() {
794 use external::{AuditReport, ExternalInfo};
795
796 let ext = Some(ExternalInfo {
797 github_repo: None,
798 audit_reports: vec![AuditReport {
799 auditor: "Auditor".to_string(),
800 url: "https://audit.com".to_string(),
801 date: None,
802 scope: "Full".to_string(),
803 }],
804 sourcify_verified: None,
805 deployer: None,
806 explorer_url: String::new(),
807 metadata: vec![],
808 });
809 let score = compute_security_score(true, &None, &None, &[], &None, &ext);
810 assert_eq!(score, 80); }
812
813 #[test]
814 fn test_generate_security_summary_proxy_no_implementation() {
815 use proxy::ProxyInfo;
816
817 let proxy = Some(ProxyInfo {
818 is_proxy: true,
819 proxy_type: "EIP-1967".to_string(),
820 implementation_address: None,
821 admin_address: None,
822 beacon_address: None,
823 details: vec![],
824 });
825 let summary = generate_security_summary(true, &proxy, &None, &[], 65);
826 assert!(summary.contains("EIP-1967"));
827 assert!(summary.contains("proxy"));
828 assert!(!summary.contains("pointing to"));
829 }
830
831 #[test]
832 fn test_generate_security_summary_renounced_only() {
833 use access::{AccessControlMap, AuthAnalysis};
834
835 let ac = Some(AccessControlMap {
836 ownership_pattern: None,
837 has_renounced_ownership: true,
838 has_role_based_access: false,
839 uses_tx_origin: false,
840 tx_origin_locations: vec![],
841 modifiers: vec![],
842 privileged_functions: vec![],
843 roles: vec![],
844 auth_analysis: AuthAnalysis {
845 msg_sender_checks: 0,
846 tx_origin_checks: 0,
847 has_origin_sender_comparison: false,
848 summary: String::new(),
849 },
850 });
851 let summary = generate_security_summary(true, &None, &ac, &[], 80);
852 assert!(summary.contains("Ownership has been renounced"));
853 }
854
855 #[test]
856 fn test_generate_security_summary_critical_and_high_vulns() {
857 let vulns = vec![
858 vulnerability::VulnerabilityFinding {
859 id: "C-001".to_string(),
860 title: "Critical".to_string(),
861 severity: vulnerability::Severity::Critical,
862 category: vulnerability::VulnCategory::Reentrancy,
863 description: "Critical".to_string(),
864 source_location: None,
865 recommendation: "Fix".to_string(),
866 },
867 vulnerability::VulnerabilityFinding {
868 id: "H-001".to_string(),
869 title: "High".to_string(),
870 severity: vulnerability::Severity::High,
871 category: vulnerability::VulnCategory::AccessControl,
872 description: "High".to_string(),
873 source_location: None,
874 recommendation: "Fix".to_string(),
875 },
876 ];
877 let summary = generate_security_summary(true, &None, &None, &vulns, 40);
878 assert!(summary.contains("critical"));
879 assert!(summary.contains("high"));
880 assert!(summary.contains("1 critical"));
881 assert!(summary.contains("1 high"));
882 }
883}