holmesgpt 0.13.0__py3-none-any.whl → 0.13.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of holmesgpt might be problematic. Click here for more details.

Files changed (54) hide show
  1. holmes/__init__.py +1 -1
  2. holmes/common/env_vars.py +4 -0
  3. holmes/core/llm.py +3 -1
  4. holmes/core/tool_calling_llm.py +112 -11
  5. holmes/core/toolset_manager.py +1 -5
  6. holmes/core/tracing.py +1 -1
  7. holmes/main.py +7 -1
  8. holmes/plugins/prompts/_fetch_logs.jinja2 +4 -0
  9. holmes/plugins/runbooks/CLAUDE.md +85 -0
  10. holmes/plugins/runbooks/README.md +24 -0
  11. holmes/plugins/toolsets/bash/argocd/__init__.py +65 -0
  12. holmes/plugins/toolsets/bash/argocd/constants.py +120 -0
  13. holmes/plugins/toolsets/bash/aws/__init__.py +66 -0
  14. holmes/plugins/toolsets/bash/aws/constants.py +529 -0
  15. holmes/plugins/toolsets/bash/azure/__init__.py +56 -0
  16. holmes/plugins/toolsets/bash/azure/constants.py +339 -0
  17. holmes/plugins/toolsets/bash/bash_instructions.jinja2 +6 -7
  18. holmes/plugins/toolsets/bash/bash_toolset.py +47 -13
  19. holmes/plugins/toolsets/bash/common/bash_command.py +131 -0
  20. holmes/plugins/toolsets/bash/common/stringify.py +14 -1
  21. holmes/plugins/toolsets/bash/common/validators.py +91 -0
  22. holmes/plugins/toolsets/bash/docker/__init__.py +59 -0
  23. holmes/plugins/toolsets/bash/docker/constants.py +255 -0
  24. holmes/plugins/toolsets/bash/helm/__init__.py +61 -0
  25. holmes/plugins/toolsets/bash/helm/constants.py +92 -0
  26. holmes/plugins/toolsets/bash/kubectl/__init__.py +80 -79
  27. holmes/plugins/toolsets/bash/kubectl/constants.py +0 -14
  28. holmes/plugins/toolsets/bash/kubectl/kubectl_describe.py +38 -56
  29. holmes/plugins/toolsets/bash/kubectl/kubectl_events.py +28 -76
  30. holmes/plugins/toolsets/bash/kubectl/kubectl_get.py +39 -99
  31. holmes/plugins/toolsets/bash/kubectl/kubectl_logs.py +34 -15
  32. holmes/plugins/toolsets/bash/kubectl/kubectl_run.py +1 -1
  33. holmes/plugins/toolsets/bash/kubectl/kubectl_top.py +38 -77
  34. holmes/plugins/toolsets/bash/parse_command.py +106 -32
  35. holmes/plugins/toolsets/bash/utilities/__init__.py +0 -0
  36. holmes/plugins/toolsets/bash/utilities/base64_util.py +12 -0
  37. holmes/plugins/toolsets/bash/utilities/cut.py +12 -0
  38. holmes/plugins/toolsets/bash/utilities/grep/__init__.py +10 -0
  39. holmes/plugins/toolsets/bash/utilities/head.py +12 -0
  40. holmes/plugins/toolsets/bash/utilities/jq.py +79 -0
  41. holmes/plugins/toolsets/bash/utilities/sed.py +164 -0
  42. holmes/plugins/toolsets/bash/utilities/sort.py +15 -0
  43. holmes/plugins/toolsets/bash/utilities/tail.py +12 -0
  44. holmes/plugins/toolsets/bash/utilities/tr.py +57 -0
  45. holmes/plugins/toolsets/bash/utilities/uniq.py +12 -0
  46. holmes/plugins/toolsets/bash/utilities/wc.py +12 -0
  47. holmes/plugins/toolsets/prometheus/prometheus.py +42 -12
  48. holmes/utils/console/logging.py +6 -1
  49. {holmesgpt-0.13.0.dist-info → holmesgpt-0.13.1.dist-info}/METADATA +1 -1
  50. {holmesgpt-0.13.0.dist-info → holmesgpt-0.13.1.dist-info}/RECORD +53 -30
  51. holmes/plugins/toolsets/bash/grep/__init__.py +0 -52
  52. {holmesgpt-0.13.0.dist-info → holmesgpt-0.13.1.dist-info}/LICENSE.txt +0 -0
  53. {holmesgpt-0.13.0.dist-info → holmesgpt-0.13.1.dist-info}/WHEEL +0 -0
  54. {holmesgpt-0.13.0.dist-info → holmesgpt-0.13.1.dist-info}/entry_points.txt +0 -0
