splunkctl 0.1.0__tar.gz → 0.3.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 (54) hide show
  1. {splunkctl-0.1.0 → splunkctl-0.3.0}/PKG-INFO +88 -12
  2. splunkctl-0.3.0/README.md +157 -0
  3. {splunkctl-0.1.0 → splunkctl-0.3.0}/pyproject.toml +15 -2
  4. {splunkctl-0.1.0 → splunkctl-0.3.0}/splunkctl/__init__.py +1 -1
  5. splunkctl-0.3.0/splunkctl/client.py +333 -0
  6. splunkctl-0.3.0/splunkctl/commands/alerts.py +131 -0
  7. {splunkctl-0.1.0 → splunkctl-0.3.0}/splunkctl/commands/apps.py +19 -14
  8. {splunkctl-0.1.0 → splunkctl-0.3.0}/splunkctl/commands/commands_meta.py +8 -0
  9. splunkctl-0.3.0/splunkctl/commands/common.py +161 -0
  10. splunkctl-0.3.0/splunkctl/commands/dashboards.py +467 -0
  11. splunkctl-0.3.0/splunkctl/commands/doctor.py +268 -0
  12. splunkctl-0.3.0/splunkctl/commands/hec.py +254 -0
  13. {splunkctl-0.1.0 → splunkctl-0.3.0}/splunkctl/commands/indexes.py +51 -12
  14. {splunkctl-0.1.0 → splunkctl-0.3.0}/splunkctl/commands/inputs.py +5 -0
  15. {splunkctl-0.1.0 → splunkctl-0.3.0}/splunkctl/commands/lookups.py +66 -86
  16. splunkctl-0.3.0/splunkctl/commands/parsers.py +380 -0
  17. splunkctl-0.3.0/splunkctl/commands/parsers_io.py +228 -0
  18. {splunkctl-0.1.0 → splunkctl-0.3.0}/splunkctl/commands/rules.py +145 -11
  19. splunkctl-0.3.0/splunkctl/commands/rules_io.py +302 -0
  20. {splunkctl-0.1.0 → splunkctl-0.3.0}/splunkctl/commands/search.py +134 -12
  21. splunkctl-0.3.0/splunkctl/commands/server.py +107 -0
  22. splunkctl-0.3.0/splunkctl/commands/users.py +430 -0
  23. {splunkctl-0.1.0 → splunkctl-0.3.0}/splunkctl/guard.py +14 -0
  24. {splunkctl-0.1.0 → splunkctl-0.3.0}/splunkctl/main.py +36 -2
  25. {splunkctl-0.1.0 → splunkctl-0.3.0}/splunkctl/output.py +47 -4
  26. {splunkctl-0.1.0 → splunkctl-0.3.0}/splunkctl/skill/SKILL.md +108 -14
  27. {splunkctl-0.1.0 → splunkctl-0.3.0}/splunkctl.egg-info/PKG-INFO +88 -12
  28. {splunkctl-0.1.0 → splunkctl-0.3.0}/splunkctl.egg-info/SOURCES.txt +7 -0
  29. splunkctl-0.3.0/splunkctl.egg-info/requires.txt +15 -0
  30. splunkctl-0.3.0/tests/test_client.py +199 -0
  31. splunkctl-0.3.0/tests/test_main_hoisting.py +60 -0
  32. {splunkctl-0.1.0 → splunkctl-0.3.0}/tests/test_output.py +70 -0
  33. {splunkctl-0.1.0 → splunkctl-0.3.0}/tests/test_version.py +1 -1
  34. splunkctl-0.1.0/README.md +0 -91
  35. splunkctl-0.1.0/splunkctl/client.py +0 -103
  36. splunkctl-0.1.0/splunkctl/commands/alerts.py +0 -94
  37. splunkctl-0.1.0/splunkctl/commands/dashboards.py +0 -216
  38. splunkctl-0.1.0/splunkctl/commands/parsers.py +0 -177
  39. splunkctl-0.1.0/splunkctl/commands/users.py +0 -211
  40. splunkctl-0.1.0/splunkctl.egg-info/requires.txt +0 -4
  41. splunkctl-0.1.0/tests/test_client.py +0 -56
  42. {splunkctl-0.1.0 → splunkctl-0.3.0}/LICENSE +0 -0
  43. {splunkctl-0.1.0 → splunkctl-0.3.0}/setup.cfg +0 -0
  44. {splunkctl-0.1.0 → splunkctl-0.3.0}/splunkctl/__main__.py +0 -0
  45. {splunkctl-0.1.0 → splunkctl-0.3.0}/splunkctl/commands/__init__.py +0 -0
  46. {splunkctl-0.1.0 → splunkctl-0.3.0}/splunkctl/commands/config_cmd.py +0 -0
  47. {splunkctl-0.1.0 → splunkctl-0.3.0}/splunkctl/commands/info.py +0 -0
  48. {splunkctl-0.1.0 → splunkctl-0.3.0}/splunkctl/commands/skill_cmd.py +0 -0
  49. {splunkctl-0.1.0 → splunkctl-0.3.0}/splunkctl/config.py +0 -0
  50. {splunkctl-0.1.0 → splunkctl-0.3.0}/splunkctl.egg-info/dependency_links.txt +0 -0
  51. {splunkctl-0.1.0 → splunkctl-0.3.0}/splunkctl.egg-info/entry_points.txt +0 -0
  52. {splunkctl-0.1.0 → splunkctl-0.3.0}/splunkctl.egg-info/top_level.txt +0 -0
  53. {splunkctl-0.1.0 → splunkctl-0.3.0}/tests/test_config.py +0 -0
  54. {splunkctl-0.1.0 → splunkctl-0.3.0}/tests/test_guard.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: splunkctl
