holmesgpt 0.12.6__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 (125) hide show
  1. holmes/__init__.py +1 -1
  2. holmes/clients/robusta_client.py +19 -1
  3. holmes/common/env_vars.py +17 -0
  4. holmes/config.py +69 -9
  5. holmes/core/conversations.py +11 -0
  6. holmes/core/investigation.py +16 -3
  7. holmes/core/investigation_structured_output.py +12 -0
  8. holmes/core/llm.py +13 -1
  9. holmes/core/models.py +9 -1
  10. holmes/core/openai_formatting.py +72 -12
  11. holmes/core/prompt.py +13 -0
  12. holmes/core/supabase_dal.py +3 -0
  13. holmes/core/todo_manager.py +88 -0
  14. holmes/core/tool_calling_llm.py +230 -157
  15. holmes/core/tools.py +10 -1
  16. holmes/core/tools_utils/tool_executor.py +7 -2
  17. holmes/core/tools_utils/toolset_utils.py +7 -2
  18. holmes/core/toolset_manager.py +1 -5
  19. holmes/core/tracing.py +4 -3
  20. holmes/interactive.py +1 -0
  21. holmes/main.py +9 -2
  22. holmes/plugins/prompts/__init__.py +7 -1
  23. holmes/plugins/prompts/_current_date_time.jinja2 +1 -0
  24. holmes/plugins/prompts/_default_log_prompt.jinja2 +4 -2
  25. holmes/plugins/prompts/_fetch_logs.jinja2 +10 -1
  26. holmes/plugins/prompts/_general_instructions.jinja2 +14 -0
  27. holmes/plugins/prompts/_permission_errors.jinja2 +1 -1
  28. holmes/plugins/prompts/_toolsets_instructions.jinja2 +4 -4
  29. holmes/plugins/prompts/generic_ask.jinja2 +4 -3
  30. holmes/plugins/prompts/investigation_procedure.jinja2 +210 -0
  31. holmes/plugins/prompts/kubernetes_workload_ask.jinja2 +2 -0
  32. holmes/plugins/runbooks/CLAUDE.md +85 -0
  33. holmes/plugins/runbooks/README.md +24 -0
  34. holmes/plugins/toolsets/__init__.py +19 -6
  35. holmes/plugins/toolsets/atlas_mongodb/mongodb_atlas.py +27 -0
  36. holmes/plugins/toolsets/azure_sql/tools/analyze_connection_failures.py +2 -2
  37. holmes/plugins/toolsets/azure_sql/tools/analyze_database_connections.py +2 -1
  38. holmes/plugins/toolsets/azure_sql/tools/analyze_database_health_status.py +3 -1
  39. holmes/plugins/toolsets/azure_sql/tools/analyze_database_performance.py +2 -1
  40. holmes/plugins/toolsets/azure_sql/tools/analyze_database_storage.py +2 -1
  41. holmes/plugins/toolsets/azure_sql/tools/get_active_alerts.py +3 -1
  42. holmes/plugins/toolsets/azure_sql/tools/get_slow_queries.py +2 -1
  43. holmes/plugins/toolsets/azure_sql/tools/get_top_cpu_queries.py +2 -1
  44. holmes/plugins/toolsets/azure_sql/tools/get_top_data_io_queries.py +2 -1
  45. holmes/plugins/toolsets/azure_sql/tools/get_top_log_io_queries.py +2 -1
  46. holmes/plugins/toolsets/bash/argocd/__init__.py +65 -0
  47. holmes/plugins/toolsets/bash/argocd/constants.py +120 -0
  48. holmes/plugins/toolsets/bash/aws/__init__.py +66 -0
  49. holmes/plugins/toolsets/bash/aws/constants.py +529 -0
  50. holmes/plugins/toolsets/bash/azure/__init__.py +56 -0
  51. holmes/plugins/toolsets/bash/azure/constants.py +339 -0
  52. holmes/plugins/toolsets/bash/bash_instructions.jinja2 +6 -7
  53. holmes/plugins/toolsets/bash/bash_toolset.py +47 -13
  54. holmes/plugins/toolsets/bash/common/bash_command.py +131 -0
  55. holmes/plugins/toolsets/bash/common/stringify.py +14 -1
  56. holmes/plugins/toolsets/bash/common/validators.py +91 -0
  57. holmes/plugins/toolsets/bash/docker/__init__.py +59 -0
  58. holmes/plugins/toolsets/bash/docker/constants.py +255 -0
  59. holmes/plugins/toolsets/bash/helm/__init__.py +61 -0
  60. holmes/plugins/toolsets/bash/helm/constants.py +92 -0
  61. holmes/plugins/toolsets/bash/kubectl/__init__.py +80 -79
  62. holmes/plugins/toolsets/bash/kubectl/constants.py +0 -14
  63. holmes/plugins/toolsets/bash/kubectl/kubectl_describe.py +38 -56
  64. holmes/plugins/toolsets/bash/kubectl/kubectl_events.py +28 -76
  65. holmes/plugins/toolsets/bash/kubectl/kubectl_get.py +39 -99
  66. holmes/plugins/toolsets/bash/kubectl/kubectl_logs.py +34 -15
  67. holmes/plugins/toolsets/bash/kubectl/kubectl_run.py +1 -1
  68. holmes/plugins/toolsets/bash/kubectl/kubectl_top.py +38 -77
  69. holmes/plugins/toolsets/bash/parse_command.py +106 -32
  70. holmes/plugins/toolsets/bash/utilities/__init__.py +0 -0
  71. holmes/plugins/toolsets/bash/utilities/base64_util.py +12 -0
  72. holmes/plugins/toolsets/bash/utilities/cut.py +12 -0
  73. holmes/plugins/toolsets/bash/utilities/grep/__init__.py +10 -0
  74. holmes/plugins/toolsets/bash/utilities/head.py +12 -0
  75. holmes/plugins/toolsets/bash/utilities/jq.py +79 -0
  76. holmes/plugins/toolsets/bash/utilities/sed.py +164 -0
  77. holmes/plugins/toolsets/bash/utilities/sort.py +15 -0
  78. holmes/plugins/toolsets/bash/utilities/tail.py +12 -0
  79. holmes/plugins/toolsets/bash/utilities/tr.py +57 -0
  80. holmes/plugins/toolsets/bash/utilities/uniq.py +12 -0
  81. holmes/plugins/toolsets/bash/utilities/wc.py +12 -0
  82. holmes/plugins/toolsets/coralogix/api.py +6 -6
  83. holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py +7 -1
  84. holmes/plugins/toolsets/datadog/datadog_api.py +20 -8
  85. holmes/plugins/toolsets/datadog/datadog_metrics_instructions.jinja2 +8 -1
  86. holmes/plugins/toolsets/datadog/datadog_rds_instructions.jinja2 +82 -0
  87. holmes/plugins/toolsets/datadog/toolset_datadog_logs.py +12 -5
  88. holmes/plugins/toolsets/datadog/toolset_datadog_metrics.py +20 -11
  89. holmes/plugins/toolsets/datadog/toolset_datadog_rds.py +735 -0
  90. holmes/plugins/toolsets/datadog/toolset_datadog_traces.py +18 -11
  91. holmes/plugins/toolsets/git.py +15 -15
  92. holmes/plugins/toolsets/grafana/grafana_api.py +12 -1
  93. holmes/plugins/toolsets/grafana/toolset_grafana.py +5 -1
  94. holmes/plugins/toolsets/grafana/toolset_grafana_loki.py +9 -4
  95. holmes/plugins/toolsets/grafana/toolset_grafana_tempo.py +12 -5
  96. holmes/plugins/toolsets/internet/internet.py +2 -1
  97. holmes/plugins/toolsets/internet/notion.py +2 -1
  98. holmes/plugins/toolsets/investigator/__init__.py +0 -0
  99. holmes/plugins/toolsets/investigator/core_investigation.py +157 -0
  100. holmes/plugins/toolsets/investigator/investigator_instructions.jinja2 +253 -0
  101. holmes/plugins/toolsets/investigator/model.py +15 -0
  102. holmes/plugins/toolsets/kafka.py +14 -7
  103. holmes/plugins/toolsets/kubernetes_logs.py +454 -25
  104. holmes/plugins/toolsets/logging_utils/logging_api.py +115 -55
  105. holmes/plugins/toolsets/mcp/toolset_mcp.py +1 -1
  106. holmes/plugins/toolsets/newrelic.py +8 -3
  107. holmes/plugins/toolsets/opensearch/opensearch.py +8 -4
  108. holmes/plugins/toolsets/opensearch/opensearch_logs.py +9 -2
  109. holmes/plugins/toolsets/opensearch/opensearch_traces.py +6 -2
  110. holmes/plugins/toolsets/prometheus/prometheus.py +179 -44
  111. holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py +8 -2
  112. holmes/plugins/toolsets/robusta/robusta.py +4 -4
  113. holmes/plugins/toolsets/runbook/runbook_fetcher.py +6 -5
  114. holmes/plugins/toolsets/servicenow/servicenow.py +18 -3
  115. holmes/plugins/toolsets/utils.py +8 -1
  116. holmes/utils/console/logging.py +6 -1
  117. holmes/utils/llms.py +20 -0
  118. holmes/utils/stream.py +90 -0
  119. {holmesgpt-0.12.6.dist-info → holmesgpt-0.13.1.dist-info}/METADATA +47 -34
  120. {holmesgpt-0.12.6.dist-info → holmesgpt-0.13.1.dist-info}/RECORD +123 -91
  121. holmes/plugins/toolsets/bash/grep/__init__.py +0 -52
  122. holmes/utils/robusta.py +0 -9
  123. {holmesgpt-0.12.6.dist-info → holmesgpt-0.13.1.dist-info}/LICENSE.txt +0 -0
  124. {holmesgpt-0.12.6.dist-info → holmesgpt-0.13.1.dist-info}/WHEEL +0 -0
  125. {holmesgpt-0.12.6.dist-info → holmesgpt-0.13.1.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,339 @@
1
+ ALLOWED_AZURE_COMMANDS: dict[str, dict] = {
2
+ # Basic account and resource management (read-only)
3
+ "account": {"list": {}, "show": {}, "list-locations": {}, "tenant": {"list": {}}},
4
+ "group": {"list": {}, "show": {}, "exists": {}},
5
+ "resource": {"list": {}, "show": {}},
6
+ # Virtual Machine commands (read-only)
7
+ "vm": {
8
+ "list": {},
9
+ "show": {},
10
+ "list-ip-addresses": {},
11
+ "list-sizes": {},
12
+ "list-skus": {},
13
+ "list-usage": {},
14
+ "list-vm-resize-options": {},
15
+ "get-instance-view": {},
16
+ },
17
+ # Network commands (read-only)
18
+ "network": {
19
+ "vnet": {"list": {}, "show": {}, "subnet": {"list": {}, "show": {}}},
20
+ "nsg": {"list": {}, "show": {}, "rule": {"list": {}, "show": {}}},
21
+ "public-ip": {"list": {}, "show": {}},
22
+ "lb": {
23
+ "list": {},
24
+ "show": {},
25
+ "frontend-ip": {"list": {}, "show": {}},
26
+ "rule": {"list": {}, "show": {}},
27
+ },
28
+ "application-gateway": {"list": {}, "show": {}, "show-backend-health": {}},
29
+ "nic": {
30
+ "list": {},
31
+ "show": {},
32
+ "list-effective-nsg": {},
33
+ "show-effective-route-table": {},
34
+ },
35
+ "route-table": {"list": {}, "show": {}, "route": {"list": {}, "show": {}}},
36
+ "dns": {
37
+ "zone": {"list": {}, "show": {}, "record-set": {"list": {}, "show": {}}}
38
+ },
39
+ "traffic-manager": {
40
+ "profile": {"list": {}, "show": {}, "endpoint": {"list": {}, "show": {}}}
41
+ },
42
+ },
43
+ # Storage commands (read-only)
44
+ "storage": {
45
+ "account": {"list": {}, "show": {}, "show-usage": {}, "check-name": {}},
46
+ "container": {"list": {}, "show": {}, "exists": {}},
47
+ "blob": {"list": {}, "show": {}, "exists": {}},
48
+ "share": {"list": {}, "show": {}, "exists": {}},
49
+ "queue": {"list": {}, "show": {}, "exists": {}},
50
+ "table": {"list": {}, "show": {}, "exists": {}},
51
+ },
52
+ # Azure Kubernetes Service (read-only)
53
+ "aks": {
54
+ "list": {},
55
+ "show": {},
56
+ "get-versions": {},
57
+ "get-upgrades": {},
58
+ "nodepool": {"list": {}, "show": {}},
59
+ "check-acr": {},
60
+ },
61
+ # Monitoring (read-only)
62
+ "monitor": {
63
+ "metrics": {
64
+ "list": {},
65
+ "list-definitions": {},
66
+ "list-namespaces": {},
67
+ "alert": {"list": {}, "show": {}},
68
+ },
69
+ "activity-log": {
70
+ "list": {},
71
+ "list-categories": {},
72
+ "alert": {"list": {}, "show": {}},
73
+ },
74
+ "log-analytics": {
75
+ "workspace": {
76
+ "list": {},
77
+ "show": {},
78
+ "get-schema": {},
79
+ },
80
+ "query": {},
81
+ },
82
+ "diagnostic-settings": {"list": {}, "show": {}},
83
+ "autoscale": {"list": {}, "show": {}},
84
+ },
85
+ # App Service (read-only)
86
+ "appservice": {"plan": {"list": {}, "show": {}}, "list-locations": {}},
87
+ "webapp": {"list": {}, "show": {}, "list-runtimes": {}, "config": {"show": {}}},
88
+ # Key Vault (limited safe operations)
89
+ "keyvault": {"list": {}, "show": {}, "list-deleted": {}, "check-name": {}},
90
+ # SQL Database (read-only)
91
+ "sql": {
92
+ "server": {"list": {}, "show": {}},
93
+ "db": {"list": {}, "show": {}, "list-editions": {}, "show-usage": {}},
94
+ "elastic-pool": {"list": {}, "show": {}},
95
+ },
96
+ # CosmosDB (read-only)
97
+ "cosmosdb": {
98
+ "list": {},
99
+ "show": {},
100
+ "database": {"list": {}, "show": {}},
101
+ "collection": {"list": {}, "show": {}},
102
+ },
103
+ # Container Registry (read-only)
104
+ "acr": {
105
+ "list": {},
106
+ "show": {},
107
+ "repository": {"list": {}, "show": {}, "show-tags": {}, "show-manifests": {}},
108
+ "check-name": {},
109
+ "check-health": {},
110
+ },
111
+ # Container Instances (read-only)
112
+ "container": {"list": {}, "show": {}, "logs": {}},
113
+ # Batch (read-only)
114
+ "batch": {
115
+ "account": {"list": {}, "show": {}},
116
+ "pool": {"list": {}, "show": {}},
117
+ "job": {"list": {}, "show": {}},
118
+ "task": {"list": {}, "show": {}},
119
+ },
120
+ # CDN (read-only)
121
+ "cdn": {"profile": {"list": {}, "show": {}}, "endpoint": {"list": {}, "show": {}}},
122
+ # Event Hub (read-only)
123
+ "eventhubs": {
124
+ "namespace": {"list": {}, "show": {}},
125
+ "eventhub": {"list": {}, "show": {}},
126
+ },
127
+ # Service Bus (read-only)
128
+ "servicebus": {
129
+ "namespace": {"list": {}, "show": {}},
130
+ "queue": {"list": {}, "show": {}},
131
+ "topic": {"list": {}, "show": {}},
132
+ },
133
+ # IoT Hub (read-only)
134
+ "iot": {"hub": {"list": {}, "show": {}}, "device": {"list": {}, "show": {}}},
135
+ # Logic Apps (read-only)
136
+ "logic": {"workflow": {"list": {}, "show": {}}},
137
+ # Functions (read-only)
138
+ "functionapp": {"list": {}, "show": {}, "config": {"show": {}}},
139
+ # Redis Cache (read-only)
140
+ "redis": {"list": {}, "show": {}},
141
+ # Search (read-only)
142
+ "search": {"service": {"list": {}, "show": {}}},
143
+ # API Management (read-only)
144
+ "apim": {"list": {}, "show": {}, "api": {"list": {}, "show": {}}},
145
+ # Help and information
146
+ "help": {},
147
+ "version": {},
148
+ # Completion
149
+ "completion": {},
150
+ }
151
+
152
+ # Blocked Azure operations (state-modifying or dangerous)
153
+ DENIED_AZURE_COMMANDS: dict[str, dict] = {
154
+ # Account and subscription management
155
+ "account": {
156
+ "set": {},
157
+ "clear": {},
158
+ "get-access-token": {}, # Returns sensitive tokens
159
+ },
160
+ # Resource lifecycle operations
161
+ "group": {"create": {}, "delete": {}, "update": {}},
162
+ "resource": {
163
+ "create": {},
164
+ "delete": {},
165
+ "update": {},
166
+ "move": {},
167
+ "tag": {},
168
+ "untag": {},
169
+ "lock": {},
170
+ "unlock": {},
171
+ },
172
+ # VM operations
173
+ "vm": {
174
+ "create": {},
175
+ "delete": {},
176
+ "start": {},
177
+ "stop": {},
178
+ "restart": {},
179
+ "deallocate": {},
180
+ "capture": {},
181
+ "generalize": {},
182
+ "resize": {},
183
+ "redeploy": {},
184
+ "reapply": {},
185
+ "run-command": {}, # Executes commands on VMs
186
+ "attach": {},
187
+ "detach": {},
188
+ },
189
+ # Storage operations
190
+ "storage": {
191
+ "account": {
192
+ "create": {},
193
+ "delete": {},
194
+ "update": {},
195
+ "keys": {},
196
+ },
197
+ "container": {"create": {}, "delete": {}},
198
+ "blob": {
199
+ "upload": {},
200
+ "download": {},
201
+ "delete": {},
202
+ "copy": {},
203
+ "sync": {},
204
+ "generate-sas": {}, # Generates access signatures
205
+ },
206
+ "share": {"create": {}, "delete": {}},
207
+ "queue": {"create": {}, "delete": {}},
208
+ "table": {"create": {}, "delete": {}},
209
+ },
210
+ # Network operations
211
+ "network": {
212
+ "vnet": {
213
+ "create": {},
214
+ "delete": {},
215
+ "update": {},
216
+ "subnet": {"create": {}, "delete": {}, "update": {}},
217
+ },
218
+ "nsg": {
219
+ "create": {},
220
+ "delete": {},
221
+ "update": {},
222
+ "rule": {"create": {}, "delete": {}, "update": {}},
223
+ },
224
+ "public-ip": {"create": {}, "delete": {}, "update": {}},
225
+ "lb": {"create": {}, "delete": {}, "update": {}},
226
+ },
227
+ # AKS operations
228
+ "aks": {
229
+ "create": {},
230
+ "delete": {},
231
+ "scale": {},
232
+ "upgrade": {},
233
+ "rotate-certs": {},
234
+ "get-credentials": {}, # Downloads sensitive kubeconfig
235
+ "install-cli": {},
236
+ "browse": {},
237
+ "enable-addons": {},
238
+ "disable-addons": {},
239
+ "nodepool": {"add": {}, "delete": {}, "scale": {}, "upgrade": {}},
240
+ },
241
+ # Key Vault (sensitive operations)
242
+ "keyvault": {
243
+ "create": {},
244
+ "delete": {},
245
+ "purge": {},
246
+ "recover": {},
247
+ "set-policy": {},
248
+ "delete-policy": {},
249
+ "secret": {}, # All secret operations are sensitive
250
+ "key": {}, # All key operations are sensitive
251
+ "certificate": {}, # All certificate operations are sensitive
252
+ },
253
+ # App Service operations
254
+ "webapp": {
255
+ "create": {},
256
+ "delete": {},
257
+ "restart": {},
258
+ "start": {},
259
+ "stop": {},
260
+ "deploy": {},
261
+ "config": {
262
+ "set": {},
263
+ "appsettings": {"set": {}, "delete": {}},
264
+ "connection-string": {"set": {}, "delete": {}},
265
+ },
266
+ },
267
+ "appservice": {"plan": {"create": {}, "delete": {}, "update": {}}},
268
+ # Database operations
269
+ "sql": {
270
+ "server": {"create": {}, "delete": {}, "update": {}},
271
+ "db": {
272
+ "create": {},
273
+ "delete": {},
274
+ "restore": {},
275
+ "import": {},
276
+ "export": {},
277
+ "failover": {},
278
+ },
279
+ },
280
+ # Authentication and authorization
281
+ "login": {},
282
+ "logout": {},
283
+ "ad": {}, # Active Directory operations can be sensitive
284
+ "role": {}, # Role assignments modify permissions
285
+ # Extensions and configuration
286
+ "extension": {},
287
+ "configure": {},
288
+ "feedback": {},
289
+ "find": {},
290
+ "upgrade": {},
291
+ # Deployment and ARM operations
292
+ "deployment": {},
293
+ "policy": {},
294
+ "managedapp": {},
295
+ "feature": {},
296
+ "provider": {},
297
+ "snapshot": {},
298
+ "image": {},
299
+ "sig": {}, # Shared Image Gallery operations
300
+ # Backup and recovery
301
+ "backup": {},
302
+ "restore": {},
303
+ # CDN operations
304
+ "cdn": {
305
+ "profile": {"create": {}, "delete": {}},
306
+ "endpoint": {
307
+ "create": {},
308
+ "delete": {},
309
+ "purge": {}, # Purges CDN content
310
+ },
311
+ },
312
+ # DevOps and CI/CD
313
+ "devops": {},
314
+ "repos": {},
315
+ "artifacts": {},
316
+ "boards": {},
317
+ "pipelines": {},
318
+ # Monitoring operations that expose credentials
319
+ "monitor": {
320
+ "log-analytics": {
321
+ "workspace": {
322
+ "get-shared-keys": {}, # Exposes sensitive workspace keys
323
+ },
324
+ },
325
+ },
326
+ # Function App operations
327
+ "functionapp": {
328
+ "invoke": {}, # Function invocation
329
+ },
330
+ # Batch operations
331
+ "batch": {
332
+ "execute": {}, # Command execution
333
+ "job": {
334
+ "run": {}, # Running tasks/jobs
335
+ "submit": {}, # Job submission
336
+ "cancel": {}, # Canceling operations
337
+ },
338
+ },
339
+ }
@@ -1,14 +1,13 @@
1
1
  # Bash commands
2
2
 
3
- The tool `run_bash_command` allows you to run many kubectl commands:
3
+ The tool `run_bash_command` allows you to run the following commands:
4
+ - `kubectl get|describe|logs|top|events [options]`
5
+ - `aws <service> <operation> [options]` (read-only)
6
+ - `az|docker|helm|argocd [options]`
7
+ - `grep|cut|sort|uniq|head|tail|wc|tr|base64|jq|sed [options]`
4
8
 
5
- - `kubectl get ...`
6
- - `kubectl describe ...`
7
- - `kubectl events ...`
8
- - `kubectl top ...`
9
+ Commands can be piped with `|`. No `&&` support.
9
10
 
10
- It is also possible to combine `kubectl` with `grep`. For example:
11
- - `kubectl get pods | grep holmes`
12
11
 
13
12
  The tool `kubectl_run_image` will run an image:
14
13
  - `kubectl run <name> --image=<image> --rm --attach --restart=Never --i --tty -- <command>`
@@ -6,7 +6,10 @@ import re
6
6
  import string
7
7
  from typing import Dict, Any, Optional
8
8
 
9
+ import sentry_sdk
9
10
 
11
+
12
+ from holmes.common.env_vars import BASH_TOOL_UNSAFE_ALLOW_ALL
10
13
  from holmes.core.tools import (
11
14
  CallablePrerequisite,
12
15
  StructuredToolResult,
@@ -92,9 +95,29 @@ class KubectlRunImageCommand(BaseBashTool):
92
95
  params=params,
93
96
  )
94
97
 
95
- validate_image_and_commands(
96
- image=image, container_command=command_str, config=self.toolset.config
97
- )
98
+ try:
99
+ validate_image_and_commands(
100
+ image=image, container_command=command_str, config=self.toolset.config
101
+ )
102
+ except ValueError as e:
103
+ # Report unsafe kubectl run command attempt to Sentry
104
+ sentry_sdk.capture_event(
105
+ {
106
+ "message": f"Unsafe kubectl run command attempted: {image}",
107
+ "level": "warning",
108
+ "extra": {
109
+ "image": image,
110
+ "command": command_str,
111
+ "namespace": namespace,
112
+ "error": str(e),
113
+ },
114
+ }
115
+ )
116
+ return StructuredToolResult(
117
+ status=ToolResultStatus.ERROR,
118
+ error=str(e),
119
+ params=params,
120
+ )
98
121
 
99
122
  pod_name = (
100
123
  "holmesgpt-debug-pod-"
@@ -154,18 +177,29 @@ class RunBashCommand(BaseBashTool):
154
177
  error=f"The 'command' parameter must be a string, got {type(command_str).__name__}.",
155
178
  params=params,
156
179
  )
180
+
181
+ command_to_execute = command_str
157
182
  try:
158
- safe_command_str = make_command_safe(command_str, self.toolset.config)
159
- return execute_bash_command(
160
- cmd=safe_command_str, timeout=timeout, params=params
161
- )
183
+ command_to_execute = make_command_safe(command_str, self.toolset.config)
184
+
162
185
  except (argparse.ArgumentError, ValueError) as e:
163
- logging.info(f"Refusing LLM tool call {command_str}", exc_info=True)
164
- return StructuredToolResult(
165
- status=ToolResultStatus.ERROR,
166
- error=f"Refusing to execute bash command. Only some commands are supported and this is likely because requested command is unsupported. Error: {str(e)}",
167
- params=params,
168
- )
186
+ with sentry_sdk.configure_scope() as scope:
187
+ scope.set_extra("command", command_str)
188
+ scope.set_extra("error", str(e))
189
+ scope.set_extra("unsafe_allow_all", BASH_TOOL_UNSAFE_ALLOW_ALL)
190
+ sentry_sdk.capture_exception(e)
191
+
192
+ if not BASH_TOOL_UNSAFE_ALLOW_ALL:
193
+ logging.info(f"Refusing LLM tool call {command_str}")
194
+ return StructuredToolResult(
195
+ status=ToolResultStatus.ERROR,
196
+ error=f"Refusing to execute bash command. Only some commands are supported and this is likely because requested command is unsupported. Error: {str(e)}",
197
+ params=params,
198
+ )
199
+
200
+ return execute_bash_command(
201
+ cmd=command_to_execute, timeout=timeout, params=params
202
+ )
169
203
 
170
204
  def get_parameterized_one_liner(self, params: Dict[str, Any]) -> str:
171
205
  command = params.get("command", "N/A")
@@ -0,0 +1,131 @@
1
+ from abc import ABC, abstractmethod
2
+ import argparse
3
+ from typing import Any, Optional
4
+
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 BashCommand(ABC):
10
+ """Abstract base class for bash command implementations."""
11
+
12
+ def __init__(self, name: str):
13
+ super().__init__()
14
+ self.name = name
15
+
16
+ @abstractmethod
17
+ def add_parser(self, parent_parser: Any):
18
+ """Return the argument parser for this command."""
19
+ pass
20
+
21
+ @abstractmethod
22
+ def validate_command(
23
+ self, command: Any, original_command: str, config: Optional[BashExecutorConfig]
24
+ ) -> None:
25
+ """
26
+ Validate the parsed command to ensure it's safe.
27
+ Raises ValueError if validation fails.
28
+ """
29
+ pass
30
+
31
+ @abstractmethod
32
+ def stringify_command(
33
+ self, command: Any, original_command: str, config: Optional[BashExecutorConfig]
34
+ ) -> str:
35
+ """
36
+ Convert the parsed command back to a safe command string.
37
+ """
38
+ pass
39
+
40
+
41
+ class SimpleBashCommand(BashCommand):
42
+ def __init__(
43
+ self,
44
+ name: str,
45
+ allowed_options: Optional[list[str]] = None,
46
+ denied_options: Optional[list[str]] = None,
47
+ ):
48
+ """
49
+ A simple bash command that works with a whitelist/blacklist of options
50
+ If allowed_options is not empty, an option MUST be present in the allowed_options to be allowed
51
+ If denied_options is not empty, an option MUST NOT be present in the denied_options to be allowed
52
+ """
53
+ super().__init__(name)
54
+ self.allowed_options = allowed_options or []
55
+ self.denied_options = denied_options or []
56
+
57
+ def add_parser(self, parent_parser: Any):
58
+ parser = parent_parser.add_parser(
59
+ self.name,
60
+ exit_on_error=False,
61
+ add_help=False, # Disable help to avoid conflicts
62
+ prefix_chars="\x00", # Use null character as prefix to disable option parsing
63
+ )
64
+
65
+ parser.add_argument(
66
+ "options",
67
+ nargs=argparse.REMAINDER,
68
+ default=[],
69
+ )
70
+ return parser
71
+
72
+ def validate_command(self, command, original_command, config):
73
+ for option in command.options:
74
+ allowed = False if self.allowed_options else True
75
+
76
+ # Check allowed options
77
+ for allowed_option in self.allowed_options:
78
+ if option == allowed_option:
79
+ allowed = True
80
+ break
81
+
82
+ # Check denied options
83
+ denied = False
84
+ denied_error_message = None
85
+ for denied_option in self.denied_options:
86
+ # Check for exact match
87
+ if option == denied_option:
88
+ denied = True
89
+ denied_error_message = (
90
+ f"Option {option} is not allowed for security reasons"
91
+ )
92
+ break
93
+ # Check for long option equals-form variant (--option=value)
94
+ elif denied_option.startswith("--") and option.startswith(
95
+ denied_option + "="
96
+ ):
97
+ denied = True
98
+ denied_error_message = (
99
+ f"Option {option} is not allowed for security reasons"
100
+ )
101
+ break
102
+ # Check for short option with attached value (-Tvalue)
103
+ elif (
104
+ denied_option.startswith("-")
105
+ and not denied_option.startswith("--")
106
+ and len(denied_option) == 2
107
+ and option.startswith(denied_option)
108
+ and len(option) > 2
109
+ ):
110
+ denied = True
111
+ denied_error_message = (
112
+ f"Option {option} is not allowed for security reasons"
113
+ )
114
+ break
115
+
116
+ # Raise errors with appropriate messages
117
+ if denied:
118
+ raise ValueError(denied_error_message)
119
+ elif not allowed:
120
+ raise ValueError(
121
+ f"option {option} is not part of the allowed options: {self.allowed_options}"
122
+ )
123
+
124
+ def stringify_command(
125
+ self, command: Any, original_command: str, config: Optional[BashExecutorConfig]
126
+ ) -> str:
127
+ parts = [self.name]
128
+
129
+ parts.extend(command.options)
130
+
131
+ return " ".join(escape_shell_args(parts))
@@ -1,12 +1,17 @@
1
1
  import shlex
2
+ import re
2
3
 
3
4
  SAFE_SHELL_CHARS = frozenset(".-_=/,:")
4
5
 
6
+ # POSIX character class pattern for tr command
7
+ POSIX_CHAR_CLASS_PATTERN = re.compile(r"^\[:[-\w]+:\]$")
8
+
5
9
 
6
10
  def escape_shell_args(args: list[str]) -> list[str]:
7
11
  """
8
12
  Escape shell arguments to prevent injection.
9
- Uses shlex.quote for safe shell argument quoting.
13
+ Uses manual quoting with single/double quotes as the primary approach,
14
+ falling back to shlex.quote for complex cases with nested quotes.
10
15
  """
11
16
  escaped_args = []
12
17
 
@@ -18,6 +23,14 @@ def escape_shell_args(args: list[str]) -> list[str]:
18
23
  # If argument starts with -- or - (flag), no escaping needed
19
24
  elif arg.startswith("-"):
20
25
  escaped_args.append(arg)
26
+ # POSIX character classes for tr command (e.g., [:lower:], [:upper:], [:digit:])
27
+ elif POSIX_CHAR_CLASS_PATTERN.match(arg):
28
+ escaped_args.append("'" + arg + "'")
29
+ # Avoid using shlex in case as it does not handle nested quotes well. e.g. "foo='bar'"
30
+ elif "'" not in arg:
31
+ escaped_args.append("'" + arg + "'")
32
+ elif '"' not in arg:
33
+ escaped_args.append('"' + arg + '"')
21
34
  # For everything else, use shlex.quote for proper escaping
22
35
  else:
23
36
  escaped_args.append(shlex.quote(arg))