@@ -1,81 +1,42 @@
1
- from typing import Any
1
+ import argparse
2
+ from typing import Any, Optional
2
3
 
4
+ from holmes.plugins.toolsets.bash.common.bash_command import BashCommand
5
+ from holmes.plugins.toolsets.bash.common.config import BashExecutorConfig
3
6
  from holmes.plugins.toolsets.bash.common.stringify import escape_shell_args
4
- from holmes.plugins.toolsets.bash.common.validators import regex_validator
5
- from holmes.plugins.toolsets.bash.kubectl.constants import (
6
- SAFE_NAME_PATTERN,
7
- SAFE_NAMESPACE_PATTERN,
8
- SAFE_SELECTOR_PATTERN,
9
- )
10
7
 
11
8
 
12
- def create_kubectl_top_parser(kubectl_parser: Any):
13
- parser = kubectl_parser.add_parser(
14
- "top",
15
- help="Display resource (CPU/memory) usage",
16
- exit_on_error=False,
17
- )
18
- parser.add_argument(
19
- "resource_type",
20
- choices=["nodes", "node", "pods", "pod"],
21
- help="Resource type to get usage for",
22
- )
23
- parser.add_argument(
24
- "resource_name",
25
- nargs="?",
26
- default=None,
27
- type=regex_validator("resource name", SAFE_NAME_PATTERN),
28
- )
29
- parser.add_argument(
30
- "-n", "--namespace", type=regex_validator("namespace", SAFE_NAMESPACE_PATTERN)
31
- )
32
- parser.add_argument("-A", "--all-namespaces", action="store_true")
33
- parser.add_argument(
34
- "-l", "--selector", type=regex_validator("selector", SAFE_SELECTOR_PATTERN)
35
- )
36
- parser.add_argument(
37
- "--containers",
38
- action="store_true",
39
- help="Display containers along with pods (for pods resource type)",
40
- )
41
- parser.add_argument(
42
- "--use-protocol-buffers",
43
- action="store_true",
44
- help="Use protocol buffers for fetching metrics",
45
- )
46
- parser.add_argument(
47
- "--sort-by",
48
- type=regex_validator("sort field", SAFE_NAME_PATTERN),
49
- help="Sort by cpu or memory",
50
- )
51
- parser.add_argument("--no-headers", action="store_true")
52
-
53
-
54
- def stringify_top_command(cmd: Any) -> str:
55
- parts = ["kubectl", "top", cmd.resource_type]
56
-
57
- # Add resource name if specified
58
- if cmd.resource_name:
59
- parts.append(cmd.resource_name)
60
-
61
- if cmd.all_namespaces:
62
- parts.append("--all-namespaces")
63
- elif cmd.namespace:
64
- parts.extend(["--namespace", cmd.namespace])
65
-
66
- if cmd.selector:
67
- parts.extend(["--selector", cmd.selector])
68
-
69
- if cmd.containers:
70
- parts.append("--containers")
71
-
72
- if cmd.use_protocol_buffers:
73
- parts.append("--use-protocol-buffers")
74
-
75
- if cmd.sort_by:
76
- parts.extend(["--sort-by", cmd.sort_by])
77
-
78
- if cmd.no_headers:
79
- parts.append("--no-headers")
80
-
81
- return " ".join(escape_shell_args(parts))
9
+ class KubectlTopCommand(BashCommand):
10
+ def __init__(self):
11
+ super().__init__("top")
12
+
13
+ def add_parser(self, parent_parser: Any):
14
+ parser = parent_parser.add_parser(
15
+ "top",
16
+ help="Display resource (CPU/memory) usage",
17
+ exit_on_error=False,
18
+ )
19
+ parser.add_argument(
20
+ "resource_type",
21
+ choices=["nodes", "node", "pods", "pod"],
22
+ help="Resource type to get usage for",
23
+ )
24
+ parser.add_argument(
25
+ "options",
26
+ nargs=argparse.REMAINDER, # Captures all remaining arguments
27
+ default=[], # Default to an empty list
28
+ )
29
+
30
+ def validate_command(
31
+ self, command: Any, original_command: str, config: Optional[BashExecutorConfig]
32
+ ) -> None:
33
+ pass
34
+
35
+ def stringify_command(
36
+ self, command: Any, original_command: str, config: Optional[BashExecutorConfig]
37
+ ) -> str:
38
+ parts = ["kubectl", "top", command.resource_type]
39
+
40
+ parts += command.options
41
+
42
+ return " ".join(escape_shell_args(parts))
@@ -1,39 +1,111 @@
1
1
  import argparse
