Coverage for /home/runner/work/bijux-cli/bijux-cli/src/bijux_cli/services/utils.py: 100%

24 statements  

« prev     ^ index     » next       coverage.py v7.10.4, created at 2025-08-19 23:36 +0000

1# SPDX-License-Identifier: MIT 

2# Copyright © 2025 Bijan Mousavi 

3 

4"""Provides shared utility functions for the CLI's service layer. 

5 

6This module contains common helper functions used by various service 

7implementations. It centralizes logic for tasks like input validation and 

8security checks to ensure consistency and robustness across the service layer. 

9""" 

10 

11from __future__ import annotations 

12 

13import os 

14import shutil 

15 

16from bijux_cli.core.exceptions import BijuxError 

17 

18 

19def validate_command(cmd: list[str]) -> list[str]: 

20 """Validates a command and its arguments against a whitelist. 

21 

22 This security function is designed to prevent shell injection vulnerabilities. 

23 It performs several checks: 

24 1. Verifies the command name against an allowlist defined by the 

25 `BIJUXCLI_ALLOWED_COMMANDS` environment variable. 

26 2. Resolves the command's absolute path to ensure it's on the system PATH. 

27 3. Checks arguments for forbidden shell metacharacters. 

28 

29 Args: 

30 cmd (list[str]): The command and arguments to validate, as a list of 

31 strings. 

32 

33 Returns: 

34 list[str]: A validated and safe command list, with the command name 

35 replaced by its absolute path, suitable for use with `subprocess.run` 

36 where `shell=False`. 

37 

38 Raises: 

39 BijuxError: If the command is empty, not on the allowlist, not found 

40 on the system PATH, or if any argument contains unsafe characters. 

41 """ 

42 if not cmd: 

43 raise BijuxError("Empty command not allowed", http_status=403) 

44 env_val = os.getenv("BIJUXCLI_ALLOWED_COMMANDS") 

45 allowed_commands = (env_val or "echo,ls,cat,grep").split(",") 

46 

47 cmd_name = os.path.basename(cmd[0]) 

48 if cmd_name not in allowed_commands: 

49 raise BijuxError( 

50 f"Command {cmd_name!r} not in allowed list: {allowed_commands}", 

51 http_status=403, 

52 ) 

53 resolved_cmd_path = shutil.which(cmd[0]) 

54 if not resolved_cmd_path: 

55 raise BijuxError( 

56 f"Command not found or not executable: {cmd[0]!r}", http_status=403 

57 ) 

58 if os.path.basename(resolved_cmd_path) != cmd_name: 

59 raise BijuxError(f"Disallowed command path: {cmd[0]!r}", http_status=403) 

60 cmd[0] = resolved_cmd_path 

61 forbidden = set(";|&><`!") 

62 for arg in cmd[1:]: 

63 if any(ch in arg for ch in forbidden): 

64 raise BijuxError(f"Unsafe argument: {arg!r}", http_status=403) 

65 return cmd 

66 

67 

68__all__ = ["validate_command"]