splunkctl 0.8.0__tar.gz → 0.9.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.
Files changed (70) hide show
  1. {splunkctl-0.8.0 → splunkctl-0.9.0}/PKG-INFO +43 -22
  2. {splunkctl-0.8.0 → splunkctl-0.9.0}/README.md +42 -21
  3. splunkctl-0.9.0/splunkctl/__init__.py +3 -0
  4. {splunkctl-0.8.0 → splunkctl-0.9.0}/splunkctl/mcp/server.py +76 -10
  5. {splunkctl-0.8.0 → splunkctl-0.9.0}/splunkctl/mcp/tools.py +51 -11
  6. splunkctl-0.9.0/splunkctl/soar/cimcef.py +254 -0
  7. {splunkctl-0.8.0 → splunkctl-0.9.0}/splunkctl/soar/client.py +12 -4
  8. {splunkctl-0.8.0 → splunkctl-0.9.0}/splunkctl.egg-info/PKG-INFO +43 -22
  9. {splunkctl-0.8.0 → splunkctl-0.9.0}/splunkctl.egg-info/SOURCES.txt +1 -0
  10. {splunkctl-0.8.0 → splunkctl-0.9.0}/tests/test_version.py +1 -1
  11. splunkctl-0.8.0/splunkctl/__init__.py +0 -3
  12. {splunkctl-0.8.0 → splunkctl-0.9.0}/LICENSE +0 -0
  13. {splunkctl-0.8.0 → splunkctl-0.9.0}/pyproject.toml +0 -0
  14. {splunkctl-0.8.0 → splunkctl-0.9.0}/setup.cfg +0 -0
  15. {splunkctl-0.8.0 → splunkctl-0.9.0}/splunkctl/__main__.py +0 -0
  16. {splunkctl-0.8.0 → splunkctl-0.9.0}/splunkctl/client.py +0 -0
  17. {splunkctl-0.8.0 → splunkctl-0.9.0}/splunkctl/commands/__init__.py +0 -0
  18. {splunkctl-0.8.0 → splunkctl-0.9.0}/splunkctl/commands/alerts.py +0 -0
  19. {splunkctl-0.8.0 → splunkctl-0.9.0}/splunkctl/commands/apps.py +0 -0
  20. {splunkctl-0.8.0 → splunkctl-0.9.0}/splunkctl/commands/audit.py +0 -0
  21. {splunkctl-0.8.0 → splunkctl-0.9.0}/splunkctl/commands/audit_parse.py +0 -0
  22. {splunkctl-0.8.0 → splunkctl-0.9.0}/splunkctl/commands/commands_meta.py +0 -0
  23. {splunkctl-0.8.0 → splunkctl-0.9.0}/splunkctl/commands/common.py +0 -0
  24. {splunkctl-0.8.0 → splunkctl-0.9.0}/splunkctl/commands/conf.py +0 -0
  25. {splunkctl-0.8.0 → splunkctl-0.9.0}/splunkctl/commands/conf_ops.py +0 -0
  26. {splunkctl-0.8.0 → splunkctl-0.9.0}/splunkctl/commands/config_cmd.py +0 -0
  27. {splunkctl-0.8.0 → splunkctl-0.9.0}/splunkctl/commands/dashboards.py +0 -0
  28. {splunkctl-0.8.0 → splunkctl-0.9.0}/splunkctl/commands/datamodels.py +0 -0
  29. {splunkctl-0.8.0 → splunkctl-0.9.0}/splunkctl/commands/doctor.py +0 -0
  30. {splunkctl-0.8.0 → splunkctl-0.9.0}/splunkctl/commands/es.py +0 -0
  31. {splunkctl-0.8.0 → splunkctl-0.9.0}/splunkctl/commands/hec.py +0 -0
  32. {splunkctl-0.8.0 → splunkctl-0.9.0}/splunkctl/commands/indexes.py +0 -0
  33. {splunkctl-0.8.0 → splunkctl-0.9.0}/splunkctl/commands/info.py +0 -0
  34. {splunkctl-0.8.0 → splunkctl-0.9.0}/splunkctl/commands/inputs.py +0 -0
  35. {splunkctl-0.8.0 → splunkctl-0.9.0}/splunkctl/commands/knowledge.py +0 -0
  36. {splunkctl-0.8.0 → splunkctl-0.9.0}/splunkctl/commands/kvstore.py +0 -0
  37. {splunkctl-0.8.0 → splunkctl-0.9.0}/splunkctl/commands/lookups.py +0 -0
  38. {splunkctl-0.8.0 → splunkctl-0.9.0}/splunkctl/commands/lookups_wiring.py +0 -0
  39. {splunkctl-0.8.0 → splunkctl-0.9.0}/splunkctl/commands/mcp_cmd.py +0 -0
  40. {splunkctl-0.8.0 → splunkctl-0.9.0}/splunkctl/commands/parsers.py +0 -0
  41. {splunkctl-0.8.0 → splunkctl-0.9.0}/splunkctl/commands/parsers_io.py +0 -0
  42. {splunkctl-0.8.0 → splunkctl-0.9.0}/splunkctl/commands/rules.py +0 -0
  43. {splunkctl-0.8.0 → splunkctl-0.9.0}/splunkctl/commands/rules_io.py +0 -0
  44. {splunkctl-0.8.0 → splunkctl-0.9.0}/splunkctl/commands/search.py +0 -0
  45. {splunkctl-0.8.0 → splunkctl-0.9.0}/splunkctl/commands/server.py +0 -0
  46. {splunkctl-0.8.0 → splunkctl-0.9.0}/splunkctl/commands/server_topology.py +0 -0
  47. {splunkctl-0.8.0 → splunkctl-0.9.0}/splunkctl/commands/state.py +0 -0
  48. {splunkctl-0.8.0 → splunkctl-0.9.0}/splunkctl/commands/state_io.py +0 -0
  49. {splunkctl-0.8.0 → splunkctl-0.9.0}/splunkctl/commands/state_io_blobs.py +0 -0
  50. {splunkctl-0.8.0 → splunkctl-0.9.0}/splunkctl/commands/state_io_confs.py +0 -0
  51. {splunkctl-0.8.0 → splunkctl-0.9.0}/splunkctl/commands/state_types.py +0 -0
  52. {splunkctl-0.8.0 → splunkctl-0.9.0}/splunkctl/commands/users.py +0 -0
  53. {splunkctl-0.8.0 → splunkctl-0.9.0}/splunkctl/config.py +0 -0
  54. {splunkctl-0.8.0 → splunkctl-0.9.0}/splunkctl/errors.py +0 -0
  55. {splunkctl-0.8.0 → splunkctl-0.9.0}/splunkctl/guard.py +0 -0
  56. {splunkctl-0.8.0 → splunkctl-0.9.0}/splunkctl/main.py +0 -0
  57. {splunkctl-0.8.0 → splunkctl-0.9.0}/splunkctl/mcp/__init__.py +0 -0
  58. {splunkctl-0.8.0 → splunkctl-0.9.0}/splunkctl/mcp/resources.py +0 -0
  59. {splunkctl-0.8.0 → splunkctl-0.9.0}/splunkctl/output.py +0 -0
  60. {splunkctl-0.8.0 → splunkctl-0.9.0}/splunkctl/soar/__init__.py +0 -0
  61. {splunkctl-0.8.0 → splunkctl-0.9.0}/splunkctl.egg-info/dependency_links.txt +0 -0
  62. {splunkctl-0.8.0 → splunkctl-0.9.0}/splunkctl.egg-info/entry_points.txt +0 -0
  63. {splunkctl-0.8.0 → splunkctl-0.9.0}/splunkctl.egg-info/requires.txt +0 -0
  64. {splunkctl-0.8.0 → splunkctl-0.9.0}/splunkctl.egg-info/top_level.txt +0 -0
  65. {splunkctl-0.8.0 → splunkctl-0.9.0}/tests/test_client.py +0 -0
  66. {splunkctl-0.8.0 → splunkctl-0.9.0}/tests/test_config.py +0 -0
  67. {splunkctl-0.8.0 → splunkctl-0.9.0}/tests/test_guard.py +0 -0
  68. {splunkctl-0.8.0 → splunkctl-0.9.0}/tests/test_main_errors.py +0 -0
  69. {splunkctl-0.8.0 → splunkctl-0.9.0}/tests/test_main_hoisting.py +0 -0
  70. {splunkctl-0.8.0 → splunkctl-0.9.0}/tests/test_output.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: splunkctl