2
+ import logging
2
3
  import shlex
3
4
  from typing import Any, Optional
4
5
 
6
+ from holmes.plugins.toolsets.bash.common.bash_command import BashCommand
5
7
  from holmes.plugins.toolsets.bash.common.config import BashExecutorConfig
6
- from holmes.plugins.toolsets.bash.grep import create_grep_parser, stringify_grep_command
7
- from holmes.plugins.toolsets.bash.kubectl import (
8
- create_kubectl_parser,
9
- stringify_kubectl_command,
10
- )
8
+ from holmes.plugins.toolsets.bash.kubectl import KubectlCommand
9
+ from holmes.plugins.toolsets.bash.aws import AWSCommand
10
+ from holmes.plugins.toolsets.bash.azure import AzureCommand
11
+ from holmes.plugins.toolsets.bash.argocd import ArgocdCommand
12
+ from holmes.plugins.toolsets.bash.docker import DockerCommand
13
+ from holmes.plugins.toolsets.bash.helm import HelmCommand
14
+
15
+ # Utilities imports - all now use Command classes
16
+ from holmes.plugins.toolsets.bash.utilities.wc import WCCommand
17
+ from holmes.plugins.toolsets.bash.utilities.cut import CutCommand
18
+ from holmes.plugins.toolsets.bash.utilities.sort import SortCommand
19
+ from holmes.plugins.toolsets.bash.utilities.uniq import UniqCommand
20
+ from holmes.plugins.toolsets.bash.utilities.head import HeadCommand
21
+ from holmes.plugins.toolsets.bash.utilities.tail import TailCommand
22
+ from holmes.plugins.toolsets.bash.utilities.tr import TrCommand
23
+ from holmes.plugins.toolsets.bash.utilities.base64_util import Base64Command
24
+ from holmes.plugins.toolsets.bash.utilities.jq import JqCommand
25
+ from holmes.plugins.toolsets.bash.utilities.sed import SedCommand
26
+ from holmes.plugins.toolsets.bash.utilities.grep import GrepCommand
27
+
28
+
29
+ # All commands now use BashCommand classes
30
+ AVAILABLE_COMMANDS: list[BashCommand] = [
31
+ WCCommand(),
32
+ KubectlCommand(),
33
+ AWSCommand(),
34
+ AzureCommand(),
35
+ ArgocdCommand(),
36
+ DockerCommand(),
37
+ HelmCommand(),
38
+ GrepCommand(),
39
+ CutCommand(),
40
+ SortCommand(),
41
+ UniqCommand(),
42
+ HeadCommand(),
43
+ TailCommand(),
44
+ TrCommand(),
45
+ Base64Command(),
46
+ JqCommand(),
47
+ SedCommand(),
48
+ ]
49
+
50
+ command_name_to_command_map: dict[str, BashCommand] = {
51
+ cmd.name: cmd for cmd in AVAILABLE_COMMANDS
52
+ }
53
+
54
+
55
+ class QuietArgumentParser(argparse.ArgumentParser):
56
+ def __init__(self, *args, **kwargs):
57
+ super().__init__(*args, **kwargs)
58
+
59
+ def _print_message(self, message, file=None):
60
+ if message:
61
+ logging.debug(message.strip())
62
+
63
+ def error(self, message):
64
+ logging.debug(f"Error: {message}")
65
+ self.exit(2)
11
66
 
