1mod audit;
4mod benchmark;
5mod chain;
6mod ci;
7mod deploy;
8mod deploy_safe;
9mod doctor;
10mod fuzz;
11mod gas;
12mod invariant;
13mod plugins;
14mod report;
15mod scan;
16mod security;
17mod simulate;
18mod upgrade_check;
19mod verify;
20mod watch;
21
22use clap::{Parser, Subcommand};
23use std::path::PathBuf;
24
25#[derive(Parser, Debug)]
27#[command(
28 name = "forge-guard",
29 version,
30 about = "Pre-deployment smart contract auditing framework for Foundry",
31 long_about = "The most comprehensive pre-deployment smart contract auditing framework for Foundry.\n\nTransforms security auditing from an optional step into a mandatory pre-deployment process.\nBlocks unsafe deployments by default while providing detailed vulnerability reports.",
32 author
33)]
34pub struct Cli {
35 #[command(subcommand)]
36 pub command: Commands,
37}
38
39impl Cli {
40 pub fn from_env() -> Self {
42 Self::parse()
43 }
44
45 pub fn run(&self) -> anyhow::Result<()> {
47 use anyhow::Context;
48 match &self.command {
49 Commands::Audit(args) => audit::run(args).context("Audit failed"),
50 Commands::Deploy(args) => deploy::run(args).context("Deploy failed"),
51 Commands::DeploySafe(args) => deploy_safe::run(args).context("Safe deploy failed"),
52 Commands::Fuzz(args) => fuzz::run(args).context("Fuzzing failed"),
53 Commands::Invariant(args) => invariant::run(args).context("Invariant test failed"),
54 Commands::Simulate(args) => simulate::run(args).context("Simulation failed"),
55 Commands::Gas(args) => gas::run(args).context("Gas analysis failed"),
56 Commands::Report(args) => report::run(args).context("Report generation failed"),
57 Commands::Verify(args) => verify::run(args).context("Verification failed"),
58 Commands::Doctor(args) => doctor::run(args).context("Doctor analysis failed"),
59 Commands::Watch(args) => watch::run(args).context("Watch failed"),
60 Commands::Ci(args) => ci::run(args).context("CI generation failed"),
61 Commands::Benchmark(args) => benchmark::run(args).context("Benchmark failed"),
62 Commands::Scan(args) => scan::run(args).context("Scan failed"),
63 Commands::UpgradeCheck(args) => {
64 upgrade_check::run(args).context("Upgrade check failed")
65 }
66 Commands::Plugins(args) => plugins::run(args).context("Plugin operation failed"),
67 Commands::Chain(args) => chain::run(args).context("Chain operation failed"),
68 Commands::Security(args) => security::run(args).context("Security operation failed"),
69 }
70 }
71}
72
73#[derive(Subcommand, Debug)]
75pub enum Commands {
76 Audit(AuditArgs),
78 Deploy(DeployArgs),
80 #[command(name = "deploy-safe")]
82 DeploySafe(DeploySafeArgs),
83 Fuzz(FuzzArgs),
85 Invariant(InvariantArgs),
87 Simulate(SimulateArgs),
89 Gas(GasArgs),
91 Report(ReportArgs),
93 Verify(VerifyArgs),
95 Doctor(DoctorArgs),
97 Watch(WatchArgs),
99 Ci(CiArgs),
101 Benchmark(BenchmarkArgs),
103 Scan(ScanArgs),
105 #[command(name = "upgrade-check")]
107 UpgradeCheck(UpgradeCheckArgs),
108 Plugins(PluginArgs),
110 Chain(ChainArgs),
112 Security(SecurityArgs),
114}
115
116#[derive(Debug, Default, clap::Args)]
120pub struct SharedFlags {
121 #[arg(long, global = true, default_value = "ethereum")]
123 pub chain: String,
124
125 #[arg(long, global = true, default_value = ".")]
127 pub project: PathBuf,
128
129 #[arg(long, global = true)]
131 pub json: bool,
132
133 #[arg(long, global = true)]
135 pub markdown: bool,
136
137 #[arg(long, global = true)]
139 pub html: bool,
140
141 #[arg(long, global = true)]
143 pub strict: bool,
144
145 #[arg(long, global = true)]
147 pub offline: bool,
148
149 #[arg(long, global = true)]
151 pub production: bool,
152
153 #[arg(long, global = true)]
155 pub report: bool,
156
157 #[arg(long, global = true, default_value = "4")]
159 pub parallelism: usize,
160}
161
162#[derive(Debug, clap::Args)]
165pub struct AuditArgs {
166 #[command(flatten)]
167 pub shared: SharedFlags,
168
169 #[arg(long, short)]
171 pub full: bool,
172
173 #[arg(long)]
175 pub quick: bool,
176
177 #[arg(long)]
179 pub summary: bool,
180
181 #[arg(long)]
183 pub ai: bool,
184
185 #[arg(long, default_value = "openai")]
187 pub ai_provider: String,
188
189 #[arg(long, default_value = "gpt-5")]
191 pub ai_model: String,
192
193 #[arg(long)]
195 pub ai_api_key: Option<String>,
196
197 #[arg(long)]
199 pub ollama_endpoint: Option<String>,
200
201 #[arg(long)]
203 pub ai_full: bool,
204
205 #[arg(long)]
207 pub exploit: bool,
208
209 #[arg(long)]
211 pub gas: bool,
212
213 #[arg(long)]
215 pub all_chains: bool,
216
217 #[arg(long, default_value = "src")]
219 pub sources: String,
220
221 #[arg(long)]
223 pub exclude: Option<String>,
224}
225
226#[derive(Debug, clap::Args)]
227pub struct DeployArgs {
228 #[command(flatten)]
229 pub shared: SharedFlags,
230
231 pub contract: Option<String>,
233
234 #[arg(long)]
236 pub force: bool,
237
238 #[arg(long)]
240 pub args: Option<String>,
241
242 #[arg(long)]
244 pub salt: Option<String>,
245
246 #[arg(long)]
248 pub verify: bool,
249}
250
251#[derive(Debug, clap::Args)]
252pub struct DeploySafeArgs {
253 #[command(flatten)]
254 pub shared: SharedFlags,
255
256 pub contract: Option<String>,
258
259 #[arg(long)]
261 pub args: Option<String>,
262
263 #[arg(long)]
265 pub verify: bool,
266}
267
268#[derive(Debug, clap::Args)]
269pub struct FuzzArgs {
270 #[command(flatten)]
271 pub shared: SharedFlags,
272
273 #[arg(long, default_value = "10000")]
275 pub runs: u32,
276
277 #[arg(long)]
279 pub seed: Option<u64>,
280
281 #[arg(long)]
283 pub test: Option<String>,
284
285 pub contract: Option<String>,
287}
288
289#[derive(Debug, clap::Args)]
290pub struct InvariantArgs {
291 #[command(flatten)]
292 pub shared: SharedFlags,
293
294 #[arg(long, default_value = "1000")]
296 pub runs: u32,
297
298 #[arg(long, default_value = "100")]
300 pub depth: u32,
301
302 pub contract: Option<String>,
304
305 #[arg(long)]
307 pub fail_on_revert: bool,
308}
309
310#[derive(Debug, clap::Args)]
311pub struct SimulateArgs {
312 #[command(flatten)]
313 pub shared: SharedFlags,
314
315 #[arg(long, default_value = "100")]
317 pub blocks: u32,
318
319 #[arg(long)]
321 pub deployer: Option<String>,
322
323 #[arg(long)]
325 pub mev: bool,
326
327 pub contract: Option<String>,
329}
330
331#[derive(Debug, clap::Args)]
332pub struct GasArgs {
333 #[command(flatten)]
334 pub shared: SharedFlags,
335
336 pub contract: Option<String>,
338
339 #[arg(long)]
341 pub diff: Option<String>,
342
343 #[arg(long)]
345 pub all: bool,
346
347 #[arg(long, default_value = "50000")]
349 pub warn_threshold: u64,
350}
351
352#[derive(Debug, clap::Args)]
353pub struct ReportArgs {
354 #[command(flatten)]
355 pub shared: SharedFlags,
356
357 pub input: Option<PathBuf>,
359
360 #[arg(long, default_value = "markdown")]
362 pub format: String,
363
364 #[arg(long)]
366 pub output: Option<PathBuf>,
367
368 #[arg(long)]
370 pub exploit_paths: bool,
371
372 #[arg(long)]
374 pub summary: bool,
375}
376
377#[derive(Debug, clap::Args)]
378pub struct VerifyArgs {
379 #[command(flatten)]
380 pub shared: SharedFlags,
381
382 pub address: Option<String>,
384
385 pub name: Option<String>,
387
388 #[arg(long)]
390 pub api_key: Option<String>,
391
392 #[arg(long)]
394 pub constructor_args: Option<String>,
395
396 #[arg(long)]
398 pub all: bool,
399}
400
401#[derive(Debug, clap::Args)]
402pub struct DoctorArgs {
403 #[command(flatten)]
404 pub shared: SharedFlags,
405
406 #[arg(long)]
408 pub fix: bool,
409
410 #[arg(long, short)]
412 pub verbose: bool,
413
414 #[arg(long)]
416 pub check: Option<String>,
417}
418
419#[derive(Debug, clap::Args)]
420pub struct WatchArgs {
421 #[command(flatten)]
422 pub shared: SharedFlags,
423
424 #[arg(long, default_value = "src")]
426 pub dirs: String,
427
428 #[arg(long, default_value = "500")]
430 pub debounce_ms: u64,
431
432 #[arg(long)]
434 pub exclude: Option<String>,
435
436 #[arg(long)]
438 pub full: bool,
439}
440
441#[derive(Debug, clap::Args)]
442pub struct CiArgs {
443 #[command(flatten)]
444 pub shared: SharedFlags,
445
446 #[arg(long, default_value = "github")]
448 pub platform: String,
449
450 #[arg(long, default_value = ".github/workflows")]
452 pub output: PathBuf,
453
454 #[arg(long)]
456 pub include_deploy: bool,
457
458 #[arg(long)]
460 pub overwrite: bool,
461}
462
463#[derive(Debug, clap::Args)]
464pub struct BenchmarkArgs {
465 #[command(flatten)]
466 pub shared: SharedFlags,
467
468 #[arg(long, default_value = "10")]
470 pub iterations: u32,
471
472 #[arg(long)]
474 pub compare: Option<PathBuf>,
475
476 #[arg(long)]
478 pub save: Option<PathBuf>,
479
480 #[arg(long)]
482 pub module: Option<String>,
483
484 #[arg(long, default_value = "3")]
486 pub warmup: u32,
487}
488
489#[derive(Debug, clap::Args)]
490pub struct ScanArgs {
491 #[command(flatten)]
492 pub shared: SharedFlags,
493
494 #[arg(long, default_value = "1")]
496 pub depth: u32,
497
498 #[arg(long)]
500 pub update: bool,
501
502 #[arg(long)]
504 pub vulnerable_only: bool,
505
506 #[arg(long)]
508 pub fail_fast: bool,
509}
510
511#[derive(Debug, clap::Args)]
512pub struct UpgradeCheckArgs {
513 #[command(flatten)]
514 pub shared: SharedFlags,
515
516 pub proxy: Option<String>,
518
519 pub implementation: Option<String>,
521
522 #[arg(long)]
524 pub all: bool,
525
526 #[arg(long)]
528 pub storage_collision: bool,
529
530 #[arg(long)]
532 pub uups: bool,
533}
534
535#[derive(Debug, clap::Args)]
536pub struct PluginArgs {
537 #[command(flatten)]
538 pub shared: SharedFlags,
539
540 #[command(subcommand)]
542 pub action: Option<PluginAction>,
543}
544
545#[derive(Debug, Subcommand)]
546pub enum PluginAction {
547 List,
549 Install {
551 name: String,
552 source: Option<String>,
553 },
554 Remove { name: String },
556 Enable { name: String },
558 Disable { name: String },
560 New { name: String },
562}
563
564#[derive(Debug, clap::Args)]
565pub struct ChainArgs {
566 #[command(flatten)]
567 pub shared: SharedFlags,
568
569 #[command(subcommand)]
571 pub action: Option<ChainAction>,
572}
573
574#[derive(Debug, Subcommand)]
575pub enum ChainAction {
576 List,
578 Info { chain: String },
580 Add {
582 name: String,
583 rpc_url: Option<String>,
584 chain_id: Option<u64>,
585 },
586 Remove { name: String },
588 Test {
590 chain: String,
591 rpc_url: Option<String>,
592 },
593}
594
595#[derive(Debug, clap::Args)]
596pub struct SecurityArgs {
597 #[command(flatten)]
598 pub shared: SharedFlags,
599
600 #[command(subcommand)]
602 pub action: Option<SecurityAction>,
603}
604
605#[derive(Debug, Subcommand)]
606pub enum SecurityAction {
607 Config,
609 Threshold { score: u8 },
611 Enable { check: String },
613 Disable { check: String },
615 List,
617 Info { check: String },
619}
620
621#[cfg(test)]
622mod tests {
623 use super::*;
624 use clap::Parser;
625
626 #[test]
627 fn test_cli_parse_audit() {
628 let cli = Cli::try_parse_from(["forge-guard", "audit"]).unwrap();
629 assert!(matches!(cli.command, Commands::Audit(_)));
630 }
631
632 #[test]
633 fn test_cli_parse_deploy() {
634 let cli = Cli::try_parse_from(["forge-guard", "deploy"]).unwrap();
635 assert!(matches!(cli.command, Commands::Deploy(_)));
636 }
637
638 #[test]
639 fn test_cli_parse_deploy_safe() {
640 let cli = Cli::try_parse_from(["forge-guard", "deploy-safe"]).unwrap();
641 assert!(matches!(cli.command, Commands::DeploySafe(_)));
642 }
643
644 #[test]
645 fn test_cli_parse_fuzz() {
646 let cli = Cli::try_parse_from(["forge-guard", "fuzz"]).unwrap();
647 assert!(matches!(cli.command, Commands::Fuzz(_)));
648 }
649
650 #[test]
651 fn test_cli_parse_invariant() {
652 let cli = Cli::try_parse_from(["forge-guard", "invariant"]).unwrap();
653 assert!(matches!(cli.command, Commands::Invariant(_)));
654 }
655
656 #[test]
657 fn test_cli_parse_simulate() {
658 let cli = Cli::try_parse_from(["forge-guard", "simulate"]).unwrap();
659 assert!(matches!(cli.command, Commands::Simulate(_)));
660 }
661
662 #[test]
663 fn test_cli_parse_gas() {
664 let cli = Cli::try_parse_from(["forge-guard", "gas"]).unwrap();
665 assert!(matches!(cli.command, Commands::Gas(_)));
666 }
667
668 #[test]
669 fn test_cli_parse_report() {
670 let cli = Cli::try_parse_from(["forge-guard", "report"]).unwrap();
671 assert!(matches!(cli.command, Commands::Report(_)));
672 }
673
674 #[test]
675 fn test_cli_parse_verify() {
676 let cli = Cli::try_parse_from(["forge-guard", "verify"]).unwrap();
677 assert!(matches!(cli.command, Commands::Verify(_)));
678 }
679
680 #[test]
681 fn test_cli_parse_doctor() {
682 let cli = Cli::try_parse_from(["forge-guard", "doctor"]).unwrap();
683 assert!(matches!(cli.command, Commands::Doctor(_)));
684 }
685
686 #[test]
687 fn test_cli_parse_watch() {
688 let cli = Cli::try_parse_from(["forge-guard", "watch"]).unwrap();
689 assert!(matches!(cli.command, Commands::Watch(_)));
690 }
691
692 #[test]
693 fn test_cli_parse_ci() {
694 let cli = Cli::try_parse_from(["forge-guard", "ci"]).unwrap();
695 assert!(matches!(cli.command, Commands::Ci(_)));
696 }
697
698 #[test]
699 fn test_cli_parse_benchmark() {
700 let cli = Cli::try_parse_from(["forge-guard", "benchmark"]).unwrap();
701 assert!(matches!(cli.command, Commands::Benchmark(_)));
702 }
703
704 #[test]
705 fn test_cli_parse_scan() {
706 let cli = Cli::try_parse_from(["forge-guard", "scan"]).unwrap();
707 assert!(matches!(cli.command, Commands::Scan(_)));
708 }
709
710 #[test]
711 fn test_cli_parse_upgrade_check() {
712 let cli = Cli::try_parse_from(["forge-guard", "upgrade-check"]).unwrap();
713 assert!(matches!(cli.command, Commands::UpgradeCheck(_)));
714 }
715
716 #[test]
717 fn test_cli_parse_plugins() {
718 let cli = Cli::try_parse_from(["forge-guard", "plugins"]).unwrap();
719 assert!(matches!(cli.command, Commands::Plugins(_)));
720 }
721
722 #[test]
723 fn test_cli_parse_chain() {
724 let cli = Cli::try_parse_from(["forge-guard", "chain"]).unwrap();
725 assert!(matches!(cli.command, Commands::Chain(_)));
726 }
727
728 #[test]
729 fn test_cli_parse_security() {
730 let cli = Cli::try_parse_from(["forge-guard", "security"]).unwrap();
731 assert!(matches!(cli.command, Commands::Security(_)));
732 }
733
734 #[test]
735 fn test_shared_flags_defaults() {
736 let cli = Cli::try_parse_from(["forge-guard", "audit"]).unwrap();
737 if let Commands::Audit(args) = cli.command {
738 assert_eq!(args.shared.chain, "ethereum");
739 assert!(!args.shared.json);
740 assert!(!args.shared.markdown);
741 assert!(!args.shared.strict);
742 assert!(!args.shared.offline);
743 assert!(!args.shared.production);
744 assert!(!args.shared.report);
745 assert_eq!(args.shared.parallelism, 4);
746 } else {
747 panic!("Expected Audit command");
748 }
749 }
750
751 #[test]
752 fn test_shared_flags_custom() {
753 let cli = Cli::try_parse_from([
754 "forge-guard",
755 "audit",
756 "--chain",
757 "base",
758 "--json",
759 "--strict",
760 "--offline",
761 "--production",
762 "--parallelism",
763 "8",
764 "--project",
765 "/tmp/project",
766 ])
767 .unwrap();
768 if let Commands::Audit(args) = cli.command {
769 assert_eq!(args.shared.chain, "base");
770 assert!(args.shared.json);
771 assert!(args.shared.strict);
772 assert!(args.shared.offline);
773 assert!(args.shared.production);
774 assert_eq!(args.shared.parallelism, 8);
775 assert_eq!(
776 args.shared.project,
777 std::path::PathBuf::from("/tmp/project")
778 );
779 } else {
780 panic!("Expected Audit command");
781 }
782 }
783
784 #[test]
785 fn test_audit_args_defaults() {
786 let cli = Cli::try_parse_from(["forge-guard", "audit"]).unwrap();
787 if let Commands::Audit(args) = cli.command {
788 assert!(!args.full);
789 assert!(!args.quick);
790 assert!(!args.summary);
791 assert!(!args.ai);
792 assert_eq!(args.ai_provider, "openai");
793 assert_eq!(args.ai_model, "gpt-5");
794 assert!(args.ai_api_key.is_none());
795 assert!(!args.exploit);
796 assert!(!args.gas);
797 assert!(!args.all_chains);
798 assert_eq!(args.sources, "src");
799 assert!(args.exclude.is_none());
800 } else {
801 panic!("Expected Audit command");
802 }
803 }
804
805 #[test]
806 fn test_audit_args_full() {
807 let cli = Cli::try_parse_from([
808 "forge-guard",
809 "audit",
810 "--full",
811 "--chain",
812 "arbitrum",
813 "--json",
814 "--report",
815 "--exploit",
816 "--gas",
817 ])
818 .unwrap();
819 if let Commands::Audit(args) = cli.command {
820 assert!(args.full);
821 assert!(args.exploit);
822 assert!(args.gas);
823 assert!(args.shared.json);
824 assert!(args.shared.report);
825 assert_eq!(args.shared.chain, "arbitrum");
826 } else {
827 panic!("Expected Audit command");
828 }
829 }
830
831 #[test]
832 fn test_audit_ai_args() {
833 let cli = Cli::try_parse_from([
834 "forge-guard",
835 "audit",
836 "--ai",
837 "--ai-provider",
838 "claude",
839 "--ai-model",
840 "claude-5-sonnet-20260701",
841 "--ai-full",
842 ])
843 .unwrap();
844 if let Commands::Audit(args) = cli.command {
845 assert!(args.ai);
846 assert_eq!(args.ai_provider, "claude");
847 assert_eq!(args.ai_model, "claude-5-sonnet-20260701");
848 assert!(args.ai_full);
849 } else {
850 panic!("Expected Audit command");
851 }
852 }
853
854 #[test]
855 fn test_deploy_args() {
856 let cli = Cli::try_parse_from([
857 "forge-guard",
858 "deploy",
859 "MyContract",
860 "--force",
861 "--salt",
862 "0xabc",
863 "--verify",
864 "--args",
865 "arg1,arg2",
866 ])
867 .unwrap();
868 if let Commands::Deploy(args) = cli.command {
869 assert_eq!(args.contract.as_deref(), Some("MyContract"));
870 assert!(args.force);
871 assert_eq!(args.salt.as_deref(), Some("0xabc"));
872 assert!(args.verify);
873 assert_eq!(args.args.as_deref(), Some("arg1,arg2"));
874 } else {
875 panic!("Expected Deploy command");
876 }
877 }
878
879 #[test]
880 fn test_fuzz_args() {
881 let cli = Cli::try_parse_from([
882 "forge-guard",
883 "fuzz",
884 "--runs",
885 "50000",
886 "--seed",
887 "42",
888 "--test",
889 "testFuzzDeposit",
890 "Vault",
891 ])
892 .unwrap();
893 if let Commands::Fuzz(args) = cli.command {
894 assert_eq!(args.runs, 50_000);
895 assert_eq!(args.seed, Some(42));
896 assert_eq!(args.test.as_deref(), Some("testFuzzDeposit"));
897 assert_eq!(args.contract.as_deref(), Some("Vault"));
898 } else {
899 panic!("Expected Fuzz command");
900 }
901 }
902
903 #[test]
904 fn test_simulate_args() {
905 let cli = Cli::try_parse_from([
906 "forge-guard",
907 "simulate",
908 "MyContract",
909 "--blocks",
910 "200",
911 "--mev",
912 ])
913 .unwrap();
914 if let Commands::Simulate(args) = cli.command {
915 assert_eq!(args.contract.as_deref(), Some("MyContract"));
916 assert_eq!(args.blocks, 200);
917 assert!(args.mev);
918 } else {
919 panic!("Expected Simulate command");
920 }
921 }
922
923 #[test]
924 fn test_invariant_args() {
925 let cli = Cli::try_parse_from([
926 "forge-guard",
927 "invariant",
928 "--runs",
929 "2000",
930 "--depth",
931 "150",
932 "--fail-on-revert",
933 ])
934 .unwrap();
935 if let Commands::Invariant(args) = cli.command {
936 assert_eq!(args.runs, 2000);
937 assert_eq!(args.depth, 150);
938 assert!(args.fail_on_revert);
939 } else {
940 panic!("Expected Invariant command");
941 }
942 }
943
944 #[test]
945 fn test_ci_args_defaults() {
946 let cli = Cli::try_parse_from(["forge-guard", "ci"]).unwrap();
947 if let Commands::Ci(args) = cli.command {
948 assert_eq!(args.platform, "github");
949 assert_eq!(args.output, std::path::PathBuf::from(".github/workflows"));
950 assert!(!args.include_deploy);
951 assert!(!args.overwrite);
952 } else {
953 panic!("Expected Ci command");
954 }
955 }
956
957 #[test]
958 fn test_ci_args_custom() {
959 let cli = Cli::try_parse_from([
960 "forge-guard",
961 "ci",
962 "--platform",
963 "gitlab",
964 "--include-deploy",
965 "--overwrite",
966 "--output",
967 ".gitlab",
968 ])
969 .unwrap();
970 if let Commands::Ci(args) = cli.command {
971 assert_eq!(args.platform, "gitlab");
972 assert!(args.include_deploy);
973 assert!(args.overwrite);
974 assert_eq!(args.output, std::path::PathBuf::from(".gitlab"));
975 } else {
976 panic!("Expected Ci command");
977 }
978 }
979
980 #[test]
981 fn test_benchmark_args() {
982 let cli = Cli::try_parse_from([
983 "forge-guard",
984 "benchmark",
985 "--iterations",
986 "50",
987 "--warmup",
988 "5",
989 "--module",
990 "pattern_matching",
991 ])
992 .unwrap();
993 if let Commands::Benchmark(args) = cli.command {
994 assert_eq!(args.iterations, 50);
995 assert_eq!(args.warmup, 5);
996 assert_eq!(args.module.as_deref(), Some("pattern_matching"));
997 } else {
998 panic!("Expected Benchmark command");
999 }
1000 }
1001
1002 #[test]
1003 fn test_gas_args() {
1004 let cli =
1005 Cli::try_parse_from(["forge-guard", "gas", "--all", "--warn-threshold", "100000"])
1006 .unwrap();
1007 if let Commands::Gas(args) = cli.command {
1008 assert!(args.all);
1009 assert_eq!(args.warn_threshold, 100000);
1010 assert!(args.contract.is_none());
1011 } else {
1012 panic!("Expected Gas command");
1013 }
1014 }
1015
1016 #[test]
1017 fn test_scan_args() {
1018 let cli = Cli::try_parse_from([
1019 "forge-guard",
1020 "scan",
1021 "--depth",
1022 "2",
1023 "--update",
1024 "--vulnerable-only",
1025 "--fail-fast",
1026 ])
1027 .unwrap();
1028 if let Commands::Scan(args) = cli.command {
1029 assert_eq!(args.depth, 2);
1030 assert!(args.update);
1031 assert!(args.vulnerable_only);
1032 assert!(args.fail_fast);
1033 } else {
1034 panic!("Expected Scan command");
1035 }
1036 }
1037
1038 #[test]
1039 fn test_upgrade_check_args() {
1040 let cli = Cli::try_parse_from([
1041 "forge-guard",
1042 "upgrade-check",
1043 "--storage-collision",
1044 "--uups",
1045 "--all",
1046 ])
1047 .unwrap();
1048 if let Commands::UpgradeCheck(args) = cli.command {
1049 assert!(args.storage_collision);
1050 assert!(args.uups);
1051 assert!(args.all);
1052 } else {
1053 panic!("Expected UpgradeCheck command");
1054 }
1055 }
1056
1057 #[test]
1058 fn test_doctor_args() {
1059 let cli = Cli::try_parse_from(["forge-guard", "doctor", "--fix", "--verbose"]).unwrap();
1060 if let Commands::Doctor(args) = cli.command {
1061 assert!(args.fix);
1062 assert!(args.verbose);
1063 assert!(args.check.is_none());
1064 } else {
1065 panic!("Expected Doctor command");
1066 }
1067 }
1068
1069 #[test]
1070 fn test_verify_args() {
1071 let cli = Cli::try_parse_from([
1072 "forge-guard",
1073 "verify",
1074 "0x1234",
1075 "MyContract",
1076 "--api-key",
1077 "test-key",
1078 "--all",
1079 ])
1080 .unwrap();
1081 if let Commands::Verify(args) = cli.command {
1082 assert_eq!(args.address.as_deref(), Some("0x1234"));
1083 assert_eq!(args.name.as_deref(), Some("MyContract"));
1084 assert_eq!(args.api_key.as_deref(), Some("test-key"));
1085 assert!(args.all);
1086 } else {
1087 panic!("Expected Verify command");
1088 }
1089 }
1090
1091 #[test]
1092 fn test_watch_args() {
1093 let cli = Cli::try_parse_from([
1094 "forge-guard",
1095 "watch",
1096 "--dirs",
1097 "src,test-contracts",
1098 "--debounce-ms",
1099 "1000",
1100 "--full",
1101 ])
1102 .unwrap();
1103 if let Commands::Watch(args) = cli.command {
1104 assert_eq!(args.dirs, "src,test-contracts");
1105 assert_eq!(args.debounce_ms, 1000);
1106 assert!(args.full);
1107 } else {
1108 panic!("Expected Watch command");
1109 }
1110 }
1111
1112 #[test]
1113 fn test_plugins_list() {
1114 let cli = Cli::try_parse_from(["forge-guard", "plugins", "list"]).unwrap();
1115 if let Commands::Plugins(args) = cli.command {
1116 assert!(matches!(args.action, Some(PluginAction::List)));
1117 } else {
1118 panic!("Expected Plugins command");
1119 }
1120 }
1121
1122 #[test]
1123 fn test_plugins_install() {
1124 let cli = Cli::try_parse_from(["forge-guard", "plugins", "install", "my-plugin"]).unwrap();
1125 if let Commands::Plugins(args) = cli.command {
1126 assert!(matches!(args.action, Some(PluginAction::Install { .. })));
1127 } else {
1128 panic!("Expected Plugins command");
1129 }
1130 }
1131
1132 #[test]
1133 fn test_plugins_remove() {
1134 let cli = Cli::try_parse_from(["forge-guard", "plugins", "remove", "bad-plugin"]).unwrap();
1135 if let Commands::Plugins(args) = cli.command {
1136 assert!(matches!(args.action, Some(PluginAction::Remove { .. })));
1137 } else {
1138 panic!("Expected Plugins command");
1139 }
1140 }
1141
1142 #[test]
1143 fn test_plugins_enable() {
1144 let cli = Cli::try_parse_from(["forge-guard", "plugins", "enable", "my-plugin"]).unwrap();
1145 if let Commands::Plugins(args) = cli.command {
1146 assert!(matches!(args.action, Some(PluginAction::Enable { .. })));
1147 } else {
1148 panic!("Expected Plugins command");
1149 }
1150 }
1151
1152 #[test]
1153 fn test_plugins_disable() {
1154 let cli = Cli::try_parse_from(["forge-guard", "plugins", "disable", "my-plugin"]).unwrap();
1155 if let Commands::Plugins(args) = cli.command {
1156 assert!(matches!(args.action, Some(PluginAction::Disable { .. })));
1157 } else {
1158 panic!("Expected Plugins command");
1159 }
1160 }
1161
1162 #[test]
1163 fn test_plugins_new() {
1164 let cli =
1165 Cli::try_parse_from(["forge-guard", "plugins", "new", "my-awesome-plugin"]).unwrap();
1166 if let Commands::Plugins(args) = cli.command {
1167 assert!(matches!(args.action, Some(PluginAction::New { .. })));
1168 } else {
1169 panic!("Expected Plugins command");
1170 }
1171 }
1172
1173 #[test]
1174 fn test_chain_list() {
1175 let cli = Cli::try_parse_from(["forge-guard", "chain", "list"]).unwrap();
1176 if let Commands::Chain(args) = cli.command {
1177 assert!(matches!(args.action, Some(ChainAction::List)));
1178 } else {
1179 panic!("Expected Chain command");
1180 }
1181 }
1182
1183 #[test]
1184 fn test_chain_info() {
1185 let cli = Cli::try_parse_from(["forge-guard", "chain", "info", "base"]).unwrap();
1186 if let Commands::Chain(args) = cli.command {
1187 assert!(matches!(args.action, Some(ChainAction::Info { .. })));
1188 } else {
1189 panic!("Expected Chain command");
1190 }
1191 }
1192
1193 #[test]
1194 fn test_chain_add() {
1195 let cli = Cli::try_parse_from([
1196 "forge-guard",
1197 "chain",
1198 "add",
1199 "my-chain",
1200 "https://rpc.my-chain.io",
1201 "99999",
1202 ])
1203 .unwrap();
1204 if let Commands::Chain(args) = cli.command {
1205 assert!(matches!(args.action, Some(ChainAction::Add { .. })));
1206 } else {
1207 panic!("Expected Chain command");
1208 }
1209 }
1210
1211 #[test]
1212 fn test_security_list() {
1213 let cli = Cli::try_parse_from(["forge-guard", "security", "list"]).unwrap();
1214 if let Commands::Security(args) = cli.command {
1215 assert!(matches!(args.action, Some(SecurityAction::List)));
1216 } else {
1217 panic!("Expected Security command");
1218 }
1219 }
1220
1221 #[test]
1222 fn test_security_threshold() {
1223 let cli = Cli::try_parse_from(["forge-guard", "security", "threshold", "85"]).unwrap();
1224 if let Commands::Security(args) = cli.command {
1225 assert!(matches!(
1226 args.action,
1227 Some(SecurityAction::Threshold { .. })
1228 ));
1229 } else {
1230 panic!("Expected Security command");
1231 }
1232 }
1233
1234 #[test]
1235 fn test_markdown_flag() {
1236 let cli = Cli::try_parse_from(["forge-guard", "audit", "--markdown", "--report"]).unwrap();
1237 if let Commands::Audit(args) = cli.command {
1238 assert!(args.shared.markdown);
1239 assert!(args.shared.report);
1240 } else {
1241 panic!("Expected Audit command");
1242 }
1243 }
1244
1245 #[test]
1246 fn test_quick_audit() {
1247 let cli = Cli::try_parse_from(["forge-guard", "audit", "--quick", "--summary"]).unwrap();
1248 if let Commands::Audit(args) = cli.command {
1249 assert!(args.quick);
1250 assert!(args.summary);
1251 } else {
1252 panic!("Expected Audit command");
1253 }
1254 }
1255
1256 #[test]
1257 fn test_all_chains_flag() {
1258 let cli = Cli::try_parse_from(["forge-guard", "audit", "--all-chains"]).unwrap();
1259 if let Commands::Audit(args) = cli.command {
1260 assert!(args.all_chains);
1261 } else {
1262 panic!("Expected Audit command");
1263 }
1264 }
1265
1266 #[test]
1267 fn test_deploy_safe_args() {
1268 let cli =
1269 Cli::try_parse_from(["forge-guard", "deploy-safe", "SecureVault", "--verify"]).unwrap();
1270 if let Commands::DeploySafe(args) = cli.command {
1271 assert_eq!(args.contract.as_deref(), Some("SecureVault"));
1272 assert!(args.verify);
1273 } else {
1274 panic!("Expected DeploySafe command");
1275 }
1276 }
1277
1278 #[test]
1279 fn test_report_args_json() {
1280 let cli = Cli::try_parse_from(["forge-guard", "report", "--format", "json"]).unwrap();
1281 if let Commands::Report(args) = cli.command {
1282 assert_eq!(args.format, "json");
1283 assert!(!args.summary);
1284 } else {
1285 panic!("Expected Report command");
1286 }
1287 }
1288
1289 #[test]
1290 fn test_verify_all_without_address() {
1291 let cli = Cli::try_parse_from(["forge-guard", "verify", "--all"]).unwrap();
1292 if let Commands::Verify(args) = cli.command {
1293 assert!(args.all);
1294 assert!(args.address.is_none());
1295 } else {
1296 panic!("Expected Verify command");
1297 }
1298 }
1299
1300 #[test]
1301 fn test_report_summary() {
1302 let cli = Cli::try_parse_from(["forge-guard", "report", "--summary"]).unwrap();
1303 if let Commands::Report(args) = cli.command {
1304 assert!(args.summary);
1305 } else {
1306 panic!("Expected Report command");
1307 }
1308 }
1309
1310 #[test]
1311 fn test_scan_vulnerable_only() {
1312 let cli = Cli::try_parse_from(["forge-guard", "scan", "--vulnerable-only"]).unwrap();
1313 if let Commands::Scan(args) = cli.command {
1314 assert!(args.vulnerable_only);
1315 } else {
1316 panic!("Expected Scan command");
1317 }
1318 }
1319
1320 #[test]
1321 fn test_doctor_check_category() {
1322 let cli =
1323 Cli::try_parse_from(["forge-guard", "doctor", "--check", "dependencies"]).unwrap();
1324 if let Commands::Doctor(args) = cli.command {
1325 assert_eq!(args.check.as_deref(), Some("dependencies"));
1326 } else {
1327 panic!("Expected Doctor command");
1328 }
1329 }
1330
1331 #[test]
1332 fn test_gas_contract_specific() {
1333 let cli =
1334 Cli::try_parse_from(["forge-guard", "gas", "Vault", "--diff", "prev.json"]).unwrap();
1335 if let Commands::Gas(args) = cli.command {
1336 assert_eq!(args.contract.as_deref(), Some("Vault"));
1337 assert_eq!(args.diff.as_deref(), Some("prev.json"));
1338 } else {
1339 panic!("Expected Gas command");
1340 }
1341 }
1342
1343 #[test]
1344 fn test_verify_with_constructor_args() {
1345 let cli = Cli::try_parse_from([
1346 "forge-guard",
1347 "verify",
1348 "0xabc",
1349 "Token",
1350 "--constructor-args",
1351 "0x0001",
1352 ])
1353 .unwrap();
1354 if let Commands::Verify(args) = cli.command {
1355 assert_eq!(args.constructor_args.as_deref(), Some("0x0001"));
1356 } else {
1357 panic!("Expected Verify command");
1358 }
1359 }
1360
1361 #[test]
1362 fn test_upgrade_check_proxy() {
1363 let cli =
1364 Cli::try_parse_from(["forge-guard", "upgrade-check", "0xproxy", "0ximpl"]).unwrap();
1365 if let Commands::UpgradeCheck(args) = cli.command {
1366 assert_eq!(args.proxy.as_deref(), Some("0xproxy"));
1367 assert_eq!(args.implementation.as_deref(), Some("0ximpl"));
1368 } else {
1369 panic!("Expected UpgradeCheck command");
1370 }
1371 }
1372
1373 #[test]
1374 fn test_benchmark_save_and_compare() {
1375 let cli = Cli::try_parse_from([
1376 "forge-guard",
1377 "benchmark",
1378 "--save",
1379 "results.json",
1380 "--compare",
1381 "baseline.json",
1382 ])
1383 .unwrap();
1384 if let Commands::Benchmark(args) = cli.command {
1385 assert_eq!(
1386 args.save.as_deref(),
1387 Some(std::path::Path::new("results.json"))
1388 );
1389 assert_eq!(
1390 args.compare.as_deref(),
1391 Some(std::path::Path::new("baseline.json"))
1392 );
1393 } else {
1394 panic!("Expected Benchmark command");
1395 }
1396 }
1397
1398 #[test]
1399 fn test_watch_exclude() {
1400 let cli = Cli::try_parse_from(["forge-guard", "watch", "--exclude", "*.test.sol"]).unwrap();
1401 if let Commands::Watch(args) = cli.command {
1402 assert_eq!(args.exclude.as_deref(), Some("*.test.sol"));
1403 } else {
1404 panic!("Expected Watch command");
1405 }
1406 }
1407
1408 #[test]
1409 fn test_simulate_deployer() {
1410 let cli =
1411 Cli::try_parse_from(["forge-guard", "simulate", "--deployer", "0xdeployer"]).unwrap();
1412 if let Commands::Simulate(args) = cli.command {
1413 assert_eq!(args.deployer.as_deref(), Some("0xdeployer"));
1414 } else {
1415 panic!("Expected Simulate command");
1416 }
1417 }
1418
1419 #[test]
1420 fn test_security_disable_check() {
1421 let cli = Cli::try_parse_from(["forge-guard", "security", "disable", "FA-H-001"]).unwrap();
1422 if let Commands::Security(args) = cli.command {
1423 assert!(matches!(args.action, Some(SecurityAction::Disable { .. })));
1424 } else {
1425 panic!("Expected Security command");
1426 }
1427 }
1428
1429 #[test]
1430 fn test_from_env_try_parse() {
1431 let cli = Cli::try_parse_from(["forge-guard", "audit", "--report", "--json"]).unwrap();
1434 assert!(matches!(cli.command, Commands::Audit(_)));
1435 if let Commands::Audit(args) = cli.command {
1436 assert!(args.shared.report);
1437 assert!(args.shared.json);
1438 }
1439 }
1440}