splunkctl 0.8.0__tar.gz → 0.10.0__tar.gz
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.
- {splunkctl-0.8.0 → splunkctl-0.10.0}/PKG-INFO +45 -22
- {splunkctl-0.8.0 → splunkctl-0.10.0}/README.md +44 -21
- splunkctl-0.10.0/splunkctl/__init__.py +3 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/splunkctl/client.py +5 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/splunkctl/commands/es.py +1 -1
- {splunkctl-0.8.0 → splunkctl-0.10.0}/splunkctl/mcp/server.py +124 -17
- {splunkctl-0.8.0 → splunkctl-0.10.0}/splunkctl/mcp/tools.py +107 -35
- splunkctl-0.10.0/splunkctl/soar/cimcef.py +254 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/splunkctl/soar/client.py +32 -12
- splunkctl-0.10.0/splunkctl/soar/web.py +137 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/splunkctl.egg-info/PKG-INFO +45 -22
- {splunkctl-0.8.0 → splunkctl-0.10.0}/splunkctl.egg-info/SOURCES.txt +2 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/tests/test_guard.py +33 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/tests/test_version.py +1 -1
- splunkctl-0.8.0/splunkctl/__init__.py +0 -3
- {splunkctl-0.8.0 → splunkctl-0.10.0}/LICENSE +0 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/pyproject.toml +0 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/setup.cfg +0 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/splunkctl/__main__.py +0 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/splunkctl/commands/__init__.py +0 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/splunkctl/commands/alerts.py +0 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/splunkctl/commands/apps.py +0 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/splunkctl/commands/audit.py +0 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/splunkctl/commands/audit_parse.py +0 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/splunkctl/commands/commands_meta.py +0 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/splunkctl/commands/common.py +0 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/splunkctl/commands/conf.py +0 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/splunkctl/commands/conf_ops.py +0 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/splunkctl/commands/config_cmd.py +0 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/splunkctl/commands/dashboards.py +0 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/splunkctl/commands/datamodels.py +0 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/splunkctl/commands/doctor.py +0 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/splunkctl/commands/hec.py +0 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/splunkctl/commands/indexes.py +0 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/splunkctl/commands/info.py +0 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/splunkctl/commands/inputs.py +0 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/splunkctl/commands/knowledge.py +0 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/splunkctl/commands/kvstore.py +0 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/splunkctl/commands/lookups.py +0 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/splunkctl/commands/lookups_wiring.py +0 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/splunkctl/commands/mcp_cmd.py +0 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/splunkctl/commands/parsers.py +0 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/splunkctl/commands/parsers_io.py +0 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/splunkctl/commands/rules.py +0 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/splunkctl/commands/rules_io.py +0 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/splunkctl/commands/search.py +0 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/splunkctl/commands/server.py +0 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/splunkctl/commands/server_topology.py +0 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/splunkctl/commands/state.py +0 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/splunkctl/commands/state_io.py +0 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/splunkctl/commands/state_io_blobs.py +0 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/splunkctl/commands/state_io_confs.py +0 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/splunkctl/commands/state_types.py +0 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/splunkctl/commands/users.py +0 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/splunkctl/config.py +0 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/splunkctl/errors.py +0 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/splunkctl/guard.py +0 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/splunkctl/main.py +0 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/splunkctl/mcp/__init__.py +0 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/splunkctl/mcp/resources.py +0 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/splunkctl/output.py +0 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/splunkctl/soar/__init__.py +0 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/splunkctl.egg-info/dependency_links.txt +0 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/splunkctl.egg-info/entry_points.txt +0 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/splunkctl.egg-info/requires.txt +0 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/splunkctl.egg-info/top_level.txt +0 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/tests/test_client.py +0 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/tests/test_config.py +0 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/tests/test_main_errors.py +0 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/tests/test_main_hoisting.py +0 -0
- {splunkctl-0.8.0 → splunkctl-0.10.0}/tests/test_output.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: splunkctl
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.0
|
|
4
4
|
Summary: CLI tool for Splunk Enterprise SIEM operations.
|
|
5
5
|
Author: dannyota
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -38,11 +38,11 @@ Dynamic: license-file
|
|
|
38
38
|
|
|
39
39
|
<div align="center">
|
|
40
40
|
|
|
41
|
-
<a href="https://splunk.danny.vn"><img src="docs/assets/banner.svg" alt="splunkctl — operate Splunk Enterprise as code" width="600"></a>
|
|
41
|
+
<a href="https://splunk.danny.vn"><img src="docs/assets/banner.svg" alt="splunkctl — operate Splunk Enterprise and Splunk SOAR as code" width="600"></a>
|
|
42
42
|
|
|
43
43
|
# splunkctl
|
|
44
44
|
|
|
45
|
-
**Operate Splunk Enterprise as code — for SOC teams, detection engineers, and AI agents.**
|
|
45
|
+
**Operate Splunk Enterprise and Splunk SOAR as code — for SOC teams, detection engineers, and AI agents.**
|
|
46
46
|
|
|
47
47
|
[Docs](https://splunk.danny.vn) · [Catalog](docs/design/catalog.md) · [Releases](https://github.com/dannyota/splunkctl/releases)
|
|
48
48
|
|
|
@@ -50,15 +50,19 @@ Dynamic: license-file
|
|
|
50
50
|
|
|
51
51
|
---
|
|
52
52
|
|
|
53
|
-
A Python CLI that queries, inspects, and manages
|
|
54
|
-
Enterprise
|
|
53
|
+
A Python CLI that queries, inspects, and manages **remote** Splunk
|
|
54
|
+
Enterprise and Splunk SOAR instances over their REST APIs. SIEM commands
|
|
55
|
+
are built on the
|
|
55
56
|
[splunk-sdk-python](https://github.com/dannyota/splunk-sdk-python/tree/splunkctl)
|
|
56
|
-
fork with [Click](https://click.palletsprojects.com/)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
57
|
+
fork with [Click](https://click.palletsprojects.com/); SOAR commands use
|
|
58
|
+
`SOARClient`, a requests-based Django REST client with dual auth (token +
|
|
59
|
+
Basic fallback). The SIEM core loop is **pull live state, review the diff,
|
|
60
|
+
push it back** -- one `state` engine covering rules, parsers, macros,
|
|
61
|
+
lookups, and dashboards, with a change-evidence report artifact for change
|
|
62
|
+
tickets. The SOAR surface covers ops visibility, container/artifact
|
|
63
|
+
lifecycle, the first playbooks-as-code loop anywhere, and SIEM-to-SOAR
|
|
64
|
+
ingest. Built for humans and LLM agents alike: deterministic flags,
|
|
65
|
+
`--json` everywhere, structured error envelopes, and a built-in MCP server
|
|
62
66
|
(`splunkctl mcp serve`) with progressive tool discovery.
|
|
63
67
|
|
|
64
68
|
> **Every mutation is dry-run by default.** Nothing changes until you pass
|
|
@@ -90,8 +94,23 @@ structured error envelopes, and a built-in MCP server
|
|
|
90
94
|
`--limit`/`--offset`/`--filter` on every list surface, multi-instance
|
|
91
95
|
profiles with a bank-safety guard banner
|
|
92
96
|
(`(profile: uat @ host:port)`).
|
|
93
|
-
- **
|
|
94
|
-
|
|
97
|
+
- **SOAR ops** — `soar` command tree: containers, artifacts, vault, notes,
|
|
98
|
+
cases/workbooks, approvals, custom lists, indicators, evidence, users,
|
|
99
|
+
roles, audit, and cross-object search. Full lifecycle: create, update,
|
|
100
|
+
close, assign, delete -- with the same dry-run guard and typed error
|
|
101
|
+
envelopes.
|
|
102
|
+
- **Playbooks as code** — `soar playbooks export --unpack` / `import`
|
|
103
|
+
round-trips playbook tgz bundles (the first such tool anywhere);
|
|
104
|
+
`soar playbooks delete` removes them through the Web UI route (no
|
|
105
|
+
REST deletion exists); `soar playbooks run --wait` drives and polls
|
|
106
|
+
runs to completion; `soar actions run --wait` does the same for
|
|
107
|
+
connector actions.
|
|
108
|
+
- **SIEM-to-SOAR ingest** — `soar ingest --spl` runs a SIEM search and
|
|
109
|
+
creates SOAR containers + typed CEF artifacts using the official CIM-to-CEF
|
|
110
|
+
field map, with SDI dedup, severity mapping, and last-artifact automation
|
|
111
|
+
batching.
|
|
112
|
+
- **Built for agents** — built-in MCP server with 237 auto-generated tools,
|
|
113
|
+
progressive discovery (5 meta-tools + focus/unfocus), 33 guide resources,
|
|
95
114
|
guard markers on every mutation, dual output (TTY = table, pipe = JSON).
|
|
96
115
|
|
|
97
116
|
## Install
|
|
@@ -119,10 +138,12 @@ splunkctl --version
|
|
|
119
138
|
## Quickstart
|
|
120
139
|
|
|
121
140
|
```bash
|
|
122
|
-
splunkctl config init # interactive setup
|
|
123
|
-
splunkctl
|
|
124
|
-
splunkctl
|
|
125
|
-
splunkctl
|
|
141
|
+
splunkctl config init # interactive SIEM setup
|
|
142
|
+
splunkctl config init --soar # add SOAR credentials
|
|
143
|
+
splunkctl doctor # check SIEM connection
|
|
144
|
+
splunkctl soar test # check SOAR connection
|
|
145
|
+
splunkctl search run 'index=main | head 10' # run a SIEM search
|
|
146
|
+
splunkctl soar containers list # list SOAR containers
|
|
126
147
|
splunkctl commands --json # discover every verb
|
|
127
148
|
```
|
|
128
149
|
|
|
@@ -174,6 +195,7 @@ splunkctl state push --dir config/ --report r.json --yes # deploy + evidence
|
|
|
174
195
|
| `tags` | Tags — list, get |
|
|
175
196
|
| `datamodels` | Data model definitions + acceleration health |
|
|
176
197
|
| `state` | Config-as-code pull/diff/push with change-evidence reports |
|
|
198
|
+
| `soar` | SOAR: containers, artifacts, vault, playbooks, actions, cases, ingest |
|
|
177
199
|
| `commands` | Machine-readable command tree (JSON) |
|
|
178
200
|
| `mcp` | Built-in MCP server for AI agent integration |
|
|
179
201
|
|
|
@@ -245,11 +267,12 @@ splunkctl mcp install # register in .mcp.json
|
|
|
245
267
|
splunkctl mcp serve # start stdio MCP server
|
|
246
268
|
```
|
|
247
269
|
|
|
248
|
-
The MCP server auto-generates
|
|
249
|
-
with progressive discovery
|
|
250
|
-
`usage`, `focus`, `unfocus`, `run`) and dynamically load typed
|
|
251
|
-
command group.
|
|
252
|
-
|
|
270
|
+
The MCP server auto-generates 229 typed tools from the Click command tree
|
|
271
|
+
(SIEM + SOAR) with progressive discovery -- agents start with 5 meta-tools
|
|
272
|
+
(`help`, `usage`, `focus`, `unfocus`, `run`) and dynamically load typed
|
|
273
|
+
schemas per command group. Subgroup-granular focus works for nested groups
|
|
274
|
+
like `soar containers`. 33 guide resources are served as `guide://` URIs.
|
|
275
|
+
Mutations are guarded: `yes=true` to apply (dry-run by default).
|
|
253
276
|
|
|
254
277
|
## License
|
|
255
278
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
|
-
<a href="https://splunk.danny.vn"><img src="docs/assets/banner.svg" alt="splunkctl — operate Splunk Enterprise as code" width="600"></a>
|
|
3
|
+
<a href="https://splunk.danny.vn"><img src="docs/assets/banner.svg" alt="splunkctl — operate Splunk Enterprise and Splunk SOAR as code" width="600"></a>
|
|
4
4
|
|
|
5
5
|
# splunkctl
|
|
6
6
|
|
|
7
|
-
**Operate Splunk Enterprise as code — for SOC teams, detection engineers, and AI agents.**
|
|
7
|
+
**Operate Splunk Enterprise and Splunk SOAR as code — for SOC teams, detection engineers, and AI agents.**
|
|
8
8
|
|
|
9
9
|
[Docs](https://splunk.danny.vn) · [Catalog](docs/design/catalog.md) · [Releases](https://github.com/dannyota/splunkctl/releases)
|
|
10
10
|
|
|
@@ -12,15 +12,19 @@
|
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
15
|
-
A Python CLI that queries, inspects, and manages
|
|
16
|
-
Enterprise
|
|
15
|
+
A Python CLI that queries, inspects, and manages **remote** Splunk
|
|
16
|
+
Enterprise and Splunk SOAR instances over their REST APIs. SIEM commands
|
|
17
|
+
are built on the
|
|
17
18
|
[splunk-sdk-python](https://github.com/dannyota/splunk-sdk-python/tree/splunkctl)
|
|
18
|
-
fork with [Click](https://click.palletsprojects.com/)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
fork with [Click](https://click.palletsprojects.com/); SOAR commands use
|
|
20
|
+
`SOARClient`, a requests-based Django REST client with dual auth (token +
|
|
21
|
+
Basic fallback). The SIEM core loop is **pull live state, review the diff,
|
|
22
|
+
push it back** -- one `state` engine covering rules, parsers, macros,
|
|
23
|
+
lookups, and dashboards, with a change-evidence report artifact for change
|
|
24
|
+
tickets. The SOAR surface covers ops visibility, container/artifact
|
|
25
|
+
lifecycle, the first playbooks-as-code loop anywhere, and SIEM-to-SOAR
|
|
26
|
+
ingest. Built for humans and LLM agents alike: deterministic flags,
|
|
27
|
+
`--json` everywhere, structured error envelopes, and a built-in MCP server
|
|
24
28
|
(`splunkctl mcp serve`) with progressive tool discovery.
|
|
25
29
|
|
|
26
30
|
> **Every mutation is dry-run by default.** Nothing changes until you pass
|
|
@@ -52,8 +56,23 @@ structured error envelopes, and a built-in MCP server
|
|
|
52
56
|
`--limit`/`--offset`/`--filter` on every list surface, multi-instance
|
|
53
57
|
profiles with a bank-safety guard banner
|
|
54
58
|
(`(profile: uat @ host:port)`).
|
|
55
|
-
- **
|
|
56
|
-
|
|
59
|
+
- **SOAR ops** — `soar` command tree: containers, artifacts, vault, notes,
|
|
60
|
+
cases/workbooks, approvals, custom lists, indicators, evidence, users,
|
|
61
|
+
roles, audit, and cross-object search. Full lifecycle: create, update,
|
|
62
|
+
close, assign, delete -- with the same dry-run guard and typed error
|
|
63
|
+
envelopes.
|
|
64
|
+
- **Playbooks as code** — `soar playbooks export --unpack` / `import`
|
|
65
|
+
round-trips playbook tgz bundles (the first such tool anywhere);
|
|
66
|
+
`soar playbooks delete` removes them through the Web UI route (no
|
|
67
|
+
REST deletion exists); `soar playbooks run --wait` drives and polls
|
|
68
|
+
runs to completion; `soar actions run --wait` does the same for
|
|
69
|
+
connector actions.
|
|
70
|
+
- **SIEM-to-SOAR ingest** — `soar ingest --spl` runs a SIEM search and
|
|
71
|
+
creates SOAR containers + typed CEF artifacts using the official CIM-to-CEF
|
|
72
|
+
field map, with SDI dedup, severity mapping, and last-artifact automation
|
|
73
|
+
batching.
|
|
74
|
+
- **Built for agents** — built-in MCP server with 237 auto-generated tools,
|
|
75
|
+
progressive discovery (5 meta-tools + focus/unfocus), 33 guide resources,
|
|
57
76
|
guard markers on every mutation, dual output (TTY = table, pipe = JSON).
|
|
58
77
|
|
|
59
78
|
## Install
|
|
@@ -81,10 +100,12 @@ splunkctl --version
|
|
|
81
100
|
## Quickstart
|
|
82
101
|
|
|
83
102
|
```bash
|
|
84
|
-
splunkctl config init # interactive setup
|
|
85
|
-
splunkctl
|
|
86
|
-
splunkctl
|
|
87
|
-
splunkctl
|
|
103
|
+
splunkctl config init # interactive SIEM setup
|
|
104
|
+
splunkctl config init --soar # add SOAR credentials
|
|
105
|
+
splunkctl doctor # check SIEM connection
|
|
106
|
+
splunkctl soar test # check SOAR connection
|
|
107
|
+
splunkctl search run 'index=main | head 10' # run a SIEM search
|
|
108
|
+
splunkctl soar containers list # list SOAR containers
|
|
88
109
|
splunkctl commands --json # discover every verb
|
|
89
110
|
```
|
|
90
111
|
|
|
@@ -136,6 +157,7 @@ splunkctl state push --dir config/ --report r.json --yes # deploy + evidence
|
|
|
136
157
|
| `tags` | Tags — list, get |
|
|
137
158
|
| `datamodels` | Data model definitions + acceleration health |
|
|
138
159
|
| `state` | Config-as-code pull/diff/push with change-evidence reports |
|
|
160
|
+
| `soar` | SOAR: containers, artifacts, vault, playbooks, actions, cases, ingest |
|
|
139
161
|
| `commands` | Machine-readable command tree (JSON) |
|
|
140
162
|
| `mcp` | Built-in MCP server for AI agent integration |
|
|
141
163
|
|
|
@@ -207,11 +229,12 @@ splunkctl mcp install # register in .mcp.json
|
|
|
207
229
|
splunkctl mcp serve # start stdio MCP server
|
|
208
230
|
```
|
|
209
231
|
|
|
210
|
-
The MCP server auto-generates
|
|
211
|
-
with progressive discovery
|
|
212
|
-
`usage`, `focus`, `unfocus`, `run`) and dynamically load typed
|
|
213
|
-
command group.
|
|
214
|
-
|
|
232
|
+
The MCP server auto-generates 229 typed tools from the Click command tree
|
|
233
|
+
(SIEM + SOAR) with progressive discovery -- agents start with 5 meta-tools
|
|
234
|
+
(`help`, `usage`, `focus`, `unfocus`, `run`) and dynamically load typed
|
|
235
|
+
schemas per command group. Subgroup-granular focus works for nested groups
|
|
236
|
+
like `soar containers`. 33 guide resources are served as `guide://` URIs.
|
|
237
|
+
Mutations are guarded: `yes=true` to apply (dry-run by default).
|
|
215
238
|
|
|
216
239
|
## License
|
|
217
240
|
|
|
@@ -15,6 +15,7 @@ from typing import Any
|
|
|
15
15
|
import click
|
|
16
16
|
import requests
|
|
17
17
|
import splunklib.client as splunk_client
|
|
18
|
+
import urllib3
|
|
18
19
|
|
|
19
20
|
from splunkctl import config as cfg_mod
|
|
20
21
|
|
|
@@ -186,6 +187,10 @@ class _WebSession:
|
|
|
186
187
|
self._timeout = timeout
|
|
187
188
|
self._session = requests.Session()
|
|
188
189
|
self._session.verify = verify
|
|
190
|
+
if not verify:
|
|
191
|
+
# verify=false is an explicit config choice (lab/self-signed);
|
|
192
|
+
# without this every request spams InsecureRequestWarning.
|
|
193
|
+
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
189
194
|
self._csrf_token: str | None = None
|
|
190
195
|
self._logged_in = False
|
|
191
196
|
|
|
@@ -57,7 +57,7 @@ def _status_to_int(value: str) -> str:
|
|
|
57
57
|
key = value.strip().lower()
|
|
58
58
|
if key in _STATUS_MAP:
|
|
59
59
|
return _STATUS_MAP[key]
|
|
60
|
-
if value.isdigit():
|
|
60
|
+
if value.isascii() and value.isdigit():
|
|
61
61
|
return value
|
|
62
62
|
known = ", ".join(_STATUS_MAP)
|
|
63
63
|
raise click.BadParameter(f"'{value}' is not a known status ({known}, or 0-5)")
|
|
@@ -8,6 +8,7 @@ import subprocess
|
|
|
8
8
|
import sys
|
|
9
9
|
from typing import Any
|
|
10
10
|
|
|
11
|
+
import click
|
|
11
12
|
from mcp.server.fastmcp import Context, FastMCP
|
|
12
13
|
from mcp.server.fastmcp.utilities.func_metadata import ArgModelBase, FuncMetadata
|
|
13
14
|
from mcp.types import ToolAnnotations
|
|
@@ -20,9 +21,13 @@ from splunkctl.mcp.tools import (
|
|
|
20
21
|
ToolEntry,
|
|
21
22
|
ToolIndex,
|
|
22
23
|
build_tool_index,
|
|
24
|
+
direct_commands,
|
|
23
25
|
group_names,
|
|
24
26
|
group_summary,
|
|
25
27
|
group_tools,
|
|
28
|
+
has_subgroups,
|
|
29
|
+
leaf_count,
|
|
30
|
+
subgroup_names,
|
|
26
31
|
)
|
|
27
32
|
|
|
28
33
|
_INSTRUCTIONS = """\
|
|
@@ -32,28 +37,45 @@ the preferred way to run commands (validated arguments, full schemas). \
|
|
|
32
37
|
`usage <command>` previews one command's schema and auto-loads it as a \
|
|
33
38
|
callable tool. `run` is an escape hatch for raw command strings (no \
|
|
34
39
|
validation). `unfocus` when done to free context. Mutations are guarded: \
|
|
35
|
-
pass yes=true to apply (dry-run by default).
|
|
40
|
+
pass yes=true to apply (dry-run by default). \
|
|
41
|
+
The soar tree has nested subgroups (containers, playbooks, actions, ...); \
|
|
42
|
+
focus at subgroup granularity: `focus "soar containers"`, not `focus soar`. \
|
|
43
|
+
`help soar` lists available subgroups."""
|
|
36
44
|
|
|
37
45
|
_FORCE_FLAGS = ["--json"]
|
|
38
46
|
|
|
39
47
|
_STRIP_PARAMS = SKIP_PARAMS
|
|
40
48
|
|
|
41
49
|
|
|
50
|
+
def _decode_stream(data: bytes) -> str:
|
|
51
|
+
"""Decode subprocess output, tolerating binary payloads.
|
|
52
|
+
|
|
53
|
+
Commands like ``soar playbooks export`` emit raw bytes (a tgz) on
|
|
54
|
+
stdout when ``--out`` is omitted — surface a hint instead of dying
|
|
55
|
+
on a UTF-8 decode error.
|
|
56
|
+
"""
|
|
57
|
+
try:
|
|
58
|
+
return data.decode()
|
|
59
|
+
except UnicodeDecodeError:
|
|
60
|
+
return f"(binary output: {len(data)} bytes — pass --out FILE to save it)"
|
|
61
|
+
|
|
62
|
+
|
|
42
63
|
def _exec_cli(args: list[str]) -> str:
|
|
43
64
|
"""Run ``splunkctl <args>`` as a subprocess and return output."""
|
|
44
65
|
cmd = [sys.executable, "-m", "splunkctl", *args, *_FORCE_FLAGS]
|
|
45
66
|
result = subprocess.run( # noqa: S603
|
|
46
67
|
cmd,
|
|
47
68
|
capture_output=True,
|
|
48
|
-
text=True,
|
|
49
69
|
timeout=120,
|
|
50
70
|
)
|
|
51
|
-
out = result.stdout.strip()
|
|
52
|
-
err = result.stderr.strip()
|
|
71
|
+
out = _decode_stream(result.stdout).strip()
|
|
72
|
+
err = _decode_stream(result.stderr).strip()
|
|
53
73
|
if result.returncode != 0:
|
|
54
74
|
return err or out or f"Command failed with exit code {result.returncode}"
|
|
55
75
|
if err and out:
|
|
56
|
-
|
|
76
|
+
# stderr carries the guard banner / info lines — they precede
|
|
77
|
+
# the payload semantically, so keep them first.
|
|
78
|
+
return f"{err}\n\n{out}"
|
|
57
79
|
return out or err or "(no output)"
|
|
58
80
|
|
|
59
81
|
|
|
@@ -96,10 +118,15 @@ def _build_cli_args(entry: ToolEntry, params: dict[str, Any]) -> list[str]:
|
|
|
96
118
|
items = value if isinstance(value, list) else [value]
|
|
97
119
|
positional[pname] = [str(v) for v in items]
|
|
98
120
|
continue
|
|
99
|
-
|
|
121
|
+
# Renamed Click options (--severity → severity_override) make the
|
|
122
|
+
# schema name diverge from the CLI flag; the entry's flag map is
|
|
123
|
+
# authoritative.
|
|
124
|
+
flag = entry.flags.get(pname, f"--{pname.replace('_', '-')}")
|
|
100
125
|
if isinstance(value, bool):
|
|
101
126
|
if value:
|
|
102
127
|
args.append(flag)
|
|
128
|
+
elif pname in entry.neg_flags:
|
|
129
|
+
args.append(entry.neg_flags[pname])
|
|
103
130
|
elif isinstance(value, list):
|
|
104
131
|
for item in value:
|
|
105
132
|
args.extend([flag, str(item)])
|
|
@@ -149,11 +176,40 @@ def create_server() -> FastMCP:
|
|
|
149
176
|
"""List command groups, or subcommands within a group."""
|
|
150
177
|
if group is None:
|
|
151
178
|
rows = group_summary(cli)
|
|
152
|
-
lines = [
|
|
153
|
-
|
|
154
|
-
|
|
179
|
+
lines: list[str] = []
|
|
180
|
+
for r in rows:
|
|
181
|
+
line = f"{r['group']:16s} {r['description']} ({r['count']} cmds)"
|
|
182
|
+
lines.append(line)
|
|
183
|
+
if "subgroups" in r:
|
|
184
|
+
lines.append(f"{'':16s} subgroups: {r['subgroups']}")
|
|
185
|
+
lines.append(
|
|
186
|
+
f"{'':16s} focus at subgroup level: "
|
|
187
|
+
f'focus "{r["group"]} <subgroup>"'
|
|
188
|
+
)
|
|
155
189
|
return "\n".join(lines)
|
|
156
|
-
|
|
190
|
+
# For groups with subgroups, show the two-level layout
|
|
191
|
+
parts = _split_command(group)
|
|
192
|
+
if len(parts) == 1 and has_subgroups(cli, parts[0]):
|
|
193
|
+
grp = parts[0]
|
|
194
|
+
top_cmd = cli.commands.get(grp)
|
|
195
|
+
if not isinstance(top_cmd, click.Group): # guaranteed by has_subgroups
|
|
196
|
+
raise TypeError(f"expected group for {grp!r}")
|
|
197
|
+
sg = subgroup_names(cli, grp)
|
|
198
|
+
dc = direct_commands(cli, grp)
|
|
199
|
+
lines = [f'{grp} subgroups (focus "{grp} <subgroup>"):']
|
|
200
|
+
for s in sg:
|
|
201
|
+
sub_cmd = top_cmd.commands[s]
|
|
202
|
+
desc = (sub_cmd.help or "").split("\n")[0]
|
|
203
|
+
n = leaf_count(sub_cmd) if isinstance(sub_cmd, click.Group) else 0
|
|
204
|
+
lines.append(f" {grp} {s:16s} {desc} ({n} cmds)")
|
|
205
|
+
if dc:
|
|
206
|
+
lines.append(f"\n{grp} direct commands:")
|
|
207
|
+
for c in dc:
|
|
208
|
+
sub_cmd = top_cmd.commands[c]
|
|
209
|
+
desc = (sub_cmd.help or "").split("\n")[0]
|
|
210
|
+
lines.append(f" {grp} {c:16s} {desc}")
|
|
211
|
+
return "\n".join(lines)
|
|
212
|
+
return _exec_cli([*parts, "--help"])
|
|
157
213
|
|
|
158
214
|
# --- Meta-tool: usage ---
|
|
159
215
|
|
|
@@ -161,7 +217,8 @@ def create_server() -> FastMCP:
|
|
|
161
217
|
name="usage",
|
|
162
218
|
description=(
|
|
163
219
|
"Show flags, args, and description for one command. "
|
|
164
|
-
"Auto-loads it as a callable tool."
|
|
220
|
+
"Auto-loads it as a callable tool. "
|
|
221
|
+
'Accepts nested paths: usage "soar playbooks run".'
|
|
165
222
|
),
|
|
166
223
|
annotations=ToolAnnotations(readOnlyHint=True),
|
|
167
224
|
)
|
|
@@ -193,7 +250,8 @@ def create_server() -> FastMCP:
|
|
|
193
250
|
name="focus",
|
|
194
251
|
description=(
|
|
195
252
|
"Load typed tools for a command group (enables full schemas). "
|
|
196
|
-
"Preferred over run for validated arguments."
|
|
253
|
+
"Preferred over run for validated arguments. "
|
|
254
|
+
'Nested groups like soar require subgroup: focus "soar containers".'
|
|
197
255
|
),
|
|
198
256
|
annotations=ToolAnnotations(readOnlyHint=True),
|
|
199
257
|
)
|
|
@@ -203,6 +261,26 @@ def create_server() -> FastMCP:
|
|
|
203
261
|
count = len(focused[group])
|
|
204
262
|
return f"Group '{group}' already focused ({count} tools)."
|
|
205
263
|
|
|
264
|
+
# Reject bare groups that have nested subgroups (too many tools).
|
|
265
|
+
parts = _split_command(group)
|
|
266
|
+
if len(parts) == 1 and has_subgroups(cli, parts[0]):
|
|
267
|
+
sg = subgroup_names(cli, parts[0])
|
|
268
|
+
dc = direct_commands(cli, parts[0])
|
|
269
|
+
lines = [
|
|
270
|
+
f"'{parts[0]}' has nested subgroups — focus at "
|
|
271
|
+
f"subgroup level to avoid flooding context.",
|
|
272
|
+
"",
|
|
273
|
+
"Subgroups:",
|
|
274
|
+
]
|
|
275
|
+
for s in sg:
|
|
276
|
+
lines.append(f' focus "{parts[0]} {s}"')
|
|
277
|
+
if dc:
|
|
278
|
+
lines.append(
|
|
279
|
+
f"\nDirect commands ({', '.join(dc)}) can be "
|
|
280
|
+
f"loaded individually via usage."
|
|
281
|
+
)
|
|
282
|
+
return "\n".join(lines)
|
|
283
|
+
|
|
206
284
|
entries = group_tools(all_tools, group)
|
|
207
285
|
if not entries:
|
|
208
286
|
available = group_names(cli)
|
|
@@ -249,14 +327,22 @@ def create_server() -> FastMCP:
|
|
|
249
327
|
await ctx.session.send_tool_list_changed()
|
|
250
328
|
return f"Unloaded all focused tools ({removed} total)."
|
|
251
329
|
|
|
252
|
-
|
|
330
|
+
# Try exact key first, then normalized form for subgroup paths
|
|
331
|
+
key = group if group in focused else None
|
|
332
|
+
if key is None:
|
|
333
|
+
norm = group.replace(" ", "_").replace("-", "_")
|
|
334
|
+
for k in focused:
|
|
335
|
+
if k.replace(" ", "_").replace("-", "_") == norm:
|
|
336
|
+
key = k
|
|
337
|
+
break
|
|
338
|
+
if key is None:
|
|
253
339
|
return f"Group '{group}' is not focused."
|
|
254
|
-
for n in focused[
|
|
340
|
+
for n in focused[key]:
|
|
255
341
|
try:
|
|
256
342
|
mcp.remove_tool(n)
|
|
257
343
|
except Exception: # noqa: BLE001, S110
|
|
258
344
|
pass
|
|
259
|
-
count = len(focused.pop(
|
|
345
|
+
count = len(focused.pop(key))
|
|
260
346
|
if ctx is not None:
|
|
261
347
|
await ctx.session.send_tool_list_changed()
|
|
262
348
|
return f"Unloaded {count} tools for '{group}'."
|
|
@@ -276,6 +362,19 @@ def create_server() -> FastMCP:
|
|
|
276
362
|
async def run_tool(command: str, yes: bool = False) -> str:
|
|
277
363
|
"""Execute a raw splunkctl command string."""
|
|
278
364
|
tokens = _split_command(command)
|
|
365
|
+
# The tool's yes parameter is the only mutation switch — a --yes
|
|
366
|
+
# smuggled inside the command string must not bypass the guard.
|
|
367
|
+
# Reject rather than silently strip: after shlex a quoted option
|
|
368
|
+
# VALUE of '-y' is indistinguishable from the flag, and eating
|
|
369
|
+
# it would shift arguments or corrupt the value.
|
|
370
|
+
if any(t in ("--yes", "-y") for t in tokens):
|
|
371
|
+
return (
|
|
372
|
+
"Error: '--yes'/'-y' is not accepted inside the command "
|
|
373
|
+
"string — the tool's yes parameter is the only mutation "
|
|
374
|
+
"switch. Remove it and pass yes=true to apply. If you "
|
|
375
|
+
"need the literal value '-y', use a focused typed tool "
|
|
376
|
+
"instead."
|
|
377
|
+
)
|
|
279
378
|
if yes:
|
|
280
379
|
tokens.append("--yes")
|
|
281
380
|
result = _exec_cli(tokens)
|
|
@@ -286,9 +385,17 @@ def create_server() -> FastMCP:
|
|
|
286
385
|
if tool_name in all_tools and not any(
|
|
287
386
|
tool_name in names for names in focused.values()
|
|
288
387
|
):
|
|
388
|
+
if len(tokens) >= 2 and tokens[1] in subgroup_names(cli, tokens[0]):
|
|
389
|
+
suggestion = f'focus "{tokens[0]} {tokens[1]}"'
|
|
390
|
+
elif has_subgroups(cli, tokens[0]):
|
|
391
|
+
# Bare focus on a nested group is refused; point at usage
|
|
392
|
+
# for direct commands instead.
|
|
393
|
+
suggestion = f'usage "{" ".join(tokens[:2])}"'
|
|
394
|
+
else:
|
|
395
|
+
suggestion = f"focus {tokens[0]}"
|
|
289
396
|
result += (
|
|
290
|
-
f"\n\nTip: run `
|
|
291
|
-
f"with validated
|
|
397
|
+
f"\n\nTip: run `{suggestion}` to load a typed tool "
|
|
398
|
+
f"with a validated schema for this command."
|
|
292
399
|
)
|
|
293
400
|
return result
|
|
294
401
|
|