splunkctl 0.4.1__tar.gz → 0.6.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.6.0/PKG-INFO +256 -0
- splunkctl-0.6.0/README.md +218 -0
- {splunkctl-0.4.1 → splunkctl-0.6.0}/pyproject.toml +7 -2
- {splunkctl-0.4.1 → splunkctl-0.6.0}/splunkctl/__init__.py +1 -1
- {splunkctl-0.4.1 → splunkctl-0.6.0}/splunkctl/commands/doctor.py +43 -25
- {splunkctl-0.4.1 → splunkctl-0.6.0}/splunkctl/commands/knowledge.py +21 -7
- splunkctl-0.6.0/splunkctl/commands/mcp_cmd.py +47 -0
- splunkctl-0.6.0/splunkctl/commands/server.py +179 -0
- splunkctl-0.4.1/splunkctl/commands/server.py → splunkctl-0.6.0/splunkctl/commands/server_topology.py +125 -115
- {splunkctl-0.4.1 → splunkctl-0.6.0}/splunkctl/main.py +2 -2
- splunkctl-0.6.0/splunkctl/mcp/__init__.py +0 -0
- splunkctl-0.6.0/splunkctl/mcp/resources.py +35 -0
- splunkctl-0.6.0/splunkctl/mcp/server.py +369 -0
- splunkctl-0.6.0/splunkctl/mcp/tools.py +233 -0
- splunkctl-0.6.0/splunkctl.egg-info/PKG-INFO +256 -0
- {splunkctl-0.4.1 → splunkctl-0.6.0}/splunkctl.egg-info/SOURCES.txt +6 -2
- {splunkctl-0.4.1 → splunkctl-0.6.0}/splunkctl.egg-info/requires.txt +1 -0
- {splunkctl-0.4.1 → splunkctl-0.6.0}/tests/test_version.py +1 -1
- splunkctl-0.4.1/PKG-INFO +0 -194
- splunkctl-0.4.1/README.md +0 -157
- splunkctl-0.4.1/splunkctl/commands/skill_cmd.py +0 -43
- splunkctl-0.4.1/splunkctl/skill/SKILL.md +0 -1046
- splunkctl-0.4.1/splunkctl.egg-info/PKG-INFO +0 -194
- {splunkctl-0.4.1 → splunkctl-0.6.0}/LICENSE +0 -0
- {splunkctl-0.4.1 → splunkctl-0.6.0}/setup.cfg +0 -0
- {splunkctl-0.4.1 → splunkctl-0.6.0}/splunkctl/__main__.py +0 -0
- {splunkctl-0.4.1 → splunkctl-0.6.0}/splunkctl/client.py +0 -0
- {splunkctl-0.4.1 → splunkctl-0.6.0}/splunkctl/commands/__init__.py +0 -0
- {splunkctl-0.4.1 → splunkctl-0.6.0}/splunkctl/commands/alerts.py +0 -0
- {splunkctl-0.4.1 → splunkctl-0.6.0}/splunkctl/commands/apps.py +0 -0
- {splunkctl-0.4.1 → splunkctl-0.6.0}/splunkctl/commands/audit.py +0 -0
- {splunkctl-0.4.1 → splunkctl-0.6.0}/splunkctl/commands/audit_parse.py +0 -0
- {splunkctl-0.4.1 → splunkctl-0.6.0}/splunkctl/commands/commands_meta.py +0 -0
- {splunkctl-0.4.1 → splunkctl-0.6.0}/splunkctl/commands/common.py +0 -0
- {splunkctl-0.4.1 → splunkctl-0.6.0}/splunkctl/commands/conf.py +0 -0
- {splunkctl-0.4.1 → splunkctl-0.6.0}/splunkctl/commands/conf_ops.py +0 -0
- {splunkctl-0.4.1 → splunkctl-0.6.0}/splunkctl/commands/config_cmd.py +0 -0
- {splunkctl-0.4.1 → splunkctl-0.6.0}/splunkctl/commands/dashboards.py +0 -0
- {splunkctl-0.4.1 → splunkctl-0.6.0}/splunkctl/commands/datamodels.py +0 -0
- {splunkctl-0.4.1 → splunkctl-0.6.0}/splunkctl/commands/es.py +0 -0
- {splunkctl-0.4.1 → splunkctl-0.6.0}/splunkctl/commands/hec.py +0 -0
- {splunkctl-0.4.1 → splunkctl-0.6.0}/splunkctl/commands/indexes.py +0 -0
- {splunkctl-0.4.1 → splunkctl-0.6.0}/splunkctl/commands/info.py +0 -0
- {splunkctl-0.4.1 → splunkctl-0.6.0}/splunkctl/commands/inputs.py +0 -0
- {splunkctl-0.4.1 → splunkctl-0.6.0}/splunkctl/commands/kvstore.py +0 -0
- {splunkctl-0.4.1 → splunkctl-0.6.0}/splunkctl/commands/lookups.py +0 -0
- {splunkctl-0.4.1 → splunkctl-0.6.0}/splunkctl/commands/lookups_wiring.py +0 -0
- {splunkctl-0.4.1 → splunkctl-0.6.0}/splunkctl/commands/parsers.py +0 -0
- {splunkctl-0.4.1 → splunkctl-0.6.0}/splunkctl/commands/parsers_io.py +0 -0
- {splunkctl-0.4.1 → splunkctl-0.6.0}/splunkctl/commands/rules.py +0 -0
- {splunkctl-0.4.1 → splunkctl-0.6.0}/splunkctl/commands/rules_io.py +0 -0
- {splunkctl-0.4.1 → splunkctl-0.6.0}/splunkctl/commands/search.py +0 -0
- {splunkctl-0.4.1 → splunkctl-0.6.0}/splunkctl/commands/state.py +0 -0
- {splunkctl-0.4.1 → splunkctl-0.6.0}/splunkctl/commands/state_io.py +0 -0
- {splunkctl-0.4.1 → splunkctl-0.6.0}/splunkctl/commands/state_io_blobs.py +0 -0
- {splunkctl-0.4.1 → splunkctl-0.6.0}/splunkctl/commands/state_io_confs.py +0 -0
- {splunkctl-0.4.1 → splunkctl-0.6.0}/splunkctl/commands/state_types.py +0 -0
- {splunkctl-0.4.1 → splunkctl-0.6.0}/splunkctl/commands/users.py +0 -0
- {splunkctl-0.4.1 → splunkctl-0.6.0}/splunkctl/config.py +0 -0
- {splunkctl-0.4.1 → splunkctl-0.6.0}/splunkctl/errors.py +0 -0
- {splunkctl-0.4.1 → splunkctl-0.6.0}/splunkctl/guard.py +0 -0
- {splunkctl-0.4.1 → splunkctl-0.6.0}/splunkctl/output.py +0 -0
- {splunkctl-0.4.1 → splunkctl-0.6.0}/splunkctl.egg-info/dependency_links.txt +0 -0
- {splunkctl-0.4.1 → splunkctl-0.6.0}/splunkctl.egg-info/entry_points.txt +0 -0
- {splunkctl-0.4.1 → splunkctl-0.6.0}/splunkctl.egg-info/top_level.txt +0 -0
- {splunkctl-0.4.1 → splunkctl-0.6.0}/tests/test_client.py +0 -0
- {splunkctl-0.4.1 → splunkctl-0.6.0}/tests/test_config.py +0 -0
- {splunkctl-0.4.1 → splunkctl-0.6.0}/tests/test_guard.py +0 -0
- {splunkctl-0.4.1 → splunkctl-0.6.0}/tests/test_main_errors.py +0 -0
- {splunkctl-0.4.1 → splunkctl-0.6.0}/tests/test_main_hoisting.py +0 -0
- {splunkctl-0.4.1 → splunkctl-0.6.0}/tests/test_output.py +0 -0
splunkctl-0.6.0/PKG-INFO
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: splunkctl
|
|
3
|
+
Version: 0.6.0
|
|
4
|
+
Summary: CLI tool for Splunk Enterprise SIEM operations.
|
|
5
|
+
Author: dannyota
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/dannyota/splunkctl
|
|
8
|
+
Project-URL: Repository, https://github.com/dannyota/splunkctl
|
|
9
|
+
Project-URL: Issues, https://github.com/dannyota/splunkctl/issues
|
|
10
|
+
Keywords: splunk,siem,security,cli,detection,spl
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: System Administrators
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Security
|
|
18
|
+
Classifier: Topic :: System :: Systems Administration
|
|
19
|
+
Requires-Python: >=3.13
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: splunk-sdk>=2.0
|
|
23
|
+
Requires-Dist: click>=8.2
|
|
24
|
+
Requires-Dist: pyyaml>=6.0
|
|
25
|
+
Requires-Dist: tabulate>=0.9
|
|
26
|
+
Requires-Dist: requests>=2.32
|
|
27
|
+
Requires-Dist: defusedxml>=0.7
|
|
28
|
+
Requires-Dist: mcp>=1.23
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
31
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
32
|
+
Requires-Dist: mypy>=1.11; extra == "dev"
|
|
33
|
+
Requires-Dist: types-requests; extra == "dev"
|
|
34
|
+
Requires-Dist: types-PyYAML; extra == "dev"
|
|
35
|
+
Requires-Dist: types-tabulate; extra == "dev"
|
|
36
|
+
Requires-Dist: types-defusedxml; extra == "dev"
|
|
37
|
+
Dynamic: license-file
|
|
38
|
+
|
|
39
|
+
<div align="center">
|
|
40
|
+
|
|
41
|
+
<a href="https://splunk.danny.vn"><img src="docs/assets/banner.svg" alt="splunkctl — operate Splunk Enterprise as code" width="600"></a>
|
|
42
|
+
|
|
43
|
+
# splunkctl
|
|
44
|
+
|
|
45
|
+
**Operate Splunk Enterprise as code — for SOC teams, detection engineers, and AI agents.**
|
|
46
|
+
|
|
47
|
+
[Docs](https://splunk.danny.vn) · [Catalog](docs/design/catalog.md) · [Releases](https://github.com/dannyota/splunkctl/releases)
|
|
48
|
+
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
A Python CLI that queries, inspects, and manages a **remote** Splunk
|
|
54
|
+
Enterprise instance over the REST API. Built on the
|
|
55
|
+
[splunk-sdk-python](https://github.com/dannyota/splunk-sdk-python/tree/splunkctl)
|
|
56
|
+
fork with [Click](https://click.palletsprojects.com/). The core loop is
|
|
57
|
+
**pull live state → review the diff → push it back** — one `state` engine
|
|
58
|
+
covering rules, parsers, macros, lookups, and dashboards, with a
|
|
59
|
+
change-evidence report artifact for bank change tickets. It's built for
|
|
60
|
+
humans and LLM agents alike: deterministic flags, `--json` everywhere,
|
|
61
|
+
structured error envelopes, and a built-in MCP server
|
|
62
|
+
(`splunkctl mcp serve`) with progressive tool discovery.
|
|
63
|
+
|
|
64
|
+
> **Every mutation is dry-run by default.** Nothing changes until you pass
|
|
65
|
+
> `--yes`. Always preview, read it, then apply.
|
|
66
|
+
|
|
67
|
+
## What it does
|
|
68
|
+
|
|
69
|
+
- **Config as code** — `state pull` → edit → `state diff` → `state push`
|
|
70
|
+
across rules, parsers, macros, lookups, and dashboards (diff-only). Push
|
|
71
|
+
writes a before→after JSON report artifact usable as change-ticket
|
|
72
|
+
evidence. Push never deletes.
|
|
73
|
+
- **Detection engineering** — rules CRUD + YAML import/export, macros,
|
|
74
|
+
eventtypes, tags, data model acceleration health, lookup definitions +
|
|
75
|
+
automatic lookups (transforms.conf/props.conf wiring), and first-class
|
|
76
|
+
`--email-to`/`--webhook-url` alert-action flags.
|
|
77
|
+
- **ES incident review** — `es notables list/get/update` for the SOC
|
|
78
|
+
triage loop (status, owner, urgency, disposition, comment via
|
|
79
|
+
`notable_update`); feature-detected on Enterprise Security.
|
|
80
|
+
- **Compliance & audit** — `audit changes` normalizes both `_audit` event
|
|
81
|
+
shapes into one schema; `audit rbac` produces a users × roles ×
|
|
82
|
+
capabilities attestation view for access recertification.
|
|
83
|
+
- **KV store** — collection + document CRUD, JSONL import/export with
|
|
84
|
+
500-doc batch chunking, query with server-side filtering.
|
|
85
|
+
- **Topology health** — `server cluster/shcluster/deployment` reads
|
|
86
|
+
distinguish "no threat" from "an indexer is down" in clustered
|
|
87
|
+
deployments.
|
|
88
|
+
- **Agent reliability** — structured JSON error envelope with typed
|
|
89
|
+
taxonomy (`auth`/`permission`/`not_found`/`timeout`/...), uniform
|
|
90
|
+
`--limit`/`--offset`/`--filter` on every list surface, multi-instance
|
|
91
|
+
profiles with a bank-safety guard banner
|
|
92
|
+
(`(profile: uat @ host:port)`).
|
|
93
|
+
- **Built for agents** — built-in MCP server with 129 auto-generated tools,
|
|
94
|
+
progressive discovery (5 meta-tools + focus/unfocus), 22 guide resources,
|
|
95
|
+
guard markers on every mutation, dual output (TTY = table, pipe = JSON).
|
|
96
|
+
|
|
97
|
+
## Install
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
pip install splunkctl
|
|
101
|
+
pip install git+https://github.com/dannyota/splunk-sdk-python@splunkctl
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Requires Python 3.13+. The second line installs the
|
|
105
|
+
[forked SDK](https://github.com/dannyota/splunk-sdk-python/tree/splunkctl)
|
|
106
|
+
which adds dashboard, lookup, and HEC token entity classes. Without it,
|
|
107
|
+
core commands (search, rules, alerts, indexes, inputs, apps, users) still
|
|
108
|
+
work.
|
|
109
|
+
|
|
110
|
+
### Development
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
git clone https://github.com/dannyota/splunkctl
|
|
114
|
+
cd splunkctl
|
|
115
|
+
pip install -e '.[dev]'
|
|
116
|
+
splunkctl --version
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Quickstart
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
splunkctl config init # interactive setup
|
|
123
|
+
splunkctl doctor # check connection, auth, permissions
|
|
124
|
+
splunkctl search run 'index=main | head 10' # run a search
|
|
125
|
+
splunkctl rules list # list detection rules
|
|
126
|
+
splunkctl commands --json # discover every verb
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## CLI usage
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
# Read
|
|
133
|
+
splunkctl rules list --app Splunk_Security_Essentials --json
|
|
134
|
+
splunkctl alerts list --json
|
|
135
|
+
splunkctl datamodels acceleration
|
|
136
|
+
splunkctl audit rbac --format csv --out rbac.csv
|
|
137
|
+
|
|
138
|
+
# Mutate (dry-run first, --yes to apply)
|
|
139
|
+
splunkctl rules disable 'My Rule' # preview
|
|
140
|
+
splunkctl rules disable 'My Rule' --yes # apply
|
|
141
|
+
splunkctl es notables update <id> --status closed --owner analyst --yes
|
|
142
|
+
|
|
143
|
+
# Config-as-code
|
|
144
|
+
splunkctl state pull --dir config/ # snapshot live state
|
|
145
|
+
splunkctl state diff --dir config/ # structured drift report
|
|
146
|
+
splunkctl state push --dir config/ --report r.json --yes # deploy + evidence
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Commands
|
|
150
|
+
|
|
151
|
+
| Group | Description |
|
|
152
|
+
|---|---|
|
|
153
|
+
| `doctor` | Connection, auth, health, and permissions check |
|
|
154
|
+
| `config` | Setup, profiles (dev/UAT/prod), test connectivity |
|
|
155
|
+
| `info` | Server info (version, OS, license) |
|
|
156
|
+
| `search` | Run, export, oneshot, upload, job management |
|
|
157
|
+
| `rules` | Detection rules — CRUD, import/export (YAML), alert-action flags |
|
|
158
|
+
| `alerts` | Fired alerts, alert actions, suppression |
|
|
159
|
+
| `dashboards` | Dashboard CRUD (XML/JSON) |
|
|
160
|
+
| `indexes` | Index management |
|
|
161
|
+
| `inputs` | Data inputs (monitor, tcp, udp, script, http) |
|
|
162
|
+
| `lookups` | Lookup tables, definitions, automatic lookups |
|
|
163
|
+
| `hec` | HEC token management |
|
|
164
|
+
| `parsers` | Source types, field extractions, import/export |
|
|
165
|
+
| `apps` | App install (.spl/.tar.gz), uninstall, update |
|
|
166
|
+
| `users` | User and role management |
|
|
167
|
+
| `server` | Messages, license, KV store, cluster/SHC/deployment health |
|
|
168
|
+
| `es` | ES notable-event triage (feature-detected) |
|
|
169
|
+
| `audit` | Change audit + RBAC attestation |
|
|
170
|
+
| `kvstore` | KV store collection + document CRUD |
|
|
171
|
+
| `conf` | Generic conf file/stanza editor (any .conf) |
|
|
172
|
+
| `macros` | Search macros — list, get, set |
|
|
173
|
+
| `eventtypes` | Event types — list, get |
|
|
174
|
+
| `tags` | Tags — list, get |
|
|
175
|
+
| `datamodels` | Data model definitions + acceleration health |
|
|
176
|
+
| `state` | Config-as-code pull/diff/push with change-evidence reports |
|
|
177
|
+
| `commands` | Machine-readable command tree (JSON) |
|
|
178
|
+
| `mcp` | Built-in MCP server for AI agent integration |
|
|
179
|
+
|
|
180
|
+
## Global flags
|
|
181
|
+
|
|
182
|
+
```
|
|
183
|
+
--json Force JSON output
|
|
184
|
+
--format FMT Output format: table, json, csv, jsonl
|
|
185
|
+
--fields f1,f2 Project specific fields
|
|
186
|
+
--out FILE Write output to file
|
|
187
|
+
--yes / -y Apply mutations (skip dry-run preview)
|
|
188
|
+
--timeout N Request timeout in seconds (default 30)
|
|
189
|
+
--config FILE Config file path
|
|
190
|
+
--profile NAME Named profile (dev/UAT/prod)
|
|
191
|
+
--debug HTTP request/response logging
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Dry-run by default
|
|
195
|
+
|
|
196
|
+
All write operations preview what would change. Pass `--yes` to apply.
|
|
197
|
+
Every preview and confirmation includes the target profile and host so an
|
|
198
|
+
agent never mistakes UAT for prod.
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
splunkctl rules delete 'My Rule'
|
|
202
|
+
# [DRY RUN] Delete saved search 'My Rule' (profile: uat @ uat.splunk.internal:8089)
|
|
203
|
+
# Pass --yes to apply.
|
|
204
|
+
|
|
205
|
+
splunkctl rules delete 'My Rule' --yes
|
|
206
|
+
# Applying: Delete saved search 'My Rule' (profile: uat @ uat.splunk.internal:8089)
|
|
207
|
+
# Deleted saved search 'My Rule'.
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Structured errors
|
|
211
|
+
|
|
212
|
+
Under `--json` or piped output, errors emit a single-line JSON envelope on
|
|
213
|
+
stderr with a typed `kind` for programmatic branching:
|
|
214
|
+
|
|
215
|
+
```json
|
|
216
|
+
{"error": {"kind": "not_found", "http_status": 404, "message": "..."}}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Kinds: `auth`, `permission`, `not_found`, `conflict`, `http`, `connection`,
|
|
220
|
+
`timeout`, `error` (fallback).
|
|
221
|
+
|
|
222
|
+
## SDK fork
|
|
223
|
+
|
|
224
|
+
splunkctl depends on a
|
|
225
|
+
[fork of splunk-sdk-python](https://github.com/dannyota/splunk-sdk-python/tree/splunkctl)
|
|
226
|
+
that adds entity classes missing from the upstream SDK:
|
|
227
|
+
|
|
228
|
+
| Entity | Service property | Purpose |
|
|
229
|
+
|---|---|---|
|
|
230
|
+
| `Dashboard` | `service.dashboards` | Dashboard CRUD |
|
|
231
|
+
| `LookupTableFile` | `service.lookup_table_files` | Lookup table metadata + download |
|
|
232
|
+
| `HECToken` | `service.hec_tokens` | HEC token management |
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
pip install git+https://github.com/dannyota/splunk-sdk-python@splunkctl
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Agent integration (MCP)
|
|
239
|
+
|
|
240
|
+
splunkctl ships with a built-in
|
|
241
|
+
[MCP](https://modelcontextprotocol.io) server for AI agent integration:
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
splunkctl mcp install # register in .mcp.json
|
|
245
|
+
splunkctl mcp serve # start stdio MCP server
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
The MCP server auto-generates 129 typed tools from the Click command tree
|
|
249
|
+
with progressive discovery — agents start with 5 meta-tools (`help`,
|
|
250
|
+
`usage`, `focus`, `unfocus`, `run`) and dynamically load typed schemas per
|
|
251
|
+
command group. 22 guide resources are served as `guide://` URIs. Mutations
|
|
252
|
+
are guarded: `yes=true` to apply (dry-run by default).
|
|
253
|
+
|
|
254
|
+
## License
|
|
255
|
+
|
|
256
|
+
Apache-2.0
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
<a href="https://splunk.danny.vn"><img src="docs/assets/banner.svg" alt="splunkctl — operate Splunk Enterprise as code" width="600"></a>
|
|
4
|
+
|
|
5
|
+
# splunkctl
|
|
6
|
+
|
|
7
|
+
**Operate Splunk Enterprise as code — for SOC teams, detection engineers, and AI agents.**
|
|
8
|
+
|
|
9
|
+
[Docs](https://splunk.danny.vn) · [Catalog](docs/design/catalog.md) · [Releases](https://github.com/dannyota/splunkctl/releases)
|
|
10
|
+
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
A Python CLI that queries, inspects, and manages a **remote** Splunk
|
|
16
|
+
Enterprise instance over the REST API. Built on the
|
|
17
|
+
[splunk-sdk-python](https://github.com/dannyota/splunk-sdk-python/tree/splunkctl)
|
|
18
|
+
fork with [Click](https://click.palletsprojects.com/). The core loop is
|
|
19
|
+
**pull live state → review the diff → push it back** — one `state` engine
|
|
20
|
+
covering rules, parsers, macros, lookups, and dashboards, with a
|
|
21
|
+
change-evidence report artifact for bank change tickets. It's built for
|
|
22
|
+
humans and LLM agents alike: deterministic flags, `--json` everywhere,
|
|
23
|
+
structured error envelopes, and a built-in MCP server
|
|
24
|
+
(`splunkctl mcp serve`) with progressive tool discovery.
|
|
25
|
+
|
|
26
|
+
> **Every mutation is dry-run by default.** Nothing changes until you pass
|
|
27
|
+
> `--yes`. Always preview, read it, then apply.
|
|
28
|
+
|
|
29
|
+
## What it does
|
|
30
|
+
|
|
31
|
+
- **Config as code** — `state pull` → edit → `state diff` → `state push`
|
|
32
|
+
across rules, parsers, macros, lookups, and dashboards (diff-only). Push
|
|
33
|
+
writes a before→after JSON report artifact usable as change-ticket
|
|
34
|
+
evidence. Push never deletes.
|
|
35
|
+
- **Detection engineering** — rules CRUD + YAML import/export, macros,
|
|
36
|
+
eventtypes, tags, data model acceleration health, lookup definitions +
|
|
37
|
+
automatic lookups (transforms.conf/props.conf wiring), and first-class
|
|
38
|
+
`--email-to`/`--webhook-url` alert-action flags.
|
|
39
|
+
- **ES incident review** — `es notables list/get/update` for the SOC
|
|
40
|
+
triage loop (status, owner, urgency, disposition, comment via
|
|
41
|
+
`notable_update`); feature-detected on Enterprise Security.
|
|
42
|
+
- **Compliance & audit** — `audit changes` normalizes both `_audit` event
|
|
43
|
+
shapes into one schema; `audit rbac` produces a users × roles ×
|
|
44
|
+
capabilities attestation view for access recertification.
|
|
45
|
+
- **KV store** — collection + document CRUD, JSONL import/export with
|
|
46
|
+
500-doc batch chunking, query with server-side filtering.
|
|
47
|
+
- **Topology health** — `server cluster/shcluster/deployment` reads
|
|
48
|
+
distinguish "no threat" from "an indexer is down" in clustered
|
|
49
|
+
deployments.
|
|
50
|
+
- **Agent reliability** — structured JSON error envelope with typed
|
|
51
|
+
taxonomy (`auth`/`permission`/`not_found`/`timeout`/...), uniform
|
|
52
|
+
`--limit`/`--offset`/`--filter` on every list surface, multi-instance
|
|
53
|
+
profiles with a bank-safety guard banner
|
|
54
|
+
(`(profile: uat @ host:port)`).
|
|
55
|
+
- **Built for agents** — built-in MCP server with 129 auto-generated tools,
|
|
56
|
+
progressive discovery (5 meta-tools + focus/unfocus), 22 guide resources,
|
|
57
|
+
guard markers on every mutation, dual output (TTY = table, pipe = JSON).
|
|
58
|
+
|
|
59
|
+
## Install
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install splunkctl
|
|
63
|
+
pip install git+https://github.com/dannyota/splunk-sdk-python@splunkctl
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Requires Python 3.13+. The second line installs the
|
|
67
|
+
[forked SDK](https://github.com/dannyota/splunk-sdk-python/tree/splunkctl)
|
|
68
|
+
which adds dashboard, lookup, and HEC token entity classes. Without it,
|
|
69
|
+
core commands (search, rules, alerts, indexes, inputs, apps, users) still
|
|
70
|
+
work.
|
|
71
|
+
|
|
72
|
+
### Development
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
git clone https://github.com/dannyota/splunkctl
|
|
76
|
+
cd splunkctl
|
|
77
|
+
pip install -e '.[dev]'
|
|
78
|
+
splunkctl --version
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Quickstart
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
splunkctl config init # interactive setup
|
|
85
|
+
splunkctl doctor # check connection, auth, permissions
|
|
86
|
+
splunkctl search run 'index=main | head 10' # run a search
|
|
87
|
+
splunkctl rules list # list detection rules
|
|
88
|
+
splunkctl commands --json # discover every verb
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## CLI usage
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
# Read
|
|
95
|
+
splunkctl rules list --app Splunk_Security_Essentials --json
|
|
96
|
+
splunkctl alerts list --json
|
|
97
|
+
splunkctl datamodels acceleration
|
|
98
|
+
splunkctl audit rbac --format csv --out rbac.csv
|
|
99
|
+
|
|
100
|
+
# Mutate (dry-run first, --yes to apply)
|
|
101
|
+
splunkctl rules disable 'My Rule' # preview
|
|
102
|
+
splunkctl rules disable 'My Rule' --yes # apply
|
|
103
|
+
splunkctl es notables update <id> --status closed --owner analyst --yes
|
|
104
|
+
|
|
105
|
+
# Config-as-code
|
|
106
|
+
splunkctl state pull --dir config/ # snapshot live state
|
|
107
|
+
splunkctl state diff --dir config/ # structured drift report
|
|
108
|
+
splunkctl state push --dir config/ --report r.json --yes # deploy + evidence
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Commands
|
|
112
|
+
|
|
113
|
+
| Group | Description |
|
|
114
|
+
|---|---|
|
|
115
|
+
| `doctor` | Connection, auth, health, and permissions check |
|
|
116
|
+
| `config` | Setup, profiles (dev/UAT/prod), test connectivity |
|
|
117
|
+
| `info` | Server info (version, OS, license) |
|
|
118
|
+
| `search` | Run, export, oneshot, upload, job management |
|
|
119
|
+
| `rules` | Detection rules — CRUD, import/export (YAML), alert-action flags |
|
|
120
|
+
| `alerts` | Fired alerts, alert actions, suppression |
|
|
121
|
+
| `dashboards` | Dashboard CRUD (XML/JSON) |
|
|
122
|
+
| `indexes` | Index management |
|
|
123
|
+
| `inputs` | Data inputs (monitor, tcp, udp, script, http) |
|
|
124
|
+
| `lookups` | Lookup tables, definitions, automatic lookups |
|
|
125
|
+
| `hec` | HEC token management |
|
|
126
|
+
| `parsers` | Source types, field extractions, import/export |
|
|
127
|
+
| `apps` | App install (.spl/.tar.gz), uninstall, update |
|
|
128
|
+
| `users` | User and role management |
|
|
129
|
+
| `server` | Messages, license, KV store, cluster/SHC/deployment health |
|
|
130
|
+
| `es` | ES notable-event triage (feature-detected) |
|
|
131
|
+
| `audit` | Change audit + RBAC attestation |
|
|
132
|
+
| `kvstore` | KV store collection + document CRUD |
|
|
133
|
+
| `conf` | Generic conf file/stanza editor (any .conf) |
|
|
134
|
+
| `macros` | Search macros — list, get, set |
|
|
135
|
+
| `eventtypes` | Event types — list, get |
|
|
136
|
+
| `tags` | Tags — list, get |
|
|
137
|
+
| `datamodels` | Data model definitions + acceleration health |
|
|
138
|
+
| `state` | Config-as-code pull/diff/push with change-evidence reports |
|
|
139
|
+
| `commands` | Machine-readable command tree (JSON) |
|
|
140
|
+
| `mcp` | Built-in MCP server for AI agent integration |
|
|
141
|
+
|
|
142
|
+
## Global flags
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
--json Force JSON output
|
|
146
|
+
--format FMT Output format: table, json, csv, jsonl
|
|
147
|
+
--fields f1,f2 Project specific fields
|
|
148
|
+
--out FILE Write output to file
|
|
149
|
+
--yes / -y Apply mutations (skip dry-run preview)
|
|
150
|
+
--timeout N Request timeout in seconds (default 30)
|
|
151
|
+
--config FILE Config file path
|
|
152
|
+
--profile NAME Named profile (dev/UAT/prod)
|
|
153
|
+
--debug HTTP request/response logging
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Dry-run by default
|
|
157
|
+
|
|
158
|
+
All write operations preview what would change. Pass `--yes` to apply.
|
|
159
|
+
Every preview and confirmation includes the target profile and host so an
|
|
160
|
+
agent never mistakes UAT for prod.
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
splunkctl rules delete 'My Rule'
|
|
164
|
+
# [DRY RUN] Delete saved search 'My Rule' (profile: uat @ uat.splunk.internal:8089)
|
|
165
|
+
# Pass --yes to apply.
|
|
166
|
+
|
|
167
|
+
splunkctl rules delete 'My Rule' --yes
|
|
168
|
+
# Applying: Delete saved search 'My Rule' (profile: uat @ uat.splunk.internal:8089)
|
|
169
|
+
# Deleted saved search 'My Rule'.
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Structured errors
|
|
173
|
+
|
|
174
|
+
Under `--json` or piped output, errors emit a single-line JSON envelope on
|
|
175
|
+
stderr with a typed `kind` for programmatic branching:
|
|
176
|
+
|
|
177
|
+
```json
|
|
178
|
+
{"error": {"kind": "not_found", "http_status": 404, "message": "..."}}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Kinds: `auth`, `permission`, `not_found`, `conflict`, `http`, `connection`,
|
|
182
|
+
`timeout`, `error` (fallback).
|
|
183
|
+
|
|
184
|
+
## SDK fork
|
|
185
|
+
|
|
186
|
+
splunkctl depends on a
|
|
187
|
+
[fork of splunk-sdk-python](https://github.com/dannyota/splunk-sdk-python/tree/splunkctl)
|
|
188
|
+
that adds entity classes missing from the upstream SDK:
|
|
189
|
+
|
|
190
|
+
| Entity | Service property | Purpose |
|
|
191
|
+
|---|---|---|
|
|
192
|
+
| `Dashboard` | `service.dashboards` | Dashboard CRUD |
|
|
193
|
+
| `LookupTableFile` | `service.lookup_table_files` | Lookup table metadata + download |
|
|
194
|
+
| `HECToken` | `service.hec_tokens` | HEC token management |
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
pip install git+https://github.com/dannyota/splunk-sdk-python@splunkctl
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Agent integration (MCP)
|
|
201
|
+
|
|
202
|
+
splunkctl ships with a built-in
|
|
203
|
+
[MCP](https://modelcontextprotocol.io) server for AI agent integration:
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
splunkctl mcp install # register in .mcp.json
|
|
207
|
+
splunkctl mcp serve # start stdio MCP server
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
The MCP server auto-generates 129 typed tools from the Click command tree
|
|
211
|
+
with progressive discovery — agents start with 5 meta-tools (`help`,
|
|
212
|
+
`usage`, `focus`, `unfocus`, `run`) and dynamically load typed schemas per
|
|
213
|
+
command group. 22 guide resources are served as `guide://` URIs. Mutations
|
|
214
|
+
are guarded: `yes=true` to apply (dry-run by default).
|
|
215
|
+
|
|
216
|
+
## License
|
|
217
|
+
|
|
218
|
+
Apache-2.0
|
|
@@ -29,6 +29,7 @@ dependencies = [
|
|
|
29
29
|
"tabulate>=0.9",
|
|
30
30
|
"requests>=2.32",
|
|
31
31
|
"defusedxml>=0.7",
|
|
32
|
+
"mcp>=1.23",
|
|
32
33
|
]
|
|
33
34
|
|
|
34
35
|
[project.optional-dependencies]
|
|
@@ -51,13 +52,13 @@ Repository = "https://github.com/dannyota/splunkctl"
|
|
|
51
52
|
Issues = "https://github.com/dannyota/splunkctl/issues"
|
|
52
53
|
|
|
53
54
|
[tool.setuptools]
|
|
54
|
-
packages = ["splunkctl", "splunkctl.commands", "splunkctl.
|
|
55
|
+
packages = ["splunkctl", "splunkctl.commands", "splunkctl.mcp"]
|
|
55
56
|
|
|
56
57
|
[tool.setuptools.dynamic]
|
|
57
58
|
version = { attr = "splunkctl.__version__" }
|
|
58
59
|
|
|
59
60
|
[tool.setuptools.package-data]
|
|
60
|
-
"splunkctl.
|
|
61
|
+
"splunkctl.mcp" = ["py.typed"]
|
|
61
62
|
|
|
62
63
|
# --- Ruff (lint + format) ---
|
|
63
64
|
|
|
@@ -102,6 +103,10 @@ warn_unused_configs = true
|
|
|
102
103
|
module = "splunklib.*"
|
|
103
104
|
ignore_missing_imports = true
|
|
104
105
|
|
|
106
|
+
[[tool.mypy.overrides]]
|
|
107
|
+
module = "mcp.*"
|
|
108
|
+
ignore_missing_imports = true
|
|
109
|
+
|
|
105
110
|
# --- pytest ---
|
|
106
111
|
|
|
107
112
|
[tool.pytest.ini_options]
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import importlib.resources
|
|
6
5
|
import json
|
|
7
6
|
import ssl
|
|
8
7
|
import urllib.request
|
|
@@ -34,11 +33,10 @@ _HINTS: dict[str, str] = {
|
|
|
34
33
|
"Dismiss with 'splunkctl server messages --dismiss NAME --yes'."
|
|
35
34
|
),
|
|
36
35
|
"Web UI reachable": ("Web UI is optional; lookup-upload and some ops need it."),
|
|
37
|
-
"
|
|
36
|
+
"KV Store": ("Ask your Splunk admin to restart Splunk or check mongod.log."),
|
|
37
|
+
"MCP registered": "Run 'splunkctl mcp install' to register the MCP server.",
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
_INSTALL_DIR = Path.home() / ".claude" / "skills" / "splunkctl"
|
|
41
|
-
|
|
42
40
|
|
|
43
41
|
def _check(
|
|
44
42
|
label: str,
|
|
@@ -160,7 +158,22 @@ def doctor_cmd(ctx: click.Context, *, strict: bool = False) -> None:
|
|
|
160
158
|
else:
|
|
161
159
|
results.append(_check("System messages", True, "no errors"))
|
|
162
160
|
|
|
163
|
-
# --- 5.
|
|
161
|
+
# --- 5. KV Store ---
|
|
162
|
+
click.echo("[KV Store]", err=True)
|
|
163
|
+
try:
|
|
164
|
+
resp = svc.get("/services/kvstore/status", output_mode="json")
|
|
165
|
+
kv_data = json.loads(resp.body.read())
|
|
166
|
+
kv_status = kv_data["entry"][0]["content"].get("current", {})
|
|
167
|
+
kv_state = kv_status.get("status", "unknown")
|
|
168
|
+
kv_ok = kv_state == "ready"
|
|
169
|
+
kv_detail = kv_state
|
|
170
|
+
if not kv_ok and kv_state == "failed":
|
|
171
|
+
kv_detail = "failed — restart Splunk or check mongod.log"
|
|
172
|
+
results.append(_check("KV Store", kv_ok, kv_detail, warn=not kv_ok))
|
|
173
|
+
except Exception:
|
|
174
|
+
results.append(_check("KV Store", False, "could not query status", warn=True))
|
|
175
|
+
|
|
176
|
+
# --- 6. User permissions ---
|
|
164
177
|
click.echo("[Permissions]", err=True)
|
|
165
178
|
try:
|
|
166
179
|
user = svc.users[svc.username]
|
|
@@ -181,7 +194,7 @@ def doctor_cmd(ctx: click.Context, *, strict: bool = False) -> None:
|
|
|
181
194
|
except Exception as exc:
|
|
182
195
|
results.append(_check("User lookup", False, str(exc)))
|
|
183
196
|
|
|
184
|
-
# ---
|
|
197
|
+
# --- 7. Web UI ---
|
|
185
198
|
click.echo("[Web UI]", err=True)
|
|
186
199
|
try:
|
|
187
200
|
web_conf = svc.confs["web"]["settings"]
|
|
@@ -216,31 +229,36 @@ def doctor_cmd(ctx: click.Context, *, strict: bool = False) -> None:
|
|
|
216
229
|
)
|
|
217
230
|
)
|
|
218
231
|
|
|
219
|
-
# ---
|
|
220
|
-
click.echo("[
|
|
221
|
-
|
|
232
|
+
# --- 8. MCP registration ---
|
|
233
|
+
click.echo("[MCP]", err=True)
|
|
234
|
+
_check_mcp_registered(results)
|
|
222
235
|
|
|
223
236
|
_finish(ctx, results, strict=strict)
|
|
224
237
|
|
|
225
238
|
|
|
226
|
-
def
|
|
227
|
-
|
|
228
|
-
if not
|
|
229
|
-
results.append(_check("Skill freshness", False, "not installed", warn=True))
|
|
230
|
-
return
|
|
231
|
-
try:
|
|
232
|
-
ref = importlib.resources.files("splunkctl.skill").joinpath("SKILL.md")
|
|
233
|
-
embedded = ref.read_text(encoding="utf-8")
|
|
234
|
-
except FileNotFoundError:
|
|
235
|
-
results.append(_check("Skill freshness", False, "embedded SKILL.md missing"))
|
|
236
|
-
return
|
|
237
|
-
installed_text = installed.read_text(encoding="utf-8")
|
|
238
|
-
if installed_text == embedded:
|
|
239
|
-
results.append(_check("Skill freshness", True, "up to date"))
|
|
240
|
-
else:
|
|
239
|
+
def _check_mcp_registered(results: list[dict[str, str]]) -> None:
|
|
240
|
+
mcp_json = Path.cwd() / ".mcp.json"
|
|
241
|
+
if not mcp_json.exists():
|
|
241
242
|
results.append(
|
|
242
|
-
_check("
|
|
243
|
+
_check("MCP registered", False, ".mcp.json not found", warn=True)
|
|
243
244
|
)
|
|
245
|
+
return
|
|
246
|
+
try:
|
|
247
|
+
data = json.loads(mcp_json.read_text(encoding="utf-8"))
|
|
248
|
+
servers = data.get("mcpServers", {})
|
|
249
|
+
if "splunkctl" in servers:
|
|
250
|
+
results.append(_check("MCP registered", True, ".mcp.json"))
|
|
251
|
+
else:
|
|
252
|
+
results.append(
|
|
253
|
+
_check(
|
|
254
|
+
"MCP registered",
|
|
255
|
+
False,
|
|
256
|
+
"splunkctl entry missing from .mcp.json",
|
|
257
|
+
warn=True,
|
|
258
|
+
)
|
|
259
|
+
)
|
|
260
|
+
except Exception as exc:
|
|
261
|
+
results.append(_check("MCP registered", False, str(exc)))
|
|
244
262
|
|
|
245
263
|
|
|
246
264
|
def _finish(
|
|
@@ -17,6 +17,7 @@ uses.
|
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
19
|
import re
|
|
20
|
+
import urllib.parse
|
|
20
21
|
from typing import Any
|
|
21
22
|
|
|
22
23
|
import click
|
|
@@ -275,9 +276,15 @@ def eventtypes_get(ctx: click.Context, name: str, app: str | None) -> None:
|
|
|
275
276
|
# each key inside the stanza is a tag name whose value is enabled/disabled.
|
|
276
277
|
# `disabled` itself is the stanza-level flag, not a tag, and eai:* keys are
|
|
277
278
|
# read-only REST metadata — both are excluded from the tag-name views below.
|
|
279
|
+
# REST returns stanza names URL-encoded (`eventtype=cim%3Aauth`); the views
|
|
280
|
+
# decode them, and `tags get` accepts either form.
|
|
278
281
|
_TAG_METADATA_KEYS = {"disabled"}
|
|
279
282
|
|
|
280
283
|
|
|
284
|
+
def _decode_stanza(name: str) -> str:
|
|
285
|
+
return urllib.parse.unquote(name)
|
|
286
|
+
|
|
287
|
+
|
|
281
288
|
def _is_tag_key(key: str) -> bool:
|
|
282
289
|
return key not in _TAG_METADATA_KEYS and not key.startswith("eai:")
|
|
283
290
|
|
|
@@ -335,7 +342,7 @@ def tags_list(
|
|
|
335
342
|
)
|
|
336
343
|
rows: list[dict[str, Any]] = [
|
|
337
344
|
{
|
|
338
|
-
"field_value": s.name,
|
|
345
|
+
"field_value": _decode_stanza(s.name),
|
|
339
346
|
"tags": ";".join(_enabled_tag_names(dict(s.content))),
|
|
340
347
|
"app": dict(s.access).get("app", ""),
|
|
341
348
|
}
|
|
@@ -359,9 +366,16 @@ def tags_get(ctx: click.Context, field_value: str, app: str | None) -> None:
|
|
|
359
366
|
try:
|
|
360
367
|
entity = conf_ops.get_stanza(client, "tags", field_value, app=app)
|
|
361
368
|
except KeyError:
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
369
|
+
encoded = urllib.parse.quote(field_value, safe="=")
|
|
370
|
+
try:
|
|
371
|
+
entity = conf_ops.get_stanza(client, "tags", encoded, app=app)
|
|
372
|
+
except KeyError:
|
|
373
|
+
output.error(
|
|
374
|
+
f"Stanza '{field_value}' not found in tags.conf.", kind="not_found"
|
|
375
|
+
)
|
|
376
|
+
ctx.exit(1)
|
|
377
|
+
return
|
|
378
|
+
output.render(
|
|
379
|
+
ctx,
|
|
380
|
+
{"name": _decode_stanza(entity.name), **_tag_states(dict(entity.content))},
|
|
381
|
+
)
|