12
67
 
13
68
  def create_parser() -> argparse.ArgumentParser:
14
- parser = argparse.ArgumentParser(
15
- description="Parser for commands", exit_on_error=False
69
+ parser = QuietArgumentParser(
70
+ prog="command_parser", # Set explicit program name
71
+ description="Parser for commands",
72
+ exit_on_error=False,
73
+ add_help=False, # Disable help to avoid conflicts with -h in subcommands
16
74
  )
17
75
  commands_parser = parser.add_subparsers(
18
76
  dest="cmd", required=True, help="The tool to command (e.g., kubectl)"
19
77
  )
20
78
 
21
- create_kubectl_parser(commands_parser)
22
- create_grep_parser(commands_parser)
79
+ # Add all BashCommand classes
80
+ for command in AVAILABLE_COMMANDS:
81
+ command.add_parser(commands_parser)
82
+
23
83
  return parser
24
84
 
25
85
 
86
+ def validate_command(
87
+ command: Any, original_command: str, config: Optional[BashExecutorConfig]
88
+ ):
89
+ bash_command_instance = command_name_to_command_map.get(command.cmd)
90
+
91
+ if bash_command_instance:
92
+ bash_command_instance.validate_command(command, original_command, config)
93
+
94
+
26
95
  def stringify_command(
27
96
  command: Any, original_command: str, config: Optional[BashExecutorConfig]
28
97
  ) -> str:
29
- if command.cmd == "kubectl":
30
- return stringify_kubectl_command(command, config)
31
- elif command.cmd == "grep":
32
- return stringify_grep_command(command)
98
+ bash_command_instance = command_name_to_command_map.get(command.cmd)
99
+
100
+ if bash_command_instance:
101
+ return bash_command_instance.stringify_command(
102
+ command, original_command, config
103
+ )
33
104
  else:
34
105
  # This code path should not happen b/c the parsing of the command should catch an unsupported command
106
+ supported_commands = [cmd.name for cmd in AVAILABLE_COMMANDS]
35
107
  raise ValueError(
36
- f"Unsupported command '{command.cmd}' in {original_command}. Supported commands are: kubectl, grep"
108
+ f"Unsupported command '{command.cmd}' in {original_command}. Supported commands are: {', '.join(supported_commands)}"
37
109
  )
38
110
 
39
111
 
@@ -81,23 +153,25 @@ def split_into_separate_commands(command_str: str) -> list[list[str]]:
81
153
  def make_command_safe(command_str: str, config: Optional[BashExecutorConfig]) -> str:
82
154
  commands = split_into_separate_commands(command_str)
83
155
 
84
- try:
85
- safe_commands = [
86
- command_parser.parse_args(command_parts) for command_parts in commands
87
- ]
88
- if safe_commands and safe_commands[0].cmd == "grep":
156
+ parsed_commands = []
157
+ for individual_command in commands:
158
+ try:
159
+ parsed_commands.append(command_parser.parse_args(individual_command))
160
+
161
+ except SystemExit:
162
+ # argparse throws a SystemExit error when it can't parse command or arguments
163
+ # This ideally should be captured differently by ensuring all possible args
164
+ # are accounted for in the implementation for each command.
165
+ # When falling back, we raise a generic error
89
166
  raise ValueError(
90
- "The command grep can only be used after another command using the pipe `|` character to connect both commands"
91
- )
92
- safe_commands_str = [
93
- stringify_command(cmd, original_command=command_str, config=config)
94
- for cmd in safe_commands
95
- ]
167
+ f"The following command failed to be parsed for safety: {command_str}"
168
+ ) from None
169
+ for command in parsed_commands:
170
+ validate_command(command=command, original_command=command_str, config=config)
171
+
172
+ safe_commands_str = [
173
+ stringify_command(command=command, original_command=command_str, config=config)
174
+ for command in parsed_commands
175
+ ]
96
176
 
