├── .coveragerc ├── docs ├── CSS │ ├── splunk_customizations.css │ └── epub.css ├── _templates │ └── layout.html ├── results.rst ├── data.rst ├── munge_links.sh ├── searchcommandsvalidators.rst ├── Makefile ├── modularinput.rst ├── binding.rst ├── searchcommands.rst ├── client.rst ├── index.rst └── make.bat ├── tests ├── unit │ ├── searchcommands │ │ ├── apps │ │ │ ├── app_with_logging_configuration │ │ │ │ ├── bin │ │ │ │ │ └── empty-directory │ │ │ │ ├── logging.conf │ │ │ │ └── default │ │ │ │ │ ├── logging.conf │ │ │ │ │ └── alternative-logging.conf │ │ │ └── app_without_logging_configuration │ │ │ │ ├── bin │ │ │ │ └── empty-directory │ │ │ │ └── default │ │ │ │ └── empty-directory │ │ ├── test_multibyte_processing.py │ │ ├── __init__.py │ │ ├── test_reporting_command.py │ │ ├── chunked_data_stream.py │ │ ├── test_generator_command.py │ │ └── test_streaming_command.py │ ├── modularinput │ │ ├── data │ │ │ ├── validation_error.xml │ │ │ ├── validation.xml │ │ │ ├── conf_with_0_inputs.xml │ │ │ ├── conf_with_2_inputs.xml │ │ │ ├── conf_with_invalid_inputs.xml │ │ │ ├── event_minimal.xml │ │ │ ├── argument_with_defaults.xml │ │ │ ├── event_maximal.xml │ │ │ ├── argument_without_defaults.xml │ │ │ ├── stream_with_one_event.xml │ │ │ ├── scheme_with_defaults.xml │ │ │ ├── stream_with_two_events.xml │ │ │ ├── scheme_without_defaults.xml │ │ │ └── scheme_without_defaults_and_argument_title.xml │ │ ├── modularinput_testlib.py │ │ ├── test_validation_definition.py │ │ ├── test_input_definition.py │ │ └── test_scheme.py │ ├── data │ │ ├── custom_search │ │ │ ├── multibyte_input.gz │ │ │ ├── v1_search_input.gz │ │ │ ├── usercount.baseline │ │ │ └── tophashtags.baseline │ │ ├── services.server.info.xml │ │ └── services.xml │ └── test_utils.py ├── system │ ├── test_apps │ │ ├── modularinput_app │ │ │ ├── default │ │ │ │ ├── inputs.conf │ │ │ │ └── app.conf │ │ │ ├── README │ │ │ │ └── inputs.conf.spec │ │ │ └── bin │ │ │ │ └── modularinput.py │ │ ├── eventing_app │ │ │ ├── default │ │ │ │ ├── commands.conf │ │ │ │ └── app.conf │ │ │ ├── metadata │ │ │ │ └── default.meta │ │ │ └── bin │ │ │ │ └── eventingcsc.py │ │ ├── reporting_app │ │ │ ├── default │ │ │ │ ├── commands.conf │ │ │ │ └── app.conf │ │ │ ├── metadata │ │ │ │ └── default.meta │ │ │ └── bin │ │ │ │ └── reportingcsc.py │ │ ├── streaming_app │ │ │ ├── default │ │ │ │ ├── commands.conf │ │ │ │ └── app.conf │ │ │ ├── metadata │ │ │ │ └── default.meta │ │ │ └── bin │ │ │ │ └── streamingcsc.py │ │ ├── generating_app │ │ │ ├── default │ │ │ │ ├── commands.conf │ │ │ │ └── app.conf │ │ │ ├── metadata │ │ │ │ └── default.meta │ │ │ └── bin │ │ │ │ └── generatingcsc.py │ │ └── cre_app │ │ │ ├── default │ │ │ ├── restmap.conf │ │ │ └── app.conf │ │ │ └── bin │ │ │ └── execute.py │ ├── test_modularinput_app.py │ └── test_cre_apps.py └── integration │ ├── data │ └── streaming_results.xml │ ├── test_logger.py │ ├── test_message.py │ ├── test_kvstore_batch.py │ ├── test_modular_input_kinds.py │ ├── test_user.py │ ├── test_event_type.py │ ├── test_fired_alert.py │ ├── test_conf.py │ ├── test_kvstore_conf.py │ ├── test_app.py │ ├── test_kvstore_data.py │ └── test_role.py ├── pytest.ini ├── .github ├── ISSUE_TEMPLATE │ ├── custom.md │ ├── feature_request.md │ └── bug_report.md ├── dependabot.yaml ├── workflows │ ├── fossa.yml │ ├── pre-release.yml │ ├── release.yml │ └── test.yml └── PULL_REQUEST_TEMPLATE │ └── pr_template.md ├── .env ├── splunklib ├── modularinput │ ├── __init__.py │ ├── input_definition.py │ ├── utils.py │ ├── validation_definition.py │ ├── scheme.py │ ├── event_writer.py │ ├── argument.py │ └── event.py ├── __init__.py ├── utils.py ├── searchcommands │ ├── environment.py │ └── eventing_command.py └── results.py ├── sitecustomize.py ├── Makefile ├── CONTRIBUTING.md ├── docker-compose.yml ├── pyproject.toml └── utils ├── __init__.py └── cmdopts.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | tests/* 4 | -------------------------------------------------------------------------------- /docs/CSS/splunk_customizations.css: -------------------------------------------------------------------------------- 1 | a.headerlink { display: none; } 2 | 3 | -------------------------------------------------------------------------------- /tests/unit/searchcommands/apps/app_with_logging_configuration/bin/empty-directory: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/searchcommands/apps/app_without_logging_configuration/bin/empty-directory: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/searchcommands/apps/app_without_logging_configuration/default/empty-directory: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/system/test_apps/modularinput_app/default/inputs.conf: -------------------------------------------------------------------------------- 1 | [modularinput] 2 | python.version = python3 3 | -------------------------------------------------------------------------------- /tests/unit/modularinput/data/validation_error.xml: -------------------------------------------------------------------------------- 1 | Big fat validation error! -------------------------------------------------------------------------------- /tests/system/test_apps/modularinput_app/README/inputs.conf.spec: -------------------------------------------------------------------------------- 1 | [modularinput://] 2 | 3 | endpoint = 4 | -------------------------------------------------------------------------------- /tests/unit/modularinput/data/validation.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splunk/splunk-sdk-python/HEAD/tests/unit/modularinput/data/validation.xml -------------------------------------------------------------------------------- /tests/system/test_apps/eventing_app/default/commands.conf: -------------------------------------------------------------------------------- 1 | [eventingcsc] 2 | filename = eventingcsc.py 3 | chunked = true 4 | python.version = python3 5 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | markers = 3 | app: requires sdk-app-collection 4 | smoke: essential smoke tests 5 | 6 | junit_family = 7 | xunit2 8 | -------------------------------------------------------------------------------- /tests/system/test_apps/reporting_app/default/commands.conf: -------------------------------------------------------------------------------- 1 | [reportingcsc] 2 | filename = reportingcsc.py 3 | chunked = true 4 | python.version = python3 5 | -------------------------------------------------------------------------------- /tests/system/test_apps/streaming_app/default/commands.conf: -------------------------------------------------------------------------------- 1 | [streamingcsc] 2 | filename = streamingcsc.py 3 | chunked = true 4 | python.version = python3 5 | -------------------------------------------------------------------------------- /tests/unit/data/custom_search/multibyte_input.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splunk/splunk-sdk-python/HEAD/tests/unit/data/custom_search/multibyte_input.gz -------------------------------------------------------------------------------- /tests/unit/data/custom_search/v1_search_input.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splunk/splunk-sdk-python/HEAD/tests/unit/data/custom_search/v1_search_input.gz -------------------------------------------------------------------------------- /tests/system/test_apps/generating_app/default/commands.conf: -------------------------------------------------------------------------------- 1 | [generatingcsc] 2 | filename = generatingcsc.py 3 | chunked = true 4 | python.version = python3 5 | -------------------------------------------------------------------------------- /tests/unit/modularinput/data/conf_with_0_inputs.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splunk/splunk-sdk-python/HEAD/tests/unit/modularinput/data/conf_with_0_inputs.xml -------------------------------------------------------------------------------- /tests/unit/modularinput/data/conf_with_2_inputs.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splunk/splunk-sdk-python/HEAD/tests/unit/modularinput/data/conf_with_2_inputs.xml -------------------------------------------------------------------------------- /docs/_templates/layout.html: -------------------------------------------------------------------------------- 1 | {% extends '!layout.html' %} 2 | 3 | {% set css_files = css_files + ["_static/splunk_customizations.css"] %} 4 | 5 | {% set reldelim2 = "" %} 6 | -------------------------------------------------------------------------------- /docs/results.rst: -------------------------------------------------------------------------------- 1 | splunklib.results 2 | ----------------- 3 | 4 | .. automodule:: splunklib.results 5 | 6 | .. autoclass:: Message 7 | 8 | .. autoclass:: JSONResultsReader 9 | -------------------------------------------------------------------------------- /tests/unit/modularinput/data/conf_with_invalid_inputs.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splunk/splunk-sdk-python/HEAD/tests/unit/modularinput/data/conf_with_invalid_inputs.xml -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom issue template 3 | about: Describe this issue template's purpose here. 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | -------------------------------------------------------------------------------- /tests/system/test_apps/cre_app/default/restmap.conf: -------------------------------------------------------------------------------- 1 | [script:execute] 2 | match = /execute 3 | scripttype = python 4 | handler = execute.Handler 5 | 6 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | target-branch: "develop" 6 | schedule: 7 | interval: "weekly" 8 | -------------------------------------------------------------------------------- /tests/unit/data/custom_search/usercount.baseline: -------------------------------------------------------------------------------- 1 | coreaudiod,daemon,itay,locationd,mdnsresponder,root,spotlight,usbmuxd,windowserver,www 2 | 1,1,73,1,1,37,1,1,1,1 3 | 1,1,73,1,1,37,1,1,1,1 4 | -------------------------------------------------------------------------------- /.github/workflows/fossa.yml: -------------------------------------------------------------------------------- 1 | name: Fossa OSS Scan 2 | on: [push] 3 | 4 | jobs: 5 | fossa-scan: 6 | uses: splunk/oss-scanning-public/.github/workflows/oss-scan.yml@main 7 | secrets: inherit 8 | -------------------------------------------------------------------------------- /tests/unit/data/custom_search/tophashtags.baseline: -------------------------------------------------------------------------------- 1 | count,hashtag,percentage 2 | 1,#2,0.3333333333333333333333333333 3 | 1,#redsox,0.3333333333333333333333333333 4 | 1,#mlb,0.3333333333333333333333333333 5 | -------------------------------------------------------------------------------- /tests/unit/modularinput/data/event_minimal.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | This is a test of the emergency broadcast system. 4 | 5 | -------------------------------------------------------------------------------- /docs/data.rst: -------------------------------------------------------------------------------- 1 | splunklib.data 2 | -------------- 3 | 4 | .. automodule:: splunklib.data 5 | 6 | .. autofunction:: load 7 | 8 | .. autofunction:: record 9 | 10 | .. autoclass:: Record 11 | :members: 12 | 13 | -------------------------------------------------------------------------------- /docs/munge_links.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | TARGET=$1 4 | 5 | for file in $TARGET/*.html; do 6 | echo ${file} 7 | sed -i -e 's/class="reference external"/class="reference external" target="_blank"/g' "${file}" 8 | done 9 | -------------------------------------------------------------------------------- /tests/unit/modularinput/data/argument_with_defaults.xml: -------------------------------------------------------------------------------- 1 | 2 | string 3 | false 4 | false 5 | -------------------------------------------------------------------------------- /docs/searchcommandsvalidators.rst: -------------------------------------------------------------------------------- 1 | splunklib.searchcommands.validators 2 | ----------------------------------- 3 | 4 | .. automodule:: splunklib.searchcommands.validators 5 | 6 | .. autoclass:: Fieldname 7 | :members: 8 | :inherited-members: 9 | 10 | .. autoclass:: Validator 11 | :members: 12 | :inherited-members: 13 | -------------------------------------------------------------------------------- /tests/unit/modularinput/data/event_maximal.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | hilda 4 | misc 5 | main 6 | localhost 7 | This is a test of the emergency broadcast system. 8 | 9 | -------------------------------------------------------------------------------- /tests/unit/modularinput/data/argument_without_defaults.xml: -------------------------------------------------------------------------------- 1 | 2 | 쎼 and 쎶 and <&> für 3 | is_pos_int('some_name') 4 | boolean 5 | true 6 | true 7 | -------------------------------------------------------------------------------- /tests/system/test_apps/eventing_app/default/app.conf: -------------------------------------------------------------------------------- 1 | # 2 | # Splunk app configuration file 3 | # 4 | 5 | [install] 6 | is_configured = 0 7 | 8 | [ui] 9 | is_visible = 1 10 | label = [EXAMPLE] Eventing CSC App 11 | 12 | [launcher] 13 | description = Example app for eventing Custom Search Commands 14 | version = 1.0.0 15 | author = Splunk 16 | 17 | -------------------------------------------------------------------------------- /tests/system/test_apps/generating_app/default/app.conf: -------------------------------------------------------------------------------- 1 | # 2 | # Splunk app configuration file 3 | # 4 | 5 | [install] 6 | is_configured = 0 7 | 8 | [ui] 9 | is_visible = 1 10 | label = [EXAMPLE] Generating CSC App 11 | 12 | [launcher] 13 | description = Example app for generating Custom Search Commands 14 | version = 1.0.0 15 | author = Splunk 16 | 17 | -------------------------------------------------------------------------------- /tests/system/test_apps/reporting_app/default/app.conf: -------------------------------------------------------------------------------- 1 | # 2 | # Splunk app configuration file 3 | # 4 | 5 | [install] 6 | is_configured = 0 7 | 8 | [ui] 9 | is_visible = 1 10 | label = [EXAMPLE] Reporting CSC App 11 | 12 | [launcher] 13 | description = Example app for reporting Custom Search Commands 14 | version = 1.0.0 15 | author = Splunk 16 | 17 | -------------------------------------------------------------------------------- /tests/system/test_apps/streaming_app/default/app.conf: -------------------------------------------------------------------------------- 1 | # 2 | # Splunk app configuration file 3 | # 4 | 5 | [install] 6 | is_configured = 0 7 | 8 | [ui] 9 | is_visible = 1 10 | label = [EXAMPLE] Streaming CSC App 11 | 12 | [launcher] 13 | description = Example app for streaming Custom Search Commands 14 | version = 1.0.0 15 | author = Splunk 16 | 17 | -------------------------------------------------------------------------------- /tests/unit/modularinput/data/stream_with_one_event.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | hilda 5 | misc 6 | main 7 | localhost 8 | This is a test of the emergency broadcast system. 9 | 10 | 11 | -------------------------------------------------------------------------------- /tests/system/test_apps/modularinput_app/default/app.conf: -------------------------------------------------------------------------------- 1 | # 2 | # Splunk app configuration file 3 | # 4 | 5 | [install] 6 | is_configured = 0 7 | 8 | [ui] 9 | is_visible = 1 10 | label = [EXAMPLE] Modular Input Test App 11 | 12 | [launcher] 13 | description = Example app for Modular Inputs 14 | version = 1.0.0 15 | author = Splunk 16 | 17 | [package] 18 | check_for_updates = false 19 | -------------------------------------------------------------------------------- /tests/unit/modularinput/data/scheme_with_defaults.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | abcd 4 | true 5 | false 6 | xml 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /tests/system/test_apps/cre_app/default/app.conf: -------------------------------------------------------------------------------- 1 | [id] 2 | name = cre_app 3 | version = 0.1.0 4 | 5 | [package] 6 | id = cre_app 7 | check_for_updates = False 8 | 9 | [install] 10 | is_configured = 0 11 | state = enabled 12 | 13 | [ui] 14 | is_visible = 1 15 | label = [EXAMPLE] CRE app 16 | 17 | [launcher] 18 | description = Example app that exposes custom rest endpoints 19 | version = 0.0.1 20 | author = Splunk 21 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # Splunk host (default: localhost) 2 | host=localhost 3 | # Splunk admin port (default: 8089) 4 | port=8089 5 | # Splunk username 6 | username=admin 7 | # Splunk password 8 | password=changed! 9 | # Access scheme (default: https) 10 | scheme=https 11 | # Your version of Splunk (default: 6.2) 12 | version=9.0 13 | # Bearer token for authentication 14 | #splunkToken="" 15 | # Session key for authentication 16 | #token="" 17 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Makefile for Sphinx docs generation 3 | # 4 | 5 | SPHINXBUILD = sphinx-build 6 | BUILDDIR = ./_build 7 | HTMLDIR = ${BUILDDIR}/html 8 | 9 | # Internal variables 10 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees . 11 | 12 | .PHONY: html 13 | html: 14 | @rm -rf $(BUILDDIR) 15 | @$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(HTMLDIR) 16 | @sh munge_links.sh $(HTMLDIR) 17 | @echo 18 | @echo "Build finished. HTML pages available at docs/$(HTMLDIR)." -------------------------------------------------------------------------------- /splunklib/modularinput/__init__.py: -------------------------------------------------------------------------------- 1 | """The following imports allow these classes to be imported via 2 | the splunklib.modularinput package like so: 3 | 4 | from splunklib.modularinput import * 5 | """ 6 | 7 | from .argument import Argument 8 | from .event import Event 9 | from .event_writer import EventWriter 10 | from .input_definition import InputDefinition 11 | from .scheme import Scheme 12 | from .script import Script 13 | from .validation_definition import ValidationDefinition 14 | -------------------------------------------------------------------------------- /docs/modularinput.rst: -------------------------------------------------------------------------------- 1 | splunklib.modularinput 2 | ---------------------- 3 | 4 | .. automodule:: splunklib.modularinput 5 | 6 | .. autoclass:: Argument 7 | :members: 8 | 9 | .. autoclass:: Event 10 | :members: 11 | 12 | .. autoclass:: EventWriter 13 | :members: 14 | 15 | .. autoclass:: InputDefinition 16 | :members: 17 | 18 | .. autoclass:: Scheme 19 | :members: 20 | 21 | .. autoclass:: Script 22 | :members: 23 | 24 | .. autoclass:: ValidationDefinition 25 | :members: 26 | -------------------------------------------------------------------------------- /docs/binding.rst: -------------------------------------------------------------------------------- 1 | splunklib.binding 2 | ----------------- 3 | 4 | .. automodule:: splunklib.binding 5 | 6 | .. autofunction:: connect 7 | 8 | .. autofunction:: handler 9 | 10 | .. autofunction:: namespace 11 | 12 | .. autoclass:: AuthenticationError 13 | :members: 14 | 15 | .. autoclass:: Context 16 | :members: connect, delete, get, get_cookies, has_cookies, login, logout, post, request 17 | 18 | .. autoclass:: HTTPError 19 | :members: 20 | 21 | .. autoclass:: HttpLib 22 | :members: delete, get, post, request 23 | 24 | .. autoclass:: ResponseReader 25 | :members: close, empty, peek, read 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /tests/system/test_apps/eventing_app/metadata/default.meta: -------------------------------------------------------------------------------- 1 | 2 | # Application-level permissions 3 | 4 | [] 5 | access = read : [ * ], write : [ admin, power ] 6 | 7 | ### EVENT TYPES 8 | 9 | [eventtypes] 10 | export = system 11 | 12 | 13 | ### PROPS 14 | 15 | [props] 16 | export = system 17 | 18 | 19 | ### TRANSFORMS 20 | 21 | [transforms] 22 | export = system 23 | 24 | 25 | ### LOOKUPS 26 | 27 | [lookups] 28 | export = system 29 | 30 | 31 | ### VIEWSTATES: even normal users should be able to create shared viewstates 32 | 33 | [viewstates] 34 | access = read : [ * ], write : [ * ] 35 | export = system 36 | 37 | [commands/eventingcsc] 38 | access = read : [ * ], write : [ * ] 39 | export = system 40 | owner = Splunk 41 | -------------------------------------------------------------------------------- /tests/system/test_apps/generating_app/metadata/default.meta: -------------------------------------------------------------------------------- 1 | 2 | # Application-level permissions 3 | 4 | [] 5 | access = read : [ * ], write : [ admin, power ] 6 | 7 | ### EVENT TYPES 8 | 9 | [eventtypes] 10 | export = system 11 | 12 | 13 | ### PROPS 14 | 15 | [props] 16 | export = system 17 | 18 | 19 | ### TRANSFORMS 20 | 21 | [transforms] 22 | export = system 23 | 24 | 25 | ### LOOKUPS 26 | 27 | [lookups] 28 | export = system 29 | 30 | 31 | ### VIEWSTATES: even normal users should be able to create shared viewstates 32 | 33 | [viewstates] 34 | access = read : [ * ], write : [ * ] 35 | export = system 36 | 37 | [commands/generatingcsc] 38 | access = read : [ * ], write : [ * ] 39 | export = system 40 | owner = nobody 41 | -------------------------------------------------------------------------------- /tests/system/test_apps/reporting_app/metadata/default.meta: -------------------------------------------------------------------------------- 1 | 2 | # Application-level permissions 3 | 4 | [] 5 | access = read : [ * ], write : [ admin, power ] 6 | 7 | ### EVENT TYPES 8 | 9 | [eventtypes] 10 | export = system 11 | 12 | 13 | ### PROPS 14 | 15 | [props] 16 | export = system 17 | 18 | 19 | ### TRANSFORMS 20 | 21 | [transforms] 22 | export = system 23 | 24 | 25 | ### LOOKUPS 26 | 27 | [lookups] 28 | export = system 29 | 30 | 31 | ### VIEWSTATES: even normal users should be able to create shared viewstates 32 | 33 | [viewstates] 34 | access = read : [ * ], write : [ * ] 35 | export = system 36 | 37 | [commands/reportingcsc] 38 | access = read : [ * ], write : [ * ] 39 | export = system 40 | owner = nobody 41 | -------------------------------------------------------------------------------- /tests/system/test_apps/streaming_app/metadata/default.meta: -------------------------------------------------------------------------------- 1 | 2 | # Application-level permissions 3 | 4 | [] 5 | access = read : [ * ], write : [ admin, power ] 6 | 7 | ### EVENT TYPES 8 | 9 | [eventtypes] 10 | export = system 11 | 12 | 13 | ### PROPS 14 | 15 | [props] 16 | export = system 17 | 18 | 19 | ### TRANSFORMS 20 | 21 | [transforms] 22 | export = system 23 | 24 | 25 | ### LOOKUPS 26 | 27 | [lookups] 28 | export = system 29 | 30 | 31 | ### VIEWSTATES: even normal users should be able to create shared viewstates 32 | 33 | [viewstates] 34 | access = read : [ * ], write : [ * ] 35 | export = system 36 | 37 | [commands/streamingcsc] 38 | access = read : [ * ], write : [ * ] 39 | export = system 40 | owner = nobody 41 | -------------------------------------------------------------------------------- /tests/unit/modularinput/data/stream_with_two_events.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | hilda 5 | misc 6 | main 7 | localhost 8 | This is a test of the emergency broadcast system. 9 | 10 | 11 | 12 | 13 | hilda 14 | misc 15 | main 16 | localhost 17 | This is a test of the emergency broadcast system. 18 | 19 | 20 | -------------------------------------------------------------------------------- /sitecustomize.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright © 2011-2024 Splunk, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"): you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | # This file is required for running coverage.py 18 | 19 | try: 20 | import coverage 21 | 22 | coverage.process_startup() 23 | except: 24 | pass 25 | -------------------------------------------------------------------------------- /.github/workflows/pre-release.yml: -------------------------------------------------------------------------------- 1 | name: Publish SDK to Test PyPI 2 | on: [workflow_dispatch] 3 | 4 | env: 5 | PYTHON_VERSION: 3.9 6 | 7 | jobs: 8 | publish-sdk-test-pypi: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | id-token: write 12 | environment: 13 | name: splunk-test-pypi 14 | steps: 15 | - name: Checkout source 16 | uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e 17 | - name: Set up Python ${{ env.PYTHON_VERSION }} 18 | uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 19 | with: 20 | python-version: ${{ env.PYTHON_VERSION }} 21 | - name: Install dependencies 22 | run: python -m pip install . --group build 23 | - name: Build packages for distribution 24 | run: python -m build 25 | - name: Publish packages to Test PyPI 26 | uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e 27 | with: 28 | repository-url: https://test.pypi.org/legacy/ 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '...' 17 | 3. Scroll down to '...' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Logs or Screenshots** 24 | If applicable, add logs or screenshots to help explain your problem. 25 | 26 | **Splunk (please complete the following information):** 27 | 28 | - Version: [e.g. 8.0.5] 29 | - OS: [e.g. Ubuntu 20.04.1] 30 | - Deployment: [e.g. single-instance] 31 | 32 | **SDK (please complete the following information):** 33 | 34 | - Version: [e.g. 1.6.14] 35 | - Language Runtime Version: [e.g. Python 3.7] 36 | - OS: [e.g. MacOS 10.15.7] 37 | 38 | **Additional context** 39 | Add any other context about the problem here. 40 | -------------------------------------------------------------------------------- /tests/unit/modularinput/data/scheme_without_defaults.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | abcd 4 | 쎼 and 쎶 and <&> für 5 | false 6 | true 7 | simple 8 | 9 | 10 | 11 | string 12 | false 13 | false 14 | 15 | 16 | 쎼 and 쎶 and <&> für 17 | is_pos_int('some_name') 18 | number 19 | true 20 | true 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /tests/unit/modularinput/modularinput_testlib.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright © 2011-2024 Splunk, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"): you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | import io 18 | import os 19 | import sys 20 | import unittest 21 | 22 | sys.path.insert(0, os.path.join("../../splunklib", "..")) 23 | 24 | from splunklib.modularinput.utils import xml_compare, parse_xml_data, parse_parameters 25 | 26 | 27 | def data_open(filepath): 28 | return io.open( 29 | os.path.join(os.path.dirname(os.path.abspath(__file__)), filepath), "rb" 30 | ) 31 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CONTAINER_NAME := "splunk" 2 | 3 | .PHONY: docs 4 | docs: 5 | @make -C ./docs html 6 | 7 | .PHONY: test 8 | test: 9 | @python -m pytest ./tests 10 | 11 | .PHONY: test-unit 12 | test-unit: 13 | @python -m pytest ./tests/unit 14 | 15 | .PHONY: test-integration 16 | test-integration: 17 | @python -m pytest ./tests/integration ./tests/system 18 | 19 | .PHONY: docker-up 20 | docker-up: 21 | @docker-compose up -d 22 | 23 | .PHONY: docker-ensure-up 24 | docker-ensure-up: 25 | @for i in `seq 0 180`; do \ 26 | if docker exec -it $(CONTAINER_NAME) /bin/bash -c "/sbin/checkstate.sh &> /dev/null"; then \ 27 | break; \ 28 | fi; \ 29 | printf "\rWaiting for Splunk for %s seconds..." $$i; \ 30 | sleep 1; \ 31 | done 32 | 33 | .PHONY: docker-start 34 | docker-start: docker-up docker-ensure-up 35 | 36 | .PHONY: docker-down 37 | docker-down: 38 | @docker-compose stop 39 | 40 | .PHONY: docker-restart 41 | docker-restart: docker-down docker-start 42 | 43 | .PHONY: docker-remove 44 | docker-remove: 45 | @docker-compose rm -f -s 46 | 47 | .PHONY: docker-refresh 48 | docker-refresh: docker-remove docker-start -------------------------------------------------------------------------------- /tests/unit/searchcommands/test_multibyte_processing.py: -------------------------------------------------------------------------------- 1 | import io 2 | import gzip 3 | import sys 4 | 5 | from os import path 6 | 7 | from splunklib.searchcommands import StreamingCommand, Configuration 8 | 9 | 10 | def build_test_command(): 11 | @Configuration() 12 | class TestSearchCommand(StreamingCommand): 13 | def stream(self, records): 14 | for record in records: 15 | yield record 16 | 17 | return TestSearchCommand() 18 | 19 | 20 | def get_input_file(name): 21 | return path.join( 22 | path.dirname(path.dirname(__file__)), "data", "custom_search", name + ".gz" 23 | ) 24 | 25 | 26 | def test_multibyte_chunked(): 27 | data = gzip.open(get_input_file("multibyte_input")) 28 | data = io.TextIOWrapper(data) 29 | cmd = build_test_command() 30 | cmd._process_protocol_v2(sys.argv, data, sys.stdout) 31 | 32 | 33 | def test_v1_searchcommand(): 34 | data = gzip.open(get_input_file("v1_search_input")) 35 | data = io.TextIOWrapper(data) 36 | cmd = build_test_command() 37 | cmd._process_protocol_v1(["test_script.py", "__EXECUTE__"], data, sys.stdout) 38 | -------------------------------------------------------------------------------- /tests/unit/modularinput/data/scheme_without_defaults_and_argument_title.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | abcd 4 | 쎼 and 쎶 and <&> für 5 | false 6 | true 7 | simple 8 | 9 | 10 | 11 | string 12 | false 13 | false 14 | 15 | 16 | Argument for ``test_scheme`` 17 | 쎼 and 쎶 and <&> für 18 | is_pos_int('some_name') 19 | number 20 | true 21 | true 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish SDK to PyPI 2 | on: 3 | release: 4 | types: [published] 5 | 6 | env: 7 | PYTHON_VERSION: 3.9 8 | 9 | jobs: 10 | publish-sdk-pypi: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | id-token: write 14 | environment: 15 | name: splunk-pypi 16 | steps: 17 | - name: Checkout source 18 | uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e 19 | - name: Set up Python ${{ env.PYTHON_VERSION }} 20 | uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 21 | with: 22 | python-version: ${{ env.PYTHON_VERSION }} 23 | - name: Install dependencies 24 | run: python -m pip install . --group release 25 | - name: Build packages for distribution 26 | run: python -m build 27 | - name: Publish packages to PyPI 28 | uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e 29 | - name: Generate API reference 30 | run: make -C ./docs html 31 | - name: Upload docs artifact 32 | uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 33 | with: 34 | name: python-sdk-docs 35 | path: docs/_build/html 36 | -------------------------------------------------------------------------------- /tests/integration/data/streaming_results.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | count 5 | 6 | 7 | 8 | search context: user="admin", app="search", bs-pathname="/an/obviously/bogus/path/to/splunk/etc" 9 | 10 | 11 | 12 | 13 | 1 14 | 15 | 16 | 17 | 18 | 19 | 20 | count 21 | 22 | 23 | 24 | search context: user="admin", app="search", bs-pathname="/an/obviously/bogus/path/to/splunk/etc" 25 | 26 | 27 | 28 | 29 | 3 30 | 31 | 32 | 33 | 34 | 35 | 36 | count 37 | 38 | 39 | 40 | search context: user="admin", app="search", bs-pathname="/an/obviously/bogus/path/to/splunk/etc" 41 | 42 | 43 | 44 | 45 | 54 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/pr_template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Pull Request Template 3 | about: Create a Pull Request to contribute to the SDK 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | ## Description of PR 10 | 11 | Provide the **context and motivation** for this PR. 12 | Briefly explain the **type of changes** (bug fix, feature request, doc update, etc.) made in this PR. Provide reference to issue # fixed, if applicable. 13 | 14 | Describe the approach to the solution, the changes made, and any resulting change in behavior or impact to the user. 15 | 16 | ## Testing the changes 17 | 18 | Please ensure tests are added for your changes. 19 | Include details of **types of tests** written for the changes in the PR and any **test setup and configuration** required to run the tests. 20 | Mention the **versions of the SDK, language runtime, OS and details of Splunk deployment** used in testing. 21 | 22 | ## Documentation 23 | 24 | Please ensure **comments** are added for your changes and any **relevant docs** (readme, reference docs, etc.) are updated. 25 | Include any references to documentation related to the changes. 26 | 27 | ## Dependencies and other resources 28 | 29 | Provide references to PRs or things **dependent on this change** and any relevant PRs or resources like style guides and tools used in this PR. 30 | -------------------------------------------------------------------------------- /splunklib/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright © 2011-2024 Splunk, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | """Python library for Splunk.""" 16 | 17 | import logging 18 | 19 | DEFAULT_LOG_FORMAT = ( 20 | "%(asctime)s, Level=%(levelname)s, Pid=%(process)s, Logger=%(name)s, File=%(filename)s, " 21 | "Line=%(lineno)s, %(message)s" 22 | ) 23 | DEFAULT_DATE_FORMAT = "%Y-%m-%d %H:%M:%S %Z" 24 | 25 | 26 | # To set the logging level of splunklib 27 | # ex. To enable debug logs, call this method with parameter 'logging.DEBUG' 28 | # default logging level is set to 'WARNING' 29 | def setup_logging( 30 | level, log_format=DEFAULT_LOG_FORMAT, date_format=DEFAULT_DATE_FORMAT 31 | ): 32 | logging.basicConfig(level=level, format=log_format, datefmt=date_format) 33 | 34 | 35 | __version_info__ = (2, 2, 0, "alpha") 36 | __version__ = ".".join(map(str, __version_info__)) 37 | -------------------------------------------------------------------------------- /tests/system/test_apps/streaming_app/bin/streamingcsc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding=utf-8 3 | # 4 | # Copyright © 2011-2024 Splunk, Inc. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"): you may 7 | # not use this file except in compliance with the License. You may obtain 8 | # a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | # License for the specific language governing permissions and limitations 16 | # under the License. 17 | 18 | import sys 19 | 20 | from splunklib.searchcommands import ( 21 | Configuration, 22 | StreamingCommand, 23 | dispatch, 24 | ) 25 | 26 | 27 | @Configuration() 28 | class StreamingCSC(StreamingCommand): 29 | """ 30 | The streamingapp command returns events with a one new field 'fahrenheit'. 31 | 32 | Example: 33 | 34 | ``| makeresults count=5 | eval celsius = random()%100 | streamingcsc`` 35 | 36 | returns a records with one new filed 'fahrenheit'. 37 | """ 38 | 39 | def stream(self, records): 40 | for record in records: 41 | record["fahrenheit"] = (float(record["celsius"]) * 1.8) + 32 42 | yield record 43 | 44 | 45 | dispatch(StreamingCSC, sys.argv, sys.stdin, sys.stdout, __name__) 46 | -------------------------------------------------------------------------------- /splunklib/utils.py: -------------------------------------------------------------------------------- 1 | # Copyright © 2011-2024 Splunk, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | """The **splunklib.utils** File for utility functions.""" 16 | 17 | 18 | def ensure_binary(s, encoding="utf-8", errors="strict"): 19 | """ 20 | - `str` -> encoded to `bytes` 21 | - `bytes` -> `bytes` 22 | """ 23 | if isinstance(s, str): 24 | return s.encode(encoding, errors) 25 | 26 | if isinstance(s, bytes): 27 | return s 28 | 29 | raise TypeError(f"not expecting type '{type(s)}'") 30 | 31 | 32 | def ensure_str(s, encoding="utf-8", errors="strict"): 33 | """ 34 | - `str` -> `str` 35 | - `bytes` -> decoded to `str` 36 | """ 37 | if isinstance(s, bytes): 38 | return s.decode(encoding, errors) 39 | 40 | if isinstance(s, str): 41 | return s 42 | 43 | raise TypeError(f"not expecting type '{type(s)}'") 44 | 45 | 46 | def assertRegex(self, *args, **kwargs): 47 | return getattr(self, "assertRegex")(*args, **kwargs) 48 | -------------------------------------------------------------------------------- /tests/unit/searchcommands/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding=utf-8 3 | # 4 | # Copyright © 2011-2024 Splunk, Inc. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"): you may 7 | # not use this file except in compliance with the License. You may obtain 8 | # a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | # License for the specific language governing permissions and limitations 16 | # under the License. 17 | 18 | from os import path 19 | import logging 20 | 21 | from splunklib.searchcommands import environment 22 | from splunklib import searchcommands 23 | 24 | package_directory = path.dirname(path.realpath(__file__)) 25 | project_root = path.dirname(path.dirname(package_directory)) 26 | 27 | 28 | def rebase_environment(name): 29 | environment.app_root = path.join(package_directory, "apps", name) 30 | logging.Logger.manager.loggerDict.clear() 31 | del logging.root.handlers[:] 32 | 33 | environment.splunklib_logger, environment.logging_configuration = ( 34 | environment.configure_logging("splunklib") 35 | ) 36 | searchcommands.logging_configuration = environment.logging_configuration 37 | searchcommands.splunklib_logger = environment.splunklib_logger 38 | searchcommands.app_root = environment.app_root 39 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Python CI 2 | on: [push, workflow_dispatch] 3 | 4 | jobs: 5 | run-test-suite: 6 | runs-on: ${{ matrix.os }} 7 | strategy: 8 | fail-fast: false 9 | matrix: 10 | os: [ubuntu-latest] 11 | python-version: [3.9] 12 | splunk-version: [9.4, latest] 13 | include: 14 | # Oldest possible configuration 15 | # Last Ubuntu version with Python 3.7 binaries available 16 | - os: ubuntu-22.04 17 | python-version: 3.7 18 | splunk-version: 9.1 19 | # Latest possible configuration 20 | - os: ubuntu-latest 21 | python-version: 3.13 22 | splunk-version: latest 23 | steps: 24 | - name: Checkout code 25 | uses: actions/checkout@71cf2267d89c5cb81562390fa70a37fa40b1305e 26 | - name: Launch Splunk Docker instance 27 | run: SPLUNK_VERSION=${{ matrix.splunk-version }} docker compose up -d 28 | - name: Setup Python ${{ matrix.python-version }} 29 | uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 30 | with: 31 | python-version: ${{ matrix.python-version }} 32 | - name: (Python 3.7) Install dependencies 33 | if: ${{ matrix.python-version == '3.7' }} 34 | run: python -m pip install python-dotenv pytest 35 | - name: (Python >= 3.9) Install dependencies 36 | if: ${{ matrix.python-version != '3.7' }} 37 | run: python -m pip install . --group test 38 | - name: Run entire test suite 39 | run: python -m pytest ./tests 40 | -------------------------------------------------------------------------------- /tests/system/test_apps/cre_app/bin/execute.py: -------------------------------------------------------------------------------- 1 | import splunk.rest 2 | import json 3 | 4 | 5 | class Handler(splunk.rest.BaseRestHandler): 6 | def handle_GET(self): 7 | self.response.setHeader("Content-Type", "application/json") 8 | self.response.setHeader("x-foo", "bar") 9 | self.response.status = 200 10 | self.response.write( 11 | json.dumps( 12 | { 13 | "headers": self.headers(), 14 | "method": "GET", 15 | } 16 | ) 17 | ) 18 | 19 | def handle_DELETE(self): 20 | self.handle_with_payload("DELETE") 21 | 22 | def handle_POST(self): 23 | self.handle_with_payload("POST") 24 | 25 | def handle_PUT(self): 26 | self.handle_with_payload("PUT") 27 | 28 | def handle_PATCH(self): 29 | self.handle_with_payload("PATCH") 30 | 31 | def handle_with_payload(self, method): 32 | self.response.setHeader("Content-Type", "application/json") 33 | self.response.setHeader("x-foo", "bar") 34 | self.response.status = 200 35 | self.response.write( 36 | json.dumps( 37 | { 38 | "payload": self.request.get("payload"), 39 | "headers": self.headers(), 40 | "method": method, 41 | } 42 | ) 43 | ) 44 | 45 | def headers(self): 46 | return { 47 | k: v 48 | for k, v in self.request.get("headers", {}).items() 49 | if k.lower().startswith("x") 50 | } 51 | -------------------------------------------------------------------------------- /tests/system/test_apps/generating_app/bin/generatingcsc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding=utf-8 3 | # 4 | # Copyright © 2011-2024 Splunk, Inc. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"): you may 7 | # not use this file except in compliance with the License. You may obtain 8 | # a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | # License for the specific language governing permissions and limitations 16 | # under the License. 17 | 18 | import sys 19 | import time 20 | 21 | from splunklib.searchcommands import ( 22 | Configuration, 23 | GeneratingCommand, 24 | Option, 25 | dispatch, 26 | validators, 27 | ) 28 | 29 | 30 | @Configuration() 31 | class GeneratingCSC(GeneratingCommand): 32 | """ 33 | The `generatingcsc` command generates a specific number of records. 34 | 35 | Example: 36 | 37 | ``| generatingcsc count=4`` 38 | 39 | Returns a 4 records having text 'Test Event'. 40 | """ 41 | 42 | count = Option(require=True, validate=validators.Integer(0)) 43 | 44 | def generate(self): 45 | self.logger.debug("Generating %s events" % self.count) 46 | for i in range(1, self.count + 1): 47 | text = f"Test Event {i}" 48 | yield {"_time": time.time(), "event_no": i, "_raw": text} 49 | 50 | 51 | dispatch(GeneratingCSC, sys.argv, sys.stdin, sys.stdout, __name__) 52 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | ## How to contribute 4 | 5 | If you would like to contribute to this project, see [Contributions to Splunk](https://www.splunk.com/en_us/form/contributions.html) for more information. 6 | 7 | ## Issues and bug reports 8 | 9 | If you're seeing some unexpected behavior with this project, please create an [issue](https://github.com/splunk/splunk-sdk-python/issues) on GitHub with the following information: 10 | 11 | 1. Version of this project you're using (ex: 1.5.0) 12 | 2. Platform version (ex: Windows Server 2012 R2) 13 | 3. Framework version (ex: Python 3.7) 14 | 4. Splunk Enterprise version (ex: 9.0) 15 | 5. Other relevant information (ex: local/remote environment, Splunk network configuration, standalone or distributed deployment, are load balancers used) 16 | 17 | Alternatively, if you have a Splunk question please ask on [Splunk Answers](https://community.splunk.com/t5/Splunk-Development/ct-p/developer-tools). 18 | 19 | ## Pull requests 20 | 21 | We love to see pull requests! 22 | 23 | To create a pull request: 24 | 25 | 1. Fill out the [Individual Contributor Agreement](https://www.splunk.com/en_us/form/contributions.html). 26 | 2. Fork the [repository](https://github.com/splunk/splunk-sdk-python). 27 | 3. Make changes to the **develop** branch, preferably with tests. 28 | 4. Create a [pull request](https://github.com/splunk/splunk-sdk-python/pulls) against the **develop** branch. 29 | 30 | ## Contact us 31 | 32 | If you have a paid Splunk Enterprise or Splunk Cloud license, you can contact [Support](https://www.splunk.com/en_us/support-and-services.html) with questions. 33 | 34 | You can reach the Splunk Developer Platform team at _devinfo@splunk.com_. 35 | 36 | -------------------------------------------------------------------------------- /tests/system/test_apps/eventing_app/bin/eventingcsc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding=utf-8 3 | # 4 | # Copyright © 2011-2024 Splunk, Inc. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"): you may 7 | # not use this file except in compliance with the License. You may obtain 8 | # a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | # License for the specific language governing permissions and limitations 16 | # under the License. 17 | 18 | import sys 19 | 20 | from splunklib.searchcommands import ( 21 | Configuration, 22 | EventingCommand, 23 | Option, 24 | dispatch, 25 | ) 26 | 27 | 28 | @Configuration() 29 | class EventingCSC(EventingCommand): 30 | """ 31 | The `eventingcsc` command filters records from the events stream 32 | returning only those for which the status is same as search query. 33 | 34 | Example: 35 | 36 | ``index="_internal" | head 4000 | eventingcsc status=200`` 37 | 38 | Returns records having status 200 as mentioned in search query. 39 | """ 40 | 41 | status = Option( 42 | doc="""**Syntax:** **status=**** 43 | **Description:** record having same status value will be returned.""", 44 | require=True, 45 | ) 46 | 47 | def transform(self, records): 48 | for record in records: 49 | if str(self.status) == record["status"]: 50 | yield record 51 | 52 | 53 | dispatch(EventingCSC, sys.argv, sys.stdin, sys.stdout, __name__) 54 | -------------------------------------------------------------------------------- /tests/integration/test_logger.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright © 2011-2024 Splunk, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"): you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | from tests import testlib 18 | 19 | 20 | LEVELS = ["INFO", "WARN", "ERROR", "DEBUG", "CRIT"] 21 | 22 | 23 | class LoggerTestCase(testlib.SDKTestCase): 24 | def check_logger(self, logger): 25 | self.check_entity(logger) 26 | self.assertTrue(logger["level"] in LEVELS) 27 | 28 | def test_read(self): 29 | for logger in self.service.loggers.list(count=10): 30 | self.check_logger(logger) 31 | 32 | def test_crud(self): 33 | self.assertTrue("AuditLogger" in self.service.loggers) 34 | logger = self.service.loggers["AuditLogger"] 35 | 36 | saved = logger["level"] 37 | for level in LEVELS: 38 | logger.update(level=level) 39 | logger.refresh() 40 | self.assertEqual(self.service.loggers["AuditLogger"]["level"], level) 41 | 42 | logger.update(level=saved) 43 | logger.refresh() 44 | self.assertEqual(self.service.loggers["AuditLogger"]["level"], saved) 45 | 46 | 47 | if __name__ == "__main__": 48 | import unittest 49 | 50 | unittest.main() 51 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | splunk: 3 | image: "splunk/splunk:${SPLUNK_VERSION}" 4 | container_name: splunk 5 | environment: 6 | - SPLUNK_START_ARGS=--accept-license 7 | - SPLUNK_GENERAL_TERMS=--accept-sgt-current-at-splunk-com 8 | - SPLUNK_HEC_TOKEN=11111111-1111-1111-1111-1111111111113 9 | - SPLUNK_PASSWORD=changed! 10 | - SPLUNK_APPS_URL=https://github.com/splunk/sdk-app-collection/releases/latest/download/sdkappcollection.tgz 11 | ports: 12 | - "8000:8000" 13 | - "8088:8088" 14 | - "8089:8089" 15 | healthcheck: 16 | test: ['CMD', 'curl', '-f', 'http://localhost:8000'] 17 | interval: 5s 18 | timeout: 5s 19 | retries: 20 20 | volumes: 21 | - "./tests/system/test_apps/eventing_app:/opt/splunk/etc/apps/eventing_app" 22 | - "./tests/system/test_apps/generating_app:/opt/splunk/etc/apps/generating_app" 23 | - "./tests/system/test_apps/reporting_app:/opt/splunk/etc/apps/reporting_app" 24 | - "./tests/system/test_apps/streaming_app:/opt/splunk/etc/apps/streaming_app" 25 | - "./tests/system/test_apps/modularinput_app:/opt/splunk/etc/apps/modularinput_app" 26 | - "./tests/system/test_apps/cre_app:/opt/splunk/etc/apps/cre_app" 27 | - "./splunklib:/opt/splunk/etc/apps/eventing_app/bin/splunklib" 28 | - "./splunklib:/opt/splunk/etc/apps/generating_app/bin/splunklib" 29 | - "./splunklib:/opt/splunk/etc/apps/reporting_app/bin/splunklib" 30 | - "./splunklib:/opt/splunk/etc/apps/streaming_app/bin/splunklib" 31 | - "./splunklib:/opt/splunk/etc/apps/modularinput_app/bin/splunklib" 32 | -------------------------------------------------------------------------------- /tests/unit/searchcommands/apps/app_with_logging_configuration/logging.conf: -------------------------------------------------------------------------------- 1 | # 2 | # The format and semantics of this file are described in this article at Python.org: 3 | # 4 | # [Configuration file format](https://docs.python.org/2/library/logging.config.html#configuration-file-format) 5 | # 6 | [loggers] 7 | keys = root, splunklib, SearchCommand 8 | 9 | [logger_root] 10 | # Default: WARNING 11 | level = WARNING 12 | # Default: stderr 13 | handlers = stderr 14 | 15 | [logger_splunklib] 16 | qualname = splunklib 17 | # Default: WARNING 18 | level = NOTSET 19 | # Default: stderr 20 | handlers = splunklib 21 | # Default: 1 22 | propagate = 0 23 | 24 | [logger_SearchCommand] 25 | qualname = SearchCommand 26 | # Default: WARNING 27 | level = NOTSET 28 | # Default: stderr 29 | handlers = app 30 | # Default: 1 31 | propagate = 0 32 | 33 | [handlers] 34 | # See [logging.handlers](https://docs.python.org/2/library/logging.handlers.html) 35 | keys = app, splunklib, stderr 36 | 37 | [handler_app] 38 | # Select this handler to log events to app.log 39 | class = logging.handlers.RotatingFileHandler 40 | level = NOTSET 41 | args = ('app.log', 'a', 524288000, 9, 'utf-8', True) 42 | formatter = searchcommands 43 | 44 | [handler_splunklib] 45 | # Select this handler to log events to splunklib.log 46 | class = logging.handlers.RotatingFileHandler 47 | args = ('splunklib.log', 'a', 524288000, 9, 'utf-8', True) 48 | level = NOTSET 49 | formatter = searchcommands 50 | 51 | [handler_stderr] 52 | # Select this handler to log events to stderr which splunkd redirects to the associated job's search.log file 53 | class = logging.StreamHandler 54 | level = NOTSET 55 | args = (sys.stderr,) 56 | formatter = searchcommands 57 | 58 | [formatters] 59 | keys = searchcommands 60 | 61 | [formatter_searchcommands] 62 | format = %(asctime)s, Level=%(levelname)s, Pid=%(process)s, File=%(filename)s, Line=%(lineno)s, %(message)s 63 | -------------------------------------------------------------------------------- /tests/unit/searchcommands/apps/app_with_logging_configuration/default/logging.conf: -------------------------------------------------------------------------------- 1 | # 2 | # The format and semantics of this file are described in this article at Python.org: 3 | # 4 | # [Configuration file format](https://docs.python.org/2/library/logging.config.html#configuration-file-format) 5 | # 6 | [loggers] 7 | keys = root, splunklib, SearchCommand 8 | 9 | [logger_root] 10 | # Default: WARNING 11 | level = WARNING 12 | # Default: stderr 13 | handlers = stderr 14 | 15 | [logger_splunklib] 16 | qualname = splunklib 17 | # Default: WARNING 18 | level = NOTSET 19 | # Default: stderr 20 | handlers = splunklib 21 | # Default: 1 22 | propagate = 0 23 | 24 | [logger_SearchCommand] 25 | qualname = SearchCommand 26 | # Default: WARNING 27 | level = NOTSET 28 | # Default: stderr 29 | handlers = app 30 | # Default: 1 31 | propagate = 0 32 | 33 | [handlers] 34 | # See [logging.handlers](https://docs.python.org/2/library/logging.handlers.html) 35 | keys = app, splunklib, stderr 36 | 37 | [handler_app] 38 | # Select this handler to log events to app.log 39 | class = logging.handlers.RotatingFileHandler 40 | level = NOTSET 41 | args = ('app.log', 'a', 524288000, 9, 'utf-8', True) 42 | formatter = searchcommands 43 | 44 | [handler_splunklib] 45 | # Select this handler to log events to splunklib.log 46 | class = logging.handlers.RotatingFileHandler 47 | args = ('splunklib.log', 'a', 524288000, 9, 'utf-8', True) 48 | level = NOTSET 49 | formatter = searchcommands 50 | 51 | [handler_stderr] 52 | # Select this handler to log events to stderr which splunkd redirects to the associated job's search.log file 53 | class = logging.StreamHandler 54 | level = NOTSET 55 | args = (sys.stderr,) 56 | formatter = searchcommands 57 | 58 | [formatters] 59 | keys = searchcommands 60 | 61 | [formatter_searchcommands] 62 | format = %(asctime)s, Level=%(levelname)s, Pid=%(process)s, File=%(filename)s, Line=%(lineno)s, %(message)s 63 | -------------------------------------------------------------------------------- /tests/unit/searchcommands/apps/app_with_logging_configuration/default/alternative-logging.conf: -------------------------------------------------------------------------------- 1 | # 2 | # The format and semantics of this file are described in this article at Python.org: 3 | # 4 | # [Configuration file format](https://docs.python.org/2/library/logging.config.html#configuration-file-format) 5 | # 6 | [loggers] 7 | keys = root, splunklib, SearchCommand 8 | 9 | [logger_root] 10 | # Default: WARNING 11 | level = WARNING 12 | # Default: stderr 13 | handlers = stderr 14 | 15 | [logger_splunklib] 16 | qualname = splunklib 17 | # Default: WARNING 18 | level = NOTSET 19 | # Default: stderr 20 | handlers = splunklib 21 | # Default: 1 22 | propagate = 0 23 | 24 | [logger_SearchCommand] 25 | qualname = SearchCommand 26 | # Default: WARNING 27 | level = NOTSET 28 | # Default: stderr 29 | handlers = app 30 | # Default: 1 31 | propagate = 0 32 | 33 | [handlers] 34 | # See [logging.handlers](https://docs.python.org/2/library/logging.handlers.html) 35 | keys = app, splunklib, stderr 36 | 37 | [handler_app] 38 | # Select this handler to log events to app.log 39 | class = logging.handlers.RotatingFileHandler 40 | level = NOTSET 41 | args = ('app.log', 'a', 524288000, 9, 'utf-8', True) 42 | formatter = searchcommands 43 | 44 | [handler_splunklib] 45 | # Select this handler to log events to splunklib.log 46 | class = logging.handlers.RotatingFileHandler 47 | args = ('splunklib.log', 'a', 524288000, 9, 'utf-8', True) 48 | level = NOTSET 49 | formatter = searchcommands 50 | 51 | [handler_stderr] 52 | # Select this handler to log events to stderr which splunkd redirects to the associated job's search.log file 53 | class = logging.StreamHandler 54 | level = NOTSET 55 | args = (sys.stderr,) 56 | formatter = searchcommands 57 | 58 | [formatters] 59 | keys = searchcommands 60 | 61 | [formatter_searchcommands] 62 | format = %(asctime)s, Level=%(levelname)s, Pid=%(process)s, File=%(filename)s, Line=%(lineno)s, %(message)s 63 | -------------------------------------------------------------------------------- /tests/unit/modularinput/test_validation_definition.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright © 2011-2024 Splunk, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"): you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | 18 | from tests.unit.modularinput.modularinput_testlib import unittest, data_open 19 | from splunklib.modularinput.validation_definition import ValidationDefinition 20 | 21 | 22 | class ValidationDefinitionTestCase(unittest.TestCase): 23 | def test_validation_definition_parse(self): 24 | """Check that parsing produces expected result""" 25 | found = ValidationDefinition.parse(data_open("data/validation.xml")) 26 | 27 | expected = ValidationDefinition() 28 | expected.metadata = { 29 | "server_host": "tiny", 30 | "server_uri": "https://127.0.0.1:8089", 31 | "checkpoint_dir": "/opt/splunk/var/lib/splunk/modinputs", 32 | "session_key": "123102983109283019283", 33 | "name": "aaa", 34 | } 35 | expected.parameters = { 36 | "param1": "value1", 37 | "param2": "value2", 38 | "disabled": "0", 39 | "index": "default", 40 | "multiValue": ["value1", "value2"], 41 | "multiValue2": ["value3", "value4"], 42 | } 43 | 44 | self.assertEqual(expected, found) 45 | 46 | 47 | if __name__ == "__main__": 48 | unittest.main() 49 | -------------------------------------------------------------------------------- /docs/CSS/epub.css: -------------------------------------------------------------------------------- 1 | /* Page background color */ 2 | body { background: transparent } 3 | 4 | /* Headings */ 5 | h1 { font-size: 145% } 6 | 7 | body { 8 | font-family: Arial, sans-serif; 9 | color: #555; 10 | font-size: 13px; 11 | line-height: 18px; 12 | margin:0 auto; 13 | padding-left:5px; 14 | padding-right:5px; 15 | } 16 | 17 | pre {font-weight: bold} 18 | 19 | table { 20 | border-width: 0px; 21 | border-spacing: 0px; 22 | border-style: outset; 23 | border-color: gray; 24 | border-collapse: collapse; 25 | background-color: white; 26 | /*margin-top:15px; */ 27 | margin-bottom: 15px; 28 | } 29 | 30 | th { 31 | border-width: 0px; 32 | padding: 2px 6px 1px 6px; 33 | border-style: inset; 34 | border-color: #e2e2e2; 35 | background-color: #eeeeee; 36 | text-align: left; 37 | } 38 | 39 | td { 40 | border-width: 0px; 41 | padding: 2px 6px 1px 6px; 42 | border-style: inset; 43 | border-color: #e2e2e2; 44 | background-color: white; 45 | } 46 | 47 | a { 48 | color: rgb(8, 89, 130); 49 | text-decoration: none; 50 | } 51 | 52 | /* Font used in left-hand frame lists */ 53 | .FrameTitleFont { font-size: 100%; font-family: Arial, sans-serif; color:#000000 } 54 | .FrameHeadingFont { font-size: 100%; font-family: Arial, sans-serif; color:#000000 } 55 | .FrameItemFont { font-size: 100%; font-family: Arial, sans-serif; color:#000000 } 56 | 57 | 58 | /* Navigation bar fonts and colors */ 59 | .NavBarCell1 { background-color:#EEEEFF; color:#000000} /* Light mauve */ 60 | .NavBarCell1Rev { background-color:#00008B; color:#FFFFFF} /* Dark Blue */ 61 | .NavBarFont1 { font-family: Arial, Helvetica, sans-serif; color:#000000;color:#000000;} 62 | .NavBarFont1Rev { font-family: Arial, Helvetica, sans-serif; color:#FFFFFF;color:#FFFFFF;} 63 | 64 | .NavBarCell2 { font-family: Arial, Helvetica, sans-serif; background-color:#FFFFFF; color:#000000} 65 | .NavBarCell3 { font-family: Arial, Helvetica, sans-serif; background-color:#FFFFFF; color:#000000} 66 | -------------------------------------------------------------------------------- /splunklib/modularinput/input_definition.py: -------------------------------------------------------------------------------- 1 | # Copyright © 2011-2024 Splunk, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | import xml.etree.ElementTree as ET 16 | from .utils import parse_xml_data 17 | 18 | 19 | class InputDefinition: 20 | """``InputDefinition`` encodes the XML defining inputs that Splunk passes to 21 | a modular input script. 22 | 23 | **Example**:: 24 | 25 | i = InputDefinition() 26 | 27 | """ 28 | 29 | def __init__(self): 30 | self.metadata = {} 31 | self.inputs = {} 32 | 33 | def __eq__(self, other): 34 | if not isinstance(other, InputDefinition): 35 | return False 36 | return self.metadata == other.metadata and self.inputs == other.inputs 37 | 38 | @staticmethod 39 | def parse(stream): 40 | """Parse a stream containing XML into an ``InputDefinition``. 41 | 42 | :param stream: stream containing XML to parse. 43 | :return: definition: an ``InputDefinition`` object. 44 | """ 45 | definition = InputDefinition() 46 | 47 | # parse XML from the stream, then get the root node 48 | root = ET.parse(stream).getroot() 49 | 50 | for node in root: 51 | if node.tag == "configuration": 52 | # get config for each stanza 53 | definition.inputs = parse_xml_data(node, "stanza") 54 | else: 55 | definition.metadata[node.tag] = node.text 56 | 57 | return definition 58 | -------------------------------------------------------------------------------- /tests/system/test_apps/reporting_app/bin/reportingcsc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding=utf-8 3 | # 4 | # Copyright © 2011-2024 Splunk, Inc. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"): you may 7 | # not use this file except in compliance with the License. You may obtain 8 | # a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | # License for the specific language governing permissions and limitations 16 | # under the License. 17 | 18 | import sys 19 | 20 | from splunklib.searchcommands import ( 21 | Configuration, 22 | Option, 23 | ReportingCommand, 24 | dispatch, 25 | validators, 26 | ) 27 | 28 | 29 | @Configuration(requires_preop=True) 30 | class ReportingCSC(ReportingCommand): 31 | """ 32 | The `reportingcsc` command returns a count of students 33 | having higher total marks than cutoff marks. 34 | 35 | Example: 36 | ``` 37 | | makeresults count=10 38 | | eval math=random()%100, eng=random()%100, cs=random()%100 39 | | reportingcsc cutoff=150 math eng cs 40 | ``` 41 | 42 | Returns a count of students out of 10 having a higher total marks than cutoff. 43 | """ 44 | 45 | cutoff = Option(require=True, validate=validators.Integer(0)) 46 | 47 | @Configuration() 48 | def map(self, records): 49 | """returns a total marks of a students""" 50 | # list of subjects 51 | fieldnames = self.fieldnames 52 | for record in records: 53 | # store a total marks of a single student 54 | total = 0.0 55 | for fieldname in fieldnames: 56 | total += float(record[fieldname]) 57 | yield {"totalMarks": total} 58 | 59 | def reduce(self, records): 60 | """returns a students count having a higher total marks than cutoff""" 61 | pass_student_cnt = 0 62 | for record in records: 63 | value = float(record["totalMarks"]) 64 | if value >= float(self.cutoff): 65 | pass_student_cnt += 1 66 | yield {"student having total marks greater than cutoff ": pass_student_cnt} 67 | 68 | 69 | dispatch(ReportingCSC, sys.argv, sys.stdin, sys.stdout, __name__) 70 | -------------------------------------------------------------------------------- /tests/integration/test_message.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright © 2011-2024 Splunk, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"): you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | from tests import testlib 18 | 19 | from splunklib import client 20 | 21 | 22 | class MessageTest(testlib.SDKTestCase): 23 | def setUp(self): 24 | testlib.SDKTestCase.setUp(self) 25 | self.message_name = testlib.tmpname() 26 | self.message = self.service.messages.create( 27 | self.message_name, value="Test message created by the SDK" 28 | ) 29 | 30 | def tearDown(self): 31 | testlib.SDKTestCase.tearDown(self) 32 | self.service.messages.delete(self.message_name) 33 | 34 | 35 | class TestCreateDelete(testlib.SDKTestCase): 36 | def test_create_delete(self): 37 | message_name = testlib.tmpname() 38 | message_value = "Test message" 39 | message = self.service.messages.create(message_name, value=message_value) 40 | self.assertTrue(message_name in self.service.messages) 41 | self.assertEqual(message.value, message_value) 42 | self.check_entity(message) 43 | self.service.messages.delete(message_name) 44 | self.assertFalse(message_name in self.service.messages) 45 | 46 | def test_invalid_name(self): 47 | self.assertRaises( 48 | client.InvalidNameException, 49 | self.service.messages.create, 50 | None, 51 | value="What?", 52 | ) 53 | self.assertRaises( 54 | client.InvalidNameException, 55 | self.service.messages.create, 56 | 42, 57 | value="Who, me?", 58 | ) 59 | self.assertRaises( 60 | client.InvalidNameException, 61 | self.service.messages.create, 62 | [1, 2, 3], 63 | value="Who, me?", 64 | ) 65 | 66 | 67 | if __name__ == "__main__": 68 | import unittest 69 | 70 | unittest.main() 71 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project.urls] 2 | homepage = "https://pypi.org/project/splunk-sdk" 3 | documentation = "https://docs.splunk.com/Documentation/PythonSDK/2.1.1" 4 | issues = "https://github.com/splunk/splunk-sdk-python/issues" 5 | changelog = "https://github.com/splunk/splunk-sdk-python/blob/master/CHANGELOG.md" 6 | source = "https://github.com/splunk/splunk-sdk-python.git" 7 | download = "https://github.com/splunk/splunk-sdk-python/releases/latest" 8 | 9 | [project] 10 | name = "splunk-sdk" 11 | dynamic = ["version"] 12 | description = "Splunk Software Development Kit for Python" 13 | readme = "README.md" 14 | requires-python = ">=3.7" 15 | license = { text = "Apache-2.0" } 16 | authors = [{ name = "Splunk, Inc.", email = "devinfo@splunk.com" }] 17 | keywords = ["splunk", "sdk"] 18 | classifiers = [ 19 | "Programming Language :: Python :: 3", 20 | "Programming Language :: Python :: 3 :: Only", 21 | "Programming Language :: Python :: 3.7", 22 | "Programming Language :: Python :: 3.9", 23 | "Programming Language :: Python :: 3.13", 24 | "Development Status :: 6 - Mature", 25 | "Environment :: Other Environment", 26 | "Intended Audience :: Developers", 27 | "Operating System :: OS Independent", 28 | "Topic :: Software Development :: Libraries :: Python Modules", 29 | "Topic :: Software Development :: Libraries :: Application Frameworks", 30 | ] 31 | 32 | dependencies = ["python-dotenv>=0.21.1"] 33 | optional-dependencies = { compat = ["six>=1.17.0"] } 34 | 35 | [dependency-groups] 36 | build = ["build>=1.1.1", "twine>=4.0.2"] 37 | # Can't pin `sphinx` otherwise installation fails on python>=3.7 38 | docs = ["sphinx", "jinja2>=3.1.6"] 39 | lint = ["mypy>=1.4.1", "ruff>=0.13.1"] 40 | test = ["pytest>=7.4.4", "pytest-cov>=4.1.0"] 41 | release = [{ include-group = "build" }, { include-group = "docs" }] 42 | dev = [ 43 | { include-group = "test" }, 44 | { include-group = "lint" }, 45 | { include-group = "build" }, 46 | { include-group = "docs" }, 47 | ] 48 | 49 | [build-system] 50 | requires = ["setuptools"] 51 | build-backend = "setuptools.build_meta" 52 | 53 | [tool.setuptools] 54 | packages = ["splunklib", "splunklib.modularinput", "splunklib.searchcommands"] 55 | 56 | [tool.setuptools.dynamic] 57 | version = { attr = "splunklib.__version__" } 58 | 59 | # https://docs.astral.sh/ruff/configuration/ 60 | [tool.ruff.lint] 61 | fixable = ["ALL"] 62 | select = [ 63 | "F", # pyflakes 64 | "E", # pycodestyle 65 | "I", # isort 66 | "ANN", # flake8 type annotations 67 | "RUF", # ruff-specific rules 68 | ] 69 | -------------------------------------------------------------------------------- /tests/unit/searchcommands/test_reporting_command.py: -------------------------------------------------------------------------------- 1 | import io 2 | 3 | from splunklib import searchcommands 4 | from . import chunked_data_stream as chunky 5 | 6 | 7 | def test_simple_reporting_command(): 8 | @searchcommands.Configuration() 9 | class TestReportingCommand(searchcommands.ReportingCommand): 10 | def reduce(self, records): 11 | value = 0 12 | for record in records: 13 | value += int(record["value"]) 14 | yield {"sum": value} 15 | 16 | cmd = TestReportingCommand() 17 | ifile = io.BytesIO() 18 | data = [] 19 | for i in range(0, 10): 20 | data.append({"value": str(i)}) 21 | ifile.write(chunky.build_getinfo_chunk()) 22 | ifile.write(chunky.build_data_chunk(data)) 23 | ifile.seek(0) 24 | ofile = io.BytesIO() 25 | cmd._process_protocol_v2([], ifile, ofile) 26 | ofile.seek(0) 27 | chunk_stream = chunky.ChunkedDataStream(ofile) 28 | getinfo_response = chunk_stream.read_chunk() 29 | assert getinfo_response.meta["type"] == "reporting" 30 | data_chunk = chunk_stream.read_chunk() 31 | assert data_chunk.meta["finished"] is True # Should only be one row 32 | data = list(data_chunk.data) 33 | assert len(data) == 1 34 | assert int(data[0]["sum"]) == sum(range(0, 10)) 35 | 36 | 37 | def test_simple_reporting_command_with_map(): 38 | @searchcommands.Configuration() 39 | class MapAndReduceReportingCommand(searchcommands.ReportingCommand): 40 | def map(self, records): 41 | for record in records: 42 | record["value"] = str(int(record["value"]) * 2) 43 | yield record 44 | 45 | def reduce(self, records): 46 | total = 0 47 | for record in records: 48 | total += int(record["value"]) 49 | yield {"sum": total} 50 | 51 | cmd = MapAndReduceReportingCommand() 52 | ifile = io.BytesIO() 53 | 54 | input_data = [{"value": str(i)} for i in range(5)] 55 | 56 | mapped_data = list(cmd.map(input_data)) 57 | 58 | ifile.write(chunky.build_getinfo_chunk()) 59 | ifile.write(chunky.build_data_chunk(mapped_data)) 60 | ifile.seek(0) 61 | 62 | ofile = io.BytesIO() 63 | cmd._process_protocol_v2([], ifile, ofile) 64 | 65 | ofile.seek(0) 66 | chunk_stream = chunky.ChunkedDataStream(ofile) 67 | chunk_stream.read_chunk() 68 | data_chunk = chunk_stream.read_chunk() 69 | assert data_chunk.meta["finished"] is True 70 | 71 | result = list(data_chunk.data) 72 | expected_sum = sum(i * 2 for i in range(5)) 73 | assert int(result[0]["sum"]) == expected_sum 74 | -------------------------------------------------------------------------------- /tests/system/test_apps/modularinput_app/bin/modularinput.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright © 2011-2025 Splunk, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"): you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | import sys 18 | from urllib import parse 19 | 20 | from splunklib.modularinput import Argument, Event, Scheme, Script 21 | 22 | 23 | class ModularInput(Script): 24 | """ 25 | This app provides an example of a modular input that 26 | can be used in Settings => Data inputs => Local inputs => modularinput 27 | """ 28 | 29 | endpoint_arg = "endpoint" 30 | 31 | def get_scheme(self): 32 | scheme = Scheme("modularinput") 33 | 34 | scheme.use_external_validation = True 35 | scheme.use_single_instance = True 36 | 37 | endpoint = Argument(self.endpoint_arg) 38 | endpoint.title = "URL" 39 | endpoint.data_type = Argument.data_type_string 40 | endpoint.description = "URL" 41 | endpoint.required_on_create = True 42 | scheme.add_argument(endpoint) 43 | 44 | return scheme 45 | 46 | def validate_input(self, definition): 47 | self.check_service_access() 48 | 49 | url = definition.parameters[self.endpoint_arg] 50 | parsed = parse.urlparse(url) 51 | if parsed.scheme != "https": 52 | raise ValueError(f"non-supported scheme {parsed.scheme}") 53 | 54 | def stream_events(self, inputs, ew): 55 | self.check_service_access() 56 | 57 | for input_name, input_item in list(inputs.inputs.items()): 58 | event = Event() 59 | event.stanza = input_name 60 | event.data = "example message" 61 | ew.write_event(event) 62 | 63 | def check_service_access(self): 64 | # Both validate_input and stream_events should have access to the Splunk 65 | # instance that executed the modular input. 66 | if self.service is None: 67 | raise Exception("self.Service == None") 68 | self.service.info # make sure that we are properly authenticated and self.service works 69 | 70 | 71 | if __name__ == "__main__": 72 | sys.exit(ModularInput().run(sys.argv)) 73 | -------------------------------------------------------------------------------- /tests/unit/data/services.server.info.xml: -------------------------------------------------------------------------------- 1 | 2 | server-info 3 | https://localhost/services/server/info 4 | 2011-07-01T16:53:53-07:00 5 | 6 | 7 | Splunk 8 | 9 | 1 10 | 30 11 | 0 12 | 13 | 14 | server-info 15 | https://localhost/services/server/info/server-info 16 | 2011-07-01T16:53:53-07:00 17 | 18 | 19 | system 20 | 21 | 22 | 23 | 24 | 101089 25 | i386 26 | 00system*system 27 | 3FD18BC4-010D-4845-AD51-27963CB17412 28 | 0 29 | 0 30 | E4BF242DB47AB54B5F9DC69D00E65A4BD4E89F917B93D0FA611EDF85ABFC639D 31 | 3af106e3af0db73ca69339684cad26eb 32 | OK 33 | 3FD18BC4-010D-4845-AD51-27963CB17412 34 | normal 35 | Darwin Kernel Version 10.8.0: Tue Jun 7 16:33:36 PDT 2011; root:xnu-1504.15.3~1/RELEASE_I386 36 | Darwin 37 | 10.8.0 38 | blovering.local 39 | 4.3 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /tests/integration/test_kvstore_batch.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2011-2024 Splunk, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"): you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | from tests import testlib 18 | 19 | 20 | class KVStoreBatchTestCase(testlib.SDKTestCase): 21 | def setUp(self): 22 | super().setUp() 23 | self.service.namespace["app"] = "search" 24 | confs = self.service.kvstore 25 | if "test" in confs: 26 | confs["test"].delete() 27 | confs.create("test") 28 | 29 | self.col = confs["test"].data 30 | 31 | def test_insert_find_update_data(self): 32 | data = [{"_key": str(x), "data": "#" + str(x), "num": x} for x in range(1000)] 33 | self.col.batch_save(*data) 34 | 35 | testData = self.col.query(sort="num") 36 | self.assertEqual(len(testData), 1000) 37 | 38 | for x in range(1000): 39 | self.assertEqual(testData[x]["_key"], str(x)) 40 | self.assertEqual(testData[x]["data"], "#" + str(x)) 41 | self.assertEqual(testData[x]["num"], x) 42 | 43 | data = [ 44 | {"_key": str(x), "data": "#" + str(x + 1), "num": x + 1} 45 | for x in range(1000) 46 | ] 47 | self.col.batch_save(*data) 48 | 49 | testData = self.col.query(sort="num") 50 | self.assertEqual(len(testData), 1000) 51 | 52 | for x in range(1000): 53 | self.assertEqual(testData[x]["_key"], str(x)) 54 | self.assertEqual(testData[x]["data"], "#" + str(x + 1)) 55 | self.assertEqual(testData[x]["num"], x + 1) 56 | 57 | query = [{"query": {"num": x + 1}} for x in range(100)] 58 | testData = self.col.batch_find(*query) 59 | 60 | self.assertEqual(len(testData), 100) 61 | testData.sort(key=lambda x: x[0]["num"]) 62 | 63 | for x in range(100): 64 | self.assertEqual(testData[x][0]["_key"], str(x)) 65 | self.assertEqual(testData[x][0]["data"], "#" + str(x + 1)) 66 | self.assertEqual(testData[x][0]["num"], x + 1) 67 | 68 | def tearDown(self): 69 | confs = self.service.kvstore 70 | if "test" in confs: 71 | confs["test"].delete() 72 | 73 | 74 | if __name__ == "__main__": 75 | import unittest 76 | 77 | unittest.main() 78 | -------------------------------------------------------------------------------- /splunklib/modularinput/utils.py: -------------------------------------------------------------------------------- 1 | # Copyright © 2011-2024 Splunk, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | # File for utility functions 16 | 17 | 18 | def xml_compare(expected, found): 19 | """Checks equality of two ``ElementTree`` objects. 20 | 21 | :param expected: An ``ElementTree`` object. 22 | :param found: An ``ElementTree`` object. 23 | :return: ``Boolean``, whether the two objects are equal. 24 | """ 25 | 26 | # if comparing the same ET object 27 | if expected == found: 28 | return True 29 | 30 | # compare element attributes, ignoring order 31 | if set(expected.items()) != set(found.items()): 32 | return False 33 | 34 | # check for equal number of children 35 | expected_children = list(expected) 36 | found_children = list(found) 37 | if len(expected_children) != len(found_children): 38 | return False 39 | 40 | # compare children 41 | if not all(xml_compare(a, b) for a, b in zip(expected_children, found_children)): 42 | return False 43 | 44 | # compare elements, if there is no text node, return True 45 | if (expected.text is None or expected.text.strip() == "") and ( 46 | found.text is None or found.text.strip() == "" 47 | ): 48 | return True 49 | return ( 50 | expected.tag == found.tag 51 | and expected.text == found.text 52 | and expected.attrib == found.attrib 53 | ) 54 | 55 | 56 | def parse_parameters(param_node): 57 | if param_node.tag == "param": 58 | return param_node.text 59 | if param_node.tag == "param_list": 60 | parameters = [] 61 | for mvp in param_node: 62 | parameters.append(mvp.text) 63 | return parameters 64 | raise ValueError(f"Invalid configuration scheme, {param_node.tag} tag unexpected.") 65 | 66 | 67 | def parse_xml_data(parent_node, child_node_tag): 68 | data = {} 69 | for child in parent_node: 70 | child_name = child.get("name") 71 | if child.tag == child_node_tag: 72 | if child_node_tag == "stanza": 73 | data[child_name] = {"__app": child.get("app", None)} 74 | for param in child: 75 | data[child_name][param.get("name")] = parse_parameters(param) 76 | elif "item" == parent_node.tag: 77 | data[child_name] = parse_parameters(child) 78 | return data 79 | -------------------------------------------------------------------------------- /splunklib/modularinput/validation_definition.py: -------------------------------------------------------------------------------- 1 | # Copyright © 2011-2024 Splunk, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | 16 | import xml.etree.ElementTree as ET 17 | 18 | from .utils import parse_xml_data 19 | 20 | 21 | class ValidationDefinition: 22 | """This class represents the XML sent by Splunk for external validation of a 23 | new modular input. 24 | 25 | **Example**:: 26 | 27 | v = ValidationDefinition() 28 | 29 | """ 30 | 31 | def __init__(self): 32 | self.metadata = {} 33 | self.parameters = {} 34 | 35 | def __eq__(self, other): 36 | if not isinstance(other, ValidationDefinition): 37 | return False 38 | return self.metadata == other.metadata and self.parameters == other.parameters 39 | 40 | @staticmethod 41 | def parse(stream): 42 | """Creates a ``ValidationDefinition`` from a provided stream containing XML. 43 | 44 | The XML typically will look like this: 45 | 46 | .. code-block:: xml 47 | 48 | 49 | myHost 50 | https://127.0.0.1:8089 51 | 123102983109283019283 52 | /opt/splunk/var/lib/splunk/modinputs 53 | 54 | value1 55 | 56 | value2 57 | value3 58 | value4 59 | 60 | 61 | 62 | 63 | :param stream: ``Stream`` containing XML to parse. 64 | :return: A ``ValidationDefinition`` object. 65 | 66 | """ 67 | 68 | definition = ValidationDefinition() 69 | 70 | # parse XML from the stream, then get the root node 71 | root = ET.parse(stream).getroot() 72 | 73 | for node in root: 74 | # lone item node 75 | if node.tag == "item": 76 | # name from item node 77 | definition.metadata["name"] = node.get("name") 78 | definition.parameters = parse_xml_data(node, "") 79 | else: 80 | # Store anything else in metadata 81 | definition.metadata[node.tag] = node.text 82 | 83 | return definition 84 | -------------------------------------------------------------------------------- /tests/unit/modularinput/test_input_definition.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright © 2011-2024 Splunk, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"): you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | from tests.unit.modularinput.modularinput_testlib import unittest, data_open 18 | from splunklib.modularinput.input_definition import InputDefinition 19 | 20 | 21 | class InputDefinitionTestCase(unittest.TestCase): 22 | def test_parse_inputdef_with_zero_inputs(self): 23 | """Check parsing of XML that contains only metadata""" 24 | 25 | found = InputDefinition.parse(data_open("data/conf_with_0_inputs.xml")) 26 | 27 | expectedDefinition = InputDefinition() 28 | expectedDefinition.metadata = { 29 | "server_host": "tiny", 30 | "server_uri": "https://127.0.0.1:8089", 31 | "checkpoint_dir": "/some/dir", 32 | "session_key": "123102983109283019283", 33 | } 34 | 35 | self.assertEqual(found, expectedDefinition) 36 | 37 | def test_parse_inputdef_with_two_inputs(self): 38 | """Check parsing of XML that contains 2 inputs""" 39 | 40 | found = InputDefinition.parse(data_open("data/conf_with_2_inputs.xml")) 41 | 42 | expectedDefinition = InputDefinition() 43 | expectedDefinition.metadata = { 44 | "server_host": "tiny", 45 | "server_uri": "https://127.0.0.1:8089", 46 | "checkpoint_dir": "/some/dir", 47 | "session_key": "123102983109283019283", 48 | } 49 | expectedDefinition.inputs["foobar://aaa"] = { 50 | "__app": "search", 51 | "param1": "value1", 52 | "param2": "value2", 53 | "disabled": "0", 54 | "index": "default", 55 | } 56 | expectedDefinition.inputs["foobar://bbb"] = { 57 | "__app": "my_app", 58 | "param1": "value11", 59 | "param2": "value22", 60 | "disabled": "0", 61 | "index": "default", 62 | "multiValue": ["value1", "value2"], 63 | "multiValue2": ["value3", "value4"], 64 | } 65 | 66 | self.assertEqual(expectedDefinition, found) 67 | 68 | def test_attempt_to_parse_malformed_input_definition_will_throw_exception(self): 69 | """Does malformed XML cause the expected exception.""" 70 | 71 | with self.assertRaises(ValueError): 72 | InputDefinition.parse(data_open("data/conf_with_invalid_inputs.xml")) 73 | 74 | 75 | if __name__ == "__main__": 76 | unittest.main() 77 | -------------------------------------------------------------------------------- /tests/system/test_modularinput_app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright © 2011-2025 Splunk, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"): you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | from splunklib import results 18 | from tests import testlib 19 | from splunklib.binding import HTTPError 20 | 21 | 22 | class ModularInput(testlib.SDKTestCase): 23 | index_name = "test_modular_input" 24 | input_name = "test_modular_input" 25 | input_kind = "modularinput" 26 | 27 | def setUp(self): 28 | super().setUp() 29 | 30 | app_found = False 31 | for kind in self.service.modular_input_kinds: 32 | if kind.name == self.input_kind: 33 | app_found = True 34 | 35 | self.assertTrue(app_found, f"{self.input_kind} modular input not installed") 36 | self.clean() 37 | 38 | def tearDown(self): 39 | super().tearDown() 40 | self.clean() 41 | 42 | def clean(self): 43 | for input in self.service.inputs: 44 | if input.name == self.input_name and input.kind == self.input_kind: 45 | self.service.inputs.delete(self.input_name, self.input_kind) 46 | 47 | for index in self.service.indexes: 48 | if index.name == self.input_name: 49 | self.service.indexes.delete(self.input_name) 50 | 51 | def test_modular_input(self): 52 | self.service.indexes.create(self.index_name) 53 | 54 | self.service.inputs.create( 55 | self.input_name, 56 | self.input_kind, 57 | endpoint="https://example.com/api/endpoint", 58 | index=self.index_name, 59 | ) 60 | 61 | def query(): 62 | stream = self.service.jobs.oneshot( 63 | f'search index="{self.index_name}"', output_mode="json" 64 | ) 65 | reader = results.JSONResultsReader(stream) 66 | return list(reader) 67 | 68 | # Wait until the modular input is executed by splunk. 69 | self.assertEventuallyTrue(lambda: len(query()) != 0, timeout=10) 70 | 71 | items = query() 72 | self.assertTrue(len(items) == 1) 73 | self.assertEqual(items[0]["_raw"], "example message") 74 | 75 | def test_external_validator(self): 76 | def create(): 77 | self.service.inputs.create( 78 | self.input_name, 79 | self.input_kind, 80 | endpoint="http://example.com/api/endpoint", 81 | index=self.index_name, 82 | ) 83 | 84 | self.assertRaisesRegex(HTTPError, "non-supported scheme http", create) 85 | 86 | 87 | if __name__ == "__main__": 88 | import unittest 89 | 90 | unittest.main() 91 | -------------------------------------------------------------------------------- /tests/integration/test_modular_input_kinds.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright © 2011-2024 Splunk, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"): you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | import pytest 18 | 19 | from tests import testlib 20 | 21 | from splunklib import client 22 | 23 | 24 | class ModularInputKindTestCase(testlib.SDKTestCase): 25 | @pytest.mark.app 26 | def test_list_arguments(self): 27 | self.install_app_from_collection("modular_inputs") 28 | 29 | if self.service.splunk_version[0] < 5: 30 | # Not implemented before 5.0 31 | return 32 | 33 | test1 = self.service.modular_input_kinds["test1"] 34 | 35 | expected_args = { 36 | "name", 37 | "resname", 38 | "key_id", 39 | "no_description", 40 | "empty_description", 41 | "arg_required_on_edit", 42 | "not_required_on_edit", 43 | "required_on_create", 44 | "not_required_on_create", 45 | "number_field", 46 | "string_field", 47 | "boolean_field", 48 | } 49 | found_args = set(test1.arguments.keys()) 50 | 51 | self.assertEqual(expected_args, found_args) 52 | 53 | @pytest.mark.app 54 | def test_update_raises_exception(self): 55 | self.install_app_from_collection("modular_inputs") 56 | 57 | if self.service.splunk_version[0] < 5: 58 | # Not implemented before 5.0 59 | return 60 | 61 | test1 = self.service.modular_input_kinds["test1"] 62 | self.assertRaises(client.IllegalOperationException, test1.update, a="b") 63 | 64 | def check_modular_input_kind(self, m): 65 | if m.name == "test1": 66 | self.assertEqual('Test "Input" - 1', m["title"]) 67 | self.assertEqual("xml", m["streaming_mode"]) 68 | elif m.name == "test2": 69 | self.assertEqual("test2", m["title"]) 70 | self.assertEqual("simple", m["streaming_mode"]) 71 | 72 | @pytest.mark.app 73 | def test_list_modular_inputs(self): 74 | self.install_app_from_collection("modular_inputs") 75 | 76 | if self.service.splunk_version[0] < 5: 77 | # Not implemented before 5.0 78 | return 79 | 80 | inputs = self.service.inputs 81 | if ("abcd", "test2") not in inputs: 82 | inputs.create("abcd", "test2", field1="boris") 83 | 84 | input = inputs["abcd", "test2"] 85 | self.assertEqual(input.field1, "boris") 86 | 87 | for m in self.service.modular_input_kinds: 88 | self.check_modular_input_kind(m) 89 | 90 | 91 | if __name__ == "__main__": 92 | import unittest 93 | 94 | unittest.main() 95 | -------------------------------------------------------------------------------- /tests/unit/searchcommands/chunked_data_stream.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import csv 3 | import io 4 | import json 5 | 6 | import splunklib.searchcommands.internals 7 | from splunklib.utils import ensure_binary, ensure_str 8 | 9 | 10 | class Chunk: 11 | def __init__(self, version, meta, data): 12 | self.version = ensure_str(version) 13 | self.meta = json.loads(meta) 14 | dialect = splunklib.searchcommands.internals.CsvDialect 15 | self.data = csv.DictReader(io.StringIO(data.decode("utf-8")), dialect=dialect) 16 | 17 | 18 | class ChunkedDataStreamIter(collections.abc.Iterator): 19 | def __init__(self, chunk_stream): 20 | self.chunk_stream = chunk_stream 21 | 22 | def __next__(self): 23 | return self.next() 24 | 25 | def next(self): 26 | try: 27 | return self.chunk_stream.read_chunk() 28 | except EOFError: 29 | raise StopIteration 30 | 31 | 32 | class ChunkedDataStream(collections.abc.Iterable): 33 | def __iter__(self): 34 | return ChunkedDataStreamIter(self) 35 | 36 | def __init__(self, stream): 37 | empty = stream.read(0) 38 | assert isinstance(empty, bytes) 39 | self.stream = stream 40 | 41 | def read_chunk(self): 42 | header = self.stream.readline() 43 | 44 | while len(header) > 0 and header.strip() == b"": 45 | header = self.stream.readline() # Skip empty lines 46 | if len(header) == 0: 47 | raise EOFError 48 | 49 | version, meta, data = header.rstrip().split(b",") 50 | metabytes = self.stream.read(int(meta)) 51 | databytes = self.stream.read(int(data)) 52 | return Chunk(version, metabytes, databytes) 53 | 54 | 55 | def build_chunk(keyval, data=None): 56 | metadata = ensure_binary(json.dumps(keyval)) 57 | data_output = _build_data_csv(data) 58 | return b"chunked 1.0,%d,%d\n%s%s" % ( 59 | len(metadata), 60 | len(data_output), 61 | metadata, 62 | data_output, 63 | ) 64 | 65 | 66 | def build_empty_searchinfo(): 67 | return { 68 | "earliest_time": 0, 69 | "latest_time": 0, 70 | "search": "", 71 | "dispatch_dir": "", 72 | "sid": "", 73 | "args": [], 74 | "splunk_version": "42.3.4", 75 | } 76 | 77 | 78 | def build_getinfo_chunk(): 79 | return build_chunk( 80 | {"action": "getinfo", "preview": False, "searchinfo": build_empty_searchinfo()} 81 | ) 82 | 83 | 84 | def build_data_chunk(data, finished=True): 85 | return build_chunk({"action": "execute", "finished": finished}, data) 86 | 87 | 88 | def _build_data_csv(data): 89 | if data is None: 90 | return b"" 91 | if isinstance(data, bytes): 92 | return data 93 | csvout = io.StringIO() 94 | 95 | headers = set() 96 | for datum in data: 97 | headers.update(datum.keys()) 98 | writer = csv.DictWriter( 99 | csvout, headers, dialect=splunklib.searchcommands.internals.CsvDialect 100 | ) 101 | writer.writeheader() 102 | for datum in data: 103 | writer.writerow(datum) 104 | return ensure_binary(csvout.getvalue()) 105 | -------------------------------------------------------------------------------- /splunklib/modularinput/scheme.py: -------------------------------------------------------------------------------- 1 | # Copyright © 2011-2024 Splunk, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | import xml.etree.ElementTree as ET 16 | 17 | 18 | class Scheme: 19 | """Class representing the metadata for a modular input kind. 20 | 21 | A ``Scheme`` specifies a title, description, several options of how Splunk should run modular inputs of this 22 | kind, and a set of arguments which define a particular modular input's properties. 23 | 24 | The primary use of ``Scheme`` is to abstract away the construction of XML to feed to Splunk. 25 | """ 26 | 27 | # Constant values, do not change 28 | # These should be used for setting the value of a Scheme object's streaming_mode field. 29 | streaming_mode_simple = "SIMPLE" 30 | streaming_mode_xml = "XML" 31 | 32 | def __init__(self, title): 33 | """ 34 | :param title: ``string`` identifier for this Scheme in Splunk. 35 | """ 36 | self.title = title 37 | self.description = None 38 | self.use_external_validation = True 39 | self.use_single_instance = False 40 | self.streaming_mode = Scheme.streaming_mode_xml 41 | 42 | # list of Argument objects, each to be represented by an tag 43 | self.arguments = [] 44 | 45 | def add_argument(self, arg): 46 | """Add the provided argument, ``arg``, to the ``self.arguments`` list. 47 | 48 | :param arg: An ``Argument`` object to add to ``self.arguments``. 49 | """ 50 | self.arguments.append(arg) 51 | 52 | def to_xml(self): 53 | """Creates an ``ET.Element`` representing self, then returns it. 54 | 55 | :returns: an ``ET.Element`` representing this scheme. 56 | """ 57 | root = ET.Element("scheme") 58 | 59 | ET.SubElement(root, "title").text = self.title 60 | 61 | # add a description subelement if it's defined 62 | if self.description is not None: 63 | ET.SubElement(root, "description").text = self.description 64 | 65 | # add all other subelements to this Scheme, represented by (tag, text) 66 | subelements = [ 67 | ("use_external_validation", self.use_external_validation), 68 | ("use_single_instance", self.use_single_instance), 69 | ("streaming_mode", self.streaming_mode), 70 | ] 71 | for name, value in subelements: 72 | ET.SubElement(root, name).text = str(value).lower() 73 | 74 | endpoint = ET.SubElement(root, "endpoint") 75 | 76 | args = ET.SubElement(endpoint, "args") 77 | 78 | # add arguments as subelements to the element 79 | for arg in self.arguments: 80 | arg.add_to_document(args) 81 | 82 | return root 83 | -------------------------------------------------------------------------------- /tests/unit/searchcommands/test_generator_command.py: -------------------------------------------------------------------------------- 1 | import io 2 | import time 3 | 4 | from splunklib.searchcommands import Configuration, GeneratingCommand 5 | from . import chunked_data_stream as chunky 6 | 7 | 8 | def test_simple_generator(): 9 | @Configuration() 10 | class GeneratorTest(GeneratingCommand): 11 | def generate(self): 12 | for num in range(1, 10): 13 | yield {"_time": time.time(), "event_index": num} 14 | 15 | generator = GeneratorTest() 16 | in_stream = io.BytesIO() 17 | in_stream.write(chunky.build_getinfo_chunk()) 18 | in_stream.write(chunky.build_chunk({"action": "execute"})) 19 | in_stream.seek(0) 20 | out_stream = io.BytesIO() 21 | generator._process_protocol_v2([], in_stream, out_stream) 22 | out_stream.seek(0) 23 | 24 | ds = chunky.ChunkedDataStream(out_stream) 25 | is_first_chunk = True 26 | finished_seen = False 27 | expected = set(str(i) for i in range(1, 10)) 28 | seen = set() 29 | for chunk in ds: 30 | if is_first_chunk: 31 | assert chunk.meta["generating"] is True 32 | assert chunk.meta["type"] == "stateful" 33 | is_first_chunk = False 34 | finished_seen = chunk.meta.get("finished", False) 35 | for row in chunk.data: 36 | seen.add(row["event_index"]) 37 | print(out_stream.getvalue()) 38 | print(expected) 39 | print(seen) 40 | assert expected.issubset(seen) 41 | assert finished_seen 42 | 43 | 44 | def test_allow_empty_input_for_generating_command(): 45 | """ 46 | Passing allow_empty_input for generating command will cause an error 47 | """ 48 | 49 | @Configuration() 50 | class GeneratorTest(GeneratingCommand): 51 | def generate(self): 52 | for num in range(1, 3): 53 | yield {"_index": num} 54 | 55 | generator = GeneratorTest() 56 | in_stream = io.BytesIO() 57 | out_stream = io.BytesIO() 58 | 59 | try: 60 | generator.process([], in_stream, out_stream, allow_empty_input=False) 61 | except ValueError as error: 62 | assert str(error) == "allow_empty_input cannot be False for Generating Commands" 63 | 64 | 65 | def test_all_fieldnames_present_for_generated_records(): 66 | @Configuration() 67 | class GeneratorTest(GeneratingCommand): 68 | def generate(self): 69 | yield self.gen_record(_time=time.time(), one=1) 70 | yield self.gen_record(_time=time.time(), two=2) 71 | yield self.gen_record(_time=time.time(), three=3) 72 | yield self.gen_record(_time=time.time(), four=4) 73 | yield self.gen_record(_time=time.time(), five=5) 74 | 75 | generator = GeneratorTest() 76 | in_stream = io.BytesIO() 77 | in_stream.write(chunky.build_getinfo_chunk()) 78 | in_stream.write(chunky.build_chunk({"action": "execute"})) 79 | in_stream.seek(0) 80 | out_stream = io.BytesIO() 81 | generator._process_protocol_v2([], in_stream, out_stream) 82 | out_stream.seek(0) 83 | 84 | ds = chunky.ChunkedDataStream(out_stream) 85 | fieldnames_expected = {"_time", "one", "two", "three", "four", "five"} 86 | fieldnames_actual = set() 87 | for chunk in ds: 88 | for row in chunk.data: 89 | fieldnames_actual |= set(row.keys()) 90 | assert fieldnames_expected.issubset(fieldnames_actual) 91 | -------------------------------------------------------------------------------- /tests/integration/test_user.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright © 2011-2024 Splunk, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"): you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | from tests import testlib 18 | 19 | from splunklib import client 20 | 21 | 22 | class UserTestCase(testlib.SDKTestCase): 23 | def check_user(self, user): 24 | self.check_entity(user) 25 | # Verify expected fields exist 26 | [user[f] for f in ["email", "password", "realname", "roles"]] 27 | 28 | def setUp(self): 29 | super().setUp() 30 | self.username = testlib.tmpname() 31 | self.user = self.service.users.create( 32 | self.username, password="changeme!", roles=["power", "user"] 33 | ) 34 | 35 | def tearDown(self): 36 | super().tearDown() 37 | for user in self.service.users: 38 | if user.name.startswith("delete-me"): 39 | self.service.users.delete(user.name) 40 | 41 | def test_read(self): 42 | for user in self.service.users: 43 | self.check_user(user) 44 | for role in user.role_entities: 45 | self.assertTrue(isinstance(role, client.Entity)) 46 | self.assertTrue(role.name in self.service.roles) 47 | self.assertEqual(user.roles, [role.name for role in user.role_entities]) 48 | 49 | def test_create(self): 50 | self.assertTrue(self.username in self.service.users) 51 | self.assertEqual(self.username, self.user.name) 52 | 53 | def test_delete(self): 54 | self.service.users.delete(self.username) 55 | self.assertFalse(self.username in self.service.users) 56 | with self.assertRaises(client.HTTPError): 57 | self.user.refresh() 58 | 59 | def test_update(self): 60 | self.assertTrue(self.user["email"] is None) 61 | self.user.update(email="foo@bar.com") 62 | self.user.refresh() 63 | self.assertTrue(self.user["email"] == "foo@bar.com") 64 | 65 | def test_in_is_case_insensitive(self): 66 | # Splunk lowercases user names, verify the casing works as expected 67 | users = self.service.users 68 | self.assertTrue(self.username in users) 69 | self.assertTrue(self.username.upper() in users) 70 | 71 | def test_username_in_create_is_case_insensitive(self): 72 | name = testlib.tmpname().lower() 73 | users = self.service.users 74 | user = users.create(name.upper(), password="changeme!", roles="power") 75 | self.assertTrue(user.name == name) 76 | self.assertTrue(name in users) 77 | 78 | def test_delete_is_case_insensitive(self): 79 | users = self.service.users 80 | users.delete(self.username.upper()) 81 | self.assertFalse(self.username in users) 82 | self.assertFalse(self.username.upper() in users) 83 | 84 | 85 | if __name__ == "__main__": 86 | import unittest 87 | 88 | unittest.main() 89 | -------------------------------------------------------------------------------- /tests/integration/test_event_type.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright © 2011-2024 Splunk, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"): you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | from tests import testlib 18 | 19 | 20 | class TestRead(testlib.SDKTestCase): 21 | def test_read(self): 22 | for event_type in self.service.event_types.list(count=1): 23 | self.check_entity(event_type) 24 | 25 | 26 | class TestCreate(testlib.SDKTestCase): 27 | def test_create(self): 28 | self.event_type_name = testlib.tmpname() 29 | event_types = self.service.event_types 30 | self.assertFalse(self.event_type_name in event_types) 31 | 32 | kwargs = {} 33 | kwargs["search"] = "index=_internal *" 34 | kwargs["description"] = "An internal event" 35 | kwargs["disabled"] = 1 36 | kwargs["priority"] = 2 37 | 38 | event_type = event_types.create(self.event_type_name, **kwargs) 39 | self.assertTrue(self.event_type_name in event_types) 40 | self.assertEqual(self.event_type_name, event_type.name) 41 | 42 | def tearDown(self): 43 | super().setUp() 44 | try: 45 | self.service.event_types.delete(self.event_type_name) 46 | except KeyError: 47 | pass 48 | 49 | 50 | class TestEventType(testlib.SDKTestCase): 51 | def setUp(self): 52 | super().setUp() 53 | self.event_type_name = testlib.tmpname() 54 | self.event_type = self.service.event_types.create( 55 | self.event_type_name, search="index=_internal *" 56 | ) 57 | 58 | def tearDown(self): 59 | super().setUp() 60 | try: 61 | self.service.event_types.delete(self.event_type_name) 62 | except KeyError: 63 | pass 64 | 65 | # def test_delete(self): 66 | # self.assertTrue(self.event_type_name in self.service.event_types) 67 | # self.service.event_types.delete(self.event_type_name) 68 | # self.assertFalse(self.event_type_name in self.service.event_types) 69 | 70 | def test_update(self): 71 | kwargs = { 72 | "search": "index=_audit *", 73 | "description": "An audit event", 74 | "priority": "3", 75 | } 76 | self.event_type.update(**kwargs) 77 | self.event_type.refresh() 78 | self.assertEqual(self.event_type["search"], kwargs["search"]) 79 | self.assertEqual(self.event_type["description"], kwargs["description"]) 80 | self.assertEqual(self.event_type["priority"], kwargs["priority"]) 81 | 82 | def test_enable_disable(self): 83 | self.assertEqual(self.event_type["disabled"], "0") 84 | self.event_type.disable() 85 | self.event_type.refresh() 86 | self.assertEqual(self.event_type["disabled"], "1") 87 | self.event_type.enable() 88 | self.event_type.refresh() 89 | self.assertEqual(self.event_type["disabled"], "0") 90 | 91 | 92 | if __name__ == "__main__": 93 | import unittest 94 | 95 | unittest.main() 96 | -------------------------------------------------------------------------------- /tests/unit/searchcommands/test_streaming_command.py: -------------------------------------------------------------------------------- 1 | import io 2 | 3 | from splunklib.searchcommands import StreamingCommand, Configuration 4 | from . import chunked_data_stream as chunky 5 | 6 | 7 | def test_simple_streaming_command(): 8 | @Configuration() 9 | class TestStreamingCommand(StreamingCommand): 10 | def stream(self, records): 11 | for record in records: 12 | record["out_index"] = record["in_index"] 13 | yield record 14 | 15 | cmd = TestStreamingCommand() 16 | ifile = io.BytesIO() 17 | ifile.write(chunky.build_getinfo_chunk()) 18 | data = [] 19 | for i in range(0, 10): 20 | data.append({"in_index": str(i)}) 21 | ifile.write(chunky.build_data_chunk(data, finished=True)) 22 | ifile.seek(0) 23 | ofile = io.BytesIO() 24 | cmd._process_protocol_v2([], ifile, ofile) 25 | ofile.seek(0) 26 | output = chunky.ChunkedDataStream(ofile) 27 | getinfo_response = output.read_chunk() 28 | assert getinfo_response.meta["type"] == "streaming" 29 | 30 | 31 | def test_field_preservation_negative(): 32 | @Configuration() 33 | class TestStreamingCommand(StreamingCommand): 34 | def stream(self, records): 35 | for index, record in enumerate(records): 36 | if index % 2 != 0: 37 | record["odd_field"] = True 38 | else: 39 | record["even_field"] = True 40 | yield record 41 | 42 | cmd = TestStreamingCommand() 43 | ifile = io.BytesIO() 44 | ifile.write(chunky.build_getinfo_chunk()) 45 | data = [] 46 | for i in range(0, 10): 47 | data.append({"in_index": str(i)}) 48 | ifile.write(chunky.build_data_chunk(data, finished=True)) 49 | ifile.seek(0) 50 | ofile = io.BytesIO() 51 | cmd._process_protocol_v2([], ifile, ofile) 52 | ofile.seek(0) 53 | output_iter = chunky.ChunkedDataStream(ofile).__iter__() 54 | next(output_iter) 55 | output_records = list(next(output_iter).data) 56 | 57 | # Assert that count of records having "odd_field" is 0 58 | assert len(list(r for r in output_records if "odd_field" in r)) == 0 59 | 60 | # Assert that count of records having "even_field" is 10 61 | assert len(list(r for r in output_records if "even_field" in r)) == 10 62 | 63 | 64 | def test_field_preservation_positive(): 65 | @Configuration() 66 | class TestStreamingCommand(StreamingCommand): 67 | def stream(self, records): 68 | for index, record in enumerate(records): 69 | if index % 2 != 0: 70 | self.add_field(record, "odd_field", True) 71 | else: 72 | self.add_field(record, "even_field", True) 73 | yield record 74 | 75 | cmd = TestStreamingCommand() 76 | ifile = io.BytesIO() 77 | ifile.write(chunky.build_getinfo_chunk()) 78 | data = [] 79 | for i in range(0, 10): 80 | data.append({"in_index": str(i)}) 81 | ifile.write(chunky.build_data_chunk(data, finished=True)) 82 | ifile.seek(0) 83 | ofile = io.BytesIO() 84 | cmd._process_protocol_v2([], ifile, ofile) 85 | ofile.seek(0) 86 | output_iter = chunky.ChunkedDataStream(ofile).__iter__() 87 | next(output_iter) 88 | output_records = list(next(output_iter).data) 89 | 90 | # Assert that count of records having "odd_field" is 10 91 | assert len(list(r for r in output_records if "odd_field" in r)) == 10 92 | 93 | # Assert that count of records having "even_field" is 10 94 | assert len(list(r for r in output_records if "even_field" in r)) == 10 95 | -------------------------------------------------------------------------------- /docs/searchcommands.rst: -------------------------------------------------------------------------------- 1 | splunklib.searchcommands 2 | ------------------------ 3 | 4 | .. automodule:: splunklib.searchcommands 5 | 6 | .. autofunction:: dispatch(command_class[, argv=sys.argv, input_file=sys.stdin, output_file=sys.stdout, module_name=None, allow_empty_input=True]) 7 | 8 | .. autoclass:: EventingCommand 9 | :members: 10 | :inherited-members: 11 | :exclude-members: ConfigurationSettings, process, transform 12 | 13 | .. autoclass:: splunklib.searchcommands::EventingCommand.ConfigurationSettings 14 | :members: 15 | :inherited-members: 16 | :exclude-members: configuration_settings, fix_up, items, keys 17 | 18 | .. automethod:: splunklib.searchcommands::EventingCommand.transform 19 | 20 | .. automethod:: splunklib.searchcommands::EventingCommand.process(args=sys.argv[, input_file=sys.stdin, output_file=sys.stdout]) 21 | 22 | .. autoclass:: GeneratingCommand 23 | :members: 24 | :inherited-members: 25 | :exclude-members: ConfigurationSettings, generate, process 26 | 27 | .. autoclass:: splunklib.searchcommands::GeneratingCommand.ConfigurationSettings 28 | :members: 29 | :inherited-members: 30 | :exclude-members: configuration_settings, fix_up, items, keys 31 | 32 | .. automethod:: splunklib.searchcommands::GeneratingCommand.generate 33 | 34 | .. automethod:: splunklib.searchcommands::GeneratingCommand.process(args=sys.argv[, input_file=sys.stdin, output_file=sys.stdout, allow_empty_input=True]) 35 | 36 | .. autoclass:: ReportingCommand 37 | :members: 38 | :inherited-members: 39 | :exclude-members: ConfigurationSettings, map, process, reduce 40 | 41 | .. autoclass:: splunklib.searchcommands::ReportingCommand.ConfigurationSettings 42 | :members: 43 | :inherited-members: 44 | :exclude-members: configuration_settings, fix_up, items, keys 45 | 46 | .. automethod:: splunklib.searchcommands::ReportingCommand.map 47 | 48 | .. automethod:: splunklib.searchcommands::ReportingCommand.process(args=sys.argv[, input_file=sys.stdin, output_file=sys.stdout]) 49 | 50 | .. automethod:: splunklib.searchcommands::ReportingCommand.reduce 51 | 52 | .. autoclass:: StreamingCommand 53 | :members: 54 | :inherited-members: 55 | :exclude-members: ConfigurationSettings, process, stream 56 | 57 | .. autoclass:: splunklib.searchcommands::StreamingCommand.ConfigurationSettings 58 | :members: 59 | :inherited-members: 60 | :exclude-members: configuration_settings, fix_up, items, keys 61 | 62 | .. automethod:: splunklib.searchcommands::StreamingCommand.process(args=sys.argv[, input_file=sys.stdin, output_file=sys.stdout, allow_empty_input=True]) 63 | 64 | .. automethod:: splunklib.searchcommands::StreamingCommand.stream 65 | 66 | .. autoclass:: Configuration 67 | :members: 68 | :inherited-members: 69 | 70 | .. autoclass:: Option 71 | :members: 72 | :inherited-members: 73 | :exclude-members: Item, View, fix_up 74 | 75 | .. autoclass:: Boolean 76 | :members: 77 | :inherited-members: 78 | 79 | .. autoclass:: Duration 80 | :members: 81 | :inherited-members: 82 | 83 | .. autoclass:: File 84 | :members: 85 | :inherited-members: 86 | 87 | .. autoclass:: Integer 88 | :members: 89 | :inherited-members: 90 | 91 | .. autoclass:: Float 92 | :members: 93 | :inherited-members: 94 | 95 | .. autoclass:: RegularExpression 96 | :members: 97 | :inherited-members: 98 | 99 | .. autoclass:: Set 100 | :members: 101 | :inherited-members: 102 | 103 | -------------------------------------------------------------------------------- /tests/unit/test_utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright © 2011-2025 Splunk, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"): you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | import os 18 | from pathlib import Path 19 | import unittest 20 | 21 | from utils import dslice 22 | 23 | TEST_DICT = { 24 | "username": "admin", 25 | "password": "changeme", 26 | "port": 8089, 27 | "host": "localhost", 28 | "scheme": "https", 29 | } 30 | 31 | 32 | class TestUtils(unittest.TestCase): 33 | # Test dslice when a dict is passed to change key names 34 | def test_dslice_dict_args(self): 35 | args = { 36 | "username": "user-name", 37 | "password": "new_password", 38 | "port": "admin_port", 39 | "foo": "bar", 40 | } 41 | expected = { 42 | "user-name": "admin", 43 | "new_password": "changeme", 44 | "admin_port": 8089, 45 | } 46 | self.assertTrue(expected == dslice(TEST_DICT, args)) 47 | 48 | # Test dslice when a list is passed 49 | def test_dslice_list_args(self): 50 | test_list = ["username", "password", "port", "host", "foo"] 51 | expected = { 52 | "username": "admin", 53 | "password": "changeme", 54 | "port": 8089, 55 | "host": "localhost", 56 | } 57 | self.assertTrue(expected == dslice(TEST_DICT, test_list)) 58 | 59 | # Test dslice when a single string is passed 60 | def test_dslice_arg(self): 61 | test_arg = "username" 62 | expected = {"username": "admin"} 63 | self.assertTrue(expected == dslice(TEST_DICT, test_arg)) 64 | 65 | # Test dslice using all three types of arguments 66 | def test_dslice_all_args(self): 67 | test_args = [{"username": "new_username"}, ["password", "host"], "port"] 68 | expected = { 69 | "new_username": "admin", 70 | "password": "changeme", 71 | "host": "localhost", 72 | "port": 8089, 73 | } 74 | self.assertTrue(expected == dslice(TEST_DICT, *test_args)) 75 | 76 | 77 | class FilePermissionTest(unittest.TestCase): 78 | def setUp(self): 79 | super().setUp() 80 | 81 | # Check for any change in the default file permission(i.e 644) for all files within splunklib 82 | def test_filePermissions(self): 83 | def checkFilePermissions(dir_path): 84 | for file in os.listdir(dir_path): 85 | if file.__contains__("pycache"): 86 | continue 87 | path = os.path.join(dir_path, file) 88 | if os.path.isfile(path): 89 | permission = oct(os.stat(path).st_mode) 90 | self.assertEqual(permission, "0o100644") 91 | else: 92 | checkFilePermissions(path) 93 | 94 | test_file_path = Path(__file__) 95 | # From tests/unit/test_file_permissions.py, go up 2 levels to project root, then to splunklib 96 | splunklib_path = test_file_path.parent.parent.parent / "splunklib" 97 | checkFilePermissions(str(splunklib_path)) 98 | 99 | 100 | if __name__ == "__main__": 101 | import unittest 102 | 103 | unittest.main() 104 | -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright © 2011-2024 Splunk, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | """Utility module shared by the SDK unit tests.""" 16 | 17 | from utils.cmdopts import * 18 | 19 | 20 | def config(option, opt, value, parser): 21 | assert opt == "--config" 22 | parser.load(value) 23 | 24 | 25 | # Default Splunk cmdline rules 26 | RULES_SPLUNK = { 27 | "config": { 28 | "flags": ["--config"], 29 | "action": "callback", 30 | "callback": config, 31 | "type": "string", 32 | "nargs": "1", 33 | "help": "Load options from config file", 34 | }, 35 | "scheme": { 36 | "flags": ["--scheme"], 37 | "default": "https", 38 | "help": "Scheme (default 'https')", 39 | }, 40 | "host": { 41 | "flags": ["--host"], 42 | "default": "localhost", 43 | "help": "Host name (default 'localhost')", 44 | }, 45 | "port": { 46 | "flags": ["--port"], 47 | "default": "8089", 48 | "help": "Port number (default 8089)", 49 | }, 50 | "app": {"flags": ["--app"], "help": "The app context (optional)"}, 51 | "owner": {"flags": ["--owner"], "help": "The user context (optional)"}, 52 | "username": { 53 | "flags": ["--username"], 54 | "default": None, 55 | "help": "Username to login with", 56 | }, 57 | "password": { 58 | "flags": ["--password"], 59 | "default": None, 60 | "help": "Password to login with", 61 | }, 62 | "version": { 63 | "flags": ["--version"], 64 | "default": None, 65 | "help": "Ignore. Used by JavaScript SDK.", 66 | }, 67 | "splunkToken": { 68 | "flags": ["--bearerToken"], 69 | "default": None, 70 | "help": "Bearer token for authentication", 71 | }, 72 | "token": { 73 | "flags": ["--sessionKey"], 74 | "default": None, 75 | "help": "Session key for authentication", 76 | }, 77 | } 78 | 79 | FLAGS_SPLUNK = list(RULES_SPLUNK.keys()) 80 | 81 | 82 | # value: dict, args: [(dict | list | str)*] 83 | def dslice(value, *args): 84 | """Returns a 'slice' of the given dictionary value containing only the 85 | requested keys. The keys can be requested in a variety of ways, as an 86 | arg list of keys, as a list of keys, or as a dict whose key(s) represent 87 | the source keys and whose corresponding values represent the resulting 88 | key(s) (enabling key rename), or any combination of the above.""" 89 | result = {} 90 | for arg in args: 91 | if isinstance(arg, dict): 92 | for k, v in list(arg.items()): 93 | if k in value: 94 | result[v] = value[k] 95 | elif isinstance(arg, list): 96 | for k in arg: 97 | if k in value: 98 | result[k] = value[k] 99 | else: 100 | if arg in value: 101 | result[arg] = value[arg] 102 | return result 103 | 104 | 105 | def parse(argv, rules=None, config=None, **kwargs): 106 | """Parse the given arg vector with the default Splunk command rules.""" 107 | parser_ = parser(rules, **kwargs) 108 | if config is not None: 109 | parser_.loadenv(config) 110 | return parser_.parse(argv).result 111 | 112 | 113 | def parser(rules=None, **kwargs): 114 | """Instantiate a parser with the default Splunk command rules.""" 115 | rules = RULES_SPLUNK if rules is None else dict(RULES_SPLUNK, **rules) 116 | return Parser(rules, **kwargs) 117 | -------------------------------------------------------------------------------- /splunklib/modularinput/event_writer.py: -------------------------------------------------------------------------------- 1 | # Copyright © 2011-2024 Splunk, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | import sys 16 | import traceback 17 | 18 | from ..utils import ensure_str 19 | from .event import ET 20 | 21 | 22 | class EventWriter: 23 | """``EventWriter`` writes events and error messages to Splunk from a modular input. 24 | Its two important methods are ``writeEvent``, which takes an ``Event`` object, 25 | and ``log``, which takes a severity and an error message. 26 | """ 27 | 28 | # Severities that Splunk understands for log messages from modular inputs. 29 | # Do not change these 30 | DEBUG = "DEBUG" 31 | INFO = "INFO" 32 | WARN = "WARN" 33 | ERROR = "ERROR" 34 | FATAL = "FATAL" 35 | 36 | def __init__(self, output=sys.stdout, error=sys.stderr): 37 | """ 38 | :param output: Where to write the output; defaults to sys.stdout. 39 | :param error: Where to write any errors; defaults to sys.stderr. 40 | """ 41 | self._out = output 42 | self._err = error 43 | 44 | # has the opening tag been written yet? 45 | self.header_written = False 46 | 47 | def write_event(self, event): 48 | """Writes an ``Event`` object to Splunk. 49 | 50 | :param event: An ``Event`` object. 51 | """ 52 | 53 | if not self.header_written: 54 | self._out.write("") 55 | self.header_written = True 56 | 57 | event.write_to(self._out) 58 | 59 | def log(self, severity, message): 60 | """Logs messages about the state of this modular input to Splunk. 61 | These messages will show up in Splunk's internal logs. 62 | 63 | :param severity: ``string``, severity of message, see severities defined as class constants. 64 | :param message: ``string``, message to log. 65 | """ 66 | 67 | self._err.write(f"{severity} {message}\n") 68 | self._err.flush() 69 | 70 | def log_exception(self, message, exception=None, severity=None): 71 | """Logs messages about the exception thrown by this modular input to Splunk. 72 | These messages will show up in Splunk's internal logs. 73 | 74 | :param message: ``string``, message to log. 75 | :param exception: ``Exception``, exception thrown by this modular input; if none, sys.exc_info() is used 76 | :param severity: ``string``, severity of message, see severities defined as class constants. Default severity: ERROR 77 | """ 78 | if exception is not None: 79 | tb_str = traceback.format_exception( 80 | type(exception), exception, exception.__traceback__ 81 | ) 82 | else: 83 | tb_str = traceback.format_exc() 84 | 85 | if severity is None: 86 | severity = EventWriter.ERROR 87 | 88 | self._err.write(("%s %s - %s" % (severity, message, tb_str)).replace("\n", " ")) 89 | self._err.flush() 90 | 91 | def write_xml_document(self, document): 92 | """Writes a string representation of an 93 | ``ElementTree`` object to the output stream. 94 | 95 | :param document: An ``ElementTree`` object. 96 | """ 97 | self._out.write(ensure_str(ET.tostring(document), errors="replace")) 98 | self._out.flush() 99 | 100 | def close(self): 101 | """Write the closing tag to make this XML well formed.""" 102 | if self.header_written: 103 | self._out.write("") 104 | self._out.flush() 105 | -------------------------------------------------------------------------------- /tests/integration/test_fired_alert.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright © 2011-2024 Splunk, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"): you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | from tests import testlib 18 | 19 | 20 | class FiredAlertTestCase(testlib.SDKTestCase): 21 | def setUp(self): 22 | super().setUp() 23 | self.index_name = testlib.tmpname() 24 | self.assertFalse(self.index_name in self.service.indexes) 25 | self.index = self.service.indexes.create(self.index_name) 26 | saved_searches = self.service.saved_searches 27 | self.saved_search_name = testlib.tmpname() 28 | self.assertFalse(self.saved_search_name in saved_searches) 29 | query = f"search index={self.index_name}" 30 | kwargs = { 31 | "alert_type": "always", 32 | "alert.severity": "3", 33 | "alert.suppress": "0", 34 | "alert.track": "1", 35 | "dispatch.earliest_time": "-1h", 36 | "dispatch.latest_time": "now", 37 | "is_scheduled": "1", 38 | "cron_schedule": "* * * * *", 39 | } 40 | self.saved_search = saved_searches.create( 41 | self.saved_search_name, query, **kwargs 42 | ) 43 | 44 | def tearDown(self): 45 | super().tearDown() 46 | if self.service.splunk_version >= (5,): 47 | self.service.indexes.delete(self.index_name) 48 | for saved_search in self.service.saved_searches: 49 | if saved_search.name.startswith("delete-me"): 50 | self.service.saved_searches.delete(saved_search.name) 51 | self.assertFalse(saved_search.name in self.service.saved_searches) 52 | self.assertFalse(saved_search.name in self.service.fired_alerts) 53 | 54 | def test_new_search_is_empty(self): 55 | self.assertEqual(self.saved_search.alert_count, 0) 56 | self.assertEqual(len(self.saved_search.history()), 0) 57 | self.assertEqual(len(self.saved_search.fired_alerts), 0) 58 | self.assertFalse(self.saved_search_name in self.service.fired_alerts) 59 | 60 | def test_alerts_on_events(self): 61 | self.assertEqual(self.saved_search.alert_count, 0) 62 | self.assertEqual(len(self.saved_search.fired_alerts), 0) 63 | 64 | self.index.enable() 65 | self.assertEventuallyTrue( 66 | lambda: self.index.refresh() and self.index["disabled"] == "0", timeout=25 67 | ) 68 | 69 | eventCount = int(self.index["totalEventCount"]) 70 | self.assertEqual(self.index["sync"], "0") 71 | self.assertEqual(self.index["disabled"], "0") 72 | self.index.refresh() 73 | self.index.submit( 74 | "This is a test " + testlib.tmpname(), sourcetype="sdk_use", host="boris" 75 | ) 76 | 77 | def f(): 78 | self.index.refresh() 79 | return int(self.index["totalEventCount"]) == eventCount + 1 80 | 81 | self.assertEventuallyTrue(f, timeout=50) 82 | 83 | def g(): 84 | self.saved_search.refresh() 85 | return self.saved_search.alert_count == 1 86 | 87 | self.assertEventuallyTrue(g, timeout=200) 88 | 89 | alerts = self.saved_search.fired_alerts 90 | self.assertEqual(len(alerts), 1) 91 | 92 | def test_read(self): 93 | for alert_group in self.service.fired_alerts: 94 | alert_group.count 95 | for alert in alert_group.alerts: 96 | alert.content 97 | 98 | 99 | if __name__ == "__main__": 100 | import unittest 101 | 102 | unittest.main() 103 | -------------------------------------------------------------------------------- /tests/integration/test_conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright © 2011-2024 Splunk, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"): you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | from tests import testlib 18 | 19 | from splunklib import client 20 | 21 | 22 | class TestRead(testlib.SDKTestCase): 23 | def test_read(self): 24 | service = client.connect(**self.opts.kwargs) 25 | 26 | confs = service.confs 27 | 28 | # Make sure the collection contains some of the expected entries 29 | self.assertTrue("eventtypes" in confs) 30 | self.assertTrue("indexes" in confs) 31 | self.assertTrue("inputs" in confs) 32 | self.assertTrue("props" in confs) 33 | self.assertTrue("transforms" in confs) 34 | self.assertTrue("savedsearches" in confs) 35 | 36 | for stanza in confs["indexes"].list(count=5): 37 | self.check_entity(stanza) 38 | 39 | 40 | class TestConfs(testlib.SDKTestCase): 41 | def setUp(self): 42 | super().setUp() 43 | self.app_name = testlib.tmpname() 44 | self.app = self.service.apps.create(self.app_name) 45 | # Connect using the test app context 46 | kwargs = self.opts.kwargs.copy() 47 | kwargs["app"] = self.app_name 48 | kwargs["owner"] = "nobody" 49 | kwargs["sharing"] = "app" 50 | self.app_service = client.connect(**kwargs) 51 | 52 | def tearDown(self): 53 | self.service.apps.delete(self.app_name) 54 | self.clear_restart_message() 55 | 56 | def test_confs(self): 57 | confs = self.app_service.confs 58 | conf_name = testlib.tmpname() 59 | self.assertRaises(KeyError, confs.__getitem__, conf_name) 60 | self.assertFalse(conf_name in confs) 61 | 62 | conf = confs.create(conf_name) 63 | self.assertTrue(conf_name in confs) 64 | self.assertEqual(conf.name, conf_name) 65 | 66 | # New conf should be empty 67 | stanzas = conf.list() 68 | self.assertEqual(len(stanzas), 0) 69 | 70 | # Creating a stanza works 71 | count = len(conf) 72 | stanza_name = testlib.tmpname() 73 | stanza = conf.create(stanza_name) 74 | self.assertEqual(len(conf), count + 1) 75 | self.assertTrue(stanza_name in conf) 76 | 77 | # New stanzas are empty 78 | self.assertEqual(len(stanza), 0) 79 | 80 | # Update works 81 | key = testlib.tmpname() 82 | val = testlib.tmpname() 83 | stanza.update(**{key: val}) 84 | self.assertEventuallyTrue( 85 | lambda: stanza.refresh() and len(stanza) == 1, pause_time=0.2 86 | ) 87 | self.assertEqual(len(stanza), 1) 88 | self.assertTrue(key in stanza) 89 | 90 | values = { 91 | testlib.tmpname(): testlib.tmpname(), 92 | testlib.tmpname(): testlib.tmpname(), 93 | } 94 | stanza.submit(values) 95 | stanza.refresh() 96 | for key, value in values.items(): 97 | self.assertTrue(key in stanza) 98 | self.assertEqual(value, stanza[key]) 99 | 100 | count = len(conf) 101 | conf.delete(stanza_name) 102 | self.assertFalse(stanza_name in conf) 103 | self.assertEqual(len(conf), count - 1) 104 | 105 | # Can't actually delete configuration files directly, at least 106 | # not in current versions of Splunk. 107 | self.assertRaises(client.IllegalOperationException, confs.delete, conf_name) 108 | self.assertTrue(conf_name in confs) 109 | 110 | 111 | if __name__ == "__main__": 112 | import unittest 113 | 114 | unittest.main() 115 | -------------------------------------------------------------------------------- /tests/integration/test_kvstore_conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2011-2020 Splunk, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"): you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | import json 18 | from tests import testlib 19 | from splunklib import client 20 | 21 | 22 | class KVStoreConfTestCase(testlib.SDKTestCase): 23 | def setUp(self): 24 | super().setUp() 25 | self.service.namespace["app"] = "search" 26 | self.confs = self.service.kvstore 27 | if "test" in self.confs: 28 | self.confs["test"].delete() 29 | 30 | def test_owner_restriction(self): 31 | self.service.kvstore_owner = "admin" 32 | self.assertRaises(client.HTTPError, lambda: self.confs.list()) 33 | self.service.kvstore_owner = "nobody" 34 | 35 | def test_create_delete_collection(self): 36 | self.confs.create("test") 37 | self.assertTrue("test" in self.confs) 38 | self.confs["test"].delete() 39 | self.assertTrue("test" not in self.confs) 40 | 41 | def test_create_fields(self): 42 | self.confs.create( 43 | "test", accelerated_fields={"ind1": {"a": 1}}, fields={"a": "number1"} 44 | ) 45 | self.assertEqual(self.confs["test"]["field.a"], "number1") 46 | self.assertEqual(self.confs["test"]["accelerated_fields.ind1"], {"a": 1}) 47 | self.confs["test"].delete() 48 | 49 | def test_update_collection(self): 50 | self.confs.create("test") 51 | val = {"a": 1} 52 | self.confs["test"].post( 53 | **{"accelerated_fields.ind1": json.dumps(val), "field.a": "number"} 54 | ) 55 | self.assertEqual(self.confs["test"]["field.a"], "number") 56 | self.assertEqual(self.confs["test"]["accelerated_fields.ind1"], {"a": 1}) 57 | self.confs["test"].delete() 58 | 59 | def test_update_accelerated_fields(self): 60 | self.confs.create("test", accelerated_fields={"ind1": {"a": 1}}) 61 | self.assertEqual(self.confs["test"]["accelerated_fields.ind1"], {"a": 1}) 62 | # update accelerated_field value 63 | self.confs["test"].update_accelerated_field("ind1", {"a": -1}) 64 | self.assertEqual(self.confs["test"]["accelerated_fields.ind1"], {"a": -1}) 65 | self.confs["test"].delete() 66 | 67 | def test_update_fields(self): 68 | self.confs.create("test") 69 | self.confs["test"].post(**{"field.a": "number"}) 70 | self.assertEqual(self.confs["test"]["field.a"], "number") 71 | self.confs["test"].update_field("a", "string") 72 | self.assertEqual(self.confs["test"]["field.a"], "string") 73 | self.confs["test"].delete() 74 | 75 | def test_create_unique_collection(self): 76 | self.confs.create("test") 77 | self.assertTrue("test" in self.confs) 78 | self.assertRaises(client.HTTPError, lambda: self.confs.create("test")) 79 | self.confs["test"].delete() 80 | 81 | def test_overlapping_collections(self): 82 | self.service.namespace["app"] = "system" 83 | self.confs.create("test") 84 | self.service.namespace["app"] = "search" 85 | self.confs.create("test") 86 | self.assertEqual(self.confs["test"]["eai:appName"], "search") 87 | self.service.namespace["app"] = "system" 88 | self.assertEqual(self.confs["test"]["eai:appName"], "system") 89 | self.service.namespace["app"] = "search" 90 | self.confs["test"].delete() 91 | self.confs["test"].delete() 92 | 93 | def tearDown(self): 94 | if "test" in self.confs: 95 | self.confs["test"].delete() 96 | 97 | 98 | if __name__ == "__main__": 99 | import unittest 100 | 101 | unittest.main() 102 | -------------------------------------------------------------------------------- /tests/integration/test_app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright © 2011-2024 Splunk, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"): you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | 18 | import logging 19 | from tests import testlib 20 | from splunklib import client 21 | 22 | 23 | class TestApp(testlib.SDKTestCase): 24 | app = None 25 | app_name = None 26 | 27 | def setUp(self): 28 | super().setUp() 29 | if self.app is None: 30 | for app in self.service.apps: 31 | if app.name.startswith("delete-me"): 32 | self.service.apps.delete(app.name) 33 | # Creating apps takes 0.8s, which is too long to wait for 34 | # each test in this test suite. Therefore we create one 35 | # app and reuse it. Since apps are rather less fraught 36 | # than entities like indexes, this is okay. 37 | self.app_name = testlib.tmpname() 38 | self.app = self.service.apps.create(self.app_name) 39 | logging.debug(f"Creating app {self.app_name}") 40 | else: 41 | logging.debug(f"App {self.app_name} already exists. Skipping creation.") 42 | if self.service.restart_required: 43 | self.restart_splunk() 44 | 45 | def tearDown(self): 46 | super().tearDown() 47 | # The rest of this will leave Splunk in a state requiring a restart. 48 | # It doesn't actually matter, though. 49 | self.service = client.connect(**self.opts.kwargs) 50 | app_name = "" 51 | for app in self.service.apps: 52 | app_name = app.name 53 | if app_name.startswith("delete-me"): 54 | self.service.apps.delete(app_name) 55 | self.assertEventuallyTrue(lambda: app_name not in self.service.apps) 56 | self.clear_restart_message() 57 | 58 | def test_app_integrity(self): 59 | self.check_entity(self.app) 60 | self.app.setupInfo 61 | self.app["setupInfo"] 62 | 63 | def test_disable_enable(self): 64 | self.app.disable() 65 | self.app.refresh() 66 | self.assertEqual(self.app["disabled"], "1") 67 | self.app.enable() 68 | self.app.refresh() 69 | self.assertEqual(self.app["disabled"], "0") 70 | 71 | def test_update(self): 72 | kwargs = { 73 | "author": "Me", 74 | "description": "Test app description", 75 | "label": "SDK Test", 76 | "version": "1.2", 77 | "visible": True, 78 | } 79 | self.app.update(**kwargs) 80 | self.app.refresh() 81 | self.assertEqual(self.app["author"], "Me") 82 | self.assertEqual(self.app["label"], "SDK Test") 83 | self.assertEqual(self.app["version"], "1.2") 84 | self.assertEqual(self.app["visible"], "1") 85 | 86 | def test_delete(self): 87 | name = testlib.tmpname() 88 | self.service.apps.create(name) 89 | self.assertTrue(name in self.service.apps) 90 | self.service.apps.delete(name) 91 | self.assertFalse(name in self.service.apps) 92 | self.clear_restart_message() # We don't actually have to restart here. 93 | 94 | def test_package(self): 95 | p = self.app.package() 96 | self.assertEqual(p.name, self.app_name) 97 | self.assertTrue(p.path.endswith(self.app_name + ".spl")) 98 | # Assert string due to deprecation of this property in new Splunk versions 99 | self.assertIsInstance(p.url, str) 100 | 101 | def test_updateInfo(self): 102 | p = self.app.updateInfo() 103 | self.assertTrue(p is not None) 104 | 105 | 106 | if __name__ == "__main__": 107 | import unittest 108 | 109 | unittest.main() 110 | -------------------------------------------------------------------------------- /docs/client.rst: -------------------------------------------------------------------------------- 1 | splunklib.client 2 | ---------------- 3 | 4 | .. automodule:: splunklib.client 5 | 6 | .. autofunction:: connect 7 | 8 | .. autoclass:: AmbiguousReferenceException 9 | :members: 10 | 11 | .. autoclass:: Application 12 | :members: setupInfo, package, updateInfo 13 | :inherited-members: 14 | 15 | .. autoclass:: AlertGroup 16 | :members: alerts, count 17 | :inherited-members: 18 | 19 | .. autoclass:: Collection 20 | :members: create, delete 21 | :inherited-members: 22 | 23 | .. autoclass:: ConfigurationFile 24 | :inherited-members: 25 | 26 | .. autoclass:: Configurations 27 | :members: create, delete 28 | :inherited-members: 29 | 30 | .. autoclass:: Endpoint 31 | :members: get, post 32 | :inherited-members: 33 | 34 | .. autoclass:: Entity 35 | :members: access, delete, disable, enable, fields, get, links, name, namespace, post, refresh, reload, update 36 | :inherited-members: 37 | 38 | .. autoclass:: IllegalOperationException 39 | :members: 40 | 41 | .. autoclass:: IncomparableException 42 | :members: 43 | 44 | .. autoclass:: Index 45 | :members: attach, attached_socket, clean, disable, enable, roll_hot_buckets, submit, upload 46 | :inherited-members: 47 | 48 | .. autoclass:: Indexes 49 | :members: default, delete 50 | :inherited-members: 51 | 52 | .. autoclass:: Input 53 | :members: update 54 | :inherited-members: 55 | 56 | .. autoclass:: Inputs 57 | :members: create, delete, itemmeta, kinds, kindpath, list, iter, oneshot 58 | :inherited-members: 59 | 60 | .. autoclass:: InvalidNameException 61 | :members: 62 | 63 | .. autoclass:: Job 64 | :members: cancel, disable_preview, enable_preview, events, finalize, is_done, is_ready, name, pause, refresh, results, preview, searchlog, set_priority, summary, timeline, touch, set_ttl, unpause 65 | :inherited-members: 66 | 67 | .. autoclass:: Jobs 68 | :members: create, export, itemmeta, oneshot 69 | :inherited-members: 70 | 71 | .. autoclass:: KVStoreCollection 72 | :members: data, update_index, update_field 73 | :inherited-members: 74 | 75 | .. autoclass:: KVStoreCollectionData 76 | :members: query, query_by_id, insert, delete, delete_by_id, update, batch_save 77 | :inherited-members: 78 | 79 | .. autoclass:: KVStoreCollections 80 | :members: create 81 | :inherited-members: 82 | 83 | .. autoclass:: Loggers 84 | :members: itemmeta 85 | :inherited-members: 86 | 87 | .. autoclass:: Message 88 | :members: value 89 | :inherited-members: 90 | 91 | .. autoclass:: ModularInputKind 92 | :members: arguments, update 93 | :inherited-members: 94 | 95 | .. autoclass:: NoSuchCapability 96 | :members: 97 | 98 | .. autoclass:: NotSupportedError 99 | :members: 100 | 101 | .. autoclass:: OperationError 102 | :members: 103 | 104 | .. autoclass:: ReadOnlyCollection 105 | :members: itemmeta, iter, list, names 106 | :inherited-members: 107 | 108 | .. autoclass:: Role 109 | :members: grant, revoke 110 | :inherited-members: 111 | 112 | .. autoclass:: Roles 113 | :members: create, delete 114 | :inherited-members: 115 | 116 | .. autoclass:: SavedSearch 117 | :members: acknowledge, alert_count, dispatch, fired_alerts, history, scheduled_times, suppress, suppressed, unsuppress, update 118 | :inherited-members: 119 | 120 | .. autoclass:: SavedSearches 121 | :members: create 122 | :inherited-members: 123 | 124 | .. autoclass:: Service 125 | :members: apps, confs, capabilities, event_types, fired_alerts, indexes, info, inputs, job, jobs, kvstore, loggers, messages, modular_input_kinds, parse, restart, restart_required, roles, search, saved_searches, settings, splunk_version, storage_passwords, users 126 | :inherited-members: 127 | 128 | .. autoclass:: Settings 129 | :members: update 130 | :inherited-members: 131 | 132 | .. autoclass:: Stanza 133 | :members: submit 134 | :inherited-members: 135 | 136 | .. autoclass:: StoragePassword 137 | :members: clear_password, encrypted_password, realm, username 138 | :inherited-members: 139 | 140 | .. autoclass:: StoragePasswords 141 | :members: create, delete 142 | :inherited-members: 143 | 144 | .. autoclass:: User 145 | :members: role_entities 146 | :inherited-members: 147 | 148 | .. autoclass:: Users 149 | :members: create, delete 150 | :inherited-members: 151 | -------------------------------------------------------------------------------- /tests/integration/test_kvstore_data.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright © 2011-2024 Splunk, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"): you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | import json 18 | from tests import testlib 19 | 20 | from splunklib import client 21 | 22 | 23 | class KVStoreDataTestCase(testlib.SDKTestCase): 24 | def setUp(self): 25 | super().setUp() 26 | self.service.namespace["app"] = "search" 27 | self.confs = self.service.kvstore 28 | if "test" in self.confs: 29 | self.confs["test"].delete() 30 | self.confs.create("test") 31 | 32 | self.col = self.confs["test"].data 33 | 34 | def test_insert_query_delete_data(self): 35 | for x in range(50): 36 | self.col.insert( 37 | json.dumps({"_key": str(x), "data": "#" + str(x), "num": x}) 38 | ) 39 | self.assertEqual(len(self.col.query()), 50) 40 | self.assertEqual(len(self.col.query(query='{"num": 10}')), 1) 41 | self.assertEqual(self.col.query(query='{"num": 10}')[0]["data"], "#10") 42 | self.col.delete(json.dumps({"num": {"$gt": 39}})) 43 | self.assertEqual(len(self.col.query()), 40) 44 | self.col.delete() 45 | self.assertEqual(len(self.col.query()), 0) 46 | 47 | def test_update_delete_data(self): 48 | for x in range(50): 49 | self.col.insert( 50 | json.dumps({"_key": str(x), "data": "#" + str(x), "num": x}) 51 | ) 52 | self.assertEqual(len(self.col.query()), 50) 53 | self.assertEqual(self.col.query(query='{"num": 49}')[0]["data"], "#49") 54 | self.col.update(str(49), json.dumps({"data": "#50", "num": 50})) 55 | self.assertEqual(len(self.col.query()), 50) 56 | self.assertEqual(self.col.query(query='{"num": 50}')[0]["data"], "#50") 57 | self.assertEqual(len(self.col.query(query='{"num": 49}')), 0) 58 | self.col.delete_by_id(49) 59 | self.assertEqual(len(self.col.query(query='{"num": 50}')), 0) 60 | 61 | def test_query_data(self): 62 | if "test1" in self.confs: 63 | self.confs["test1"].delete() 64 | self.confs.create("test1") 65 | self.col = self.confs["test1"].data 66 | for x in range(10): 67 | self.col.insert( 68 | json.dumps({"_key": str(x), "data": "#" + str(x), "num": x}) 69 | ) 70 | data = self.col.query(sort="data:-1", skip=9) 71 | self.assertEqual(len(data), 1) 72 | self.assertEqual(data[0]["data"], "#0") 73 | data = self.col.query(sort="data:1") 74 | self.assertEqual(data[0]["data"], "#0") 75 | data = self.col.query(limit=2, skip=9) 76 | self.assertEqual(len(data), 1) 77 | 78 | def test_invalid_insert_update(self): 79 | self.assertRaises(client.HTTPError, lambda: self.col.insert("NOT VALID DATA")) 80 | id = self.col.insert(json.dumps({"foo": "bar"}))["_key"] 81 | self.assertRaises( 82 | client.HTTPError, lambda: self.col.update(id, "NOT VALID DATA") 83 | ) 84 | self.assertEqual(self.col.query_by_id(id)["foo"], "bar") 85 | 86 | def test_params_data_type_conversion(self): 87 | self.confs["test"].post( 88 | **{"field.data": "number", "accelerated_fields.data": '{"data": -1}'} 89 | ) 90 | for x in range(50): 91 | self.col.insert(json.dumps({"_key": str(x), "data": str(x), "ignore": x})) 92 | data = self.col.query(sort="data:-1", limit=20, fields="data,_id:0", skip=10) 93 | self.assertEqual(len(data), 20) 94 | for x in range(20): 95 | self.assertEqual(data[x]["data"], 39 - x) 96 | self.assertTrue("ignore" not in data[x]) 97 | self.assertTrue("_key" not in data[x]) 98 | 99 | def tearDown(self): 100 | if "test" in self.confs: 101 | self.confs["test"].delete() 102 | 103 | 104 | if __name__ == "__main__": 105 | import unittest 106 | 107 | unittest.main() 108 | -------------------------------------------------------------------------------- /tests/integration/test_role.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright © 2011-2024 Splunk, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"): you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | from tests import testlib 18 | import logging 19 | 20 | from splunklib import client 21 | 22 | 23 | class RoleTestCase(testlib.SDKTestCase): 24 | def setUp(self): 25 | super().setUp() 26 | self.role_name = testlib.tmpname() 27 | self.role = self.service.roles.create(self.role_name) 28 | 29 | def tearDown(self): 30 | super().tearDown() 31 | for role in self.service.roles: 32 | if role.name.startswith("delete-me"): 33 | self.service.roles.delete(role.name) 34 | 35 | def check_role(self, role): 36 | self.check_entity(role) 37 | capabilities = role.service.capabilities 38 | for capability in role.content.capabilities: 39 | self.assertTrue(capability in capabilities) 40 | 41 | def test_read(self): 42 | for role in self.service.roles: 43 | self.check_role(role) 44 | role.refresh() 45 | self.check_role(role) 46 | 47 | def test_read_case_insensitive(self): 48 | for role in self.service.roles: 49 | a = self.service.roles[role.name.upper()] 50 | b = self.service.roles[role.name.lower()] 51 | self.assertEqual(a.name, b.name) 52 | 53 | def test_create(self): 54 | self.assertTrue(self.role_name in self.service.roles) 55 | self.check_entity(self.role) 56 | 57 | def test_delete(self): 58 | self.assertTrue(self.role_name in self.service.roles) 59 | self.service.roles.delete(self.role_name) 60 | self.assertFalse(self.role_name in self.service.roles) 61 | self.assertRaises(client.HTTPError, self.role.refresh) 62 | 63 | def test_grant_and_revoke(self): 64 | self.assertFalse("edit_user" in self.role.capabilities) 65 | self.role.grant("edit_user") 66 | self.role.refresh() 67 | self.assertTrue("edit_user" in self.role.capabilities) 68 | 69 | self.assertFalse("change_own_password" in self.role.capabilities) 70 | self.role.grant("change_own_password") 71 | self.role.refresh() 72 | self.assertTrue("edit_user" in self.role.capabilities) 73 | self.assertTrue("change_own_password" in self.role.capabilities) 74 | 75 | self.role.revoke("edit_user") 76 | self.role.refresh() 77 | self.assertFalse("edit_user" in self.role.capabilities) 78 | self.assertTrue("change_own_password" in self.role.capabilities) 79 | 80 | self.role.revoke("change_own_password") 81 | self.role.refresh() 82 | self.assertFalse("edit_user" in self.role.capabilities) 83 | self.assertFalse("change_own_password" in self.role.capabilities) 84 | 85 | def test_invalid_grant(self): 86 | self.assertRaises( 87 | client.NoSuchCapability, self.role.grant, "i-am-an-invalid-capability" 88 | ) 89 | 90 | def test_invalid_revoke(self): 91 | self.assertRaises( 92 | client.NoSuchCapability, self.role.revoke, "i-am-an-invalid-capability" 93 | ) 94 | 95 | def test_revoke_capability_not_granted(self): 96 | self.role.revoke("change_own_password") 97 | 98 | def test_update(self): 99 | kwargs = {} 100 | if "user" in self.role["imported_roles"]: 101 | kwargs["imported_roles"] = "" 102 | else: 103 | kwargs["imported_roles"] = ["user"] 104 | if self.role["srchJobsQuota"] is not None: 105 | kwargs["srchJobsQuota"] = int(self.role["srchJobsQuota"]) + 1 106 | self.role.update(**kwargs) 107 | self.role.refresh() 108 | self.assertEqual(self.role["imported_roles"], kwargs["imported_roles"]) 109 | self.assertEqual(int(self.role["srchJobsQuota"]), kwargs["srchJobsQuota"]) 110 | 111 | 112 | if __name__ == "__main__": 113 | import unittest 114 | 115 | unittest.main() 116 | -------------------------------------------------------------------------------- /splunklib/modularinput/argument.py: -------------------------------------------------------------------------------- 1 | # Copyright © 2011-2024 Splunk, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | import xml.etree.ElementTree as ET 16 | 17 | 18 | class Argument: 19 | """Class representing an argument to a modular input kind. 20 | 21 | ``Argument`` is meant to be used with ``Scheme`` to generate an XML 22 | definition of the modular input kind that Splunk understands. 23 | 24 | ``name`` is the only required parameter for the constructor. 25 | 26 | **Example with least parameters**:: 27 | 28 | arg1 = Argument(name="arg1") 29 | 30 | **Example with all parameters**:: 31 | 32 | arg2 = Argument( 33 | name="arg2", 34 | description="This is an argument with lots of parameters", 35 | validation="is_pos_int('some_name')", 36 | data_type=Argument.data_type_number, 37 | required_on_edit=True, 38 | required_on_create=True 39 | ) 40 | """ 41 | 42 | # Constant values, do not change. 43 | # These should be used for setting the value of an Argument object's data_type field. 44 | data_type_boolean = "BOOLEAN" 45 | data_type_number = "NUMBER" 46 | data_type_string = "STRING" 47 | 48 | def __init__( 49 | self, 50 | name, 51 | description=None, 52 | validation=None, 53 | data_type=data_type_string, 54 | required_on_edit=False, 55 | required_on_create=False, 56 | title=None, 57 | ): 58 | """ 59 | :param name: ``string``, identifier for this argument in Splunk. 60 | :param description: ``string``, human-readable description of the argument. 61 | :param validation: ``string`` specifying how the argument should be validated, if using internal validation. 62 | If using external validation, this will be ignored. 63 | :param data_type: ``string``, data type of this field; use the class constants. 64 | "data_type_boolean", "data_type_number", or "data_type_string". 65 | :param required_on_edit: ``Boolean``, whether this arg is required when editing an existing modular input of this kind. 66 | :param required_on_create: ``Boolean``, whether this arg is required when creating a modular input of this kind. 67 | :param title: ``String``, a human-readable title for the argument. 68 | """ 69 | self.name = name 70 | self.description = description 71 | self.validation = validation 72 | self.data_type = data_type 73 | self.required_on_edit = required_on_edit 74 | self.required_on_create = required_on_create 75 | self.title = title 76 | 77 | def add_to_document(self, parent): 78 | """Adds an ``Argument`` object to this ElementTree document. 79 | 80 | Adds an subelement to the parent element, typically 81 | and sets up its subelements with their respective text. 82 | 83 | :param parent: An ``ET.Element`` to be the parent of a new subelement 84 | :returns: An ``ET.Element`` object representing this argument. 85 | """ 86 | arg = ET.SubElement(parent, "arg") 87 | arg.set("name", self.name) 88 | 89 | if self.title is not None: 90 | ET.SubElement(arg, "title").text = self.title 91 | 92 | if self.description is not None: 93 | ET.SubElement(arg, "description").text = self.description 94 | 95 | if self.validation is not None: 96 | ET.SubElement(arg, "validation").text = self.validation 97 | 98 | # add all other subelements to this Argument, represented by (tag, text) 99 | subelements = [ 100 | ("data_type", self.data_type), 101 | ("required_on_edit", self.required_on_edit), 102 | ("required_on_create", self.required_on_create), 103 | ] 104 | 105 | for name, value in subelements: 106 | ET.SubElement(arg, name).text = str(value).lower() 107 | 108 | return arg 109 | -------------------------------------------------------------------------------- /tests/unit/data/services.xml: -------------------------------------------------------------------------------- 1 | 2 | services 3 | https://localhost/services/ 4 | 2011-07-01T16:52:51-07:00 5 | 6 | 7 | Splunk 8 | 9 | 10 | alerts 11 | https://localhost/services/alerts 12 | 2011-07-01T16:52:51-07:00 13 | 14 | 15 | 16 | apps 17 | https://localhost/services/apps 18 | 2011-07-01T16:52:51-07:00 19 | 20 | 21 | 22 | authentication 23 | https://localhost/services/authentication 24 | 2011-07-01T16:52:51-07:00 25 | 26 | 27 | 28 | authorization 29 | https://localhost/services/authorization 30 | 2011-07-01T16:52:51-07:00 31 | 32 | 33 | 34 | data 35 | https://localhost/services/data 36 | 2011-07-01T16:52:51-07:00 37 | 38 | 39 | 40 | deployment 41 | https://localhost/services/deployment 42 | 2011-07-01T16:52:51-07:00 43 | 44 | 45 | 46 | licenser 47 | https://localhost/services/licenser 48 | 2011-07-01T16:52:51-07:00 49 | 50 | 51 | 52 | messages 53 | https://localhost/services/messages 54 | 2011-07-01T16:52:51-07:00 55 | 56 | 57 | 58 | configs 59 | https://localhost/services/configs 60 | 2011-07-01T16:52:51-07:00 61 | 62 | 63 | 64 | saved 65 | https://localhost/services/saved 66 | 2011-07-01T16:52:51-07:00 67 | 68 | 69 | 70 | scheduled 71 | https://localhost/services/scheduled 72 | 2011-07-01T16:52:51-07:00 73 | 74 | 75 | 76 | search 77 | https://localhost/services/search 78 | 2011-07-01T16:52:51-07:00 79 | 80 | 81 | 82 | server 83 | https://localhost/services/server 84 | 2011-07-01T16:52:51-07:00 85 | 86 | 87 | 88 | streams 89 | https://localhost/services/streams 90 | 2011-07-01T16:52:51-07:00 91 | 92 | 93 | 94 | broker 95 | https://localhost/services/broker 96 | 2011-07-01T16:52:51-07:00 97 | 98 | 99 | 100 | clustering 101 | https://localhost/services/clustering 102 | 2011-07-01T16:52:51-07:00 103 | 104 | 105 | 106 | masterlm 107 | https://localhost/services/masterlm 108 | 2011-07-01T16:52:51-07:00 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /tests/system/test_cre_apps.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright © 2011-2025 Splunk, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"): you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | import json 18 | import pytest 19 | 20 | from tests import testlib 21 | from splunklib import results 22 | 23 | 24 | class TestJSONCustomRestEndpointsSpecialMethodHelpers(testlib.SDKTestCase): 25 | app_name = "cre_app" 26 | 27 | def test_GET(self): 28 | resp = self.service.get( 29 | app=self.app_name, 30 | path_segment="execute", 31 | headers=[("x-bar", "baz")], 32 | ) 33 | self.assertIn(("x-foo", "bar"), resp.headers) 34 | self.assertEqual(resp.status, 200) 35 | self.assertEqual( 36 | json.loads(str(resp.body)), 37 | { 38 | "headers": {"x-bar": "baz"}, 39 | "method": "GET", 40 | }, 41 | ) 42 | 43 | def test_POST(self): 44 | body = json.dumps({"foo": "bar"}) 45 | resp = self.service.post( 46 | app=self.app_name, 47 | path_segment="execute", 48 | body=body, 49 | headers=[("x-bar", "baz")], 50 | ) 51 | self.assertIn(("x-foo", "bar"), resp.headers) 52 | self.assertEqual(resp.status, 200) 53 | self.assertEqual( 54 | json.loads(str(resp.body)), 55 | { 56 | "payload": '{"foo": "bar"}', 57 | "headers": {"x-bar": "baz"}, 58 | "method": "POST", 59 | }, 60 | ) 61 | 62 | def test_DELETE(self): 63 | # delete does allow specifying body and custom headers. 64 | resp = self.service.delete( 65 | app=self.app_name, 66 | path_segment="execute", 67 | ) 68 | self.assertIn(("x-foo", "bar"), resp.headers) 69 | self.assertEqual(resp.status, 200) 70 | self.assertEqual( 71 | json.loads(str(resp.body)), 72 | { 73 | "payload": "", 74 | "headers": {}, 75 | "method": "DELETE", 76 | }, 77 | ) 78 | 79 | 80 | class TestJSONCustomRestEndpointGenericRequest(testlib.SDKTestCase): 81 | app_name = "cre_app" 82 | 83 | def test_no_str_body_GET(self): 84 | def with_body(): 85 | self.service.request( 86 | app=self.app_name, method="GET", path_segment="execute", body="str" 87 | ) 88 | 89 | self.assertRaisesRegex( 90 | Exception, "Unable to set body on GET request", with_body 91 | ) 92 | 93 | def test_GET(self): 94 | resp = self.service.request( 95 | app=self.app_name, 96 | method="GET", 97 | path_segment="execute", 98 | headers=[("x-bar", "baz")], 99 | ) 100 | self.assertIn(("x-foo", "bar"), resp.headers) 101 | self.assertEqual(resp.status, 200) 102 | self.assertEqual( 103 | json.loads(str(resp.body)), 104 | { 105 | "headers": {"x-bar": "baz"}, 106 | "method": "GET", 107 | }, 108 | ) 109 | 110 | def test_POST(self): 111 | self.method("POST") 112 | 113 | def test_PUT(self): 114 | self.method("PUT") 115 | 116 | def test_PATCH(self): 117 | if self.service.splunk_version[0] < 10: 118 | self.skipTest("PATCH custom REST endpoints not supported on splunk < 10") 119 | self.method("PATCH") 120 | 121 | def test_DELETE(self): 122 | self.method("DELETE") 123 | 124 | def method(self, method: str): 125 | body = json.dumps({"foo": "bar"}) 126 | resp = self.service.request( 127 | app=self.app_name, 128 | method=method, 129 | path_segment="execute", 130 | body=body, 131 | headers=[("x-bar", "baz")], 132 | ) 133 | self.assertIn(("x-foo", "bar"), resp.headers) 134 | self.assertEqual(resp.status, 200) 135 | self.assertEqual( 136 | json.loads(str(resp.body)), 137 | { 138 | "payload": '{"foo": "bar"}', 139 | "headers": {"x-bar": "baz"}, 140 | "method": method, 141 | }, 142 | ) 143 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to the API reference for the Splunk SDK for Python, which describes the modules that are included in the SDK. 2 | For more information, see the `Splunk Developer Portal `_. 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | :name: SDK for Python API Reference 7 | 8 | binding 9 | client 10 | data 11 | results 12 | modularinput 13 | searchcommands 14 | searchcommandsvalidators 15 | 16 | 17 | :doc:`binding` 18 | -------------- 19 | 20 | :func:`~splunklib.binding.connect` function 21 | 22 | :func:`~splunklib.binding.namespace` function 23 | 24 | :class:`~splunklib.binding.Context` class 25 | 26 | :class:`~splunklib.binding.ResponseReader` class 27 | 28 | 29 | **Exceptions** 30 | 31 | :func:`~splunklib.binding.handler` function 32 | 33 | :class:`~splunklib.binding.AuthenticationError` class 34 | 35 | **Custom HTTP handler** 36 | 37 | :class:`~splunklib.binding.HTTPError` class 38 | 39 | :class:`~splunklib.binding.HttpLib` class 40 | 41 | 42 | :doc:`client` 43 | ------------- 44 | 45 | :func:`~splunklib.client.connect` function 46 | 47 | :class:`~splunklib.client.Service` class 48 | 49 | :class:`~splunklib.client.Endpoint` base class 50 | 51 | 52 | **Entities and collections** 53 | 54 | :class:`~splunklib.client.Entity` class 55 | 56 | :class:`~splunklib.client.Collection` class 57 | 58 | :class:`~splunklib.client.ReadOnlyCollection` class 59 | 60 | :class:`~splunklib.client.Application` class 61 | 62 | :class:`~splunklib.client.AlertGroup` class 63 | 64 | :class:`~splunklib.client.ConfigurationFile` class 65 | 66 | :class:`~splunklib.client.Stanza` class 67 | 68 | :class:`~splunklib.client.Configurations` class 69 | 70 | :class:`~splunklib.client.Index` class 71 | 72 | :class:`~splunklib.client.Indexes` class 73 | 74 | :class:`~splunklib.client.Input` class 75 | 76 | :class:`~splunklib.client.Inputs` class 77 | 78 | :class:`~splunklib.client.Job` class 79 | 80 | :class:`~splunklib.client.Jobs` class 81 | 82 | :class:`~splunklib.client.KVStoreCollection` class 83 | 84 | :class:`~splunklib.client.KVStoreCollectionData` class 85 | 86 | :class:`~splunklib.client.KVStoreCollections` class 87 | 88 | :class:`~splunklib.client.Loggers` class 89 | 90 | :class:`~splunklib.client.Message` class 91 | 92 | :class:`~splunklib.client.ModularInputKind` class 93 | 94 | :class:`~splunklib.client.Role` class 95 | 96 | :class:`~splunklib.client.Roles` class 97 | 98 | :class:`~splunklib.client.SavedSearch` class 99 | 100 | :class:`~splunklib.client.SavedSearches` class 101 | 102 | :class:`~splunklib.client.Settings` class 103 | 104 | :class:`~splunklib.client.StoragePassword` class 105 | 106 | :class:`~splunklib.client.StoragePasswords` class 107 | 108 | :class:`~splunklib.client.User` class 109 | 110 | :class:`~splunklib.client.Users` class 111 | 112 | 113 | **Exceptions** 114 | 115 | :class:`~splunklib.client.AmbiguousReferenceException` class 116 | 117 | :class:`~splunklib.client.IllegalOperationException` class 118 | 119 | :class:`~splunklib.client.IncomparableException` class 120 | 121 | :class:`~splunklib.client.InvalidNameException` class 122 | 123 | :class:`~splunklib.client.NoSuchCapability` class 124 | 125 | :class:`~splunklib.client.NotSupportedError` class 126 | 127 | :class:`~splunklib.client.OperationError` class 128 | 129 | 130 | :doc:`data` 131 | ----------- 132 | 133 | :func:`~splunklib.data.load` function 134 | 135 | :func:`~splunklib.data.record` function 136 | 137 | :class:`~splunklib.data.Record` class 138 | 139 | :doc:`results` 140 | -------------- 141 | 142 | :class:`~splunklib.results.ResultsReader` class 143 | 144 | :class:`~splunklib.results.Message` class 145 | 146 | :doc:`modularinput` 147 | ------------------- 148 | 149 | :class:`~splunklib.modularinput.Argument` class 150 | 151 | :class:`~splunklib.modularinput.Event` class 152 | 153 | :class:`~splunklib.modularinput.EventWriter` class 154 | 155 | :class:`~splunklib.modularinput.InputDefinition` class 156 | 157 | :class:`~splunklib.modularinput.Scheme` class 158 | 159 | :class:`~splunklib.modularinput.Script` class 160 | 161 | :class:`~splunklib.modularinput.ValidationDefinition` class 162 | 163 | :doc:`searchcommands` 164 | --------------------- 165 | 166 | :class:`~splunklib.searchcommands.EventingCommand` class 167 | 168 | :class:`~splunklib.searchcommands.GeneratingCommand` class 169 | 170 | :class:`~splunklib.searchcommands.ReportingCommand` class 171 | 172 | :class:`~splunklib.searchcommands.StreamingCommand` class 173 | 174 | :class:`~splunklib.searchcommands.Option` class 175 | -------------------------------------------------------------------------------- /utils/cmdopts.py: -------------------------------------------------------------------------------- 1 | # Copyright © 2011-2024 Splunk, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | """Command line utilities shared by command line tools & unit tests.""" 16 | 17 | from os import path 18 | from optparse import OptionParser 19 | import sys 20 | from dotenv import dotenv_values 21 | 22 | __all__ = ["error", "Parser", "cmdline"] 23 | 24 | 25 | # Print the given message to stderr, and optionally exit 26 | def error(message, exitcode=None): 27 | print(f"Error: {message}", file=sys.stderr) 28 | if exitcode is not None: 29 | sys.exit(exitcode) 30 | 31 | 32 | class record(dict): 33 | def __getattr__(self, name): 34 | try: 35 | return self[name] 36 | except KeyError: 37 | raise AttributeError(name) 38 | 39 | def __setattr__(self, name, value): 40 | self[name] = value 41 | 42 | 43 | class Parser(OptionParser): 44 | def __init__(self, rules=None, **kwargs): 45 | OptionParser.__init__(self, **kwargs) 46 | self.dests = set({}) 47 | self.result = record({"args": [], "kwargs": record()}) 48 | if rules is not None: 49 | self.init(rules) 50 | 51 | def init(self, rules): 52 | """Initialize the parser with the given command rules.""" 53 | # Initialize the option parser 54 | for dest in rules.keys(): 55 | rule = rules[dest] 56 | 57 | # Assign defaults ourselves here, instead of in the option parser 58 | # itself in order to allow for multiple calls to parse (dont want 59 | # subsequent calls to override previous values with default vals). 60 | if "default" in rule: 61 | self.result["kwargs"][dest] = rule["default"] 62 | 63 | flags = rule["flags"] 64 | kwargs = {"action": rule.get("action", "store")} 65 | # NOTE: Don't provision the parser with defaults here, per above. 66 | for key in ["callback", "help", "metavar", "type"]: 67 | if key in rule: 68 | kwargs[key] = rule[key] 69 | self.add_option(*flags, dest=dest, **kwargs) 70 | 71 | # Remember the dest vars that we see, so that we can merge results 72 | self.dests.add(dest) 73 | 74 | # Load command options from '.env' file 75 | def load(self, filepath): 76 | argv = [] 77 | try: 78 | filedata = dotenv_values(filepath) 79 | except: 80 | error("Unable to open '%s'" % filepath, 2) 81 | 82 | # update result kwargs value with .env file data 83 | for key, value in filedata.items(): 84 | value = value.strip() 85 | if len(value) == 0 or value is None: 86 | continue # Skip blank value 87 | elif key in self.dests: 88 | self.result["kwargs"][key] = value 89 | else: 90 | raise NameError("No such option --" + key) 91 | 92 | return self 93 | 94 | def loadif(self, filepath): 95 | """Load the given filepath if it exists, otherwise ignore.""" 96 | if path.isfile(filepath): 97 | self.load(filepath) 98 | return self 99 | 100 | def loadenv(self, filename): 101 | dir_path = path.dirname(path.realpath(__file__)) 102 | filepath = path.join(dir_path, "..", filename) 103 | self.loadif(filepath) 104 | return self 105 | 106 | def parse(self, argv): 107 | """Parse the given argument vector.""" 108 | kwargs, args = self.parse_args(argv) 109 | self.result["args"] += args 110 | # Annoying that parse_args doesn't just return a dict 111 | for dest in self.dests: 112 | value = getattr(kwargs, dest) 113 | if value is not None: 114 | self.result["kwargs"][dest] = value 115 | return self 116 | 117 | def format_epilog(self, formatter): 118 | return self.epilog or "" 119 | 120 | 121 | def cmdline(argv, rules=None, config=None, **kwargs): 122 | """Simplified cmdopts interface that does not default any parsing rules 123 | and that does not allow compounding calls to the parser.""" 124 | parser = Parser(rules, **kwargs) 125 | if config is not None: 126 | parser.loadenv(config) 127 | return parser.parse(argv).result 128 | -------------------------------------------------------------------------------- /splunklib/modularinput/event.py: -------------------------------------------------------------------------------- 1 | # Copyright © 2011-2024 Splunk, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | from io import TextIOBase 16 | import xml.etree.ElementTree as ET 17 | 18 | from ..utils import ensure_str 19 | 20 | 21 | class Event: 22 | """Represents an event or fragment of an event to be written by this modular input to Splunk. 23 | 24 | To write an input to a stream, call the ``write_to`` function, passing in a stream. 25 | """ 26 | 27 | def __init__( 28 | self, 29 | data=None, 30 | stanza=None, 31 | time=None, 32 | host=None, 33 | index=None, 34 | source=None, 35 | sourcetype=None, 36 | done=True, 37 | unbroken=True, 38 | ): 39 | """There are no required parameters for constructing an Event 40 | 41 | **Example with minimal configuration**:: 42 | 43 | my_event = Event( 44 | data="This is a test of my new event.", 45 | stanza="myStanzaName", 46 | time="%.3f" % 1372187084.000 47 | ) 48 | 49 | **Example with full configuration**:: 50 | 51 | excellent_event = Event( 52 | data="This is a test of my excellent event.", 53 | stanza="excellenceOnly", 54 | time="%.3f" % 1372274622.493, 55 | host="localhost", 56 | index="main", 57 | source="Splunk", 58 | sourcetype="misc", 59 | done=True, 60 | unbroken=True 61 | ) 62 | 63 | :param data: ``string``, the event's text. 64 | :param stanza: ``string``, name of the input this event should be sent to. 65 | :param time: ``float``, time in seconds, including up to 3 decimal places to represent milliseconds. 66 | :param host: ``string``, the event's host, ex: localhost. 67 | :param index: ``string``, the index this event is specified to write to, or None if default index. 68 | :param source: ``string``, the source of this event, or None to have Splunk guess. 69 | :param sourcetype: ``string``, source type currently set on this event, or None to have Splunk guess. 70 | :param done: ``boolean``, is this a complete ``Event``? False if an ``Event`` fragment. 71 | :param unbroken: ``boolean``, Is this event completely encapsulated in this ``Event`` object? 72 | """ 73 | self.data = data 74 | self.done = done 75 | self.host = host 76 | self.index = index 77 | self.source = source 78 | self.sourceType = sourcetype 79 | self.stanza = stanza 80 | self.time = time 81 | self.unbroken = unbroken 82 | 83 | def write_to(self, stream): 84 | """Write an XML representation of self, an ``Event`` object, to the given stream. 85 | 86 | The ``Event`` object will only be written if its data field is defined, 87 | otherwise a ``ValueError`` is raised. 88 | 89 | :param stream: stream to write XML to. 90 | """ 91 | if self.data is None: 92 | raise ValueError( 93 | "Events must have at least the data field set to be written to XML." 94 | ) 95 | 96 | event = ET.Element("event") 97 | if self.stanza is not None: 98 | event.set("stanza", self.stanza) 99 | event.set("unbroken", str(int(self.unbroken))) 100 | 101 | # if a time isn't set, let Splunk guess by not creating a