Skip to content

Commit

Permalink
Merged pull request from Steven (malvidin on github)
Browse files Browse the repository at this point in the history
This new version includes:
- New base58 decode function (b58)
- Updated python SDK to 1.7.2
  • Loading branch information
gjanders committed Oct 9, 2022
1 parent b3f23c7 commit dd1ae78
Show file tree
Hide file tree
Showing 7 changed files with 137 additions and 20 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ Decodes a Base64 encoded string.
`b32()`
Decodes a Base32 encoded string.

`b58()`
Decodes a Base58 encoded string.

`rotx(count)`
Implements Caesarian shift. The count argument specifies the amount to shift and must be an integer.

Expand Down Expand Up @@ -207,6 +210,13 @@ Shannon Davis (Splunk)
Steven (malvidin on github)

# Release Notes
## 2.3.11
Merged pull request from Steven (malvidin on github)

This new version includes:
- New base58 decode function (b58)
- Updated python SDK to 1.7.2

## 2.3.10
Merged pull request from Steven (malvidin on github)

Expand Down
2 changes: 1 addition & 1 deletion app.manifest
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"id": {
"group": null,
"name": "decrypt2",
"version": "2.3.10"
"version": "2.3.11"
},
"author": [
{
Expand Down
2 changes: 1 addition & 1 deletion bin/lib/splunklib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,5 @@ def setup_logging(level, log_format=DEFAULT_LOG_FORMAT, date_format=DEFAULT_DATE
format=log_format,
datefmt=date_format)

__version_info__ = (1, 6, 20)
__version_info__ = (1, 7, 2)
__version__ = ".".join(map(str, __version_info__))
14 changes: 9 additions & 5 deletions bin/lib/splunklib/binding.py
Original file line number Diff line number Diff line change
Expand Up @@ -526,23 +526,27 @@ def _auth_headers(self):
:returns: A list of 2-tuples containing key and value
"""
header = []
if self.has_cookies():
return [("Cookie", _make_cookie_header(list(self.get_cookies().items())))]
elif self.basic and (self.username and self.password):
token = 'Basic %s' % b64encode(("%s:%s" % (self.username, self.password)).encode('utf-8')).decode('ascii')
return [("Authorization", token)]
elif self.bearerToken:
token = 'Bearer %s' % self.bearerToken
return [("Authorization", token)]
elif self.token is _NoAuthenticationToken:
return []
token = []
else:
# Ensure the token is properly formatted
if self.token.startswith('Splunk '):
token = self.token
else:
token = 'Splunk %s' % self.token
return [("Authorization", token)]
if token:
header.append(("Authorization", token))
if self.get_cookies():
header.append(("Cookie", _make_cookie_header(list(self.get_cookies().items()))))

return header

def connect(self):
"""Returns an open connection (socket) to the Splunk instance.
Expand Down Expand Up @@ -1430,7 +1434,7 @@ def request(url, message, **kwargs):
head = {
"Content-Length": str(len(body)),
"Host": host,
"User-Agent": "splunk-sdk-python/1.6.20",
"User-Agent": "splunk-sdk-python/1.7.2",
"Accept": "*/*",
"Connection": "Close",
} # defaults
Expand Down
125 changes: 114 additions & 11 deletions bin/lib/splunklib/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
import datetime
import json
import logging
import re
import socket
from datetime import datetime, timedelta
from time import sleep
Expand Down Expand Up @@ -99,6 +100,7 @@
PATH_INDEXES = "data/indexes/"
PATH_INPUTS = "data/inputs/"
PATH_JOBS = "search/jobs/"
PATH_JOBS_V2 = "search/v2/jobs/"
PATH_LOGGER = "/services/server/logger/"
PATH_MESSAGES = "messages/"
PATH_MODULAR_INPUTS = "data/modular-inputs"
Expand Down Expand Up @@ -419,6 +421,7 @@ def __init__(self, **kwargs):
super(Service, self).__init__(**kwargs)
self._splunk_version = None
self._kvstore_owner = None
self._instance_type = None

@property
def apps(self):
Expand Down Expand Up @@ -570,6 +573,8 @@ def parse(self, query, **kwargs):
:type kwargs: ``dict``
:return: A semantic map of the parsed search query.
"""
if not self.disable_v2_api:
return self.post("search/v2/parser", q=query, **kwargs)
return self.get("search/parser", q=query, **kwargs)

def restart(self, timeout=None):
Expand Down Expand Up @@ -691,6 +696,22 @@ def splunk_version(self):
self._splunk_version = tuple([int(p) for p in self.info['version'].split('.')])
return self._splunk_version

@property
def splunk_instance(self):
if self._instance_type is None :
splunk_info = self.info;
if hasattr(splunk_info, 'instance_type') :
self._instance_type = splunk_info['instance_type']
else:
self._instance_type = ''
return self._instance_type

@property
def disable_v2_api(self):
if self.splunk_instance.lower() == 'cloud':
return self.splunk_version < (9,0,2209)
return self.splunk_version < (9,0,2)

@property
def kvstore_owner(self):
"""Returns the KVStore owner for this instance of Splunk.
Expand Down Expand Up @@ -741,6 +762,25 @@ def __init__(self, service, path):
self.service = service
self.path = path

def get_api_version(self, path):
"""Return the API version of the service used in the provided path.
Args:
path (str): A fully-qualified endpoint path (for example, "/services/search/jobs").
Returns:
int: Version of the API (for example, 1)
"""
# Default to v1 if undefined in the path
# For example, "/services/search/jobs" is using API v1
api_version = 1

versionSearch = re.search('(?:servicesNS\/[^/]+\/[^/]+|services)\/[^/]+\/v(\d+)\/', path)
if versionSearch:
api_version = int(versionSearch.group(1))

return api_version

def get(self, path_segment="", owner=None, app=None, sharing=None, **query):
"""Performs a GET operation on the path segment relative to this endpoint.
Expand Down Expand Up @@ -803,6 +843,22 @@ def get(self, path_segment="", owner=None, app=None, sharing=None, **query):
app=app, sharing=sharing)
# ^-- This was "%s%s" % (self.path, path_segment).
# That doesn't work, because self.path may be UrlEncoded.

# Get the API version from the path
api_version = self.get_api_version(path)

# Search API v2+ fallback to v1:
# - In v2+, /results_preview, /events and /results do not support search params.
# - Fallback from v2+ to v1 if Splunk Version is < 9.
# if api_version >= 2 and ('search' in query and path.endswith(tuple(["results_preview", "events", "results"])) or self.service.splunk_version < (9,)):
# path = path.replace(PATH_JOBS_V2, PATH_JOBS)

if api_version == 1:
if isinstance(path, UrlEncoded):
path = UrlEncoded(path.replace(PATH_JOBS_V2, PATH_JOBS), skip_encode=True)
else:
path = path.replace(PATH_JOBS_V2, PATH_JOBS)

return self.service.get(path,
owner=owner, app=app, sharing=sharing,
**query)
Expand Down Expand Up @@ -855,13 +911,29 @@ def post(self, path_segment="", owner=None, app=None, sharing=None, **query):
apps.get('nonexistant/path') # raises HTTPError
s.logout()
apps.get() # raises AuthenticationError
"""
"""
if path_segment.startswith('/'):
path = path_segment
else:
if not self.path.endswith('/') and path_segment != "":
self.path = self.path + '/'
path = self.service._abspath(self.path + path_segment, owner=owner, app=app, sharing=sharing)

# Get the API version from the path
api_version = self.get_api_version(path)

# Search API v2+ fallback to v1:
# - In v2+, /results_preview, /events and /results do not support search params.
# - Fallback from v2+ to v1 if Splunk Version is < 9.
# if api_version >= 2 and ('search' in query and path.endswith(tuple(["results_preview", "events", "results"])) or self.service.splunk_version < (9,)):
# path = path.replace(PATH_JOBS_V2, PATH_JOBS)

if api_version == 1:
if isinstance(path, UrlEncoded):
path = UrlEncoded(path.replace(PATH_JOBS_V2, PATH_JOBS), skip_encode=True)
else:
path = path.replace(PATH_JOBS_V2, PATH_JOBS)

return self.service.post(path, owner=owner, app=app, sharing=sharing, **query)


Expand Down Expand Up @@ -1846,8 +1918,6 @@ class StoragePasswords(Collection):
instance. Retrieve this collection using :meth:`Service.storage_passwords`.
"""
def __init__(self, service):
if service.namespace.owner == '-' or service.namespace.app == '-':
raise ValueError("StoragePasswords cannot have wildcards in namespace.")
super(StoragePasswords, self).__init__(service, PATH_STORAGE_PASSWORDS, item=StoragePassword)

def create(self, password, username, realm=None):
Expand All @@ -1865,6 +1935,9 @@ def create(self, password, username, realm=None):
:return: The :class:`StoragePassword` object created.
"""
if self.service.namespace.owner == '-' or self.service.namespace.app == '-':
raise ValueError("While creating StoragePasswords, namespace cannot have wildcards.")

if not isinstance(username, six.string_types):
raise ValueError("Invalid name: %s" % repr(username))

Expand Down Expand Up @@ -1896,6 +1969,9 @@ def delete(self, username, realm=None):
:return: The `StoragePassword` collection.
:rtype: ``self``
"""
if self.service.namespace.owner == '-' or self.service.namespace.app == '-':
raise ValueError("app context must be specified when removing a password.")

if realm is None:
# This case makes the username optional, so
# the full name can be passed in as realm.
Expand Down Expand Up @@ -2660,7 +2736,14 @@ def oneshot(self, path, **kwargs):
class Job(Entity):
"""This class represents a search job."""
def __init__(self, service, sid, **kwargs):
path = PATH_JOBS + sid
# Default to v2 in Splunk Version 9+
path = "{path}{sid}"
# Formatting path based on the Splunk Version
if service.disable_v2_api:
path = path.format(path=PATH_JOBS, sid=sid)
else:
path = path.format(path=PATH_JOBS_V2, sid=sid)

Entity.__init__(self, service, path, skip_refresh=True, **kwargs)
self.sid = sid

Expand Down Expand Up @@ -2714,7 +2797,11 @@ def events(self, **kwargs):
:return: The ``InputStream`` IO handle to this job's events.
"""
kwargs['segmentation'] = kwargs.get('segmentation', 'none')
return self.get("events", **kwargs).body

# Search API v1(GET) and v2(POST)
if self.service.disable_v2_api:
return self.get("events", **kwargs).body
return self.post("events", **kwargs).body

def finalize(self):
"""Stops the job and provides intermediate results for retrieval.
Expand Down Expand Up @@ -2802,7 +2889,11 @@ def results(self, **query_params):
:return: The ``InputStream`` IO handle to this job's results.
"""
query_params['segmentation'] = query_params.get('segmentation', 'none')
return self.get("results", **query_params).body

# Search API v1(GET) and v2(POST)
if self.service.disable_v2_api:
return self.get("results", **query_params).body
return self.post("results", **query_params).body

def preview(self, **query_params):
"""Returns a streaming handle to this job's preview search results.
Expand Down Expand Up @@ -2843,7 +2934,11 @@ def preview(self, **query_params):
:return: The ``InputStream`` IO handle to this job's preview results.
"""
query_params['segmentation'] = query_params.get('segmentation', 'none')
return self.get("results_preview", **query_params).body

# Search API v1(GET) and v2(POST)
if self.service.disable_v2_api:
return self.get("results_preview", **query_params).body
return self.post("results_preview", **query_params).body

def searchlog(self, **kwargs):
"""Returns a streaming handle to this job's search log.
Expand Down Expand Up @@ -2932,7 +3027,12 @@ class Jobs(Collection):
"""This class represents a collection of search jobs. Retrieve this
collection using :meth:`Service.jobs`."""
def __init__(self, service):
Collection.__init__(self, service, PATH_JOBS, item=Job)
# Splunk 9 introduces the v2 endpoint
if not service.disable_v2_api:
path = PATH_JOBS_V2
else:
path = PATH_JOBS
Collection.__init__(self, service, path, item=Job)
# The count value to say list all the contents of this
# Collection is 0, not -1 as it is on most.
self.null_count = 0
Expand Down Expand Up @@ -3208,12 +3308,15 @@ def fired_alerts(self):
item=AlertGroup)
return c

def history(self):
def history(self, **kwargs):
"""Returns a list of search jobs corresponding to this saved search.
:param `kwargs`: Additional arguments (optional).
:type kwargs: ``dict``
:return: A list of :class:`Job` objects.
"""
response = self.get("history")
response = self.get("history", **kwargs)
entries = _load_atom_entries(response)
if entries is None: return []
jobs = []
Expand Down Expand Up @@ -3770,4 +3873,4 @@ def batch_save(self, *documents):

data = json.dumps(documents)

return json.loads(self._post('batch_save', headers=KVStoreCollectionData.JSON_HEADER, body=data).body.read().decode('utf-8'))
return json.loads(self._post('batch_save', headers=KVStoreCollectionData.JSON_HEADER, body=data).body.read().decode('utf-8'))
2 changes: 1 addition & 1 deletion default/app.conf
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ is_visible = false
[launcher]
author = Gareth Anderson
description = A library of common routines to analyze malware and data exfiltration communications (based on the work of Michael Zalewski).
version = 2.3.10
version = 2.3.11

2 changes: 1 addition & 1 deletion default/searchbnf.conf
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,5 @@ usage = public
#tags = searchcommands_app
#
[decrypt-options]
syntax = field=<string> atob | b64 | btoa | b32 | hex | unhex | rol(<int>) | ror(<int>) | rotx('<string>') | xor('<string>') | rc4('<string>') | emit('<string>') | load('<string>') | save('<string>') | substr(<int>, <int>) | ascii | decode('<string>') | escape | unescape | tr('<string>', '<string>') | rev | find('<string>', <int>) | b32re | b64re
syntax = field=<string> atob | b64 | btoa | b32 | b58 | hex | unhex | rol(<int>) | ror(<int>) | rotx('<string>') | xor('<string>') | rc4('<string>') | emit('<string>') | load('<string>') | save('<string>') | substr(<int>, <int>) | ascii | decode('<string>') | escape | unescape | tr('<string>', '<string>') | rev | find('<string>', <int>) | b32re | b64re
description = Pass the field name to work with, then the command or command(s) to be used, an emit() option can be passed to choose the field to return, defaults to the field name "decrypted"

0 comments on commit dd1ae78

Please sign in to comment.