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