3
- Version: 0.8.0
3
+ Version: 0.9.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 a **remote** Splunk
54
- Enterprise instance over the REST API. Built on the
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/). 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
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,21 @@ 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
- - **Built for agents** — built-in MCP server with 129 auto-generated tools,
94
- progressive discovery (5 meta-tools + focus/unfocus), 22 guide resources,
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 run --wait` drives and polls runs to completion;
105
+ `soar actions run --wait` does the same for connector actions.
106
+ - **SIEM-to-SOAR ingest** — `soar ingest --spl` runs a SIEM search and
107
+ creates SOAR containers + typed CEF artifacts using the official CIM-to-CEF
108
+ field map, with SDI dedup, severity mapping, and last-artifact automation
109
+ batching.
110
+ - **Built for agents** — built-in MCP server with 237 auto-generated tools,
111
+ progressive discovery (5 meta-tools + focus/unfocus), 33 guide resources,
95
112
  guard markers on every mutation, dual output (TTY = table, pipe = JSON).
96
113
 
97
114
  ## Install
@@ -119,10 +136,12 @@ splunkctl --version
119
136
  ## Quickstart
120
137
 
121
138
  ```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
139
+ splunkctl config init # interactive SIEM setup
140
+ splunkctl config init --soar # add SOAR credentials
141
+ splunkctl doctor # check SIEM connection
142
+ splunkctl soar test # check SOAR connection
143
+ splunkctl search run 'index=main | head 10' # run a SIEM search
144
+ splunkctl soar containers list # list SOAR containers
126
145
  splunkctl commands --json # discover every verb
