forked from IntegralDefense/splunklib
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsplunk.py
executable file
·326 lines (268 loc) · 12 KB
/
splunk.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
#!/usr/bin/python3
# vim: sw=4:ts=4:et
import datetime
import argparse
import splunklib
import logging
import os.path
import stat
import sys
import csv
import json
import re
from configparser import SafeConfigParser
from getpass import getpass
# Remove any proxy environment variables.
os.environ['http_proxy'] = ''
os.environ['https_proxy'] = ''
# encryption support
def encrypt_password(password):
from Crypto.Cipher import ARC4
from base64 import b64encode
memorized_password = getpass("Enter encryption password: ")
memorized_password_check = getpass("Re-enter encryption password: ")
if memorized_password != memorized_password_check:
logging.fatal("passwords do not match")
sys.exit(1)
cipher = ARC4.new(memorized_password)
return b64encode(cipher.encrypt(password))
def decrypt_password(encrypted_password):
from Crypto.Cipher import ARC4
from base64 import b64decode
memorized_password = getpass("Enter encryption password: ")
cipher = ARC4.new(memorized_password)
return cipher.decrypt(b64decode(encrypted_password))
parser = argparse.ArgumentParser()
parser.add_argument('search', nargs=argparse.REMAINDER)
parser.add_argument('-c', '--config', required=False, default=None, dest='config_path',
help="Path to optional configuration file. Defaults to ~/.splunklib.ini")
parser.add_argument('--ignore-config', required=False, default=False, action='store_true', dest='ignore_config',
help="Ignore any configuration files.")
parser.add_argument('-v' , '--verbose', required=False, action='store_true', default=False, dest='verbose',
help="Log verbose messages. Helps when debugging searches.")
parser.add_argument('-q' , '--quiet', required=False, action='store_true', default=False, dest='quiet',
help="Only log error messages.")
parser.add_argument('-U', '--uri', required=False, default=None, dest='uri',
help="The splunk URI to connect to.")
parser.add_argument('-u', '--user', required=False, default=None, dest='username',
help="Your splunk username.")
parser.add_argument('-p', '--password', required=False, default=False, action='store_true', dest='password',
help="Prompt for a password (will not echo.)")
parser.add_argument('-m', '--max-result-count', required=False, default=1000, type=int, dest='max_result_count',
help="Maximum number of results to return. Defaults to 1000")
parser.add_argument('-s', '--start-time', required=False, default=None, dest='start_time',
help="Starting time in YYYY-MM-DD HH:MM:SS format. Defaults to 24 hours before now.")
parser.add_argument('-e', '--end-time', required=False, default=None, dest='end_time',
help="Ending time in YYYY-MM-DD HH:MM:SS format. Defaults to now.")
parser.add_argument('-S', '--relative-start-time', required=False, default=None, dest='relative_start_time',
help="Specify the starting time as a time relative to now in DD:HH:MM:SS format.")
parser.add_argument('-E', '--relative-end-time', required=False, default=None, dest='relative_end_time',
help="Specify the ending time as a time relative to now in DD:HH:MM:SS format.")
parser.add_argument('--enviro', action='store', required=True, default='production', dest='enviro',
help="Specify which splunk environment to query (default=production). These are the sections defined in your config file.")
# the options only apply in the default csv mode
parser.add_argument('--headers', required=False, default=False, action='store_true', dest='headers',
help="Display headers in CSV output mode.")
# json display option
parser.add_argument('--json', required=False, default=False, action='store_true', dest='json',
help="Output in JSON instead of CSV")
# redirect to a file
parser.add_argument('-o', '--output', required=False, default=None, dest='output',
help="Send output to a file. Default is stdout.")
# save the given configuration to file for use later
parser.add_argument('--save-config', required=False, default=False, action='store_true', dest='save_config',
help="Save the given configuration options to ~/.splunklib")
parser.add_argument('--encrypt', required=False, default=False, action='store_true', dest='encrypt_password',
help="Encrypt your splunk password with another password.")
parser.add_argument('--search-file', required=False, default=False, action='store', dest='search_file',
help="File containing the search query.")
# adding this for use with url_click cloudphish hunt
parser.add_argument('-i', '--use-index-time', required=False, default=None, action='store_true', dest='use_index_time',
help="Use __index time specs instead.")
parser.add_argument('--query-timeout', required=False, default=None, dest='query_timeout',
help="Amount of time (in HH:MM:SS format) until a query times out. Defaults to 30 minutes.")
args = parser.parse_args()
logging_level = logging.WARNING
if args.quiet:
logging_level = logging.ERROR
if args.verbose:
logging_level = logging.DEBUG
logging.basicConfig(
format='[%(asctime)s] [%(filename)s:%(lineno)d] [%(threadName)s] [%(levelname)s] - %(message)s',
level=logging_level)
# are we saving the configuration?
if args.save_config:
config_path = os.path.join(os.path.expanduser('~'), '.splunklib.ini')
with open(config_path, 'w') as fp:
fp.write('[production]\n')
if args.uri is not None:
fp.write('uri = {0}\n'.format(args.uri))
if args.username is not None:
fp.write('username = {0}\n'.format(args.username))
if args.password:
password = getpass("Enter password: ")
# test the authentication
if args.uri is not None and args.username is not None:
searcher = splunklib.SplunkQueryObject(
uri=args.uri,
username=args.username,
password=password)
if not searcher.authenticate():
logging.error("invalid splunk credentials")
sys.exit(1)
if args.encrypt_password:
encrypted_password = encrypt_password(password)
logging.debug("encrypted_password = {0}".format(encrypted_password))
fp.write('encrypted_password = {0}\n'.format(encrypted_password))
else:
fp.write('password = {0}\n'.format(password))
logging.warning("saving PLAIN TEXT PASSWORD (use --encrypt option)")
if args.max_result_count is not None:
fp.write('max_result_count = {0}\n'.format(str(args.max_result_count)))
os.chmod(config_path, 0o600) # sane permissions
logging.debug("updated configuration")
sys.exit(0)
# do we have a configuration file?
config_path = os.path.join(os.path.expanduser('~'), '.splunklib.ini')
if args.config_path is not None:
config_path = args.config_path
uri = None
username = None
encrypted_password = None
password = None
max_result_count = 1000
if os.path.exists(config_path) and not args.ignore_config:
# load the settings from the configuration file
config = SafeConfigParser()
config.read(config_path)
try:
uri = config.get(args.enviro, 'uri')
username = config.get(args.enviro, 'username')
if config.has_option(args.enviro, 'encrypted_password'):
encrypted_password = config.get(args.enviro, 'encrypted_password')
else:
if config.has_option(args.enviro, 'password'):
# make sure permissions are sane
if os.stat(config_path).st_mode & stat.S_IROTH:
sys.stderr.write("""
*** HEY CLOWN ***
your file permissions on {0} allow anyone to read your plain text splunk password!
use the --save-config option with --encrypt to save your configuration with an encrypted password or chmod o-rwx this file
so that other people cannot read it
*** END CLOWN MESSAGE ***
""".format(config_path))
password = config.get(args.enviro, 'password')
if config.has_option(args.enviro, 'max_result_count'):
max_result_count = config.getint(args.enviro, 'max_result_count')
except Exception as e:
logging.warning("invalid configuration file {0}: {1}".format(config_path, str(e)))
# command line options override configuration values
if args.uri is not None:
uri = args.uri
if args.username is not None:
username = args.username
if args.password:
password = getpass("Enter password: ")
if encrypted_password is not None:
password = decrypt_password(encrypted_password)
if args.max_result_count is not None:
max_result_count = args.max_result_count
# make sure we have what we need
fatal = False
if uri is None:
logging.fatal("missing uri")
fatal = True
if username is None:
logging.fatal("missing username")
fatal = True
if password is None:
logging.fatal("missing password")
fatal = True
search_text = None
if args.search_file:
if os.path.isfile(args.search_file):
with open(args.search_file, 'r') as fp:
search_text = fp.read()
# comments in the search files are lines that start with #
search_text = re.sub(r'^\s*#.*$', '', search_text, count=0, flags=re.MULTILINE)
# put it all on one line for splunk
# we don't *need* to do this except for keeping the logs clean
search_text = re.sub(r'\n', ' ', search_text, count=0)
# removeing time_spec allows us to pass hunt files from the cli
if '{time_spec}' in search_text:
search_text = search_text.format(time_spec="")
args.search = search_text
else:
logging.fatal("search file does not exist")
if len(args.search) < 1:
logging.fatal("missing search")
fatal = True
if fatal:
sys.exit(1)
query = None
if args.search_file:
query = search_text
else:
query = ' '.join(args.search)
# figure out the time range given the options
start_time = None
end_time = None
datetime_format = '%Y-%m-%d %H:%M:%S'
if args.start_time is not None:
start_time = datetime.datetime.strptime(args.start_time, datetime_format)
if args.end_time is not None:
end_time = datetime.datetime.strptime(args.end_time, datetime_format)
if args.relative_start_time is not None:
start_time = datetime.datetime.now() - splunklib.create_timedelta(args.relative_start_time)
if args.relative_end_time is not None:
end_time = datetime.datetime.now() - splunklib.create_timedelta(args.relative_end_time)
if start_time is not None and end_time is None:
end_time = datetime.datetime.now()
if start_time is None and 'earliest' not in query.lower():
logging.debug("defaulting to past 24 hours")
start_time = datetime.datetime.now() - splunklib.create_timedelta('00:24:00:00')
end_time = datetime.datetime.now()
#if args.use_index_time:
# time_spec = '_index_earliest = {0} _index_latest = {1}'.format(start_time, end_time)
searcher = splunklib.SplunkQueryObject(
uri=uri,
username=username,
password=password,
max_result_count=max_result_count,
query_timeout=args.query_timeout if args.query_timeout else '00:30:00')
search_result = False
try:
if start_time is not None and end_time is not None:
if args.use_index_time:
search_result = searcher.query_with_index_time(query, start_time, end_time)
else:
search_result = searcher.query_with_time(query, start_time, end_time)
else:
search_result = searcher.query(query)
except KeyboardInterrupt:
pass
if not search_result:
logging.error("searched failed")
sys.exit(1)
output_fp = sys.stdout
if args.output:
output_fp = open(args.output, 'w', encoding='utf-8')
# JSON output
if args.json:
output_fp.write(json.dumps({
'search': query,
'username': username,
'uri': uri,
'max_result_count': max_result_count,
'result': searcher.json() }))
sys.exit(0)
# or CSV output
writer = csv.writer(output_fp)
# write the header?
if args.headers:
writer.writerow(searcher['fields'])
for row in searcher['rows']:
# see http://stackoverflow.com/a/9942885
#row = [x.encode('utf-8') if isinstance(x, str) else x for x in row]
writer.writerow(row)
sys.exit(0)