3
- Version: 0.1.0
3
+ Version: 0.3.0
4
4
  Summary: CLI tool for Splunk Enterprise SIEM operations.
5
5
  Author: dannyota
6
6
  License-Expression: Apache-2.0
@@ -19,57 +19,116 @@ Classifier: Topic :: System :: Systems Administration
19
19
  Requires-Python: >=3.13
20
20
  Description-Content-Type: text/markdown
21
21
  License-File: LICENSE
22
- Requires-Dist: splunk-sdk>=2.1.0
23
- Requires-Dist: click>=8.0
22
+ Requires-Dist: splunk-sdk>=2.0
23
+ Requires-Dist: click>=8.2
24
24
  Requires-Dist: pyyaml>=6.0
25
25
  Requires-Dist: tabulate>=0.9
26
+ Requires-Dist: requests>=2.32
27
+ Requires-Dist: defusedxml>=0.7
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest>=8.0; extra == "dev"
30
+ Requires-Dist: ruff>=0.6; extra == "dev"
31
+ Requires-Dist: mypy>=1.11; extra == "dev"
32
+ Requires-Dist: types-requests; extra == "dev"
33
+ Requires-Dist: types-PyYAML; extra == "dev"
34
+ Requires-Dist: types-tabulate; extra == "dev"
35
+ Requires-Dist: types-defusedxml; extra == "dev"
26
36
  Dynamic: license-file
27
37
 
28
38
  # splunkctl
29
39
 
30
40
  CLI tool for Splunk Enterprise SIEM operations.
31
41
 
