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