Skip to content

Commit dd1ae78

Browse files
committed
Merged pull request from Steven (malvidin on github)
This new version includes: - New base58 decode function (b58) - Updated python SDK to 1.7.2
1 parent b3f23c7 commit dd1ae78

File tree

7 files changed

+137
-20
lines changed

7 files changed

+137
-20
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ Decodes a Base64 encoded string.
6060
`b32()`
6161
Decodes a Base32 encoded string.
6262

63+
`b58()`
64+
Decodes a Base58 encoded string.
65+
6366
`rotx(count)`
6467
Implements Caesarian shift. The count argument specifies the amount to shift and must be an integer.
6568

@@ -207,6 +210,13 @@ Shannon Davis (Splunk)
207210
Steven (malvidin on github)
208211

209212
# Release Notes
213+
## 2.3.11
214+
Merged pull request from Steven (malvidin on github)
215+
216+
This new version includes:
217+
- New base58 decode function (b58)
218+
- Updated python SDK to 1.7.2
219+
210220
## 2.3.10
211221
Merged pull request from Steven (malvidin on github)
212222

app.manifest

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"id": {
66
"group": null,
77
"name": "decrypt2",
8-
"version": "2.3.10"
8+
"version": "2.3.11"
99
},
1010
"author": [
1111
{

bin/lib/splunklib/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,5 @@ def setup_logging(level, log_format=DEFAULT_LOG_FORMAT, date_format=DEFAULT_DATE
3131
format=log_format,
3232
datefmt=date_format)
3333

34-
__version_info__ = (1, 6, 20)
34+
__version_info__ = (1, 7, 2)
3535
__version__ = ".".join(map(str, __version_info__))

bin/lib/splunklib/binding.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -526,23 +526,27 @@ def _auth_headers(self):
526526
527527
:returns: A list of 2-tuples containing key and value
528528
"""
529+
header = []
529530
if self.has_cookies():
530531
return [("Cookie", _make_cookie_header(list(self.get_cookies().items())))]
531532
elif self.basic and (self.username and self.password):
532533
token = 'Basic %s' % b64encode(("%s:%s" % (self.username, self.password)).encode('utf-8')).decode('ascii')
533-
return [("Authorization", token)]
534534
elif self.bearerToken:
535535
token = 'Bearer %s' % self.bearerToken
536-
return [("Authorization", token)]
537536
elif self.token is _NoAuthenticationToken:
538-
return []
537+
token = []
539538
else:
540539
# Ensure the token is properly formatted
541540
if self.token.startswith('Splunk '):
542541
token = self.token
543542
else:
544543
token = 'Splunk %s' % self.token
545-
return [("Authorization", token)]
544+
if token:
545+
header.append(("Authorization", token))
546+
if self.get_cookies():
547+
header.append(("Cookie", _make_cookie_header(list(self.get_cookies().items()))))
548+
549+
return header
546550

547551
def connect(self):
548552
"""Returns an open connection (socket) to the Splunk instance.
@@ -1430,7 +1434,7 @@ def request(url, message, **kwargs):
14301434
head = {
14311435
"Content-Length": str(len(body)),
14321436
"Host": host,
1433-
"User-Agent": "splunk-sdk-python/1.6.20",
1437+
"User-Agent": "splunk-sdk-python/1.7.2",
14341438
"Accept": "*/*",
14351439
"Connection": "Close",
14361440
} # defaults

bin/lib/splunklib/client.py

Lines changed: 114 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
import datetime
6363
import json
6464
import logging
65+
import re
6566
import socket
6667
from datetime import datetime, timedelta
6768
from time import sleep
@@ -99,6 +100,7 @@
99100
PATH_INDEXES = "data/indexes/"
100101
PATH_INPUTS = "data/inputs/"
101102
PATH_JOBS = "search/jobs/"
103+
PATH_JOBS_V2 = "search/v2/jobs/"
102104
PATH_LOGGER = "/services/server/logger/"
103105
PATH_MESSAGES = "messages/"
104106
PATH_MODULAR_INPUTS = "data/modular-inputs"
@@ -419,6 +421,7 @@ def __init__(self, **kwargs):
419421
super(Service, self).__init__(**kwargs)
420422
self._splunk_version = None
421423
self._kvstore_owner = None
424+
self._instance_type = None
422425

423426
@property
424427
def apps(self):
@@ -570,6 +573,8 @@ def parse(self, query, **kwargs):
570573
:type kwargs: ``dict``
571574
:return: A semantic map of the parsed search query.
572575
"""
576+
if not self.disable_v2_api:
577+
return self.post("search/v2/parser", q=query, **kwargs)
573578
return self.get("search/parser", q=query, **kwargs)
574579

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

699+
@property
700+
def splunk_instance(self):
701+
if self._instance_type is None :
702+
splunk_info = self.info;
703+
if hasattr(splunk_info, 'instance_type') :
704+
self._instance_type = splunk_info['instance_type']
705+
else:
706+
self._instance_type = ''
707+
return self._instance_type
708+
709+
@property
710+
def disable_v2_api(self):
711+
if self.splunk_instance.lower() == 'cloud':
712+
return self.splunk_version < (9,0,2209)
713+
return self.splunk_version < (9,0,2)
714+
694715
@property
695716
def kvstore_owner(self):
696717
"""Returns the KVStore owner for this instance of Splunk.
@@ -741,6 +762,25 @@ def __init__(self, service, path):
741762
self.service = service
742763
self.path = path
743764

765+
def get_api_version(self, path):
766+
"""Return the API version of the service used in the provided path.
767+
768+
Args:
769+
path (str): A fully-qualified endpoint path (for example, "/services/search/jobs").
770+
771+
Returns:
772+
int: Version of the API (for example, 1)
773+
"""
774+
# Default to v1 if undefined in the path
775+
# For example, "/services/search/jobs" is using API v1
776+
api_version = 1
777+
778+
versionSearch = re.search('(?:servicesNS\/[^/]+\/[^/]+|services)\/[^/]+\/v(\d+)\/', path)
779+
if versionSearch:
780+
api_version = int(versionSearch.group(1))
781+
782+
return api_version
783+
744784
def get(self, path_segment="", owner=None, app=None, sharing=None, **query):
745785
"""Performs a GET operation on the path segment relative to this endpoint.
746786
@@ -803,6 +843,22 @@ def get(self, path_segment="", owner=None, app=None, sharing=None, **query):
803843
app=app, sharing=sharing)
804844
# ^-- This was "%s%s" % (self.path, path_segment).
805845
# That doesn't work, because self.path may be UrlEncoded.
846+
847+
# Get the API version from the path
848+
api_version = self.get_api_version(path)
849+
850+
# Search API v2+ fallback to v1:
851+
# - In v2+, /results_preview, /events and /results do not support search params.
852+
# - Fallback from v2+ to v1 if Splunk Version is < 9.
853+
# if api_version >= 2 and ('search' in query and path.endswith(tuple(["results_preview", "events", "results"])) or self.service.splunk_version < (9,)):
854+
# path = path.replace(PATH_JOBS_V2, PATH_JOBS)
855+
856+
if api_version == 1:
857+
if isinstance(path, UrlEncoded):
858+
path = UrlEncoded(path.replace(PATH_JOBS_V2, PATH_JOBS), skip_encode=True)
859+
else:
860+
path = path.replace(PATH_JOBS_V2, PATH_JOBS)
861+
806862
return self.service.get(path,
807863
owner=owner, app=app, sharing=sharing,
808864
**query)
@@ -855,13 +911,29 @@ def post(self, path_segment="", owner=None, app=None, sharing=None, **query):
855911
apps.get('nonexistant/path') # raises HTTPError
856912
s.logout()
857913
apps.get() # raises AuthenticationError
858-
"""
914+
"""
859915
if path_segment.startswith('/'):
860916
path = path_segment
861917
else:
862918
if not self.path.endswith('/') and path_segment != "":
863919
self.path = self.path + '/'
864920
path = self.service._abspath(self.path + path_segment, owner=owner, app=app, sharing=sharing)
921+
922+
# Get the API version from the path
923+
api_version = self.get_api_version(path)
924+
925+
# Search API v2+ fallback to v1:
926+
# - In v2+, /results_preview, /events and /results do not support search params.
927+
# - Fallback from v2+ to v1 if Splunk Version is < 9.
928+
# if api_version >= 2 and ('search' in query and path.endswith(tuple(["results_preview", "events", "results"])) or self.service.splunk_version < (9,)):
929+
# path = path.replace(PATH_JOBS_V2, PATH_JOBS)
930+
931+
if api_version == 1:
932+
if isinstance(path, UrlEncoded):
933+
path = UrlEncoded(path.replace(PATH_JOBS_V2, PATH_JOBS), skip_encode=True)
934+
else:
935+
path = path.replace(PATH_JOBS_V2, PATH_JOBS)
936+
865937
return self.service.post(path, owner=owner, app=app, sharing=sharing, **query)
866938

867939

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

18531923
def create(self, password, username, realm=None):
@@ -1865,6 +1935,9 @@ def create(self, password, username, realm=None):
18651935
18661936
:return: The :class:`StoragePassword` object created.
18671937
"""
1938+
if self.service.namespace.owner == '-' or self.service.namespace.app == '-':
1939+
raise ValueError("While creating StoragePasswords, namespace cannot have wildcards.")
1940+
18681941
if not isinstance(username, six.string_types):
18691942
raise ValueError("Invalid name: %s" % repr(username))
18701943

@@ -1896,6 +1969,9 @@ def delete(self, username, realm=None):
18961969
:return: The `StoragePassword` collection.
18971970
:rtype: ``self``
18981971
"""
1972+
if self.service.namespace.owner == '-' or self.service.namespace.app == '-':
1973+
raise ValueError("app context must be specified when removing a password.")
1974+
18991975
if realm is None:
19001976
# This case makes the username optional, so
19011977
# the full name can be passed in as realm.
@@ -2660,7 +2736,14 @@ def oneshot(self, path, **kwargs):
26602736
class Job(Entity):
26612737
"""This class represents a search job."""
26622738
def __init__(self, service, sid, **kwargs):
2663-
path = PATH_JOBS + sid
2739+
# Default to v2 in Splunk Version 9+
2740+
path = "{path}{sid}"
2741+
# Formatting path based on the Splunk Version
2742+
if service.disable_v2_api:
2743+
path = path.format(path=PATH_JOBS, sid=sid)
2744+
else:
2745+
path = path.format(path=PATH_JOBS_V2, sid=sid)
2746+
26642747
Entity.__init__(self, service, path, skip_refresh=True, **kwargs)
26652748
self.sid = sid
26662749

@@ -2714,7 +2797,11 @@ def events(self, **kwargs):
27142797
:return: The ``InputStream`` IO handle to this job's events.
27152798
"""
27162799
kwargs['segmentation'] = kwargs.get('segmentation', 'none')
2717-
return self.get("events", **kwargs).body
2800+
2801+
# Search API v1(GET) and v2(POST)
2802+
if self.service.disable_v2_api:
2803+
return self.get("events", **kwargs).body
2804+
return self.post("events", **kwargs).body
27182805

27192806
def finalize(self):
27202807
"""Stops the job and provides intermediate results for retrieval.
@@ -2802,7 +2889,11 @@ def results(self, **query_params):
28022889
:return: The ``InputStream`` IO handle to this job's results.
28032890
"""
28042891
query_params['segmentation'] = query_params.get('segmentation', 'none')
2805-
return self.get("results", **query_params).body
2892+
2893+
# Search API v1(GET) and v2(POST)
2894+
if self.service.disable_v2_api:
2895+
return self.get("results", **query_params).body
2896+
return self.post("results", **query_params).body
28062897

28072898
def preview(self, **query_params):
28082899
"""Returns a streaming handle to this job's preview search results.
@@ -2843,7 +2934,11 @@ def preview(self, **query_params):
28432934
:return: The ``InputStream`` IO handle to this job's preview results.
28442935
"""
28452936
query_params['segmentation'] = query_params.get('segmentation', 'none')
2846-
return self.get("results_preview", **query_params).body
2937+
2938+
# Search API v1(GET) and v2(POST)
2939+
if self.service.disable_v2_api:
2940+
return self.get("results_preview", **query_params).body
2941+
return self.post("results_preview", **query_params).body
28472942

28482943
def searchlog(self, **kwargs):
28492944
"""Returns a streaming handle to this job's search log.
@@ -2932,7 +3027,12 @@ class Jobs(Collection):
29323027
"""This class represents a collection of search jobs. Retrieve this
29333028
collection using :meth:`Service.jobs`."""
29343029
def __init__(self, service):
2935-
Collection.__init__(self, service, PATH_JOBS, item=Job)
3030+
# Splunk 9 introduces the v2 endpoint
3031+
if not service.disable_v2_api:
3032+
path = PATH_JOBS_V2
3033+
else:
3034+
path = PATH_JOBS
3035+
Collection.__init__(self, service, path, item=Job)
29363036
# The count value to say list all the contents of this
29373037
# Collection is 0, not -1 as it is on most.
29383038
self.null_count = 0
@@ -3208,12 +3308,15 @@ def fired_alerts(self):
32083308
item=AlertGroup)
32093309
return c
32103310

3211-
def history(self):
3311+
def history(self, **kwargs):
32123312
"""Returns a list of search jobs corresponding to this saved search.
32133313
3314+
:param `kwargs`: Additional arguments (optional).
3315+
:type kwargs: ``dict``
3316+
32143317
:return: A list of :class:`Job` objects.
32153318
"""
3216-
response = self.get("history")
3319+
response = self.get("history", **kwargs)
32173320
entries = _load_atom_entries(response)
32183321
if entries is None: return []
32193322
jobs = []
@@ -3770,4 +3873,4 @@ def batch_save(self, *documents):
37703873

37713874
data = json.dumps(documents)
37723875

3773-
return json.loads(self._post('batch_save', headers=KVStoreCollectionData.JSON_HEADER, body=data).body.read().decode('utf-8'))
3876+
return json.loads(self._post('batch_save', headers=KVStoreCollectionData.JSON_HEADER, body=data).body.read().decode('utf-8'))

default/app.conf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,5 @@ is_visible = false
1212
[launcher]
1313
author = Gareth Anderson
1414
description = A library of common routines to analyze malware and data exfiltration communications (based on the work of Michael Zalewski).
15-
version = 2.3.10
15+
version = 2.3.11
1616

default/searchbnf.conf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,5 @@ usage = public
4343
#tags = searchcommands_app
4444
#
4545
[decrypt-options]
46-
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
46+
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
4747
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 commit comments

Comments
 (0)