32
- Query, inspect, and manage a Splunk Enterprise instance from the terminal.
33
- Built on the [splunk-sdk-python](https://github.com/dannyota/splunk-sdk-python)
42
+ Query, inspect, and manage a remote Splunk Enterprise instance from your
43
+ laptop. Built on the [splunk-sdk-python](https://github.com/dannyota/splunk-sdk-python/tree/splunkctl)
34
44
  fork with [Click](https://click.palletsprojects.com/).
35
45
 
36
46
  ## Install
37
47
 
38
48
  ```bash
39
49
  pip install splunkctl
50
+ pip install git+https://github.com/dannyota/splunk-sdk-python@splunkctl
40
51
  ```
41
52
 
42
- Requires Python 3.13+.
53
+ Requires Python 3.13+. The second line installs the
54
+ [forked SDK](https://github.com/dannyota/splunk-sdk-python/tree/splunkctl)
55
+ which adds dashboard, lookup, and HEC token support. Without it, core
56
+ commands (search, rules, alerts, indexes, inputs, apps, users) still work.
57
+
58
+ ### Development
59
+
60
+ ```bash
61
+ git clone https://github.com/dannyota/splunkctl
62
+ cd splunkctl
63
+ pip install -e .
64
+ splunkctl --version
65
+ ```
43
66
 
44
67
  ## Quick start
45
68
 
46
69
  ```bash
47
70
  splunkctl config init # interactive setup
48
- splunkctl config test # verify connectivity
71
+ splunkctl doctor # check connection, auth, permissions
49
72
  splunkctl search run 'index=main | head 10' # run a search
50
73
  splunkctl rules list # list detection rules
51
- splunkctl dashboards list # list dashboards
52
74
  ```
53
75
 
54
76
  ## Commands
55
77
 
56
78
  | Group | Description |
57
79
  |---|---|
80
+ | `doctor` | Connection, auth, health, and permissions check |
58
81
  | `config` | Setup, show config, test connectivity |
59
82
  | `info` | Server info (version, OS, license) |
60
- | `search` | Run, export, oneshot, job management |
61
- | `rules` | Detection rules (saved searches) full CRUD |
83
+ | `search` | Run, export, oneshot, upload, job management |
84
+ | `rules` | Detection rules CRUD, import/export (YAML) |
62
85
  | `alerts` | Fired alerts, alert actions, suppression |
63
86
  | `dashboards` | Dashboard CRUD (XML) |
64
87
  | `indexes` | Index management |
65
88
  | `inputs` | Data inputs (monitor, tcp, udp, script, http) |
66
- | `lookups` | Lookup table CRUD (CSV) |
89
+ | `lookups` | Lookup table CRUD (CSV, mmdb) |
90
+ | `hec` | HEC token management |
67
91
  | `parsers` | Source types and field extractions |
68
- | `apps` | App install, uninstall, update |
92
+ | `apps` | App install (.spl/.tar.gz), uninstall, update |
69
93
  | `users` | User and role management |
70
94
  | `commands` | Machine-readable command tree (JSON) |
71
95
  | `skill` | Embedded agent operating guide |
72
96
 
97
+ ## Key features
98
+
99
+ ### Detection-as-code
100
+
101
+ Export existing rules to YAML, version control them, deploy across
102
+ instances:
103
+
104
+ ```bash
105
+ splunkctl rules export --path detections.yml
106
+ splunkctl rules import --path detections.yml # dry-run preview
107
+ splunkctl --yes rules import --path detections.yml # apply
108
+ ```
109
+
110
+ ### Remote file operations
111
+
112
+ Upload files from your laptop without SSH access to the server:
113
+
114
+ ```bash
115
+ # Upload threat intel, logs, or sample data for indexing
116
+ splunkctl --yes search upload --path threats.csv --index threat_intel --sourcetype csv
117
+
118
+ # Upload lookup tables (CSV or GeoIP mmdb)
119
+ splunkctl --yes lookups upload --name threats.csv --path threats.csv
120
+
121
+ # Install apps from local .spl/.tar.gz packages
122
+ splunkctl --yes apps install --path TA_windows.spl
123
+ ```
124
+
125
+ ### Diagnostics
126
+
127
+ ```bash
128
+ splunkctl doctor # check everything: connection, auth, health, permissions
129
+ splunkctl doctor --json # machine-readable output
130
+ ```
131
+
73
132
  ## Global flags
74
133
 
75
134
  ```
@@ -102,6 +161,23 @@ splunkctl rules list --fields name,cron # project fields
102
161
  splunkctl rules list --out rules.json # write to file
103
162
  ```
104
163
 
164
+ ## SDK fork
165
+
166
+ splunkctl depends on a [fork of splunk-sdk-python](https://github.com/dannyota/splunk-sdk-python/tree/splunkctl)
167
+ that adds entity classes missing from the upstream SDK:
168
+
169
+ | Entity | Service property | Purpose |
170
+ |---|---|---|
171
+ | `Dashboard` | `service.dashboards` | Dashboard CRUD |
172
+ | `LookupTableFile` | `service.lookup_table_files` | Lookup table metadata + download |
173
+ | `HECToken` | `service.hec_tokens` | HEC token management |
174
+
175
+ Install the fork directly:
176
+
177
+ ```bash
178
+ pip install git+https://github.com/dannyota/splunk-sdk-python@splunkctl
179
+ ```
180
+
105
181
  ## Agent integration
106
182
 
107
183
  splunkctl ships with an embedded operating guide for AI agents
@@ -0,0 +1,157 @@
1
+ # splunkctl
2
+
3
+ CLI tool for Splunk Enterprise SIEM operations.
4
+
5
+ Query, inspect, and manage a remote Splunk Enterprise instance from your
6
+ laptop. Built on the [splunk-sdk-python](https://github.com/dannyota/splunk-sdk-python/tree/splunkctl)
7
+ fork with [Click](https://click.palletsprojects.com/).
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pip install splunkctl
13
+ pip install git+https://github.com/dannyota/splunk-sdk-python@splunkctl
14
+ ```
15
+
16
+ Requires Python 3.13+. The second line installs the
17
+ [forked SDK](https://github.com/dannyota/splunk-sdk-python/tree/splunkctl)
18
+ which adds dashboard, lookup, and HEC token support. Without it, core
19
+ commands (search, rules, alerts, indexes, inputs, apps, users) still work.
20
+
21
+ ### Development
22
+
23
+ ```bash
24
+ git clone https://github.com/dannyota/splunkctl
25
+ cd splunkctl
26
+ pip install -e .
27
+ splunkctl --version
28
+ ```
29
+
30
+ ## Quick start
31
+
32
+ ```bash
33
+ splunkctl config init # interactive setup
34
+ splunkctl doctor # check connection, auth, permissions
35
+ splunkctl search run 'index=main | head 10' # run a search
36
+ splunkctl rules list # list detection rules
37
+ ```
38
+
39
+ ## Commands
40
+
41
+ | Group | Description |
42
+ |---|---|
43
+ | `doctor` | Connection, auth, health, and permissions check |
44
+ | `config` | Setup, show config, test connectivity |
45
+ | `info` | Server info (version, OS, license) |
46
+ | `search` | Run, export, oneshot, upload, job management |
47
+ | `rules` | Detection rules — CRUD, import/export (YAML) |
48
+ | `alerts` | Fired alerts, alert actions, suppression |
49
+ | `dashboards` | Dashboard CRUD (XML) |
50
+ | `indexes` | Index management |
51
+ | `inputs` | Data inputs (monitor, tcp, udp, script, http) |
52
+ | `lookups` | Lookup table CRUD (CSV, mmdb) |
53
+ | `hec` | HEC token management |
54
+ | `parsers` | Source types and field extractions |
55
+ | `apps` | App install (.spl/.tar.gz), uninstall, update |
56
+ | `users` | User and role management |
57
+ | `commands` | Machine-readable command tree (JSON) |
58
+ | `skill` | Embedded agent operating guide |
59
+
60
+ ## Key features
61
+
62
+ ### Detection-as-code
63
+
64
+ Export existing rules to YAML, version control them, deploy across
65
+ instances:
66
+
67
+ ```bash
68
+ splunkctl rules export --path detections.yml
69
+ splunkctl rules import --path detections.yml # dry-run preview
70
+ splunkctl --yes rules import --path detections.yml # apply
71
+ ```
72
+
73
+ ### Remote file operations
74
+
75
+ Upload files from your laptop without SSH access to the server:
76
+
77
+ ```bash
78
+ # Upload threat intel, logs, or sample data for indexing
79
+ splunkctl --yes search upload --path threats.csv --index threat_intel --sourcetype csv
80
+
81
+ # Upload lookup tables (CSV or GeoIP mmdb)
82
+ splunkctl --yes lookups upload --name threats.csv --path threats.csv
83
+
84
+ # Install apps from local .spl/.tar.gz packages
85
+ splunkctl --yes apps install --path TA_windows.spl
86
+ ```
87
+
88
+ ### Diagnostics
89
+
90
+ ```bash
91
+ splunkctl doctor # check everything: connection, auth, health, permissions
92
+ splunkctl doctor --json # machine-readable output
93
+ ```
94
+
95
+ ## Global flags
96
+
97
+ ```
98
+ --json Force JSON output
99
+ --format FMT Output format: table, json, csv, jsonl
100
+ --fields f1,f2 Project specific fields
101
+ --out FILE Write output to file
102
+ --yes / -y Apply mutations (skip dry-run preview)
103
+ --timeout N Request timeout in seconds (default 30)
104
+ --config FILE Config file path
105
+ --debug HTTP request/response logging
106
+ ```
107
+
108
+ ## Dry-run by default
109
+
110
+ All write operations preview what would change. Pass `--yes` to apply.
111
+
112
+ ```bash
113
+ splunkctl rules delete 'My Rule' # shows preview only
114
+ splunkctl rules delete 'My Rule' --yes # actually deletes
115
+ ```
116
+
117
+ ## Output formats
118
+
119
+ ```bash
120
+ splunkctl rules list # table (TTY) or JSON (pipe)
121
+ splunkctl rules list --json # force JSON
122
+ splunkctl rules list --format csv # CSV
123
+ splunkctl rules list --fields name,cron # project fields
124
+ splunkctl rules list --out rules.json # write to file
125
+ ```
126
+
127
+ ## SDK fork
128
+
129
+ splunkctl depends on a [fork of splunk-sdk-python](https://github.com/dannyota/splunk-sdk-python/tree/splunkctl)
130
+ that adds entity classes missing from the upstream SDK:
131
+
132
+ | Entity | Service property | Purpose |
133
+ |---|---|---|
134
+ | `Dashboard` | `service.dashboards` | Dashboard CRUD |
135
+ | `LookupTableFile` | `service.lookup_table_files` | Lookup table metadata + download |
136
+ | `HECToken` | `service.hec_tokens` | HEC token management |
137
+
138
+ Install the fork directly:
139
+
140
+ ```bash
141
+ pip install git+https://github.com/dannyota/splunk-sdk-python@splunkctl
142
+ ```
143
+
144
+ ## Agent integration
145
+
146
+ splunkctl ships with an embedded operating guide for AI agents
147
+ (Claude Code, etc.):
148
+
149
+ ```bash
150
+ splunkctl skill # print the guide
151
+ splunkctl skill install # install to ~/.claude/skills/
152
+ splunkctl commands # JSON command tree for discovery
153
+ ```
154
+
155
+ ## License
156
+
157
+ Apache-2.0
@@ -23,10 +23,23 @@ classifiers = [
23
23
  "Topic :: System :: Systems Administration",
24
24
  ]
25
25
  dependencies = [
26
- "splunk-sdk>=2.1.0",
27
- "click>=8.0",
26
+ "splunk-sdk>=2.0",
27
+ "click>=8.2",
28
28
  "pyyaml>=6.0",
29
29
  "tabulate>=0.9",
30
+ "requests>=2.32",
31
+ "defusedxml>=0.7",
32
+ ]
33
+
34
+ [project.optional-dependencies]
35
+ dev = [
36
+ "pytest>=8.0",
37
+ "ruff>=0.6",
38
+ "mypy>=1.11",
39
+ "types-requests",
40
+ "types-PyYAML",
41
+ "types-tabulate",
42
+ "types-defusedxml",
30
43
  ]
31
44
 
32
45
  [project.scripts]
@@ -1,3 +1,3 @@
1
1
  """CLI tool for Splunk Enterprise SIEM operations."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.3.0"
@@ -0,0 +1,333 @@
1
+ """SDK wrapper — lazy connection and auth resolution.
2
+
3
+ Includes Web UI upload support for operations the REST API cannot
4
+ handle remotely (e.g. lookup file upload requires server-side staging).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import re
10
+ import urllib.parse
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ import click
15
+ import requests
16
+ import splunklib.client as splunk_client
17
+
18
+ from splunkctl import config as cfg_mod
19
+
20
+
21
+ class SplunkClient:
22
+ """Lazy-initializing Splunk SDK client.
23
+
24
+ Connection is established on first access to ``.service``.
25
+ Help, config, and offline commands never trigger auth.
26
+ """
27
+
28
+ def __init__( # noqa: D107
29
+ self,
30
+ *,
31
+ config_path: Path | None = None,
32
+ host: str | None = None,
33
+ port: int | None = None,
34
+ username: str | None = None,
35
+ password: str | None = None,
36
+ token: str | None = None,
37
+ scheme: str | None = None,
38
+ app: str | None = None,
39
+ owner: str | None = None,
40
+ verify: bool | None = None,
41
+ timeout: int = 30,
42
+ debug: bool = False,
43
+ ) -> None:
44
+ self._config_path = config_path
45
+ self._overrides: dict[str, Any] = {
46
+ k: v
47
+ for k, v in {
48
+ "host": host,
49
+ "port": port,
50
+ "username": username,
51
+ "password": password,
52
+ "token": token,
53
+ "scheme": scheme,
54
+ "app": app,
55
+ "owner": owner,
56
+ "verify": verify,
57
+ }.items()
58
+ if v is not None
59
+ }
60
+ self._timeout = timeout
61
+ self._debug = debug
62
+ self._service: Any = None
63
+ self._web_session: _WebSession | None = None
64
+
65
+ @property
66
+ def service(self) -> Any:
67
+ """Connect on first access."""
68
+ if self._service is None:
69
+ if self._debug:
70
+ import logging
71
+
72
+ logging.basicConfig(level=logging.DEBUG)
73
+ logging.getLogger("splunklib").setLevel(logging.DEBUG)
74
+
75
+ cfg = cfg_mod.load(self._config_path)
76
+ cfg.update(self._overrides)
77
+
78
+ connect_args: dict[str, Any] = {
79
+ "host": cfg.get("host", "localhost"),
80
+ "port": int(cfg.get("port", 8089)),
81
+ "scheme": cfg.get("scheme", "https"),
82
+ }
83
+
84
+ if cfg.get("token"):
85
+ connect_args["splunkToken"] = cfg["token"]
86
+ else:
87
+ connect_args["username"] = cfg.get("username", "")
88
+ connect_args["password"] = cfg.get("password", "")
89
+
90
+ if cfg.get("app"):
91
+ connect_args["app"] = cfg["app"]
92
+ if cfg.get("owner"):
93
+ connect_args["owner"] = cfg["owner"]
94
+
95
+ if not cfg.get("verify", False):
96
+ connect_args["verify"] = False
97
+
98
+ if self._timeout:
99
+ connect_args["timeout"] = self._timeout
100
+
101
+ self._service = splunk_client.connect(**connect_args)
102
+
103
+ return self._service
104
+
105
+ def _ensure_web_session(self) -> _WebSession:
106
+ if self._web_session is None:
107
+ cfg = cfg_mod.load(self._config_path)
108
+ cfg.update(self._overrides)
109
+ self._web_session = _WebSession(
110
+ self.service,
111
+ verify=bool(cfg.get("verify", False)),
112
+ debug=self._debug,
113
+ timeout=self._timeout or 30,
114
+ )
115
+ return self._web_session
116
+
117
+ def upload_lookup(
118
+ self,
119
+ name: str,
120
+ file_path: Path,
121
+ *,
122
+ app: str = "search",
123
+ update: bool = False,
124
+ ) -> None:
125
+ """Upload a lookup CSV via the Splunk Web UI form handler."""
126
+ self._ensure_web_session().upload_lookup(
127
+ name, file_path, app=app, update=update
128
+ )
129
+
130
+ def install_app(
131
+ self,
132
+ file_path: Path,
133
+ *,
134
+ force: bool = False,
135
+ ) -> None:
136
+ """Install a .spl/.tar.gz app package via the Splunk Web UI."""
137
+ self._ensure_web_session().install_app(file_path, force=force)
138
+
139
+ def set_acl(self, entity: Any, *, sharing: str, owner: str | None = None) -> None:
140
+ """Change an entity's sharing level via its ACL endpoint.
141
+
142
+ Args:
143
+ entity: Any SDK entity (saved search, dashboard, conf stanza...).
144
+ sharing: One of user, app, global.
145
+ owner: Defaults to the entity's current owner, else "nobody".
146
+ """
147
+ acl: dict[str, Any] = dict(entity.access)
148
+ entity.acl_update(sharing=sharing, owner=owner or acl.get("owner", "nobody"))
149
+
150
+
151
+ class _WebSession:
152
+ """Authenticated Splunk Web session for form-handler operations.
153
+
154
+ Uses ``requests`` deliberately: the manager form handlers answer a
155
+ plain urllib POST with a 303 that dead-ends in a 404, while a
156
+ keep-alive session receives the JSON result directly (verified
157
+ against Splunk 10.4). TLS verification is on unless the user's
158
+ config explicitly disables it for self-signed dev instances.
159
+ """
160
+
161
+ def __init__(
162
+ self,
163
+ service: Any,
164
+ *,
165
+ verify: bool = True,
166
+ debug: bool = False,
167
+ timeout: int = 30,
168
+ ) -> None:
169
+ self._host: str = service.host
170
+ self._username: str = service.username
171
+ self._password: str = service.password
172
+ if not self._username or not self._password:
173
+ raise RuntimeError(
174
+ "Lookup upload requires username/password authentication. "
175
+ "Token-only auth cannot authenticate to Splunk Web UI."
176
+ )
177
+
178
+ web_conf = service.confs["web"]["settings"]
179
+ self._web_port = int(web_conf["httpport"])
180
+ self._web_ssl = str(web_conf.content.get("enableSplunkWebSSL", "0")) == "1"
181
+
182
+ self._debug = debug
183
+ self._timeout = timeout
184
+ self._session = requests.Session()
185
+ self._session.verify = verify
186
+ self._csrf_token: str | None = None
187
+ self._logged_in = False
188
+
189
+ @property
190
+ def _base_url(self) -> str:
191
+ scheme = "https" if self._web_ssl else "http"
192
+ return f"{scheme}://{self._host}:{self._web_port}"
193
+
194
+ def _request(self, method: str, url: str, **kwargs: Any) -> requests.Response:
195
+ resp = self._session.request(method, url, timeout=self._timeout, **kwargs)
196
+ if self._debug:
197
+ click.echo(f"web {method} {url} -> {resp.status_code}", err=True)
198
+ return resp
199
+
200
+ def _login(self) -> None:
201
+ """Authenticate to Splunk Web and obtain session cookies."""
202
+ login_url = f"{self._base_url}/en-US/account/login"
203
+ page = self._request("GET", login_url).text
204
+
205
+ m = re.search(r'"cval"\s*:\s*(\d+)', page)
206
+ cval = m.group(1) if m else "0"
207
+
208
+ resp = self._request(
209
+ "POST",
210
+ login_url,
211
+ data={
212
+ "username": self._username,
213
+ "password": self._password,
214
+ "cval": cval,
215
+ },
216
+ )
217
+ try:
218
+ body: dict[str, Any] = resp.json()
219
+ except ValueError:
220
+ raise RuntimeError(
221
+ f"Splunk Web login failed: HTTP {resp.status_code}"
222
+ ) from None
223
+ if body.get("status") == "fail":
224
+ msg = body.get("msg", "unknown error")
225
+ raise RuntimeError(f"Splunk Web login failed: {msg}")
226
+
227
+ for cookie in self._session.cookies:
228
+ if cookie.name and cookie.name.startswith("splunkweb_csrf_token"):
229
+ self._csrf_token = cookie.value
230
+ break
231
+ if not self._csrf_token:
232
+ raise RuntimeError("Could not obtain CSRF token from Splunk Web")
233
+ self._logged_in = True
234
+
235
+ def upload_lookup(
236
+ self,
237
+ name: str,
238
+ file_path: Path,
239
+ *,
240
+ app: str = "search",
241
+ update: bool = False,
242
+ ) -> None:
243
+ """Upload a lookup file via multipart form POST."""
244
+ if not self._logged_in:
245
+ self._login()
246
+
247
+ if update:
248
+ url = (
249
+ f"{self._base_url}/en-US/manager/{app}"
250
+ f"/data/lookup-table-files/{urllib.parse.quote(name)}"
251
+ )
252
+ action = "edit"
253
+ else:
254
+ url = f"{self._base_url}/en-US/manager/{app}/data/lookup-table-files/_new"
255
+ action = "new"
256
+
257
+ data: dict[str, str] = {
258
+ "__action": action,
259
+ "__redirect": "",
260
+ "__ns": app,
261
+ "splunk_form_key": self._csrf_token or "",
262
+ }
263
+ if not update:
264
+ data["name"] = name
265
+
266
+ resp = self._request(
267
+ "POST",
268
+ url,
269
+ data=data,
270
+ files={"spl-ctrl_lookupfile": (name, file_path.read_bytes(), "text/csv")},
271
+ )
272
+ _expect_ok(resp, "Lookup upload")
273
+
274
+ def install_app(
275
+ self,
276
+ file_path: Path,
277
+ *,
278
+ force: bool = False,
279
+ ) -> None:
280
+ """Install a .spl/.tar.gz app via the Web UI upload handler."""
281
+ if not self._logged_in:
282
+ self._login()
283
+
284
+ upload_url = f"{self._base_url}/en-US/manager/appinstall/_upload"
285
+ page = self._request("GET", upload_url).text
286
+
287
+ m = re.search(r'name="state"\s+[^>]*value="([^"]*)"', page)
288
+ state = m.group(1) if m else ""
289
+
290
+ data: dict[str, str] = {
291
+ "state": state,
292
+ "splunk_form_key": self._csrf_token or "",
293
+ }
294
+ if force:
295
+ data["force"] = "1"
296
+
297
+ resp = self._request(
298
+ "POST",
299
+ upload_url,
300
+ data=data,
301
+ files={
302
+ "appfile": (
303
+ file_path.name,
304
+ file_path.read_bytes(),
305
+ "application/gzip",
306
+ )
307
+ },
308
+ )
309
+ if resp.status_code >= 400:
310
+ raise RuntimeError(f"App install failed: HTTP {resp.status_code}")
311
+
312
+
313
+ def _expect_ok(resp: requests.Response, what: str) -> None:
314
+ """Raise unless the form handler answered with JSON status OK."""
315
+ try:
316
+ result: dict[str, Any] = resp.json()
317
+ except ValueError:
318
+ raise RuntimeError(
319
+ f"{what} failed: HTTP {resp.status_code} — "
320
+ f"unexpected response: {resp.text[:200]!r}"
321
+ ) from None
322
+ if result.get("status") != "OK":
323
+ raise RuntimeError(f"{what} failed: {result.get('msg', 'unknown error')}")
324
+
325
+
326
+ def get_client(ctx: click.Context) -> SplunkClient:
327
+ """Build a SplunkClient from Click context. Does not connect."""
328
+ obj: dict[str, Any] = ctx.ensure_object(dict)
329
+ return SplunkClient(
330
+ config_path=Path(obj["config"]) if obj.get("config") else None,
331
+ timeout=obj.get("timeout", 30),
332
+ debug=obj.get("debug", False),
333
+ )