splunkctl 0.1.0__tar.gz → 0.2.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.1.0 → splunkctl-0.2.0}/PKG-INFO +77 -11
- splunkctl-0.2.0/README.md +157 -0
- {splunkctl-0.1.0 → splunkctl-0.2.0}/pyproject.toml +1 -1
- {splunkctl-0.1.0 → splunkctl-0.2.0}/splunkctl/__init__.py +1 -1
- splunkctl-0.2.0/splunkctl/client.py +334 -0
- {splunkctl-0.1.0 → splunkctl-0.2.0}/splunkctl/commands/apps.py +15 -14
- {splunkctl-0.1.0 → splunkctl-0.2.0}/splunkctl/commands/dashboards.py +55 -68
- splunkctl-0.2.0/splunkctl/commands/doctor.py +206 -0
- splunkctl-0.2.0/splunkctl/commands/hec.py +151 -0
- {splunkctl-0.1.0 → splunkctl-0.2.0}/splunkctl/commands/indexes.py +22 -8
- {splunkctl-0.1.0 → splunkctl-0.2.0}/splunkctl/commands/lookups.py +47 -82
- {splunkctl-0.1.0 → splunkctl-0.2.0}/splunkctl/commands/rules.py +5 -0
- splunkctl-0.2.0/splunkctl/commands/rules_io.py +222 -0
- {splunkctl-0.1.0 → splunkctl-0.2.0}/splunkctl/commands/search.py +62 -1
- {splunkctl-0.1.0 → splunkctl-0.2.0}/splunkctl/commands/users.py +39 -5
- {splunkctl-0.1.0 → splunkctl-0.2.0}/splunkctl/main.py +4 -0
- {splunkctl-0.1.0 → splunkctl-0.2.0}/splunkctl/skill/SKILL.md +54 -5
- {splunkctl-0.1.0 → splunkctl-0.2.0}/splunkctl.egg-info/PKG-INFO +77 -11
- {splunkctl-0.1.0 → splunkctl-0.2.0}/splunkctl.egg-info/SOURCES.txt +3 -0
- {splunkctl-0.1.0 → splunkctl-0.2.0}/splunkctl.egg-info/requires.txt +1 -1
- {splunkctl-0.1.0 → splunkctl-0.2.0}/tests/test_version.py +1 -1
- splunkctl-0.1.0/README.md +0 -91
- splunkctl-0.1.0/splunkctl/client.py +0 -103
- {splunkctl-0.1.0 → splunkctl-0.2.0}/LICENSE +0 -0
- {splunkctl-0.1.0 → splunkctl-0.2.0}/setup.cfg +0 -0
- {splunkctl-0.1.0 → splunkctl-0.2.0}/splunkctl/__main__.py +0 -0
- {splunkctl-0.1.0 → splunkctl-0.2.0}/splunkctl/commands/__init__.py +0 -0
- {splunkctl-0.1.0 → splunkctl-0.2.0}/splunkctl/commands/alerts.py +0 -0
- {splunkctl-0.1.0 → splunkctl-0.2.0}/splunkctl/commands/commands_meta.py +0 -0
- {splunkctl-0.1.0 → splunkctl-0.2.0}/splunkctl/commands/config_cmd.py +0 -0
- {splunkctl-0.1.0 → splunkctl-0.2.0}/splunkctl/commands/info.py +0 -0
- {splunkctl-0.1.0 → splunkctl-0.2.0}/splunkctl/commands/inputs.py +0 -0
- {splunkctl-0.1.0 → splunkctl-0.2.0}/splunkctl/commands/parsers.py +0 -0
- {splunkctl-0.1.0 → splunkctl-0.2.0}/splunkctl/commands/skill_cmd.py +0 -0
- {splunkctl-0.1.0 → splunkctl-0.2.0}/splunkctl/config.py +0 -0
- {splunkctl-0.1.0 → splunkctl-0.2.0}/splunkctl/guard.py +0 -0
- {splunkctl-0.1.0 → splunkctl-0.2.0}/splunkctl/output.py +0 -0
- {splunkctl-0.1.0 → splunkctl-0.2.0}/splunkctl.egg-info/dependency_links.txt +0 -0
- {splunkctl-0.1.0 → splunkctl-0.2.0}/splunkctl.egg-info/entry_points.txt +0 -0
- {splunkctl-0.1.0 → splunkctl-0.2.0}/splunkctl.egg-info/top_level.txt +0 -0
- {splunkctl-0.1.0 → splunkctl-0.2.0}/tests/test_client.py +0 -0
- {splunkctl-0.1.0 → splunkctl-0.2.0}/tests/test_config.py +0 -0
- {splunkctl-0.1.0 → splunkctl-0.2.0}/tests/test_guard.py +0 -0
- {splunkctl-0.1.0 → splunkctl-0.2.0}/tests/test_output.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: splunkctl
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: CLI tool for Splunk Enterprise SIEM operations.
|
|
5
5
|
Author: dannyota
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -19,7 +19,7 @@ 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.
|
|
22
|
+
Requires-Dist: splunk-sdk>=2.0
|
|
23
23
|
Requires-Dist: click>=8.0
|
|
24
24
|
Requires-Dist: pyyaml>=6.0
|
|
25
25
|
Requires-Dist: tabulate>=0.9
|
|
@@ -29,47 +29,96 @@ Dynamic: license-file
|
|
|
29
29
|
|
|
30
30
|
CLI tool for Splunk Enterprise SIEM operations.
|
|
31
31
|
|
|
32
|
-
Query, inspect, and manage a Splunk Enterprise instance from
|
|
33
|
-
Built on the [splunk-sdk-python](https://github.com/dannyota/splunk-sdk-python)
|
|
32
|
+
Query, inspect, and manage a remote Splunk Enterprise instance from your
|
|
33
|
+
laptop. Built on the [splunk-sdk-python](https://github.com/dannyota/splunk-sdk-python/tree/splunkctl)
|
|
34
34
|
fork with [Click](https://click.palletsprojects.com/).
|
|
35
35
|
|
|
36
36
|
## Install
|
|
37
37
|
|
|
38
38
|
```bash
|
|
39
39
|
pip install splunkctl
|
|
40
|
+
pip install git+https://github.com/dannyota/splunk-sdk-python@splunkctl
|
|
40
41
|
```
|
|
41
42
|
|
|
42
|
-
Requires Python 3.13+.
|
|
43
|
+
Requires Python 3.13+. The second line installs the
|
|
44
|
+
[forked SDK](https://github.com/dannyota/splunk-sdk-python/tree/splunkctl)
|
|
45
|
+
which adds dashboard, lookup, and HEC token support. Without it, core
|
|
46
|
+
commands (search, rules, alerts, indexes, inputs, apps, users) still work.
|
|
47
|
+
|
|
48
|
+
### Development
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
git clone https://github.com/dannyota/splunkctl
|
|
52
|
+
cd splunkctl
|
|
53
|
+
pip install -e .
|
|
54
|
+
splunkctl --version
|
|
55
|
+
```
|
|
43
56
|
|
|
44
57
|
## Quick start
|
|
45
58
|
|
|
46
59
|
```bash
|
|
47
60
|
splunkctl config init # interactive setup
|
|
48
|
-
splunkctl
|
|
61
|
+
splunkctl doctor # check connection, auth, permissions
|
|
49
62
|
splunkctl search run 'index=main | head 10' # run a search
|
|
50
63
|
splunkctl rules list # list detection rules
|
|
51
|
-
splunkctl dashboards list # list dashboards
|
|
52
64
|
```
|
|
53
65
|
|
|
54
66
|
## Commands
|
|
55
67
|
|
|
56
68
|
| Group | Description |
|
|
57
69
|
|---|---|
|
|
70
|
+
| `doctor` | Connection, auth, health, and permissions check |
|
|
58
71
|
| `config` | Setup, show config, test connectivity |
|
|
59
72
|
| `info` | Server info (version, OS, license) |
|
|
60
|
-
| `search` | Run, export, oneshot, job management |
|
|
61
|
-
| `rules` | Detection rules
|
|
73
|
+
| `search` | Run, export, oneshot, upload, job management |
|
|
74
|
+
| `rules` | Detection rules — CRUD, import/export (YAML) |
|
|
62
75
|
| `alerts` | Fired alerts, alert actions, suppression |
|
|
63
76
|
| `dashboards` | Dashboard CRUD (XML) |
|
|
64
77
|
| `indexes` | Index management |
|
|
65
78
|
| `inputs` | Data inputs (monitor, tcp, udp, script, http) |
|
|
66
|
-
| `lookups` | Lookup table CRUD (CSV) |
|
|
79
|
+
| `lookups` | Lookup table CRUD (CSV, mmdb) |
|
|
80
|
+
| `hec` | HEC token management |
|
|
67
81
|
| `parsers` | Source types and field extractions |
|
|
68
|
-
| `apps` | App install, uninstall, update |
|
|
82
|
+
| `apps` | App install (.spl/.tar.gz), uninstall, update |
|
|
69
83
|
| `users` | User and role management |
|
|
70
84
|
| `commands` | Machine-readable command tree (JSON) |
|
|
71
85
|
| `skill` | Embedded agent operating guide |
|
|
72
86
|
|
|
87
|
+
## Key features
|
|
88
|
+
|
|
89
|
+
### Detection-as-code
|
|
90
|
+
|
|
91
|
+
Export existing rules to YAML, version control them, deploy across
|
|
92
|
+
instances:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
splunkctl rules export --path detections.yml
|
|
96
|
+
splunkctl rules import --path detections.yml # dry-run preview
|
|
97
|
+
splunkctl --yes rules import --path detections.yml # apply
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Remote file operations
|
|
101
|
+
|
|
102
|
+
Upload files from your laptop without SSH access to the server:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
# Upload threat intel, logs, or sample data for indexing
|
|
106
|
+
splunkctl --yes search upload --path threats.csv --index threat_intel --sourcetype csv
|
|
107
|
+
|
|
108
|
+
# Upload lookup tables (CSV or GeoIP mmdb)
|
|
109
|
+
splunkctl --yes lookups upload --name threats.csv --path threats.csv
|
|
110
|
+
|
|
111
|
+
# Install apps from local .spl/.tar.gz packages
|
|
112
|
+
splunkctl --yes apps install --path TA_windows.spl
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Diagnostics
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
splunkctl doctor # check everything: connection, auth, health, permissions
|
|
119
|
+
splunkctl doctor --json # machine-readable output
|
|
120
|
+
```
|
|
121
|
+
|
|
73
122
|
## Global flags
|
|
74
123
|
|
|
75
124
|
```
|
|
@@ -102,6 +151,23 @@ splunkctl rules list --fields name,cron # project fields
|
|
|
102
151
|
splunkctl rules list --out rules.json # write to file
|
|
103
152
|
```
|
|
104
153
|
|
|
154
|
+
## SDK fork
|
|
155
|
+
|
|
156
|
+
splunkctl depends on a [fork of splunk-sdk-python](https://github.com/dannyota/splunk-sdk-python/tree/splunkctl)
|
|
157
|
+
that adds entity classes missing from the upstream SDK:
|
|
158
|
+
|
|
159
|
+
| Entity | Service property | Purpose |
|
|
160
|
+
|---|---|---|
|
|
161
|
+
| `Dashboard` | `service.dashboards` | Dashboard CRUD |
|
|
162
|
+
| `LookupTableFile` | `service.lookup_table_files` | Lookup table metadata + download |
|
|
163
|
+
| `HECToken` | `service.hec_tokens` | HEC token management |
|
|
164
|
+
|
|
165
|
+
Install the fork directly:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
pip install git+https://github.com/dannyota/splunk-sdk-python@splunkctl
|
|
169
|
+
```
|
|
170
|
+
|
|
105
171
|
## Agent integration
|
|
106
172
|
|
|
107
173
|
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
|
|
@@ -0,0 +1,334 @@
|
|
|
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 http.cookiejar
|
|
10
|
+
import json
|
|
11
|
+
import re
|
|
12
|
+
import ssl
|
|
13
|
+
import urllib.parse
|
|
14
|
+
import urllib.request
|
|
15
|
+
import uuid
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
import click
|
|
20
|
+
import splunklib.client as splunk_client
|
|
21
|
+
|
|
22
|
+
from splunkctl import config as cfg_mod
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SplunkClient:
|
|
26
|
+
"""Lazy-initializing Splunk SDK client.
|
|
27
|
+
|
|
28
|
+
Connection is established on first access to ``.service``.
|
|
29
|
+
Help, config, and offline commands never trigger auth.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__( # noqa: D107
|
|
33
|
+
self,
|
|
34
|
+
*,
|
|
35
|
+
config_path: Path | None = None,
|
|
36
|
+
host: str | None = None,
|
|
37
|
+
port: int | None = None,
|
|
38
|
+
username: str | None = None,
|
|
39
|
+
password: str | None = None,
|
|
40
|
+
token: str | None = None,
|
|
41
|
+
scheme: str | None = None,
|
|
42
|
+
app: str | None = None,
|
|
43
|
+
owner: str | None = None,
|
|
44
|
+
verify: bool | None = None,
|
|
45
|
+
timeout: int = 30,
|
|
46
|
+
debug: bool = False,
|
|
47
|
+
) -> None:
|
|
48
|
+
self._config_path = config_path
|
|
49
|
+
self._overrides: dict[str, Any] = {
|
|
50
|
+
k: v
|
|
51
|
+
for k, v in {
|
|
52
|
+
"host": host,
|
|
53
|
+
"port": port,
|
|
54
|
+
"username": username,
|
|
55
|
+
"password": password,
|
|
56
|
+
"token": token,
|
|
57
|
+
"scheme": scheme,
|
|
58
|
+
"app": app,
|
|
59
|
+
"owner": owner,
|
|
60
|
+
"verify": verify,
|
|
61
|
+
}.items()
|
|
62
|
+
if v is not None
|
|
63
|
+
}
|
|
64
|
+
self._timeout = timeout
|
|
65
|
+
self._debug = debug
|
|
66
|
+
self._service: Any = None
|
|
67
|
+
self._web_session: _WebSession | None = None
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def service(self) -> Any:
|
|
71
|
+
"""Connect on first access."""
|
|
72
|
+
if self._service is None:
|
|
73
|
+
if self._debug:
|
|
74
|
+
import logging
|
|
75
|
+
|
|
76
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
77
|
+
logging.getLogger("splunklib").setLevel(logging.DEBUG)
|
|
78
|
+
|
|
79
|
+
cfg = cfg_mod.load(self._config_path)
|
|
80
|
+
cfg.update(self._overrides)
|
|
81
|
+
|
|
82
|
+
connect_args: dict[str, Any] = {
|
|
83
|
+
"host": cfg.get("host", "localhost"),
|
|
84
|
+
"port": int(cfg.get("port", 8089)),
|
|
85
|
+
"scheme": cfg.get("scheme", "https"),
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if cfg.get("token"):
|
|
89
|
+
connect_args["splunkToken"] = cfg["token"]
|
|
90
|
+
else:
|
|
91
|
+
connect_args["username"] = cfg.get("username", "")
|
|
92
|
+
connect_args["password"] = cfg.get("password", "")
|
|
93
|
+
|
|
94
|
+
if cfg.get("app"):
|
|
95
|
+
connect_args["app"] = cfg["app"]
|
|
96
|
+
if cfg.get("owner"):
|
|
97
|
+
connect_args["owner"] = cfg["owner"]
|
|
98
|
+
|
|
99
|
+
if not cfg.get("verify", False):
|
|
100
|
+
connect_args["verify"] = False
|
|
101
|
+
|
|
102
|
+
if self._timeout:
|
|
103
|
+
connect_args["timeout"] = self._timeout
|
|
104
|
+
|
|
105
|
+
self._service = splunk_client.connect(**connect_args)
|
|
106
|
+
|
|
107
|
+
return self._service
|
|
108
|
+
|
|
109
|
+
def _ensure_web_session(self) -> _WebSession:
|
|
110
|
+
if self._web_session is None:
|
|
111
|
+
cfg = cfg_mod.load(self._config_path)
|
|
112
|
+
cfg.update(self._overrides)
|
|
113
|
+
self._web_session = _WebSession(
|
|
114
|
+
self.service, verify=bool(cfg.get("verify", False))
|
|
115
|
+
)
|
|
116
|
+
return self._web_session
|
|
117
|
+
|
|
118
|
+
def upload_lookup(
|
|
119
|
+
self,
|
|
120
|
+
name: str,
|
|
121
|
+
file_path: Path,
|
|
122
|
+
*,
|
|
123
|
+
app: str = "search",
|
|
124
|
+
update: bool = False,
|
|
125
|
+
) -> None:
|
|
126
|
+
"""Upload a lookup CSV via the Splunk Web UI form handler."""
|
|
127
|
+
self._ensure_web_session().upload_lookup(
|
|
128
|
+
name, file_path, app=app, update=update
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def install_app(
|
|
132
|
+
self,
|
|
133
|
+
file_path: Path,
|
|
134
|
+
*,
|
|
135
|
+
force: bool = False,
|
|
136
|
+
) -> None:
|
|
137
|
+
"""Install a .spl/.tar.gz app package via the Splunk Web UI."""
|
|
138
|
+
self._ensure_web_session().install_app(file_path, force=force)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class _WebSession:
|
|
142
|
+
"""Manages authentication and uploads via Splunk Web UI."""
|
|
143
|
+
|
|
144
|
+
def __init__(self, service: Any, *, verify: bool = True) -> None:
|
|
145
|
+
self._host: str = service.host
|
|
146
|
+
self._username: str = service.username
|
|
147
|
+
self._password: str = service.password
|
|
148
|
+
if not self._username or not self._password:
|
|
149
|
+
raise RuntimeError(
|
|
150
|
+
"Lookup upload requires username/password authentication. "
|
|
151
|
+
"Token-only auth cannot authenticate to Splunk Web UI."
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
web_conf = service.confs["web"]["settings"]
|
|
155
|
+
self._web_port = int(web_conf["httpport"])
|
|
156
|
+
self._web_ssl = str(web_conf.content.get("enableSplunkWebSSL", "0")) == "1"
|
|
157
|
+
|
|
158
|
+
self._cookies = http.cookiejar.CookieJar()
|
|
159
|
+
ctx = ssl.create_default_context()
|
|
160
|
+
if not verify:
|
|
161
|
+
ctx.check_hostname = False
|
|
162
|
+
ctx.verify_mode = ssl.CERT_NONE # noqa: S501
|
|
163
|
+
self._opener = urllib.request.build_opener(
|
|
164
|
+
urllib.request.HTTPCookieProcessor(self._cookies),
|
|
165
|
+
urllib.request.HTTPSHandler(context=ctx),
|
|
166
|
+
)
|
|
167
|
+
self._csrf_token: str | None = None
|
|
168
|
+
self._logged_in = False
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def _base_url(self) -> str:
|
|
172
|
+
scheme = "https" if self._web_ssl else "http"
|
|
173
|
+
return f"{scheme}://{self._host}:{self._web_port}"
|
|
174
|
+
|
|
175
|
+
def _login(self) -> None:
|
|
176
|
+
"""Authenticate to Splunk Web and obtain session cookies."""
|
|
177
|
+
login_url = f"{self._base_url}/en-US/account/login"
|
|
178
|
+
resp = self._opener.open(login_url) # noqa: S310
|
|
179
|
+
page = resp.read().decode("utf-8")
|
|
180
|
+
|
|
181
|
+
m = re.search(r'"cval"\s*:\s*(\d+)', page)
|
|
182
|
+
cval = m.group(1) if m else "0"
|
|
183
|
+
|
|
184
|
+
data = urllib.parse.urlencode(
|
|
185
|
+
{
|
|
186
|
+
"username": self._username,
|
|
187
|
+
"password": self._password,
|
|
188
|
+
"cval": cval,
|
|
189
|
+
}
|
|
190
|
+
).encode()
|
|
191
|
+
resp = self._opener.open(login_url, data) # noqa: S310
|
|
192
|
+
body = json.loads(resp.read().decode("utf-8"))
|
|
193
|
+
if body.get("status") == "fail":
|
|
194
|
+
msg = body.get("msg", "unknown error")
|
|
195
|
+
raise RuntimeError(f"Splunk Web login failed: {msg}")
|
|
196
|
+
|
|
197
|
+
for cookie in self._cookies:
|
|
198
|
+
if cookie.name.startswith("splunkweb_csrf_token"):
|
|
199
|
+
self._csrf_token = cookie.value
|
|
200
|
+
break
|
|
201
|
+
if not self._csrf_token:
|
|
202
|
+
raise RuntimeError("Could not obtain CSRF token from Splunk Web")
|
|
203
|
+
self._logged_in = True
|
|
204
|
+
|
|
205
|
+
def _multipart_post(
|
|
206
|
+
self,
|
|
207
|
+
url: str,
|
|
208
|
+
fields: list[tuple[str, str]],
|
|
209
|
+
file_field: str,
|
|
210
|
+
file_name: str,
|
|
211
|
+
file_data: bytes,
|
|
212
|
+
content_type: str = "application/octet-stream",
|
|
213
|
+
) -> bytes:
|
|
214
|
+
"""Build and send a multipart/form-data POST."""
|
|
215
|
+
if not self._logged_in:
|
|
216
|
+
self._login()
|
|
217
|
+
|
|
218
|
+
boundary = uuid.uuid4().hex
|
|
219
|
+
body = b""
|
|
220
|
+
for field_name, value in fields:
|
|
221
|
+
body += f"--{boundary}\r\n".encode()
|
|
222
|
+
body += (
|
|
223
|
+
f'Content-Disposition: form-data; name="{field_name}"\r\n'
|
|
224
|
+
f"\r\n"
|
|
225
|
+
f"{value}\r\n"
|
|
226
|
+
).encode()
|
|
227
|
+
|
|
228
|
+
body += f"--{boundary}\r\n".encode()
|
|
229
|
+
body += (
|
|
230
|
+
f'Content-Disposition: form-data; name="{file_field}";'
|
|
231
|
+
f' filename="{file_name}"\r\n'
|
|
232
|
+
f"Content-Type: {content_type}\r\n"
|
|
233
|
+
f"\r\n"
|
|
234
|
+
).encode()
|
|
235
|
+
body += file_data
|
|
236
|
+
body += f"\r\n--{boundary}--\r\n".encode()
|
|
237
|
+
|
|
238
|
+
req = urllib.request.Request( # noqa: S310
|
|
239
|
+
url,
|
|
240
|
+
data=body,
|
|
241
|
+
headers={
|
|
242
|
+
"Content-Type": f"multipart/form-data; boundary={boundary}",
|
|
243
|
+
},
|
|
244
|
+
method="POST",
|
|
245
|
+
)
|
|
246
|
+
resp = self._opener.open(req) # noqa: S310
|
|
247
|
+
return bytes(resp.read())
|
|
248
|
+
|
|
249
|
+
def upload_lookup(
|
|
250
|
+
self,
|
|
251
|
+
name: str,
|
|
252
|
+
file_path: Path,
|
|
253
|
+
*,
|
|
254
|
+
app: str = "search",
|
|
255
|
+
update: bool = False,
|
|
256
|
+
) -> None:
|
|
257
|
+
"""Upload a lookup file via multipart form POST."""
|
|
258
|
+
if update:
|
|
259
|
+
url = (
|
|
260
|
+
f"{self._base_url}/en-US/manager/{app}"
|
|
261
|
+
f"/data/lookup-table-files/{urllib.parse.quote(name)}"
|
|
262
|
+
)
|
|
263
|
+
action = "edit"
|
|
264
|
+
else:
|
|
265
|
+
url = f"{self._base_url}/en-US/manager/{app}/data/lookup-table-files/_new"
|
|
266
|
+
action = "new"
|
|
267
|
+
|
|
268
|
+
fields: list[tuple[str, str]] = [
|
|
269
|
+
("__action", action),
|
|
270
|
+
("__redirect", ""),
|
|
271
|
+
("__ns", app),
|
|
272
|
+
("splunk_form_key", self._csrf_token or ""),
|
|
273
|
+
]
|
|
274
|
+
if not update:
|
|
275
|
+
fields.append(("name", name))
|
|
276
|
+
|
|
277
|
+
resp_body = self._multipart_post(
|
|
278
|
+
url,
|
|
279
|
+
fields,
|
|
280
|
+
file_field="spl-ctrl_lookupfile",
|
|
281
|
+
file_name=name,
|
|
282
|
+
file_data=file_path.read_bytes(),
|
|
283
|
+
content_type="text/csv",
|
|
284
|
+
)
|
|
285
|
+
result = json.loads(resp_body.decode("utf-8"))
|
|
286
|
+
if result.get("status") != "OK":
|
|
287
|
+
msg = result.get("msg", "unknown error")
|
|
288
|
+
raise RuntimeError(f"Lookup upload failed: {msg}")
|
|
289
|
+
|
|
290
|
+
def install_app(
|
|
291
|
+
self,
|
|
292
|
+
file_path: Path,
|
|
293
|
+
*,
|
|
294
|
+
force: bool = False,
|
|
295
|
+
) -> None:
|
|
296
|
+
"""Install a .spl/.tar.gz app via the Web UI upload handler."""
|
|
297
|
+
if not self._logged_in:
|
|
298
|
+
self._login()
|
|
299
|
+
|
|
300
|
+
upload_url = f"{self._base_url}/en-US/manager/appinstall/_upload"
|
|
301
|
+
resp = self._opener.open(upload_url) # noqa: S310
|
|
302
|
+
page = resp.read().decode("utf-8")
|
|
303
|
+
|
|
304
|
+
m = re.search(
|
|
305
|
+
r'name="state"\s+[^>]*value="([^"]*)"',
|
|
306
|
+
page,
|
|
307
|
+
)
|
|
308
|
+
state = m.group(1) if m else ""
|
|
309
|
+
|
|
310
|
+
fields: list[tuple[str, str]] = [
|
|
311
|
+
("state", state),
|
|
312
|
+
("splunk_form_key", self._csrf_token or ""),
|
|
313
|
+
]
|
|
314
|
+
if force:
|
|
315
|
+
fields.append(("force", "1"))
|
|
316
|
+
|
|
317
|
+
self._multipart_post(
|
|
318
|
+
upload_url,
|
|
319
|
+
fields,
|
|
320
|
+
file_field="appfile",
|
|
321
|
+
file_name=file_path.name,
|
|
322
|
+
file_data=file_path.read_bytes(),
|
|
323
|
+
content_type="application/gzip",
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def get_client(ctx: click.Context) -> SplunkClient:
|
|
328
|
+
"""Build a SplunkClient from Click context. Does not connect."""
|
|
329
|
+
obj: dict[str, Any] = ctx.ensure_object(dict)
|
|
330
|
+
return SplunkClient(
|
|
331
|
+
config_path=Path(obj["config"]) if obj.get("config") else None,
|
|
332
|
+
timeout=obj.get("timeout", 30),
|
|
333
|
+
debug=obj.get("debug", False),
|
|
334
|
+
)
|