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
« 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
4"""Provides shared utility functions for the CLI's service layer.
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"""
11from __future__ import annotations
13import os
14import shutil
16from bijux_cli.core.exceptions import BijuxError
19def validate_command(cmd: list[str]) -> list[str]:
20 """Validates a command and its arguments against a whitelist.
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.
29 Args:
30 cmd (list[str]): The command and arguments to validate, as a list of
31 strings.
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`.
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(",")
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
68__all__ = ["validate_command"]