127
146
  ```
128
147
 
@@ -174,6 +193,7 @@ splunkctl state push --dir config/ --report r.json --yes # deploy + evidence
174
193
  | `tags` | Tags — list, get |
175
194
  | `datamodels` | Data model definitions + acceleration health |
176
195
  | `state` | Config-as-code pull/diff/push with change-evidence reports |
196
+ | `soar` | SOAR: containers, artifacts, vault, playbooks, actions, cases, ingest |
177
197
  | `commands` | Machine-readable command tree (JSON) |
178
198
  | `mcp` | Built-in MCP server for AI agent integration |
179
199
 
@@ -245,11 +265,12 @@ splunkctl mcp install # register in .mcp.json
245
265
  splunkctl mcp serve # start stdio MCP server
246
266
  ```
247
267
 
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).
268
+ The MCP server auto-generates 237 typed tools from the Click command tree
269
+ (SIEM + SOAR) with progressive discovery -- agents start with 5 meta-tools
270
+ (`help`, `usage`, `focus`, `unfocus`, `run`) and dynamically load typed
271
+ schemas per command group. Subgroup-granular focus works for nested groups
272
+ like `soar containers`. 33 guide resources are served as `guide://` URIs.
273
+ Mutations are guarded: `yes=true` to apply (dry-run by default).
253
274
 
254
275
  ## License
255
276
 
@@ -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 a **remote** Splunk
16
- Enterprise instance over the REST API. Built on the
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/). 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
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,21 @@ 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
- - **Built for agents** — built-in MCP server with 129 auto-generated tools,
56
- progressive discovery (5 meta-tools + focus/unfocus), 22 guide resources,
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 run --wait` drives and polls runs to completion;
67
+ `soar actions run --wait` does the same for connector actions.
68
+ - **SIEM-to-SOAR ingest** — `soar ingest --spl` runs a SIEM search and
69
+ creates SOAR containers + typed CEF artifacts using the official CIM-to-CEF
70
+ field map, with SDI dedup, severity mapping, and last-artifact automation
71
+ batching.
72
+ - **Built for agents** — built-in MCP server with 237 auto-generated tools,
73
+ progressive discovery (5 meta-tools + focus/unfocus), 33 guide resources,
57
74
  guard markers on every mutation, dual output (TTY = table, pipe = JSON).
58
75
 
59
76
  ## Install
@@ -81,10 +98,12 @@ splunkctl --version
81
98
  ## Quickstart
82
99
 
83
100
  ```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
101
+ splunkctl config init # interactive SIEM setup
102
+ splunkctl config init --soar # add SOAR credentials
103
+ splunkctl doctor # check SIEM connection
104
+ splunkctl soar test # check SOAR connection
105
+ splunkctl search run 'index=main | head 10' # run a SIEM search
106
+ splunkctl soar containers list # list SOAR containers
88
107
  splunkctl commands --json # discover every verb
89
108
  ```
90
109
 
@@ -136,6 +155,7 @@ splunkctl state push --dir config/ --report r.json --yes # deploy + evidence
136
155
  | `tags` | Tags — list, get |
137
156
  | `datamodels` | Data model definitions + acceleration health |
138
157
  | `state` | Config-as-code pull/diff/push with change-evidence reports |
158
+ | `soar` | SOAR: containers, artifacts, vault, playbooks, actions, cases, ingest |
139
159
  | `commands` | Machine-readable command tree (JSON) |
140
160
  | `mcp` | Built-in MCP server for AI agent integration |
141
161
 
@@ -207,11 +227,12 @@ splunkctl mcp install # register in .mcp.json
207
227
  splunkctl mcp serve # start stdio MCP server
208
228
  ```
209
229
 
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).
230
+ The MCP server auto-generates 237 typed tools from the Click command tree
231
+ (SIEM + SOAR) with progressive discovery -- agents start with 5 meta-tools
232
+ (`help`, `usage`, `focus`, `unfocus`, `run`) and dynamically load typed
233
+ schemas per command group. Subgroup-granular focus works for nested groups
234
+ like `soar containers`. 33 guide resources are served as `guide://` URIs.
235
+ Mutations are guarded: `yes=true` to apply (dry-run by default).
215
236
 
216
237
  ## License
217
238
 
@@ -0,0 +1,3 @@
1
+ """CLI tool for Splunk Enterprise and Splunk SOAR operations."""
2
+
3
+ __version__ = "0.9.0"
@@ -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,12 @@ 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
+ subgroup_names,
26
30
  )
27
31
 
