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.
- holmes/__init__.py +1 -1
- holmes/common/env_vars.py +4 -0
- holmes/core/llm.py +3 -1
- holmes/core/tool_calling_llm.py +112 -11
- holmes/core/toolset_manager.py +1 -5
- holmes/core/tracing.py +1 -1
- holmes/main.py +7 -1
- holmes/plugins/prompts/_fetch_logs.jinja2 +4 -0
- holmes/plugins/runbooks/CLAUDE.md +85 -0
- holmes/plugins/runbooks/README.md +24 -0
- holmes/plugins/toolsets/bash/argocd/__init__.py +65 -0
- holmes/plugins/toolsets/bash/argocd/constants.py +120 -0
- holmes/plugins/toolsets/bash/aws/__init__.py +66 -0
- holmes/plugins/toolsets/bash/aws/constants.py +529 -0
- holmes/plugins/toolsets/bash/azure/__init__.py +56 -0
- holmes/plugins/toolsets/bash/azure/constants.py +339 -0
- holmes/plugins/toolsets/bash/bash_instructions.jinja2 +6 -7
- holmes/plugins/toolsets/bash/bash_toolset.py +47 -13
- holmes/plugins/toolsets/bash/common/bash_command.py +131 -0
- holmes/plugins/toolsets/bash/common/stringify.py +14 -1
- holmes/plugins/toolsets/bash/common/validators.py +91 -0
- holmes/plugins/toolsets/bash/docker/__init__.py +59 -0
- holmes/plugins/toolsets/bash/docker/constants.py +255 -0
- holmes/plugins/toolsets/bash/helm/__init__.py +61 -0
- holmes/plugins/toolsets/bash/helm/constants.py +92 -0
- holmes/plugins/toolsets/bash/kubectl/__init__.py +80 -79
- holmes/plugins/toolsets/bash/kubectl/constants.py +0 -14
- holmes/plugins/toolsets/bash/kubectl/kubectl_describe.py +38 -56
- holmes/plugins/toolsets/bash/kubectl/kubectl_events.py +28 -76
- holmes/plugins/toolsets/bash/kubectl/kubectl_get.py +39 -99
- holmes/plugins/toolsets/bash/kubectl/kubectl_logs.py +34 -15
- holmes/plugins/toolsets/bash/kubectl/kubectl_run.py +1 -1
- holmes/plugins/toolsets/bash/kubectl/kubectl_top.py +38 -77
- holmes/plugins/toolsets/bash/parse_command.py +106 -32
- holmes/plugins/toolsets/bash/utilities/__init__.py +0 -0
- holmes/plugins/toolsets/bash/utilities/base64_util.py +12 -0
- holmes/plugins/toolsets/bash/utilities/cut.py +12 -0
- holmes/plugins/toolsets/bash/utilities/grep/__init__.py +10 -0
- holmes/plugins/toolsets/bash/utilities/head.py +12 -0
- holmes/plugins/toolsets/bash/utilities/jq.py +79 -0
- holmes/plugins/toolsets/bash/utilities/sed.py +164 -0
- holmes/plugins/toolsets/bash/utilities/sort.py +15 -0
- holmes/plugins/toolsets/bash/utilities/tail.py +12 -0
- holmes/plugins/toolsets/bash/utilities/tr.py +57 -0
- holmes/plugins/toolsets/bash/utilities/uniq.py +12 -0
- holmes/plugins/toolsets/bash/utilities/wc.py +12 -0
- holmes/plugins/toolsets/prometheus/prometheus.py +42 -12
- holmes/utils/console/logging.py +6 -1
- {holmesgpt-0.13.0.dist-info → holmesgpt-0.13.1.dist-info}/METADATA +1 -1
- {holmesgpt-0.13.0.dist-info → holmesgpt-0.13.1.dist-info}/RECORD +53 -30
- holmes/plugins/toolsets/bash/grep/__init__.py +0 -52
- {holmesgpt-0.13.0.dist-info → holmesgpt-0.13.1.dist-info}/LICENSE.txt +0 -0
- {holmesgpt-0.13.0.dist-info → holmesgpt-0.13.1.dist-info}/WHEEL +0 -0
- {holmesgpt-0.13.0.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
|
|
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
|
-
|
|
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
|
-
|
|
96
|
-
|
|
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
|
-
|
|
159
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
|
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))
|