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