28
32
  _INSTRUCTIONS = """\
@@ -32,7 +36,10 @@ the preferred way to run commands (validated arguments, full schemas). \
32
36
  `usage <command>` previews one command's schema and auto-loads it as a \
33
37
  callable tool. `run` is an escape hatch for raw command strings (no \
34
38
  validation). `unfocus` when done to free context. Mutations are guarded: \
35
- pass yes=true to apply (dry-run by default)."""
39
+ pass yes=true to apply (dry-run by default). \
40
+ The soar tree has nested subgroups (containers, playbooks, actions, ...); \
41
+ focus at subgroup granularity: `focus "soar containers"`, not `focus soar`. \
42
+ `help soar` lists available subgroups."""
36
43
 
37
44
  _FORCE_FLAGS = ["--json"]
38
45
 
@@ -149,11 +156,40 @@ def create_server() -> FastMCP:
149
156
  """List command groups, or subcommands within a group."""
150
157
  if group is None:
151
158
  rows = group_summary(cli)
152
- lines = [
153
- f"{r['group']:16s} {r['description']} ({r['count']} cmds)" for r in rows
154
- ]
159
+ lines: list[str] = []
160
+ for r in rows:
161
+ line = f"{r['group']:16s} {r['description']} ({r['count']} cmds)"
162
+ lines.append(line)
163
+ if "subgroups" in r:
164
+ lines.append(f"{'':16s} subgroups: {r['subgroups']}")
165
+ lines.append(
166
+ f"{'':16s} focus at subgroup level: "
167
+ f'focus "{r["group"]} <subgroup>"'
168
+ )
155
169
  return "\n".join(lines)
156
- return _exec_cli([group, "--help"])
170
+ # For groups with subgroups, show the two-level layout
171
+ parts = _split_command(group)
172
+ if len(parts) == 1 and has_subgroups(cli, parts[0]):
173
+ grp = parts[0]
174
+ top_cmd = cli.commands.get(grp)
175
+ if not isinstance(top_cmd, click.Group): # guaranteed by has_subgroups
176
+ raise TypeError(f"expected group for {grp!r}")
177
+ sg = subgroup_names(cli, grp)
178
+ dc = direct_commands(cli, grp)
179
+ lines = [f'{grp} subgroups (focus "{grp} <subgroup>"):']
180
+ for s in sg:
181
+ sub_cmd = top_cmd.commands[s]
182
+ desc = (sub_cmd.help or "").split("\n")[0]
183
+ n = len(sub_cmd.commands) if isinstance(sub_cmd, click.Group) else 0
184
+ lines.append(f" {grp} {s:16s} {desc} ({n} cmds)")
185
+ if dc:
186
+ lines.append(f"\n{grp} direct commands:")
187
+ for c in dc:
188
+ sub_cmd = top_cmd.commands[c]
189
+ desc = (sub_cmd.help or "").split("\n")[0]
190
+ lines.append(f" {grp} {c:16s} {desc}")
191
+ return "\n".join(lines)
192
+ return _exec_cli([*parts, "--help"])
157
193
 
158
194
  # --- Meta-tool: usage ---
159
195
 
@@ -161,7 +197,8 @@ def create_server() -> FastMCP:
161
197
  name="usage",
162
198
  description=(
163
199
  "Show flags, args, and description for one command. "
164
- "Auto-loads it as a callable tool."
200
+ "Auto-loads it as a callable tool. "
201
+ 'Accepts nested paths: usage "soar playbooks run".'
165
202
  ),
166
203
  annotations=ToolAnnotations(readOnlyHint=True),
167
204
  )
@@ -193,7 +230,8 @@ def create_server() -> FastMCP:
193
230
  name="focus",
194
231
  description=(
195
232
  "Load typed tools for a command group (enables full schemas). "
196
- "Preferred over run for validated arguments."
233
+ "Preferred over run for validated arguments. "
234
+ 'Nested groups like soar require subgroup: focus "soar containers".'
197
235
  ),
198
236
  annotations=ToolAnnotations(readOnlyHint=True),
199
237
  )
@@ -203,6 +241,26 @@ def create_server() -> FastMCP:
203
241
  count = len(focused[group])
204
242
  return f"Group '{group}' already focused ({count} tools)."
205
243
 
244
+ # Reject bare groups that have nested subgroups (too many tools).
245
+ parts = _split_command(group)
246
+ if len(parts) == 1 and has_subgroups(cli, parts[0]):
247
+ sg = subgroup_names(cli, parts[0])
248
+ dc = direct_commands(cli, parts[0])
249
+ lines = [
250
+ f"'{parts[0]}' has nested subgroups — focus at "
251
+ f"subgroup level to avoid flooding context.",
252
+ "",
253
+ "Subgroups:",
254
+ ]
255
+ for s in sg:
256
+ lines.append(f' focus "{parts[0]} {s}"')
257
+ if dc:
258
+ lines.append(
259
+ f"\nDirect commands ({', '.join(dc)}) can be "
260
+ f"loaded individually via usage."
261
+ )
262
+ return "\n".join(lines)
263
+
206
264
  entries = group_tools(all_tools, group)
207
265
  if not entries:
208
266
  available = group_names(cli)
@@ -249,14 +307,22 @@ def create_server() -> FastMCP:
249
307
  await ctx.session.send_tool_list_changed()
250
308
  return f"Unloaded all focused tools ({removed} total)."
251
309
 
252
- if group not in focused:
310
+ # Try exact key first, then normalized form for subgroup paths
311
+ key = group if group in focused else None
312
+ if key is None:
313
+ norm = group.replace(" ", "_").replace("-", "_")
314
+ for k in focused:
315
+ if k.replace(" ", "_").replace("-", "_") == norm:
316
+ key = k
317
+ break
318
+ if key is None:
253
319
  return f"Group '{group}' is not focused."
254
- for n in focused[group]:
320
+ for n in focused[key]:
255
321
  try:
256
322
  mcp.remove_tool(n)
257
323
  except Exception: # noqa: BLE001, S110
258
324
  pass
259
- count = len(focused.pop(group))
325
+ count = len(focused.pop(key))
260
326
  if ctx is not None:
261
327
  await ctx.session.send_tool_list_changed()
262
328
  return f"Unloaded {count} tools for '{group}'."
@@ -207,13 +207,51 @@ def group_names(root: click.Group) -> list[str]:
207
207
 
208
208
 
209
209
  def group_tools(index: ToolIndex, group: str) -> list[ToolEntry]:
210
- """Return all tools under a given top-level group."""
211
- prefix = group.replace("-", "_") + "_"
210
+ """Return all tools under a group or subgroup path.
211
+
212
+ Accepts space-separated paths (``"soar containers"``) as well as
213
+ plain group names (``"indexes"``).
214
+ """
215
+ prefix = group.replace(" ", "_").replace("-", "_") + "_"
212
216
  return [t for t in index.values() if t.name.startswith(prefix)]
213
217
 
214
218
 
219
+ def subgroup_names(root: click.Group, group: str) -> list[str]:
220
+ """Return subgroup names within a top-level group.
221
+
222
+ For ``soar`` this yields ``["actions", "apps", "artifacts", ...]`` —
223
+ only the children that are themselves ``click.Group`` instances.
224
+ """
225
+ top = root.commands.get(group)
226
+ if not isinstance(top, click.Group):
227
+ return []
228
+ return sorted(
229
+ name for name, cmd in top.commands.items() if isinstance(cmd, click.Group)
230
+ )
231
+
232
+
233
+ def has_subgroups(root: click.Group, group: str) -> bool:
234
+ """Return True if the group has nested subgroups."""
235
+ return bool(subgroup_names(root, group))
236
+
237
+
238
+ def direct_commands(root: click.Group, group: str) -> list[str]:
239
+ """Return direct (non-group) command names under a top-level group."""
240
+ top = root.commands.get(group)
241
+ if not isinstance(top, click.Group):
242
+ return []
243
+ return sorted(
244
+ name for name, cmd in top.commands.items() if not isinstance(cmd, click.Group)
245
+ )
246
+
247
+
215
248
  def group_summary(root: click.Group) -> list[dict[str, str]]:
216
- """Build a summary of command groups with descriptions and counts."""
249
+ """Build a summary of command groups with descriptions and counts.
250
+
251
+ Groups that contain nested subgroups (like ``soar``) include a
252
+ ``subgroups`` key with the two-level layout so agents can discover
253
+ subgroup focus paths without flooding context.
254
+ """
217
255
  skip = {"mcp", "commands", "skill", "config", "doctor", "info"}
218
256
  rows: list[dict[str, str]] = []
219
257
  for name, cmd in sorted(root.commands.items()):
@@ -222,12 +260,14 @@ def group_summary(root: click.Group) -> list[dict[str, str]]:
222
260
  if isinstance(cmd, click.Group):
223
261
  subs = list(cmd.commands.keys())
224
262
  desc = (cmd.help or "").split("\n")[0]
225
- rows.append(
226
- {
227
- "group": name,
228
- "description": desc,
229
- "subcommands": ", ".join(sorted(subs)),
230
- "count": str(len(subs)),
231
- }
232
- )
263
+ row: dict[str, str] = {
264
+ "group": name,
265
+ "description": desc,
266
+ "subcommands": ", ".join(sorted(subs)),
267
+ "count": str(len(subs)),
268
+ }
269
+ sg = subgroup_names(root, name)
270
+ if sg:
271
+ row["subgroups"] = ", ".join(sg)
272
+ rows.append(row)
233
273
  return rows
@@ -0,0 +1,254 @@
1
+ """CIM-to-CEF field mapping and row-to-artifact transform.
2
+
3
+ Pure functions and data — no I/O. The ``CIM_CEF_MAP`` encodes the official
4
+ Splunk SOAR Connector for Splunk mapping (``bytesIn`` typo fixed).
5
+ ``CEF_CONTAINS_MAP`` maps CEF keys to SOAR contains types for artifact
6
+ actionability. ``SEVERITY_MAP`` converts Splunk severity/urgency strings
7
+ to SOAR severity levels.
8
+
9
+ Used by ``soar ingest`` (CLI) and ``soar artifacts create`` (auto cef_types).
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import hashlib
15
+ import json
16
+ from typing import Any
17
+
18
+ # ---- CIM -> CEF default map ------------------------------------------------
19
+ # Source: github.com/splunk-soar-connectors/splunk src/splunk_consts.py
20
+ # Fixed: upstream has ``butesIn`` typo for bytes_in -> ships as ``bytesIn``.
21
+
22
+ CIM_CEF_MAP: dict[str, str] = {
23
+ "action": "act",
24
+ "action_name": "act",
25
+ "app": "app",
26
+ "bytes_in": "bytesIn",
27
+ "bytes_out": "bytesOut",
28
+ "category": "cat",
29
+ "dest": "destinationAddress",
30
+ "dest_ip": "destinationAddress",
31
+ "dest_mac": "destinationMacAddress",
32
+ "dest_nt_domain": "destinationNtDomain",
33
+ "dest_port": "destinationPort",
34
+ "dest_translated_ip": "destinationTranslatedAddress",
35
+ "dest_translated_port": "destinationTranslatedPort",
36
+ "direction": "deviceDirection",
37
+ "dns": "destinationDnsDomain",
38
+ "dvc": "dvc",
39
+ "dvc_ip": "deviceAddress",
40
+ "dvc_mac": "deviceMacAddress",
41
+ "file_create_time": "fileCreateTime",
42
+ "file_hash": "fileHash",
43
+ "file_modify_time": "fileModificationTime",
44
+ "file_name": "fileName",
45
+ "file_path": "filePath",
46
+ "file_size": "fileSize",
47
+ "message": "message",
48
+ "protocol": "transportProtocol",
49
+ "transport": "transportProtocol",
50
+ "request_payload": "request",
51
+ "request_payload_type": "requestMethod",
52
+ "src": "sourceAddress",
53
+ "src_dns": "sourceDnsDomain",
54
+ "src_ip": "sourceAddress",
55
+ "src_mac": "sourceMacAddress",
56
+ "src_nt_domain": "sourceNtDomain",
57
+ "src_port": "sourcePort",
58
+ "src_translated_ip": "sourceTranslatedAddress",
59
+ "src_translated_port": "sourceTranslatedPort",
60
+ "src_user": "sourceUserId",
61
+ "url": "requestURL",
62
+ "user": "destinationUserName",
63
+ "user_id": "destinationUserId",
64
+ }
65
+
66
+ # ---- CEF contains-type map (canonical) -------------------------------------
67
+ # Maps CEF key -> list of SOAR contains types. Imported by
68
+ # ``splunkctl/commands/soar/artifacts.py`` for auto cef_types on create.
69
+
70
+ CEF_CONTAINS_MAP: dict[str, list[str]] = {
71
+ "destinationAddress": ["ip", "host name"],
72
+ "destinationMacAddress": ["mac address"],
73
+ "destinationNtDomain": ["domain"],
74
+ "destinationPort": ["port"],
75
+ "destinationTranslatedAddress": ["ip"],
76
+ "destinationTranslatedPort": ["port"],
77
+ "destinationDnsDomain": ["domain"],
78
+ "dvc": ["ip", "host name"],
79
+ "deviceAddress": ["ip"],
80
+ "deviceMacAddress": ["mac address"],
81
+ "fileHash": ["hash"],
82
+ "fileName": ["file name"],
83
+ "filePath": ["file path"],
84
+ "sourceAddress": ["ip", "host name"],
85
+ "sourceDnsDomain": ["domain"],
86
+ "sourceMacAddress": ["mac address"],
87
+ "sourceNtDomain": ["domain"],
88
+ "sourcePort": ["port"],
89
+ "sourceTranslatedAddress": ["ip"],
90
+ "sourceTranslatedPort": ["port"],
91
+ "sourceUserId": ["user name"],
92
+ "requestURL": ["url"],
93
+ "destinationUserName": ["user name"],
94
+ "destinationUserId": ["user name"],
95
+ }
96
+
97
+ # ---- Severity map -----------------------------------------------------------
98
+ # Check ``severity`` first, then ``urgency``; default ``medium``.
99
+ # Multivalue -> highest wins.
100
+
101
+ _SEVERITY_ORDER: dict[str, int] = {
102
+ "informational": 0,
103
+ "low": 1,
104
+ "medium": 2,
105
+ "high": 3,
106
+ "critical": 4,
107
+ }
108
+
109
+ _SPLUNK_TO_SOAR: dict[str, str] = {
110
+ "informational": "low",
111
+ "low": "low",
112
+ "medium": "medium",
113
+ "high": "high",
114
+ "critical": "high",
115
+ }
116
+
117
+
118
+ def map_severity(row: dict[str, Any]) -> str:
119
+ """Derive SOAR severity from a Splunk result row.
120
+
121
+ Checks ``severity`` first, then ``urgency``. If both are absent,
122
+ returns ``medium``. When the field contains multiple values
123
+ (comma-separated or list), the highest wins.
124
+ """
125
+ raw = row.get("severity") or row.get("urgency") or ""
126
+ if isinstance(raw, list):
127
+ values = [str(v).strip().lower() for v in raw if v]
128
+ else:
129
+ values = [v.strip().lower() for v in str(raw).split(",") if v.strip()]
130
+
131
+ if not values:
132
+ return "medium"
133
+
134
+ best_rank = -1
135
+ best_sev = "medium"
136
+ for v in values:
137
+ rank = _SEVERITY_ORDER.get(v, -1)
138
+ if rank > best_rank:
139
+ best_rank = rank
140
+ best_sev = _SPLUNK_TO_SOAR.get(v, "medium")
141
+ return best_sev
142
+
143
+
144
+ # ---- Row -> artifact transform ----------------------------------------------
145
+
146
+
147
+ def row_sdi(row: dict[str, Any], sdi_field: str) -> str:
148
+ """Compute the source_data_identifier for a result row.
149
+
150
+ If ``sdi_field`` names a field present in the row, use its value.
151
+ Otherwise hash the full row (SHA-256).
152
+ """
153
+ val = row.get(sdi_field)
154
+ if val is not None and str(val).strip():
155
+ return str(val).strip()
156
+ canonical = json.dumps(row, sort_keys=True, default=str)
157
+ return hashlib.sha256(canonical.encode()).hexdigest()
158
+
159
+
160
+ def row_to_cef(
161
+ row: dict[str, Any],
162
+ *,
163
+ cim_map: dict[str, str] | None = None,
164
+ include_unmapped: bool = False,
165
+ ) -> dict[str, str]:
166
+ """Map a Splunk result row to a CEF dict.
167
+
168
+ Args:
169
+ row: Single Splunk result dict.
170
+ cim_map: CIM->CEF mapping; defaults to ``CIM_CEF_MAP``.
171
+ include_unmapped: If True, fields not in the map are passed
172
+ through as custom CEF keys.
173
+
174
+ Returns:
175
+ CEF dict suitable for ``POST /rest/artifact``.
176
+ """
177
+ effective = cim_map if cim_map is not None else CIM_CEF_MAP
178
+ cef: dict[str, str] = {}
179
+ mapped_keys: set[str] = set()
180
+
181
+ for cim_field, cef_key in effective.items():
182
+ val = row.get(cim_field)
183
+ if val is not None and str(val).strip():
184
+ cef[cef_key] = str(val)
185
+ mapped_keys.add(cim_field)
186
+
187
+ if include_unmapped:
188
+ skip = {"_raw", "_time", "_si", "_serial", "_sourcetype", "_indextime"}
189
+ for key, val in row.items():
190
+ if key in mapped_keys or key in skip or key.startswith("_"):
191
+ continue
192
+ if key in effective:
193
+ continue
194
+ if val is not None and str(val).strip():
195
+ cef[key] = str(val)
196
+
197
+ return cef
198
+
199
+
200
+ def auto_cef_types(
201
+ cef: dict[str, Any],
202
+ *,
203
+ contains_map: dict[str, list[str]] | None = None,
204
+ ) -> dict[str, list[str]]:
205
+ """Build cef_types from the contains map for a CEF dict."""
206
+ effective = contains_map if contains_map is not None else CEF_CONTAINS_MAP
207
+ types: dict[str, list[str]] = {}
208
+ for key in cef:
209
+ if key in effective:
210
+ types[key] = effective[key]
211
+ return types
212
+
213
+
214
+ def container_name_for_row(
215
+ row: dict[str, Any],
216
+ *,
217
+ template: str | None = None,
218
+ name_field: str | None = None,
219
+ ) -> str:
220
+ """Derive a container name for a result row.
221
+
222
+ Priority:
223
+ 1. ``name_field`` — use that row field's value.
224
+ 2. ``template`` — use as literal name.
225
+ 3. ``rule_name`` field (ES notable convention).
226
+ 4. Fallback ``"Splunk Log Entry on <_time>: <source>"``.
227
+ """
228
+ if name_field:
229
+ val = row.get(name_field)
230
+ if val and str(val).strip():
231
+ return str(val).strip()
232
+
233
+ if template:
234
+ return template
235
+
236
+ rule_name = row.get("rule_name") or row.get("search_name")
237
+ if rule_name and str(rule_name).strip():
238
+ return str(rule_name).strip()
239
+
240
+ _time = row.get("_time", "unknown")
241
+ source = row.get("source", "unknown")
242
+ return f"Splunk Log Entry on {_time}: {source}"
243
+
244
+
245
+ def artifact_name_for_row(row: dict[str, Any]) -> str:
246
+ """Derive an artifact name for a result row.
247
+
248
+ Uses ``search_name`` if present, else ``"Field Values"``
249
+ (connector convention).
250
+ """
251
+ name = row.get("search_name")
252
+ if name and str(name).strip():
253
+ return str(name).strip()
254
+ return "Field Values"
@@ -269,11 +269,13 @@ class SOARClient:
269
269
  if not isinstance(body, dict):
