holmesgpt 0.13.0__py3-none-any.whl → 0.13.2__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.
Files changed (118) hide show
  1. holmes/__init__.py +1 -1
  2. holmes/common/env_vars.py +11 -0
  3. holmes/config.py +3 -1
  4. holmes/core/conversations.py +0 -11
  5. holmes/core/investigation.py +0 -6
  6. holmes/core/llm.py +63 -2
  7. holmes/core/prompt.py +0 -2
  8. holmes/core/supabase_dal.py +2 -2
  9. holmes/core/todo_tasks_formatter.py +51 -0
  10. holmes/core/tool_calling_llm.py +277 -101
  11. holmes/core/tools.py +20 -4
  12. holmes/core/toolset_manager.py +1 -5
  13. holmes/core/tracing.py +1 -1
  14. holmes/interactive.py +63 -2
  15. holmes/main.py +7 -2
  16. holmes/plugins/prompts/_fetch_logs.jinja2 +4 -0
  17. holmes/plugins/prompts/_general_instructions.jinja2 +3 -1
  18. holmes/plugins/prompts/investigation_procedure.jinja2 +3 -13
  19. holmes/plugins/runbooks/CLAUDE.md +85 -0
  20. holmes/plugins/runbooks/README.md +24 -0
  21. holmes/plugins/toolsets/__init__.py +5 -1
  22. holmes/plugins/toolsets/argocd.yaml +1 -1
  23. holmes/plugins/toolsets/atlas_mongodb/mongodb_atlas.py +18 -6
  24. holmes/plugins/toolsets/aws.yaml +9 -5
  25. holmes/plugins/toolsets/azure_sql/tools/analyze_connection_failures.py +3 -1
  26. holmes/plugins/toolsets/azure_sql/tools/analyze_database_connections.py +3 -1
  27. holmes/plugins/toolsets/azure_sql/tools/analyze_database_health_status.py +3 -1
  28. holmes/plugins/toolsets/azure_sql/tools/analyze_database_performance.py +3 -1
  29. holmes/plugins/toolsets/azure_sql/tools/analyze_database_storage.py +3 -1
  30. holmes/plugins/toolsets/azure_sql/tools/get_active_alerts.py +3 -1
  31. holmes/plugins/toolsets/azure_sql/tools/get_slow_queries.py +3 -1
  32. holmes/plugins/toolsets/azure_sql/tools/get_top_cpu_queries.py +3 -1
  33. holmes/plugins/toolsets/azure_sql/tools/get_top_data_io_queries.py +3 -1
  34. holmes/plugins/toolsets/azure_sql/tools/get_top_log_io_queries.py +3 -1
  35. holmes/plugins/toolsets/bash/argocd/__init__.py +65 -0
  36. holmes/plugins/toolsets/bash/argocd/constants.py +120 -0
  37. holmes/plugins/toolsets/bash/aws/__init__.py +66 -0
  38. holmes/plugins/toolsets/bash/aws/constants.py +529 -0
  39. holmes/plugins/toolsets/bash/azure/__init__.py +56 -0
  40. holmes/plugins/toolsets/bash/azure/constants.py +339 -0
  41. holmes/plugins/toolsets/bash/bash_instructions.jinja2 +6 -7
  42. holmes/plugins/toolsets/bash/bash_toolset.py +62 -17
  43. holmes/plugins/toolsets/bash/common/bash_command.py +131 -0
  44. holmes/plugins/toolsets/bash/common/stringify.py +14 -1
  45. holmes/plugins/toolsets/bash/common/validators.py +91 -0
  46. holmes/plugins/toolsets/bash/docker/__init__.py +59 -0
  47. holmes/plugins/toolsets/bash/docker/constants.py +255 -0
  48. holmes/plugins/toolsets/bash/helm/__init__.py +61 -0
  49. holmes/plugins/toolsets/bash/helm/constants.py +92 -0
  50. holmes/plugins/toolsets/bash/kubectl/__init__.py +80 -79
  51. holmes/plugins/toolsets/bash/kubectl/constants.py +0 -14
  52. holmes/plugins/toolsets/bash/kubectl/kubectl_describe.py +38 -56
  53. holmes/plugins/toolsets/bash/kubectl/kubectl_events.py +28 -76
  54. holmes/plugins/toolsets/bash/kubectl/kubectl_get.py +39 -99
  55. holmes/plugins/toolsets/bash/kubectl/kubectl_logs.py +34 -15
  56. holmes/plugins/toolsets/bash/kubectl/kubectl_run.py +1 -1
  57. holmes/plugins/toolsets/bash/kubectl/kubectl_top.py +38 -77
  58. holmes/plugins/toolsets/bash/parse_command.py +106 -32
  59. holmes/plugins/toolsets/bash/utilities/__init__.py +0 -0
  60. holmes/plugins/toolsets/bash/utilities/base64_util.py +12 -0
  61. holmes/plugins/toolsets/bash/utilities/cut.py +12 -0
  62. holmes/plugins/toolsets/bash/utilities/grep/__init__.py +10 -0
  63. holmes/plugins/toolsets/bash/utilities/head.py +12 -0
  64. holmes/plugins/toolsets/bash/utilities/jq.py +79 -0
  65. holmes/plugins/toolsets/bash/utilities/sed.py +164 -0
  66. holmes/plugins/toolsets/bash/utilities/sort.py +15 -0
  67. holmes/plugins/toolsets/bash/utilities/tail.py +12 -0
  68. holmes/plugins/toolsets/bash/utilities/tr.py +57 -0
  69. holmes/plugins/toolsets/bash/utilities/uniq.py +12 -0
  70. holmes/plugins/toolsets/bash/utilities/wc.py +12 -0
  71. holmes/plugins/toolsets/confluence.yaml +1 -1
  72. holmes/plugins/toolsets/coralogix/api.py +3 -1
  73. holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py +4 -4
  74. holmes/plugins/toolsets/coralogix/utils.py +41 -14
  75. holmes/plugins/toolsets/datadog/datadog_api.py +45 -2
  76. holmes/plugins/toolsets/datadog/datadog_general_instructions.jinja2 +208 -0
  77. holmes/plugins/toolsets/datadog/datadog_logs_instructions.jinja2 +43 -0
  78. holmes/plugins/toolsets/datadog/datadog_metrics_instructions.jinja2 +12 -9
  79. holmes/plugins/toolsets/datadog/toolset_datadog_general.py +722 -0
  80. holmes/plugins/toolsets/datadog/toolset_datadog_logs.py +17 -6
  81. holmes/plugins/toolsets/datadog/toolset_datadog_metrics.py +15 -7
  82. holmes/plugins/toolsets/datadog/toolset_datadog_rds.py +6 -2
  83. holmes/plugins/toolsets/datadog/toolset_datadog_traces.py +9 -3
  84. holmes/plugins/toolsets/docker.yaml +1 -1
  85. holmes/plugins/toolsets/git.py +15 -5
  86. holmes/plugins/toolsets/grafana/toolset_grafana.py +25 -4
  87. holmes/plugins/toolsets/grafana/toolset_grafana_loki.py +4 -4
  88. holmes/plugins/toolsets/grafana/toolset_grafana_tempo.jinja2 +5 -3
  89. holmes/plugins/toolsets/grafana/toolset_grafana_tempo.py +299 -32
  90. holmes/plugins/toolsets/helm.yaml +1 -1
  91. holmes/plugins/toolsets/internet/internet.py +4 -2
  92. holmes/plugins/toolsets/internet/notion.py +4 -2
  93. holmes/plugins/toolsets/investigator/core_investigation.py +5 -17
  94. holmes/plugins/toolsets/investigator/investigator_instructions.jinja2 +1 -5
  95. holmes/plugins/toolsets/kafka.py +19 -7
  96. holmes/plugins/toolsets/kubernetes.yaml +5 -5
  97. holmes/plugins/toolsets/kubernetes_logs.py +4 -4
  98. holmes/plugins/toolsets/kubernetes_logs.yaml +1 -1
  99. holmes/plugins/toolsets/logging_utils/logging_api.py +15 -2
  100. holmes/plugins/toolsets/mcp/toolset_mcp.py +3 -1
  101. holmes/plugins/toolsets/newrelic.py +8 -4
  102. holmes/plugins/toolsets/opensearch/opensearch.py +13 -5
  103. holmes/plugins/toolsets/opensearch/opensearch_logs.py +4 -4
  104. holmes/plugins/toolsets/opensearch/opensearch_traces.py +9 -6
  105. holmes/plugins/toolsets/prometheus/prometheus.py +198 -57
  106. holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py +7 -3
  107. holmes/plugins/toolsets/robusta/robusta.py +10 -4
  108. holmes/plugins/toolsets/runbook/runbook_fetcher.py +4 -2
  109. holmes/plugins/toolsets/servicenow/servicenow.py +9 -3
  110. holmes/plugins/toolsets/slab.yaml +1 -1
  111. holmes/utils/console/logging.py +6 -1
  112. {holmesgpt-0.13.0.dist-info → holmesgpt-0.13.2.dist-info}/METADATA +3 -2
  113. {holmesgpt-0.13.0.dist-info → holmesgpt-0.13.2.dist-info}/RECORD +116 -90
  114. holmes/core/todo_manager.py +0 -88
  115. holmes/plugins/toolsets/bash/grep/__init__.py +0 -52
  116. {holmesgpt-0.13.0.dist-info → holmesgpt-0.13.2.dist-info}/LICENSE.txt +0 -0
  117. {holmesgpt-0.13.0.dist-info → holmesgpt-0.13.2.dist-info}/WHEEL +0 -0
  118. {holmesgpt-0.13.0.dist-info → holmesgpt-0.13.2.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,12 @@ 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 (
13
+ BASH_TOOL_UNSAFE_ALLOW_ALL,
14
+ )
10
15
  from holmes.core.tools import (
11
16
  CallablePrerequisite,
12
17
  StructuredToolResult,
@@ -77,7 +82,9 @@ class KubectlRunImageCommand(BaseBashTool):
77
82
  command_str = get_param_or_raise(params, "command")
78
83
  return f"kubectl run {pod_name} --image={image} --namespace={namespace} --rm --attach --restart=Never -i -- {command_str}"
79
84
 
80
- def _invoke(self, params: Dict[str, Any]) -> StructuredToolResult:
85
+ def _invoke(
86
+ self, params: dict, user_approved: bool = False
87
+ ) -> StructuredToolResult:
81
88
  timeout = params.get("timeout", 60)
82
89
 
83
90
  image = get_param_or_raise(params, "image")
@@ -92,9 +99,29 @@ class KubectlRunImageCommand(BaseBashTool):
92
99
  params=params,
93
100
  )
94
101
 
95
- validate_image_and_commands(
96
- image=image, container_command=command_str, config=self.toolset.config
97
- )
102
+ try:
103
+ validate_image_and_commands(
104
+ image=image, container_command=command_str, config=self.toolset.config
105
+ )
106
+ except ValueError as e:
107
+ # Report unsafe kubectl run command attempt to Sentry
108
+ sentry_sdk.capture_event(
109
+ {
110
+ "message": f"Unsafe kubectl run command attempted: {image}",
111
+ "level": "warning",
112
+ "extra": {
113
+ "image": image,
114
+ "command": command_str,
115
+ "namespace": namespace,
116
+ "error": str(e),
117
+ },
118
+ }
119
+ )
120
+ return StructuredToolResult(
121
+ status=ToolResultStatus.ERROR,
122
+ error=str(e),
123
+ params=params,
124
+ )
98
125
 
99
126
  pod_name = (
100
127
  "holmesgpt-debug-pod-"
@@ -137,7 +164,9 @@ class RunBashCommand(BaseBashTool):
137
164
  toolset=toolset,
138
165
  )
139
166
 
140
- def _invoke(self, params: Dict[str, Any]) -> StructuredToolResult:
167
+ def _invoke(
168
+ self, params: dict, user_approved: bool = False
169
+ ) -> StructuredToolResult:
141
170
  command_str = params.get("command")
142
171
  timeout = params.get("timeout", 60)
143
172
 
@@ -154,18 +183,34 @@ class RunBashCommand(BaseBashTool):
154
183
  error=f"The 'command' parameter must be a string, got {type(command_str).__name__}.",
155
184
  params=params,
156
185
  )
157
- 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
- )
162
- 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
+
187
+ command_to_execute = command_str
188
+
189
+ # Only run the safety check if user has NOT approved the command
190
+ if not user_approved:
191
+ try:
192
+ command_to_execute = make_command_safe(command_str, self.toolset.config)
193
+
194
+ except (argparse.ArgumentError, ValueError) as e:
195
+ with sentry_sdk.configure_scope() as scope:
196
+ scope.set_extra("command", command_str)
197
+ scope.set_extra("error", str(e))
198
+ scope.set_extra("unsafe_allow_all", BASH_TOOL_UNSAFE_ALLOW_ALL)
199
+ sentry_sdk.capture_exception(e)
200
+
201
+ if not BASH_TOOL_UNSAFE_ALLOW_ALL:
202
+ logging.info(f"Refusing LLM tool call {command_str}")
203
+
204
+ return StructuredToolResult(
205
+ status=ToolResultStatus.APPROVAL_REQUIRED,
206
+ error=f"Refusing to execute bash command. {str(e)}",
207
+ params=params,
208
+ invocation=command_str,
209
+ )
210
+
211
+ return execute_bash_command(
212
+ cmd=command_to_execute, timeout=timeout, params=params
213
+ )
169
214
 
170
215
  def get_parameterized_one_liner(self, params: Dict[str, Any]) -> str:
171
216
  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))