""" Fuzzy matching and smart suggestions for FuzzForge CLI. Provides "Did you mean...?" functionality and intelligent command/parameter suggestions. """ # Copyright (c) 2025 FuzzingLabs # # Licensed under the Business Source License 1.1 (BSL). See the LICENSE file # at the root of this repository for details. # # After the Change Date (four years from publication), this version of the # Licensed Work will be made available under the Apache License, Version 2.0. # See the LICENSE-APACHE file or http://www.apache.org/licenses/LICENSE-2.0 # # Additional attribution and requirements are provided in the NOTICE file. import difflib from typing import List, Optional, Dict, Any, Tuple from rich.console import Console from rich.panel import Panel from rich.text import Text console = Console() class FuzzyMatcher: """Fuzzy matching engine for CLI commands and parameters.""" def __init__(self): # Known commands and subcommands self.commands = { "init": ["project"], "workflows": ["list", "info"], "runs": ["submit", "status", "list", "rerun"], "findings": ["get", "list", "export", "all"], "monitor": ["stats", "crashes", "live"], "config": ["set", "get", "list", "init"], "ai": ["ask", "summarize", "explain"], "ingest": ["project", "findings"] } # Common workflow names self.workflow_names = [ "security_assessment", "language_fuzzing", "infrastructure_scan", "static_analysis_scan", "penetration_testing_scan", "secret_detection_scan" ] # Common parameter names self.parameter_names = [ "target_path", "volume_mode", "timeout", "workflow", "param", "param-file", "interactive", "wait", "live", "format", "output", "severity", "since", "limit", "stats", "export" ] # Common values self.common_values = { "volume_mode": ["ro", "rw"], "format": ["json", "csv", "html", "sarif"], "severity": ["critical", "high", "medium", "low", "info"] } def find_closest_command(self, user_input: str, command_group: Optional[str] = None) -> Optional[Tuple[str, float]]: """Find the closest matching command.""" if command_group and command_group in self.commands: # Search within a specific command group candidates = self.commands[command_group] else: # Search all main commands candidates = list(self.commands.keys()) matches = difflib.get_close_matches( user_input, candidates, n=1, cutoff=0.6 ) if matches: match = matches[0] # Calculate similarity ratio ratio = difflib.SequenceMatcher(None, user_input, match).ratio() return match, ratio return None def find_closest_workflow(self, user_input: str) -> Optional[Tuple[str, float]]: """Find the closest matching workflow name.""" matches = difflib.get_close_matches( user_input, self.workflow_names, n=1, cutoff=0.6 ) if matches: match = matches[0] ratio = difflib.SequenceMatcher(None, user_input, match).ratio() return match, ratio return None def find_closest_parameter(self, user_input: str) -> Optional[Tuple[str, float]]: """Find the closest matching parameter name.""" # Remove leading dashes clean_input = user_input.lstrip('-') matches = difflib.get_close_matches( clean_input, self.parameter_names, n=1, cutoff=0.6 ) if matches: match = matches[0] ratio = difflib.SequenceMatcher(None, clean_input, match).ratio() return match, ratio return None def suggest_parameter_values(self, parameter: str, user_input: str) -> List[str]: """Suggest parameter values based on known options.""" if parameter in self.common_values: values = self.common_values[parameter] if user_input: # Filter values that start with user input return [v for v in values if v.startswith(user_input.lower())] else: return values return [] def get_command_suggestions(self, user_command: List[str]) -> Optional[Dict[str, Any]]: """Get suggestions for a user command that may have typos.""" if not user_command: return None suggestions = {"type": None, "original": user_command, "suggestions": []} # Check main command main_cmd = user_command[0] if main_cmd not in self.commands: closest = self.find_closest_command(main_cmd) if closest: match, confidence = closest suggestions["type"] = "main_command" suggestions["suggestions"].append({ "text": match, "confidence": confidence, "type": "command" }) # Check subcommand if present elif len(user_command) > 1: sub_cmd = user_command[1] if main_cmd in self.commands and sub_cmd not in self.commands[main_cmd]: closest = self.find_closest_command(sub_cmd, main_cmd) if closest: match, confidence = closest suggestions["type"] = "subcommand" suggestions["suggestions"].append({ "text": f"{main_cmd} {match}", "confidence": confidence, "type": "subcommand" }) return suggestions if suggestions["suggestions"] else None def suggest_workflow_fix(self, user_workflow: str) -> Optional[str]: """Suggest a workflow name correction.""" closest = self.find_closest_workflow(user_workflow) if closest: match, confidence = closest if confidence > 0.6: # Only suggest if reasonably confident return match return None def display_command_suggestion(suggestions: Dict[str, Any]): """Display command suggestions to the user.""" if not suggestions or not suggestions["suggestions"]: return original = " ".join(suggestions["original"]) suggestion_type = suggestions["type"] # Create suggestion text text = Text() text.append("ā“ Command not found: ", style="red") text.append(f"'{original}'", style="bold red") text.append("\n\n") text.append("šŸ’” Did you mean:\n", style="yellow") for i, suggestion in enumerate(suggestions["suggestions"], 1): confidence_percent = int(suggestion["confidence"] * 100) text.append(f" {i}. ", style="bold cyan") text.append(f"{suggestion['text']}", style="bold white") text.append(f" ({confidence_percent}% match)", style="dim") text.append("\n") # Add helpful context if suggestion_type == "main_command": text.append("\nšŸ’” Use 'fuzzforge --help' to see all available commands", style="dim") elif suggestion_type == "subcommand": main_cmd = suggestions["original"][0] text.append(f"\nšŸ’” Use 'fuzzforge {main_cmd} --help' to see available subcommands", style="dim") console.print(Panel( text, title="šŸ¤” Command Suggestion", border_style="yellow", expand=False )) def display_workflow_suggestion(original: str, suggestion: str): """Display workflow name suggestion.""" text = Text() text.append("ā“ Workflow not found: ", style="red") text.append(f"'{original}'", style="bold red") text.append("\n\n") text.append("šŸ’” Did you mean: ", style="yellow") text.append(f"'{suggestion}'", style="bold green") text.append("?\n\n") text.append("šŸ’” Use 'fuzzforge workflows' to see all available workflows", style="dim") console.print(Panel( text, title="šŸ”§ Workflow Suggestion", border_style="yellow", expand=False )) def display_parameter_suggestion(original: str, suggestion: str): """Display parameter name suggestion.""" text = Text() text.append("ā“ Unknown parameter: ", style="red") text.append(f"'{original}'", style="bold red") text.append("\n\n") text.append("šŸ’” Did you mean: ", style="yellow") text.append(f"'--{suggestion}'", style="bold green") text.append("?\n\n") text.append("šŸ’” Use '--help' to see all available parameters", style="dim") console.print(Panel( text, title="āš™ļø Parameter Suggestion", border_style="yellow", expand=False )) def enhanced_command_not_found_handler(command_parts: List[str]): """Handle command not found with fuzzy matching suggestions.""" matcher = FuzzyMatcher() suggestions = matcher.get_command_suggestions(command_parts) if suggestions: display_command_suggestion(suggestions) else: # Fallback to generic help console.print("āŒ [red]Command not found[/red]") console.print("šŸ’” Use 'fuzzforge --help' to see available commands") def enhanced_workflow_not_found_handler(workflow_name: str): """Handle workflow not found with suggestions.""" matcher = FuzzyMatcher() suggestion = matcher.suggest_workflow_fix(workflow_name) if suggestion: display_workflow_suggestion(workflow_name, suggestion) else: console.print(f"āŒ [red]Workflow '{workflow_name}' not found[/red]") console.print("šŸ’” Use 'fuzzforge workflows' to see available workflows") def enhanced_parameter_not_found_handler(parameter_name: str): """Handle unknown parameter with suggestions.""" matcher = FuzzyMatcher() closest = matcher.find_closest_parameter(parameter_name) if closest: match, confidence = closest if confidence > 0.6: display_parameter_suggestion(parameter_name, match) return console.print(f"āŒ [red]Unknown parameter: '{parameter_name}'[/red]") console.print("šŸ’” Use '--help' to see available parameters") # Global fuzzy matcher instance fuzzy_matcher = FuzzyMatcher()