270
270
  return body
271
271
 
272
- # {results} envelope (search)
272
+ # {results} envelope (search) — server paginates (1-based pages)
273
+ # and returns real num_pages; forward it instead of hardcoding 1.
273
274
  if endpoint_root in _RESULTS_ENVELOPE_ENDPOINTS and "results" in body:
274
275
  results = body["results"]
275
276
  count = body.get("count", len(results))
276
- return {"count": count, "num_pages": 1, "data": results}
277
+ num_pages = body.get("num_pages", 1)
278
+ return {"count": count, "num_pages": num_pages, "data": results}
277
279
 
278
280
  return body
279
281
 
@@ -368,8 +370,14 @@ class SOARClient:
368
370
 
369
371
  Yields individual items from each page, transparently fetching
370
372
  subsequent pages as needed.
373
+
374
+ Pagination origin varies by endpoint:
375
+ - ``search`` (``_RESULTS_ENVELOPE_ENDPOINTS``): 1-based pages.
376
+ - All other endpoints: 0-based pages.
371
377
  """
372
- page = 0
378
+ endpoint_root = path.lstrip("/").split("/")[0]
379
+ origin = 1 if endpoint_root in _RESULTS_ENVELOPE_ENDPOINTS else 0
380
+ page = origin
373
381
  while True:
374
382
  page_params = dict(params or {})
375
383
  page_params["page"] = page
@@ -381,5 +389,5 @@ class SOARClient:
381
389
 
382
390
  num_pages = result.get("num_pages", 1)
383
391
  page += 1
384
- if page >= num_pages or not data:
392
+ if page - origin >= num_pages or not data:
385
393
  break
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: splunkctl
3
- Version: 0.8.0
3
+ Version: 0.9.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 a **remote** Splunk
54
- Enterprise instance over the REST API. Built on the
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/). 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
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,21 @@ 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
- - **Built for agents** — built-in MCP server with 129 auto-generated tools,
94
- progressive discovery (5 meta-tools + focus/unfocus), 22 guide resources,
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 run --wait` drives and polls runs to completion;
105
+ `soar actions run --wait` does the same for connector actions.
106
+ - **SIEM-to-SOAR ingest** — `soar ingest --spl` runs a SIEM search and
107
+ creates SOAR containers + typed CEF artifacts using the official CIM-to-CEF
108
+ field map, with SDI dedup, severity mapping, and last-artifact automation
109
+ batching.
110
+ - **Built for agents** — built-in MCP server with 237 auto-generated tools,
111
+ progressive discovery (5 meta-tools + focus/unfocus), 33 guide resources,
95
112
  guard markers on every mutation, dual output (TTY = table, pipe = JSON).
96
113
 
97
114
  ## Install
@@ -119,10 +136,12 @@ splunkctl --version
119
136
  ## Quickstart
120
137
 
121
138
  ```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
139
+ splunkctl config init # interactive SIEM setup
140
+ splunkctl config init --soar # add SOAR credentials
141
+ splunkctl doctor # check SIEM connection
142
+ splunkctl soar test # check SOAR connection
143
+ splunkctl search run 'index=main | head 10' # run a SIEM search
144
+ splunkctl soar containers list # list SOAR containers
126
145
  splunkctl commands --json # discover every verb
127
146
  ```
128
147
 
@@ -174,6 +193,7 @@ splunkctl state push --dir config/ --report r.json --yes # deploy + evidence
174
193
  | `tags` | Tags — list, get |
175
194
  | `datamodels` | Data model definitions + acceleration health |
176
195
  | `state` | Config-as-code pull/diff/push with change-evidence reports |
196
+ | `soar` | SOAR: containers, artifacts, vault, playbooks, actions, cases, ingest |
177
197
  | `commands` | Machine-readable command tree (JSON) |
178
198
  | `mcp` | Built-in MCP server for AI agent integration |
179
199
 
@@ -245,11 +265,12 @@ splunkctl mcp install # register in .mcp.json
245
265
  splunkctl mcp serve # start stdio MCP server
246
266
  ```
247
267
 
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).
268
+ The MCP server auto-generates 237 typed tools from the Click command tree
269
+ (SIEM + SOAR) with progressive discovery -- agents start with 5 meta-tools
270
+ (`help`, `usage`, `focus`, `unfocus`, `run`) and dynamically load typed
271
+ schemas per command group. Subgroup-granular focus works for nested groups
272
+ like `soar containers`. 33 guide resources are served as `guide://` URIs.
273
+ Mutations are guarded: `yes=true` to apply (dry-run by default).
253
274
 
254
275
  ## License
255
276
 
@@ -56,6 +56,7 @@ splunkctl/mcp/resources.py
56
56
  splunkctl/mcp/server.py
57
57
  splunkctl/mcp/tools.py
58
58
  splunkctl/soar/__init__.py
59
+ splunkctl/soar/cimcef.py
59
60
  splunkctl/soar/client.py
60
61
  tests/test_client.py
61
62
  tests/test_config.py
@@ -4,4 +4,4 @@ from splunkctl import __version__
4
4
 
5
5
 
6
6
  def test_version() -> None:
7
- assert __version__ == "0.8.0"
7
+ assert __version__ == "0.9.0"
@@ -1,3 +0,0 @@
1
- """CLI tool for Splunk Enterprise SIEM operations."""
2
-
3
- __version__ = "0.8.0"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes