1212#
1313# Additional attribution and requirements are provided in the NOTICE file.
1414
15+ import sys
16+ from typing import List , Optional
1517
1618import typer
1719from rich .console import Console
1820from rich .traceback import install
19- from typing import Optional , List
20- import sys
2121
2222from .commands import (
23- init ,
24- workflows ,
25- workflow_exec ,
23+ ai ,
2624 findings ,
25+ ingest ,
26+ init ,
2727 monitor ,
28+ workflow_exec ,
29+ workflows ,
30+ )
31+ from .commands import (
2832 config as config_cmd ,
29- ai ,
30- ingest ,
3133)
3234from .constants import DEFAULT_VOLUME_MODE
3335from .fuzzy import enhanced_command_not_found_handler
7880
7981# === Top-level commands ===
8082
83+
8184@app .command ()
8285def init (
8386 name : Optional [str ] = typer .Option (
84- None , "--name" , "-n" ,
85- help = "Project name (defaults to current directory name)"
87+ None , "--name" , "-n" , help = "Project name (defaults to current directory name)"
8688 ),
8789 api_url : Optional [str ] = typer .Option (
88- None , "--api-url" , "-u" ,
89- help = "FuzzForge API URL (defaults to http://localhost:8000)"
90+ None ,
91+ "--api-url" ,
92+ "-u" ,
93+ help = "FuzzForge API URL (defaults to http://localhost:8000)" ,
9094 ),
9195 force : bool = typer .Option (
92- False , "--force" , "-f" ,
93- help = "Force initialization even if project already exists"
94- )
96+ False ,
97+ "--force" ,
98+ "-f" ,
99+ help = "Force initialization even if project already exists" ,
100+ ),
95101):
96102 """
97103 📁 Initialize a new FuzzForge project
98104 """
99105 from .commands .init import project
106+
100107 project (name = name , api_url = api_url , force = force )
101108
102109
@@ -106,18 +113,18 @@ def status():
106113 📊 Show project and latest execution status
107114 """
108115 from .commands .status import show_status
116+
109117 show_status ()
110118
111119
112120@app .command ()
113121def config (
114122 key : Optional [str ] = typer .Argument (None , help = "Configuration key" ),
115- value : Optional [str ] = typer .Argument (None , help = "Configuration value to set" )
123+ value : Optional [str ] = typer .Argument (None , help = "Configuration value to set" ),
116124):
117125 """
118126 ⚙️ Manage configuration (show all, get, or set values)
119127 """
120- from .commands import config as config_cmd
121128
122129 if key is None :
123130 # No arguments: show all config
@@ -133,13 +140,11 @@ def config(
133140@app .command ()
134141def clean (
135142 days : int = typer .Option (
136- 90 , "--days" , "-d" ,
137- help = "Remove data older than this many days"
143+ 90 , "--days" , "-d" , help = "Remove data older than this many days"
138144 ),
139145 dry_run : bool = typer .Option (
140- False , "--dry-run" ,
141- help = "Show what would be deleted without actually deleting"
142- )
146+ False , "--dry-run" , help = "Show what would be deleted without actually deleting"
147+ ),
143148):
144149 """
145150 🧹 Clean old execution data and findings
@@ -155,7 +160,9 @@ def clean(
155160 raise typer .Exit (1 )
156161
157162 if dry_run :
158- console .print (f"🔍 [bold]Dry run:[/bold] Would clean data older than { days } days" )
163+ console .print (
164+ f"🔍 [bold]Dry run:[/bold] Would clean data older than { days } days"
165+ )
159166
160167 deleted = db .cleanup_old_runs (keep_days = days )
161168
@@ -177,35 +184,41 @@ def clean(
177184workflow_app .command ("info" )(workflows .workflow_info )
178185workflow_app .command ("params" )(workflows .workflow_parameters )
179186
187+
180188@workflow_app .command ("run" )
181189def run_workflow (
182190 workflow : str = typer .Argument (help = "Workflow name" ),
183191 target : str = typer .Argument (help = "Target path" ),
184- params : List [str ] = typer .Argument (default = None , help = "Parameters as key=value pairs" ),
192+ params : List [str ] = typer .Argument (
193+ default = None , help = "Parameters as key=value pairs"
194+ ),
185195 param_file : Optional [str ] = typer .Option (
186- None , "--param-file" , "-f" ,
187- help = "JSON file containing workflow parameters"
196+ None , "--param-file" , "-f" , help = "JSON file containing workflow parameters"
188197 ),
189198 volume_mode : str = typer .Option (
190- DEFAULT_VOLUME_MODE , "--volume-mode" , "-v" ,
191- help = "Volume mount mode: ro (read-only) or rw (read-write)"
199+ DEFAULT_VOLUME_MODE ,
200+ "--volume-mode" ,
201+ "-v" ,
202+ help = "Volume mount mode: ro (read-only) or rw (read-write)" ,
192203 ),
193204 timeout : Optional [int ] = typer .Option (
194- None , "--timeout" , "-t" ,
195- help = "Execution timeout in seconds"
205+ None , "--timeout" , "-t" , help = "Execution timeout in seconds"
196206 ),
197207 interactive : bool = typer .Option (
198- True , "--interactive/--no-interactive" , "-i/-n" ,
199- help = "Interactive parameter input for missing required parameters"
208+ True ,
209+ "--interactive/--no-interactive" ,
210+ "-i/-n" ,
211+ help = "Interactive parameter input for missing required parameters" ,
200212 ),
201213 wait : bool = typer .Option (
202- False , "--wait" , "-w" ,
203- help = "Wait for execution to complete"
214+ False , "--wait" , "-w" , help = "Wait for execution to complete"
204215 ),
205216 live : bool = typer .Option (
206- False , "--live" , "-l" ,
207- help = "Start live monitoring after execution (useful for fuzzing workflows)"
208- )
217+ False ,
218+ "--live" ,
219+ "-l" ,
220+ help = "Start live monitoring after execution (useful for fuzzing workflows)" ,
221+ ),
209222):
210223 """
211224 🚀 Execute a security testing workflow
@@ -221,9 +234,10 @@ def run_workflow(
221234 timeout = timeout ,
222235 interactive = interactive ,
223236 wait = wait ,
224- live = live
237+ live = live ,
225238 )
226239
240+
227241@workflow_app .callback ()
228242def workflow_main ():
229243 """
@@ -239,17 +253,18 @@ def workflow_main():
239253
240254# === Finding commands (singular) ===
241255
256+
242257@finding_app .command ("export" )
243258def export_finding (
244- execution_id : Optional [str ] = typer .Argument (None , help = "Execution ID (defaults to latest)" ),
259+ execution_id : Optional [str ] = typer .Argument (
260+ None , help = "Execution ID (defaults to latest)"
261+ ),
245262 format : str = typer .Option (
246- "sarif" , "--format" , "-f" ,
247- help = "Export format: sarif, json, csv"
263+ "sarif" , "--format" , "-f" , help = "Export format: sarif, json, csv"
248264 ),
249265 output : Optional [str ] = typer .Option (
250- None , "--output" , "-o" ,
251- help = "Output file (defaults to stdout)"
252- )
266+ None , "--output" , "-o" , help = "Output file (defaults to stdout)"
267+ ),
253268):
254269 """
255270 📤 Export findings to file
@@ -270,7 +285,9 @@ def export_finding(
270285 execution_id = recent_runs [0 ].run_id
271286 console .print (f"🔍 Using most recent execution: { execution_id } " )
272287 else :
273- console .print ("⚠️ No findings found in project database" , style = "yellow" )
288+ console .print (
289+ "⚠️ No findings found in project database" , style = "yellow"
290+ )
274291 return
275292 else :
276293 console .print ("❌ No project database found" , style = "red" )
@@ -283,14 +300,16 @@ def export_finding(
283300
284301@finding_app .command ("analyze" )
285302def analyze_finding (
286- finding_id : Optional [str ] = typer .Argument (None , help = "Finding ID to analyze" )
303+ finding_id : Optional [str ] = typer .Argument (None , help = "Finding ID to analyze" ),
287304):
288305 """
289306 🤖 AI analysis of a finding
290307 """
291308 from .commands .ai import analyze_finding as ai_analyze
309+
292310 ai_analyze (finding_id )
293311
312+
294313@finding_app .callback (invoke_without_command = True )
295314def finding_main (
296315 ctx : typer .Context ,
@@ -309,7 +328,7 @@ def finding_main(
309328 return
310329
311330 # Get remaining arguments for direct viewing
312- args = ctx .args if hasattr (ctx , ' args' ) else []
331+ args = ctx .args if hasattr (ctx , " args" ) else []
313332 finding_id = args [0 ] if args else None
314333
315334 # Direct viewing: fuzzforge finding [id]
@@ -329,7 +348,9 @@ def finding_main(
329348 finding_id = recent_runs [0 ].run_id
330349 console .print (f"🔍 Using most recent execution: { finding_id } " )
331350 else :
332- console .print ("⚠️ No findings found in project database" , style = "yellow" )
351+ console .print (
352+ "⚠️ No findings found in project database" , style = "yellow"
353+ )
333354 return
334355 else :
335356 console .print ("❌ No project database found" , style = "red" )
@@ -355,6 +376,7 @@ def finding_main(
355376app .add_typer (ai .app , name = "ai" , help = "🤖 AI integration features" )
356377app .add_typer (ingest .app , name = "ingest" , help = "🧠 Ingest knowledge into AI" )
357378
379+
358380# Help and utility commands
359381@app .command ()
360382def examples ():
@@ -372,7 +394,6 @@ def examples():
372394[bold]Execute Workflows:[/bold]
373395 ff workflow afl-fuzzing ./target # Run fuzzing on target
374396 ff workflow afl-fuzzing . --live # Run with live monitoring
375- ff workflow scan-c ./src timeout=300 threads=4 # With parameters
376397
377398[bold]Monitor Execution:[/bold]
378399 ff status # Check latest execution
@@ -399,16 +420,16 @@ def version():
399420 📦 Show version information
400421 """
401422 from . import __version__
423+
402424 console .print (f"FuzzForge CLI v{ __version__ } " )
403- console .print (f "Short command: ff" )
425+ console .print ("Short command: ff" )
404426
405427
406428@app .callback ()
407429def main_callback (
408430 ctx : typer .Context ,
409431 version : Optional [bool ] = typer .Option (
410- None , "--version" , "-v" ,
411- help = "Show version information"
432+ None , "--version" , "-v" , help = "Show version information"
412433 ),
413434):
414435 """
@@ -422,6 +443,7 @@ def main_callback(
422443 """
423444 if version :
424445 from . import __version__
446+
425447 console .print (f"FuzzForge CLI v{ __version__ } " )
426448 raise typer .Exit ()
427449
@@ -432,12 +454,11 @@ def main():
432454 if len (sys .argv ) > 1 :
433455 args = sys .argv [1 :]
434456
435-
436457 # Handle finding command with pattern recognition
437- if len (args ) >= 2 and args [0 ] == ' finding' :
438- finding_subcommands = [' export' , ' analyze' ]
458+ if len (args ) >= 2 and args [0 ] == " finding" :
459+ finding_subcommands = [" export" , " analyze" ]
439460 # Skip custom dispatching if help flags are present
440- if not any (arg in [' --help' , '-h' , ' --version' , '-v' ] for arg in args ):
461+ if not any (arg in [" --help" , "-h" , " --version" , "-v" ] for arg in args ):
441462 if args [1 ] not in finding_subcommands :
442463 # Direct finding display: ff finding <id>
443464 from .commands .findings import get_findings
@@ -457,18 +478,26 @@ def main():
457478 app ()
458479 except SystemExit as e :
459480 # Enhanced error handling for command not found
460- if hasattr (e , ' code' ) and e .code != 0 and len (sys .argv ) > 1 :
481+ if hasattr (e , " code" ) and e .code != 0 and len (sys .argv ) > 1 :
461482 command_parts = sys .argv [1 :]
462- clean_parts = [part for part in command_parts if not part .startswith ('-' )]
483+ clean_parts = [part for part in command_parts if not part .startswith ("-" )]
463484
464485 if clean_parts :
465486 main_cmd = clean_parts [0 ]
466487 valid_commands = [
467- 'init' , 'status' , 'config' , 'clean' ,
468- 'workflows' , 'workflow' ,
469- 'findings' , 'finding' ,
470- 'monitor' , 'ai' , 'ingest' ,
471- 'examples' , 'version'
488+ "init" ,
489+ "status" ,
490+ "config" ,
491+ "clean" ,
492+ "workflows" ,
493+ "workflow" ,
494+ "findings" ,
495+ "finding" ,
496+ "monitor" ,
497+ "ai" ,
498+ "ingest" ,
499+ "examples" ,
500+ "version" ,
472501 ]
473502
474503 if main_cmd not in valid_commands :
0 commit comments