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.
- {splunkctl-0.1.0 → splunkctl-0.3.0}/PKG-INFO +88 -12
- splunkctl-0.3.0/README.md +157 -0
- {splunkctl-0.1.0 → splunkctl-0.3.0}/pyproject.toml +15 -2
- {splunkctl-0.1.0 → splunkctl-0.3.0}/splunkctl/__init__.py +1 -1
- splunkctl-0.3.0/splunkctl/client.py +333 -0
- splunkctl-0.3.0/splunkctl/commands/alerts.py +131 -0
- {splunkctl-0.1.0 → splunkctl-0.3.0}/splunkctl/commands/apps.py +19 -14
- {splunkctl-0.1.0 → splunkctl-0.3.0}/splunkctl/commands/commands_meta.py +8 -0
- splunkctl-0.3.0/splunkctl/commands/common.py +161 -0
- splunkctl-0.3.0/splunkctl/commands/dashboards.py +467 -0
- splunkctl-0.3.0/splunkctl/commands/doctor.py +268 -0
- splunkctl-0.3.0/splunkctl/commands/hec.py +254 -0
- {splunkctl-0.1.0 → splunkctl-0.3.0}/splunkctl/commands/indexes.py +51 -12
- {splunkctl-0.1.0 → splunkctl-0.3.0}/splunkctl/commands/inputs.py +5 -0
- {splunkctl-0.1.0 → splunkctl-0.3.0}/splunkctl/commands/lookups.py +66 -86
- splunkctl-0.3.0/splunkctl/commands/parsers.py +380 -0
- splunkctl-0.3.0/splunkctl/commands/parsers_io.py +228 -0
- {splunkctl-0.1.0 → splunkctl-0.3.0}/splunkctl/commands/rules.py +145 -11
- splunkctl-0.3.0/splunkctl/commands/rules_io.py +302 -0
- {splunkctl-0.1.0 → splunkctl-0.3.0}/splunkctl/commands/search.py +134 -12
- splunkctl-0.3.0/splunkctl/commands/server.py +107 -0
- splunkctl-0.3.0/splunkctl/commands/users.py +430 -0
- {splunkctl-0.1.0 → splunkctl-0.3.0}/splunkctl/guard.py +14 -0
- {splunkctl-0.1.0 → splunkctl-0.3.0}/splunkctl/main.py +36 -2
- {splunkctl-0.1.0 → splunkctl-0.3.0}/splunkctl/output.py +47 -4
- {splunkctl-0.1.0 → splunkctl-0.3.0}/splunkctl/skill/SKILL.md +108 -14
- {splunkctl-0.1.0 → splunkctl-0.3.0}/splunkctl.egg-info/PKG-INFO +88 -12
- {splunkctl-0.1.0 → splunkctl-0.3.0}/splunkctl.egg-info/SOURCES.txt +7 -0
- splunkctl-0.3.0/splunkctl.egg-info/requires.txt +15 -0
- splunkctl-0.3.0/tests/test_client.py +199 -0
- splunkctl-0.3.0/tests/test_main_hoisting.py +60 -0
- {splunkctl-0.1.0 → splunkctl-0.3.0}/tests/test_output.py +70 -0
- {splunkctl-0.1.0 → splunkctl-0.3.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/commands/alerts.py +0 -94
- splunkctl-0.1.0/splunkctl/commands/dashboards.py +0 -216
- splunkctl-0.1.0/splunkctl/commands/parsers.py +0 -177
- splunkctl-0.1.0/splunkctl/commands/users.py +0 -211
- splunkctl-0.1.0/splunkctl.egg-info/requires.txt +0 -4
- splunkctl-0.1.0/tests/test_client.py +0 -56
- {splunkctl-0.1.0 → splunkctl-0.3.0}/LICENSE +0 -0
- {splunkctl-0.1.0 → splunkctl-0.3.0}/setup.cfg +0 -0
- {splunkctl-0.1.0 → splunkctl-0.3.0}/splunkctl/__main__.py +0 -0
- {splunkctl-0.1.0 → splunkctl-0.3.0}/splunkctl/commands/__init__.py +0 -0
- {splunkctl-0.1.0 → splunkctl-0.3.0}/splunkctl/commands/config_cmd.py +0 -0
- {splunkctl-0.1.0 → splunkctl-0.3.0}/splunkctl/commands/info.py +0 -0
- {splunkctl-0.1.0 → splunkctl-0.3.0}/splunkctl/commands/skill_cmd.py +0 -0
- {splunkctl-0.1.0 → splunkctl-0.3.0}/splunkctl/config.py +0 -0
- {splunkctl-0.1.0 → splunkctl-0.3.0}/splunkctl.egg-info/dependency_links.txt +0 -0
- {splunkctl-0.1.0 → splunkctl-0.3.0}/splunkctl.egg-info/entry_points.txt +0 -0
- {splunkctl-0.1.0 → splunkctl-0.3.0}/splunkctl.egg-info/top_level.txt +0 -0
- {splunkctl-0.1.0 → splunkctl-0.3.0}/tests/test_config.py +0 -0
- {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.
|
|
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.
|
|
23
|
-
Requires-Dist: click>=8.
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
27
|
-
"click>=8.
|
|
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]
|
|
@@ -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
|
+
)
|