diff --git a/app/shellgen/safety/checker.py b/app/shellgen/safety/checker.py new file mode 100644 index 0000000..82af4aa --- /dev/null +++ b/app/shellgen/safety/checker.py @@ -0,0 +1,146 @@ +"""Safety checking for generated commands.""" + +import re +from typing import Tuple, List + + +class SafetyChecker: + """Check if commands are safe to execute.""" + + DANGEROUS_PATTERNS: List[str] = [ + r"rm\s+-rf\s+[/]", + r"rm\s+-rf\s+[/a-zA-Z0-9_/.-]+\s*$", + r"dd\s+if=", + r"mkfs", + r"format", + r"shred", + r":\(\)\s*:\|\s*:\s*&\s*;", + r"chmod\s+777", + r"chown\s+[a-zA-Z0-9]+:[a-zA-Z0-9]+\s+[/]", + r"echo\s+.*>\s*/dev/sd[a-z]", + r">\s*/dev/[sh]d[a-z]", + r"mv\s+.*\s+[/dev]/null", + r"cp\s+.*\s+[/dev]/null", + r"cat\s+[/dev/][ur]andom", + ] + + SAFE_PATTERNS: List[str] = [ + r"^ls", + r"^cat\s+[a-zA-Z0-9_./-]", + r"^grep", + r"^find", + r"^echo", + r"^pwd", + r"^cd\s", + r"^git", + r"^npm\s+install", + r"^pip\s+install", + r"^docker\s+(ps|images|logs)", + r"^ps\s", + r"^top", + r"^htop", + r"^free", + r"^df\s", + r"^du\s", + r"^head\s", + r"^tail\s", + r"^less\s", + r"^more\s", + r"^wc\s", + r"^sort\s", + r"^uniq\s", + r"^cut\s", + r"^sed\s", + r"^awk\s", + r"^jq\s", + r"^tree", + r"^which\s", + r"^type\s", + r"^alias\s", + r"^unalias", + r"^history", + r"^export\s", + r"^unset\s", + r"^env\s", + r"^printenv", + ] + + def __init__(self): + """Initialize safety checker with compiled patterns.""" + self._dangerous_regexes = [ + re.compile(pattern, re.IGNORECASE) + for pattern in self.DANGEROUS_PATTERNS + ] + self._safe_regexes = [ + re.compile(pattern, re.IGNORECASE) + for pattern in self.SAFE_PATTERNS + ] + + def check(self, command: str) -> Tuple[bool, str]: + """Check if a command is safe to execute. + + Args: + command: The shell command to check. + + Returns: + Tuple of (is_safe, warning_message). + """ + if not command or not command.strip(): + return False, "Empty command" + + normalized = command.strip() + + for regex in self._dangerous_regexes: + if regex.search(normalized): + return False, f"Dangerous pattern detected: {regex.pattern}" + + for regex in self._safe_regexes: + if regex.search(normalized): + return True, "Command appears safe" + + if normalized.startswith("#") or normalized.startswith("//"): + return False, "Comment-only command" + + return True, "Command safety unclear - confirmation recommended" + + def is_safe(self, command: str) -> bool: + """Quick check if command is definitely safe. + + Args: + command: The shell command to check. + + Returns: + True if command is in safe patterns. + """ + is_safe, _ = self.check(command) + return is_safe + + def is_dangerous(self, command: str) -> bool: + """Quick check if command is definitely dangerous. + + Args: + command: The shell command to check. + + Returns: + True if command matches dangerous patterns. + """ + is_safe, _ = self.check(command) + return not is_safe + + def get_risk_level(self, command: str) -> str: + """Get risk level for a command. + + Args: + command: The shell command to check. + + Returns: + Risk level: "safe", "caution", or "dangerous". + """ + is_safe, warning = self.check(command) + + if "dangerous" in warning.lower(): + return "dangerous" + elif "unclear" in warning.lower(): + return "caution" + else: + return "safe"