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, 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!(args.shared.project, std::path::PathBuf::from("/tmp/project"));
772 } else {
773 panic!("Expected Audit command");
774 }
775 }
776
777 #[test]
778 fn test_audit_args_defaults() {
779 let cli = Cli::try_parse_from(["forge-guard", "audit"]).unwrap();
780 if let Commands::Audit(args) = cli.command {
781 assert!(!args.full);
782 assert!(!args.quick);
783 assert!(!args.summary);
784 assert!(!args.ai);
785 assert_eq!(args.ai_provider, "openai");
786 assert_eq!(args.ai_model, "gpt-5");
787 assert!(args.ai_api_key.is_none());
788 assert!(!args.exploit);
789 assert!(!args.gas);
790 assert!(!args.all_chains);
791 assert_eq!(args.sources, "src");
792 assert!(args.exclude.is_none());
793 } else {
794 panic!("Expected Audit command");
795 }
796 }
797
798 #[test]
799 fn test_audit_args_full() {
800 let cli = Cli::try_parse_from([
801 "forge-guard",
802 "audit",
803 "--full",
804 "--chain",
805 "arbitrum",
806 "--json",
807 "--report",
808 "--exploit",
809 "--gas",
810 ])
811 .unwrap();
812 if let Commands::Audit(args) = cli.command {
813 assert!(args.full);
814 assert!(args.exploit);
815 assert!(args.gas);
816 assert!(args.shared.json);
817 assert!(args.shared.report);
818 assert_eq!(args.shared.chain, "arbitrum");
819 } else {
820 panic!("Expected Audit command");
821 }
822 }
823
824 #[test]
825 fn test_audit_ai_args() {
826 let cli = Cli::try_parse_from([
827 "forge-guard",
828 "audit",
829 "--ai",
830 "--ai-provider",
831 "claude",
832 "--ai-model",
833 "claude-5-sonnet-20260701",
834 "--ai-full",
835 ])
836 .unwrap();
837 if let Commands::Audit(args) = cli.command {
838 assert!(args.ai);
839 assert_eq!(args.ai_provider, "claude");
840 assert_eq!(args.ai_model, "claude-5-sonnet-20260701");
841 assert!(args.ai_full);
842 } else {
843 panic!("Expected Audit command");
844 }
845 }
846
847 #[test]
848 fn test_deploy_args() {
849 let cli = Cli::try_parse_from([
850 "forge-guard",
851 "deploy",
852 "MyContract",
853 "--force",
854 "--salt",
855 "0xabc",
856 "--verify",
857 "--args",
858 "arg1,arg2",
859 ])
860 .unwrap();
861 if let Commands::Deploy(args) = cli.command {
862 assert_eq!(args.contract.as_deref(), Some("MyContract"));
863 assert!(args.force);
864 assert_eq!(args.salt.as_deref(), Some("0xabc"));
865 assert!(args.verify);
866 assert_eq!(args.args.as_deref(), Some("arg1,arg2"));
867 } else {
868 panic!("Expected Deploy command");
869 }
870 }
871
872 #[test]
873 fn test_fuzz_args() {
874 let cli = Cli::try_parse_from([
875 "forge-guard",
876 "fuzz",
877 "--runs",
878 "50000",
879 "--seed",
880 "42",
881 "--test",
882 "testFuzzDeposit",
883 "Vault",
884 ])
885 .unwrap();
886 if let Commands::Fuzz(args) = cli.command {
887 assert_eq!(args.runs, 50_000);
888 assert_eq!(args.seed, Some(42));
889 assert_eq!(args.test.as_deref(), Some("testFuzzDeposit"));
890 assert_eq!(args.contract.as_deref(), Some("Vault"));
891 } else {
892 panic!("Expected Fuzz command");
893 }
894 }
895
896 #[test]
897 fn test_simulate_args() {
898 let cli = Cli::try_parse_from([
899 "forge-guard",
900 "simulate",
901 "MyContract",
902 "--blocks",
903 "200",
904 "--mev",
905 ])
906 .unwrap();
907 if let Commands::Simulate(args) = cli.command {
908 assert_eq!(args.contract.as_deref(), Some("MyContract"));
909 assert_eq!(args.blocks, 200);
910 assert!(args.mev);
911 } else {
912 panic!("Expected Simulate command");
913 }
914 }
915
916 #[test]
917 fn test_invariant_args() {
918 let cli = Cli::try_parse_from([
919 "forge-guard",
920 "invariant",
921 "--runs",
922 "2000",
923 "--depth",
924 "150",
925 "--fail-on-revert",
926 ])
927 .unwrap();
928 if let Commands::Invariant(args) = cli.command {
929 assert_eq!(args.runs, 2000);
930 assert_eq!(args.depth, 150);
931 assert!(args.fail_on_revert);
932 } else {
933 panic!("Expected Invariant command");
934 }
935 }
936
937 #[test]
938 fn test_ci_args_defaults() {
939 let cli = Cli::try_parse_from(["forge-guard", "ci"]).unwrap();
940 if let Commands::Ci(args) = cli.command {
941 assert_eq!(args.platform, "github");
942 assert_eq!(args.output, std::path::PathBuf::from(".github/workflows"));
943 assert!(!args.include_deploy);
944 assert!(!args.overwrite);
945 } else {
946 panic!("Expected Ci command");
947 }
948 }
949
950 #[test]
951 fn test_ci_args_custom() {
952 let cli = Cli::try_parse_from([
953 "forge-guard",
954 "ci",
955 "--platform",
956 "gitlab",
957 "--include-deploy",
958 "--overwrite",
959 "--output",
960 ".gitlab",
961 ])
962 .unwrap();
963 if let Commands::Ci(args) = cli.command {
964 assert_eq!(args.platform, "gitlab");
965 assert!(args.include_deploy);
966 assert!(args.overwrite);
967 assert_eq!(args.output, std::path::PathBuf::from(".gitlab"));
968 } else {
969 panic!("Expected Ci command");
970 }
971 }
972
973 #[test]
974 fn test_benchmark_args() {
975 let cli = Cli::try_parse_from([
976 "forge-guard",
977 "benchmark",
978 "--iterations",
979 "50",
980 "--warmup",
981 "5",
982 "--module",
983 "pattern_matching",
984 ])
985 .unwrap();
986 if let Commands::Benchmark(args) = cli.command {
987 assert_eq!(args.iterations, 50);
988 assert_eq!(args.warmup, 5);
989 assert_eq!(args.module.as_deref(), Some("pattern_matching"));
990 } else {
991 panic!("Expected Benchmark command");
992 }
993 }
994
995 #[test]
996 fn test_gas_args() {
997 let cli = Cli::try_parse_from([
998 "forge-guard",
999 "gas",
1000 "--all",
1001 "--warn-threshold",
1002 "100000",
1003 ])
1004 .unwrap();
1005 if let Commands::Gas(args) = cli.command {
1006 assert!(args.all);
1007 assert_eq!(args.warn_threshold, 100000);
1008 assert!(args.contract.is_none());
1009 } else {
1010 panic!("Expected Gas command");
1011 }
1012 }
1013
1014 #[test]
1015 fn test_scan_args() {
1016 let cli = Cli::try_parse_from([
1017 "forge-guard",
1018 "scan",
1019 "--depth",
1020 "2",
1021 "--update",
1022 "--vulnerable-only",
1023 "--fail-fast",
1024 ])
1025 .unwrap();
1026 if let Commands::Scan(args) = cli.command {
1027 assert_eq!(args.depth, 2);
1028 assert!(args.update);
1029 assert!(args.vulnerable_only);
1030 assert!(args.fail_fast);
1031 } else {
1032 panic!("Expected Scan command");
1033 }
1034 }
1035
1036 #[test]
1037 fn test_upgrade_check_args() {
1038 let cli = Cli::try_parse_from([
1039 "forge-guard",
1040 "upgrade-check",
1041 "--storage-collision",
1042 "--uups",
1043 "--all",
1044 ])
1045 .unwrap();
1046 if let Commands::UpgradeCheck(args) = cli.command {
1047 assert!(args.storage_collision);
1048 assert!(args.uups);
1049 assert!(args.all);
1050 } else {
1051 panic!("Expected UpgradeCheck command");
1052 }
1053 }
1054
1055 #[test]
1056 fn test_doctor_args() {
1057 let cli = Cli::try_parse_from(["forge-guard", "doctor", "--fix", "--verbose"]).unwrap();
1058 if let Commands::Doctor(args) = cli.command {
1059 assert!(args.fix);
1060 assert!(args.verbose);
1061 assert!(args.check.is_none());
1062 } else {
1063 panic!("Expected Doctor command");
1064 }
1065 }
1066
1067 #[test]
1068 fn test_verify_args() {
1069 let cli = Cli::try_parse_from([
1070 "forge-guard",
1071 "verify",
1072 "0x1234",
1073 "MyContract",
1074 "--api-key",
1075 "test-key",
1076 "--all",
1077 ])
1078 .unwrap();
1079 if let Commands::Verify(args) = cli.command {
1080 assert_eq!(args.address.as_deref(), Some("0x1234"));
1081 assert_eq!(args.name.as_deref(), Some("MyContract"));
1082 assert_eq!(args.api_key.as_deref(), Some("test-key"));
1083 assert!(args.all);
1084 } else {
1085 panic!("Expected Verify command");
1086 }
1087 }
1088
1089 #[test]
1090 fn test_watch_args() {
1091 let cli = Cli::try_parse_from([
1092 "forge-guard",
1093 "watch",
1094 "--dirs",
1095 "src,test-contracts",
1096 "--debounce-ms",
1097 "1000",
1098 "--full",
1099 ])
1100 .unwrap();
1101 if let Commands::Watch(args) = cli.command {
1102 assert_eq!(args.dirs, "src,test-contracts");
1103 assert_eq!(args.debounce_ms, 1000);
1104 assert!(args.full);
1105 } else {
1106 panic!("Expected Watch command");
1107 }
1108 }
1109
1110 #[test]
1111 fn test_plugins_list() {
1112 let cli = Cli::try_parse_from(["forge-guard", "plugins", "list"]).unwrap();
1113 if let Commands::Plugins(args) = cli.command {
1114 assert!(matches!(args.action, Some(PluginAction::List)));
1115 } else {
1116 panic!("Expected Plugins command");
1117 }
1118 }
1119
1120 #[test]
1121 fn test_plugins_install() {
1122 let cli =
1123 Cli::try_parse_from(["forge-guard", "plugins", "install", "my-plugin"]).unwrap();
1124 if let Commands::Plugins(args) = cli.command {
1125 assert!(matches!(
1126 args.action,
1127 Some(PluginAction::Install { .. })
1128 ));
1129 } else {
1130 panic!("Expected Plugins command");
1131 }
1132 }
1133
1134 #[test]
1135 fn test_plugins_remove() {
1136 let cli =
1137 Cli::try_parse_from(["forge-guard", "plugins", "remove", "bad-plugin"]).unwrap();
1138 if let Commands::Plugins(args) = cli.command {
1139 assert!(matches!(
1140 args.action,
1141 Some(PluginAction::Remove { .. })
1142 ));
1143 } else {
1144 panic!("Expected Plugins command");
1145 }
1146 }
1147
1148 #[test]
1149 fn test_plugins_enable() {
1150 let cli =
1151 Cli::try_parse_from(["forge-guard", "plugins", "enable", "my-plugin"]).unwrap();
1152 if let Commands::Plugins(args) = cli.command {
1153 assert!(matches!(
1154 args.action,
1155 Some(PluginAction::Enable { .. })
1156 ));
1157 } else {
1158 panic!("Expected Plugins command");
1159 }
1160 }
1161
1162 #[test]
1163 fn test_plugins_disable() {
1164 let cli =
1165 Cli::try_parse_from(["forge-guard", "plugins", "disable", "my-plugin"]).unwrap();
1166 if let Commands::Plugins(args) = cli.command {
1167 assert!(matches!(
1168 args.action,
1169 Some(PluginAction::Disable { .. })
1170 ));
1171 } else {
1172 panic!("Expected Plugins command");
1173 }
1174 }
1175
1176 #[test]
1177 fn test_plugins_new() {
1178 let cli =
1179 Cli::try_parse_from(["forge-guard", "plugins", "new", "my-awesome-plugin"]).unwrap();
1180 if let Commands::Plugins(args) = cli.command {
1181 assert!(matches!(args.action, Some(PluginAction::New { .. })));
1182 } else {
1183 panic!("Expected Plugins command");
1184 }
1185 }
1186
1187 #[test]
1188 fn test_chain_list() {
1189 let cli = Cli::try_parse_from(["forge-guard", "chain", "list"]).unwrap();
1190 if let Commands::Chain(args) = cli.command {
1191 assert!(matches!(args.action, Some(ChainAction::List)));
1192 } else {
1193 panic!("Expected Chain command");
1194 }
1195 }
1196
1197 #[test]
1198 fn test_chain_info() {
1199 let cli = Cli::try_parse_from(["forge-guard", "chain", "info", "base"]).unwrap();
1200 if let Commands::Chain(args) = cli.command {
1201 assert!(matches!(args.action, Some(ChainAction::Info { .. })));
1202 } else {
1203 panic!("Expected Chain command");
1204 }
1205 }
1206
1207 #[test]
1208 fn test_chain_add() {
1209 let cli = Cli::try_parse_from([
1210 "forge-guard",
1211 "chain",
1212 "add",
1213 "my-chain",
1214 "--rpc-url",
1215 "https://rpc.my-chain.io",
1216 "--chain-id",
1217 "99999",
1218 ])
1219 .unwrap();
1220 if let Commands::Chain(args) = cli.command {
1221 assert!(matches!(args.action, Some(ChainAction::Add { .. })));
1222 } else {
1223 panic!("Expected Chain command");
1224 }
1225 }
1226
1227 #[test]
1228 fn test_security_list() {
1229 let cli = Cli::try_parse_from(["forge-guard", "security", "list"]).unwrap();
1230 if let Commands::Security(args) = cli.command {
1231 assert!(matches!(args.action, Some(SecurityAction::List)));
1232 } else {
1233 panic!("Expected Security command");
1234 }
1235 }
1236
1237 #[test]
1238 fn test_security_threshold() {
1239 let cli = Cli::try_parse_from(["forge-guard", "security", "threshold", "85"]).unwrap();
1240 if let Commands::Security(args) = cli.command {
1241 assert!(matches!(args.action, Some(SecurityAction::Threshold { .. })));
1242 } else {
1243 panic!("Expected Security command");
1244 }
1245 }
1246
1247 #[test]
1248 fn test_markdown_flag() {
1249 let cli = Cli::try_parse_from(["forge-guard", "audit", "--markdown", "--report"]).unwrap();
1250 if let Commands::Audit(args) = cli.command {
1251 assert!(args.shared.markdown);
1252 assert!(args.shared.report);
1253 } else {
1254 panic!("Expected Audit command");
1255 }
1256 }
1257
1258 #[test]
1259 fn test_quick_audit() {
1260 let cli = Cli::try_parse_from(["forge-guard", "audit", "--quick", "--summary"]).unwrap();
1261 if let Commands::Audit(args) = cli.command {
1262 assert!(args.quick);
1263 assert!(args.summary);
1264 } else {
1265 panic!("Expected Audit command");
1266 }
1267 }
1268
1269 #[test]
1270 fn test_all_chains_flag() {
1271 let cli =
1272 Cli::try_parse_from(["forge-guard", "audit", "--all-chains"]).unwrap();
1273 if let Commands::Audit(args) = cli.command {
1274 assert!(args.all_chains);
1275 } else {
1276 panic!("Expected Audit command");
1277 }
1278 }
1279
1280 #[test]
1281 fn test_deploy_safe_args() {
1282 let cli = Cli::try_parse_from([
1283 "forge-guard",
1284 "deploy-safe",
1285 "SecureVault",
1286 "--verify",
1287 ])
1288 .unwrap();
1289 if let Commands::DeploySafe(args) = cli.command {
1290 assert_eq!(args.contract.as_deref(), Some("SecureVault"));
1291 assert!(args.verify);
1292 } else {
1293 panic!("Expected DeploySafe command");
1294 }
1295 }
1296
1297 #[test]
1298 fn test_report_args_json() {
1299 let cli = Cli::try_parse_from(["forge-guard", "report", "--format", "json"]).unwrap();
1300 if let Commands::Report(args) = cli.command {
1301 assert_eq!(args.format, "json");
1302 assert!(!args.summary);
1303 } else {
1304 panic!("Expected Report command");
1305 }
1306 }
1307
1308 #[test]
1309 fn test_verify_all_without_address() {
1310 let cli = Cli::try_parse_from(["forge-guard", "verify", "--all"]).unwrap();
1311 if let Commands::Verify(args) = cli.command {
1312 assert!(args.all);
1313 assert!(args.address.is_none());
1314 } else {
1315 panic!("Expected Verify command");
1316 }
1317 }
1318
1319 #[test]
1320 fn test_report_summary() {
1321 let cli = Cli::try_parse_from(["forge-guard", "report", "--summary"]).unwrap();
1322 if let Commands::Report(args) = cli.command {
1323 assert!(args.summary);
1324 } else {
1325 panic!("Expected Report command");
1326 }
1327 }
1328
1329 #[test]
1330 fn test_scan_vulnerable_only() {
1331 let cli =
1332 Cli::try_parse_from(["forge-guard", "scan", "--vulnerable-only"]).unwrap();
1333 if let Commands::Scan(args) = cli.command {
1334 assert!(args.vulnerable_only);
1335 } else {
1336 panic!("Expected Scan command");
1337 }
1338 }
1339
1340 #[test]
1341 fn test_doctor_check_category() {
1342 let cli =
1343 Cli::try_parse_from(["forge-guard", "doctor", "--check", "dependencies"]).unwrap();
1344 if let Commands::Doctor(args) = cli.command {
1345 assert_eq!(args.check.as_deref(), Some("dependencies"));
1346 } else {
1347 panic!("Expected Doctor command");
1348 }
1349 }
1350
1351 #[test]
1352 fn test_gas_contract_specific() {
1353 let cli = Cli::try_parse_from(["forge-guard", "gas", "Vault", "--diff", "prev.json"])
1354 .unwrap();
1355 if let Commands::Gas(args) = cli.command {
1356 assert_eq!(args.contract.as_deref(), Some("Vault"));
1357 assert_eq!(args.diff.as_deref(), Some("prev.json"));
1358 } else {
1359 panic!("Expected Gas command");
1360 }
1361 }
1362
1363 #[test]
1364 fn test_verify_with_constructor_args() {
1365 let cli = Cli::try_parse_from([
1366 "forge-guard",
1367 "verify",
1368 "0xabc",
1369 "Token",
1370 "--constructor-args",
1371 "0x0001",
1372 ])
1373 .unwrap();
1374 if let Commands::Verify(args) = cli.command {
1375 assert_eq!(args.constructor_args.as_deref(), Some("0x0001"));
1376 } else {
1377 panic!("Expected Verify command");
1378 }
1379 }
1380
1381 #[test]
1382 fn test_upgrade_check_proxy() {
1383 let cli = Cli::try_parse_from([
1384 "forge-guard",
1385 "upgrade-check",
1386 "0xproxy",
1387 "0ximpl",
1388 ])
1389 .unwrap();
1390 if let Commands::UpgradeCheck(args) = cli.command {
1391 assert_eq!(args.proxy.as_deref(), Some("0xproxy"));
1392 assert_eq!(args.implementation.as_deref(), Some("0ximpl"));
1393 } else {
1394 panic!("Expected UpgradeCheck command");
1395 }
1396 }
1397
1398 #[test]
1399 fn test_benchmark_save_and_compare() {
1400 let cli = Cli::try_parse_from([
1401 "forge-guard",
1402 "benchmark",
1403 "--save",
1404 "results.json",
1405 "--compare",
1406 "baseline.json",
1407 ])
1408 .unwrap();
1409 if let Commands::Benchmark(args) = cli.command {
1410 assert_eq!(args.save.as_deref(), Some(&std::path::PathBuf::from("results.json")));
1411 assert_eq!(args.compare.as_deref(), Some(&std::path::PathBuf::from("baseline.json")));
1412 } else {
1413 panic!("Expected Benchmark command");
1414 }
1415 }
1416
1417 #[test]
1418 fn test_watch_exclude() {
1419 let cli = Cli::try_parse_from(["forge-guard", "watch", "--exclude", "*.test.sol"])
1420 .unwrap();
1421 if let Commands::Watch(args) = cli.command {
1422 assert_eq!(args.exclude.as_deref(), Some("*.test.sol"));
1423 } else {
1424 panic!("Expected Watch command");
1425 }
1426 }
1427
1428 #[test]
1429 fn test_simulate_deployer() {
1430 let cli = Cli::try_parse_from([
1431 "forge-guard",
1432 "simulate",
1433 "--deployer",
1434 "0xdeployer",
1435 ])
1436 .unwrap();
1437 if let Commands::Simulate(args) = cli.command {
1438 assert_eq!(args.deployer.as_deref(), Some("0xdeployer"));
1439 } else {
1440 panic!("Expected Simulate command");
1441 }
1442 }
1443
1444 #[test]
1445 fn test_security_disable_check() {
1446 let cli = Cli::try_parse_from(["forge-guard", "security", "disable", "FA-H-001"])
1447 .unwrap();
1448 if let Commands::Security(args) = cli.command {
1449 assert!(matches!(
1450 args.action,
1451 Some(SecurityAction::Disable { .. })
1452 ));
1453 } else {
1454 panic!("Expected Security command");
1455 }
1456 }
1457
1458 #[test]
1459 fn test_from_env_try_parse() {
1460 let cli = Cli::try_parse_from(["forge-guard", "audit","--report","--json"]).unwrap();
1463 assert!(matches!(cli.command, Commands::Audit(_)));
1464 if let Commands::Audit(args) = cli.command {
1465 assert!(args.shared.report);
1466 assert!(args.shared.json);
1467 }
1468 }
1469}