97
- return " | ".join(safe_commands_str)
98
- except SystemExit:
99
- # argparse throws a SystemExit error when it can't parse command or arguments
100
- # This ideally should be captured differently by ensuring all possible args
101
- # are accounted for in the implementation for each command.
102
- # When falling back, we raise a generic error
103
- raise ValueError("The command failed to be parsed for safety") from None
177
+ return " | ".join(safe_commands_str)
File without changes
@@ -0,0 +1,12 @@
1
+ from holmes.plugins.toolsets.bash.common.bash_command import (
2
+ SimpleBashCommand,
3
+ )
4
+
5
+
6
+ class Base64Command(SimpleBashCommand):
7
+ def __init__(self):
8
+ super().__init__(
9
+ name="base64",
10
+ allowed_options=[], # Allow all options except file operations
11
+ denied_options=[],
12
+ )
@@ -0,0 +1,12 @@
1
+ from holmes.plugins.toolsets.bash.common.bash_command import (
2
+ SimpleBashCommand,
3
+ )
4
+
5
+
6
+ class CutCommand(SimpleBashCommand):
7
+ def __init__(self):
8
+ super().__init__(
9
+ name="cut",
10
+ allowed_options=[], # Allow all options except file operations
11
+ denied_options=[],
12
+ )
@@ -0,0 +1,10 @@
1
+ from holmes.plugins.toolsets.bash.common.bash_command import SimpleBashCommand
2
+
3
+
4
+ class GrepCommand(SimpleBashCommand):
5
+ def __init__(self):
6
+ super().__init__(
7
+ name="grep",
8
+ allowed_options=[],
9
+ denied_options=[],
10
+ )
@@ -0,0 +1,12 @@
1
+ from holmes.plugins.toolsets.bash.common.bash_command import (
2
+ SimpleBashCommand,
3
+ )
4
+
5
+
6
+ class HeadCommand(SimpleBashCommand):
7
+ def __init__(self):
8
+ super().__init__(
9
+ name="head",
10
+ allowed_options=[],
11
+ denied_options=[],
12
+ )
@@ -0,0 +1,79 @@
1
+ import argparse
2
+ from typing import Any, Optional
3
+
4
+ from holmes.plugins.toolsets.bash.common.bash_command import BashCommand
5
+ from holmes.plugins.toolsets.bash.common.config import BashExecutorConfig
6
+ from holmes.plugins.toolsets.bash.common.stringify import escape_shell_args
7
+
8
+
9
+ class JqCommand(BashCommand):
10
+ def __init__(self):
11
+ super().__init__("jq")
12
+
13
+ def add_parser(self, parent_parser: Any):
14
+ jq_parser = parent_parser.add_parser(
15
+ "jq",
16
+ help="JSON processor",
17
+ exit_on_error=False,
18
+ add_help=False, # Disable help to avoid conflicts
19
+ prefix_chars="\x00", # Use null character as prefix to disable option parsing
20
+ )
21
+
22
+ # Capture all arguments for validation
23
+ jq_parser.add_argument(
24
+ "options",
25
+ nargs=argparse.REMAINDER,
26
+ default=[],
27
+ help="Jq filter and options",
28
+ )
29
+ return jq_parser
30
+
31
+ def validate_command(
32
+ self, command: Any, original_command: str, config: Optional[BashExecutorConfig]
33
+ ) -> None:
34
+ if hasattr(command, "options") and command.options:
35
+ validate_jq_options(command.options)
36
+
37
+ def stringify_command(
38
+ self, command: Any, original_command: str, config: Optional[BashExecutorConfig]
39
+ ) -> str:
40
+ parts = ["jq"]
41
+
42
+ # Add validated options
43
+ if hasattr(command, "options") and command.options:
44
+ validated_options = validate_jq_options(command.options)
45
+ parts.extend(validated_options)
46
+
47
+ return " ".join(escape_shell_args(parts))
48
+
49
+
50
+ def validate_jq_options(options: list[str]) -> list[str]:
51
+ """Validate jq CLI options - block file operations."""
52
+ i = 0
53
+ while i < len(options):
54
+ option = options[i]
55
+
56
+ # Block file reading operations
57
+ if option in {"--slurpfile", "--rawfile"}:
58
+ raise ValueError(f"Option {option} is not allowed for security reasons")
59
+
60
+ # Skip over option-value pairs
61
+ if option in {"--arg", "--argjson"} and i + 2 < len(options):
62
+ i += 3 # Skip option, name, value
63
+ continue
64
+ elif (
65
+ option.startswith("--")
66
+ and i + 1 < len(options)
67
+ and not options[i + 1].startswith("-")
68
+ ):
69
+ i += 2 # Skip option and value
70
+ continue
71
+
72
+ # No file arguments allowed (except filter expressions)
73
+ if not option.startswith("-") and "=" not in option:
74
+ # This could be a filter expression, allow it
75
+ pass
76
+
77
+ i += 1
78
+
79
+ return options
@@ -0,0 +1,164 @@
1
+ import argparse
2
+ import re
3
+ from typing import Any, Optional
4
+
5
+ from holmes.plugins.toolsets.bash.common.bash_command import BashCommand
6
+ from holmes.plugins.toolsets.bash.common.config import BashExecutorConfig
7
+ from holmes.plugins.toolsets.bash.common.stringify import escape_shell_args
8
+
9
+ # Blocked sed commands for security
10
+ BLOCKED_SED_COMMANDS = {
11
+ "w", # Write to file
12
+ "W", # Write first line of pattern space to file
13
+ "r", # Read file
14
+ "R", # Read one line from file
15
+ "e", # Execute command
16
+ "v", # Version (in some contexts)
17
+ "z", # Clear pattern space (can be misused)
18
+ "q", # Quit (can be misused to skip processing)
19
+ "Q", # Quit immediately
20
+ }
21
+
22
+ # Pattern to detect dangerous sed commands
23
+ DANGEROUS_SED_PATTERN = re.compile(
24
+ r"(?:^|;|\n)\s*(?:[0-9,]*\s*)?[wWrRezvqQ](?:\s|$|/)", re.MULTILINE
25
+ )
26
+
27
+
28
+ class SedCommand(BashCommand):
29
+ def __init__(self):
30
+ super().__init__("sed")
31
+
32
+ def add_parser(self, parent_parser: Any):
33
+ sed_parser = parent_parser.add_parser(
34
+ "sed",
35
+ help="Stream editor for filtering and transforming text",
36
+ exit_on_error=False,
37
+ add_help=False, # Disable help to avoid conflicts
38
+ prefix_chars="\x00", # Use null character as prefix to disable option parsing
39
+ )
40
+
41
+ # Capture all arguments for validation
42
+ sed_parser.add_argument(
43
+ "options",
44
+ nargs=argparse.REMAINDER,
45
+ default=[],
46
+ help="Sed script and options",
47
+ )
48
+ return sed_parser
49
+
50
+ def validate_command(
51
+ self, command: Any, original_command: str, config: Optional[BashExecutorConfig]
52
+ ) -> None:
53
+ if hasattr(command, "options") and command.options:
54
+ validate_sed_options(command.options)
55
+
56
+ def stringify_command(
57
+ self, command: Any, original_command: str, config: Optional[BashExecutorConfig]
58
+ ) -> str:
59
+ parts = ["sed"]
60
+
61
+ # Add validated options
62
+ if hasattr(command, "options") and command.options:
63
+ validated_options = validate_sed_options(command.options)
64
+ parts.extend(validated_options)
65
+
66
+ return " ".join(escape_shell_args(parts))
67
+
68
+
69
+ def validate_sed_script(script: str) -> None:
70
+ """Validate a sed script for safety."""
71
+ # Check for blocked commands - only check for actual command patterns
72
+ if DANGEROUS_SED_PATTERN.search(script):
73
+ raise ValueError("sed script contains blocked commands (w, r, e, q, etc.)")
74
+
75
+ # Check for execute command specifically (more targeted)
76
+ if re.search(r"(?:^|;|\n)\s*(?:[0-9,]*\s*)?e\b", script, re.MULTILINE):
77
+ raise ValueError("sed script contains execute commands")
78
+
79
+ # Check for file operations (simpler pattern to catch write/read commands)
80
+ if re.search(r"[wWrR]\s+\S", script):
81
+ raise ValueError("sed script contains file operations")
82
+
83
+
84
+ def validate_sed_options(options: list[str]) -> list[str]:
85
+ """Validate sed CLI options - block file operations and in-place editing."""
86
+ i = 0
87
+ script_found = False
88
+
89
+ while i < len(options):
90
+ option = options[i]
91
+
92
+ # Check for attached/inlined forms of blocked options
93
+ if (option.startswith("-i") and len(option) > 2) or option.startswith(
94
+ "--in-place="
95
+ ):
96
+ raise ValueError(
97
+ f"Attached in-place option {option} is not allowed for security reasons"
98
+ )
99
+ elif (option.startswith("-f") and len(option) > 2) or option.startswith(
100
+ "--file="
101
+ ):
102
+ raise ValueError(
103
+ f"Attached file option {option} is not allowed for security reasons"
104
+ )
105
+
106
+ # Block file reading and in-place editing for security
107
+ elif option in {"-f", "--file", "-i", "--in-place"}:
108
+ raise ValueError(f"Option {option} is not allowed for security reasons")
109
+
110
+ # Handle -e and --expression with attached scripts
111
+ elif option.startswith("-e") and len(option) > 2:
112
+ # Handle -eSCRIPT form
113
+ script = option[2:] # Extract script after "-e"
114
+ validate_sed_script(script)
115
+ script_found = True
116
+ i += 1
117
+ continue
118
+ elif option.startswith("--expression="):
119
+ # Handle --expression=SCRIPT form
120
+ script = option[13:] # Extract script after "--expression="
121
+ validate_sed_script(script)
122
+ script_found = True
123
+ i += 1
124
+ continue
125
+
126
+ # Handle -e and --expression with separate arguments
127
+ elif option in {"-e", "--expression"}:
128
+ if i + 1 >= len(options) or options[i + 1].startswith("-"):
129
+ raise ValueError(f"Option {option} requires a script argument")
130
+ script = options[i + 1]
131
+ validate_sed_script(script)
132
+ script_found = True
133
+ i += 2
134
+ continue
135
+
136
+ # Handle long options with values (--opt=val)
137
+ elif "=" in option and option.startswith("--"):
138
+ # Long option with attached value, skip as single unit
139
+ i += 1
140
+ continue
141
+
142
+ # Handle other long options with separate values
143
+ elif (
144
+ option.startswith("--")
145
+ and i + 1 < len(options)
146
+ and not options[i + 1].startswith("-")
147
+ ):
148
+ i += 2 # Skip option and value
149
+ continue
150
+
151
+ # Handle sed script (non-flag argument)
152
+ elif not option.startswith("-"):
153
+ if not script_found:
154
+ validate_sed_script(option)
155
+ script_found = True
156
+ else:
157
+ # Multiple scripts not allowed
158
+ pass
159
+ i += 1
160
+ continue
161
+
162
+ i += 1
163
+
164
+ return options
@@ -0,0 +1,15 @@
1
+ from holmes.plugins.toolsets.bash.common.bash_command import (
2
+ SimpleBashCommand,
3
+ )
4
+
5
+
6
+ class SortCommand(SimpleBashCommand):
7
+ def __init__(self):
8
+ super().__init__(
9
+ name="sort",
10
+ allowed_options=[],
11
+ denied_options=[
12
+ "-T",
13
+ "--temporary-directory",
14
+ ],
15
+ )
@@ -0,0 +1,12 @@
1
+ from holmes.plugins.toolsets.bash.common.bash_command import (
2
+ SimpleBashCommand,
3
+ )
4
+
5
+
6
+ class TailCommand(SimpleBashCommand):
7
+ def __init__(self):
8
+ super().__init__(
9
+ name="tail",
10
+ allowed_options=[],
11
+ denied_options=[],
12
+ )
@@ -0,0 +1,57 @@
1
+ import argparse
2
+ from typing import Any, Optional
3
+
4
+ from holmes.plugins.toolsets.bash.common.bash_command import BashCommand
5
+ from holmes.plugins.toolsets.bash.common.config import BashExecutorConfig
6
+ from holmes.plugins.toolsets.bash.common.stringify import escape_shell_args
7
+
8
+
9
+ class TrCommand(BashCommand):
10
+ def __init__(self):
11
+ super().__init__("tr")
12
+
13
+ def add_parser(self, parent_parser: Any):
14
+ parser = parent_parser.add_parser(
15
+ "tr",
16
+ help="Translate or delete characters",
17
+ exit_on_error=False,
18
+ add_help=False, # Disable help to avoid conflicts
19
+ prefix_chars="\x00", # Use null character as prefix to disable option parsing
20
+ )
21
+
22
+ # Capture all arguments for validation
23
+ parser.add_argument(
24
+ "options",
25
+ nargs=argparse.REMAINDER,
26
+ default=[],
27
+ help="tr options and character sets",
28
+ )
29
+ return parser
30
+
31
+ def validate_command(
32
+ self, command: Any, original_command: str, config: Optional[BashExecutorConfig]
33
+ ) -> None:
34
+ # tr is allowed to have character set arguments, but not file paths
35
+ # Block absolute paths, home-relative paths, relative paths, and common file extensions
36
+ blocked_extensions = (".txt", ".log", ".py", ".js", ".json")
37
+
38
+ for option in command.options:
39
+ if not option.startswith("-"):
40
+ # Allow character sets but block obvious file paths
41
+ if (
42
+ option.startswith("/")
43
+ or option.startswith("~")
44
+ or option.startswith("./")
45
+ or option.startswith("../")
46
+ or option.endswith(blocked_extensions)
47
+ ):
48
+ raise ValueError(
49
+ "File arguments are not allowed - tr can only process piped input"
50
+ )
51
+
52
+ def stringify_command(
53
+ self, command: Any, original_command: str, config: Optional[BashExecutorConfig]
54
+ ) -> str:
55
+ parts = ["tr"]
56
+ parts.extend(command.options)
57
+ return " ".join(escape_shell_args(parts))