pgsqlpot 2.0.0__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- core/__init__.py +0 -0
- core/config.py +50 -0
- core/logfile.py +74 -0
- core/output.py +39 -0
- core/paths.py +53 -0
- core/protocol.py +161 -0
- core/tools.py +170 -0
- output_plugins/__init__.py +0 -0
- output_plugins/couch.py +68 -0
- output_plugins/datadog.py +74 -0
- output_plugins/discord.py +133 -0
- output_plugins/elastic.py +137 -0
- output_plugins/hpfeed.py +43 -0
- output_plugins/influx2.py +66 -0
- output_plugins/jsonlog.py +36 -0
- output_plugins/kafka.py +57 -0
- output_plugins/localsyslog.py +66 -0
- output_plugins/mongodb.py +83 -0
- output_plugins/mysql.py +210 -0
- output_plugins/nlcvapi.py +119 -0
- output_plugins/postgres.py +154 -0
- output_plugins/redisdb.py +47 -0
- output_plugins/rethinkdblog.py +46 -0
- output_plugins/slack.py +94 -0
- output_plugins/socketlog.py +40 -0
- output_plugins/sqlite.py +141 -0
- output_plugins/telegram.py +141 -0
- output_plugins/textlog.py +46 -0
- output_plugins/xmpp.py +193 -0
- pgsqlpot/__init__.py +25 -0
- pgsqlpot/cli.py +512 -0
- pgsqlpot/data/Dockerfile +56 -0
- pgsqlpot/data/docs/INSTALL.md +400 -0
- pgsqlpot/data/docs/INSTALLWIN.md +411 -0
- pgsqlpot/data/docs/PLUGINS.md +21 -0
- pgsqlpot/data/docs/TODO.md +8 -0
- pgsqlpot/data/docs/datadog/README.md +32 -0
- pgsqlpot/data/docs/discord/README.md +58 -0
- pgsqlpot/data/docs/geoipupdtask.ps1 +270 -0
- pgsqlpot/data/docs/mysql/README.md +176 -0
- pgsqlpot/data/docs/mysql/READMEWIN.md +157 -0
- pgsqlpot/data/docs/mysql/mysql.sql +85 -0
- pgsqlpot/data/docs/postgres/README.md +184 -0
- pgsqlpot/data/docs/postgres/READMEWIN.md +196 -0
- pgsqlpot/data/docs/postgres/postgres.sql +73 -0
- pgsqlpot/data/docs/slack/README.md +68 -0
- pgsqlpot/data/docs/sqlite3/README.md +131 -0
- pgsqlpot/data/docs/sqlite3/READMEWIN.md +123 -0
- pgsqlpot/data/docs/sqlite3/sqlite3.sql +69 -0
- pgsqlpot/data/docs/telegram/README.md +103 -0
- pgsqlpot/data/etc/honeypot.cfg +415 -0
- pgsqlpot/data/etc/honeypot.cfg.base +418 -0
- pgsqlpot/data/test/.gitignore +3 -0
- pgsqlpot/data/test/test.py +51 -0
- pgsqlpot/honeypot.py +117 -0
- pgsqlpot-2.0.0.dist-info/METADATA +152 -0
- pgsqlpot-2.0.0.dist-info/RECORD +61 -0
- pgsqlpot-2.0.0.dist-info/WHEEL +6 -0
- pgsqlpot-2.0.0.dist-info/entry_points.txt +2 -0
- pgsqlpot-2.0.0.dist-info/licenses/LICENSE +674 -0
- pgsqlpot-2.0.0.dist-info/top_level.txt +3 -0
pgsqlpot/cli.py
ADDED
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
pgsqlpot command-line entry point.
|
|
7
|
+
|
|
8
|
+
After `pip install pgsqlpot` the user gets a `pgsqlpot` command:
|
|
9
|
+
|
|
10
|
+
pgsqlpot init [DIR] -- scaffold a working directory (default: cwd)
|
|
11
|
+
pgsqlpot run -- start the honeypot in the foreground
|
|
12
|
+
pgsqlpot start -- start in the background (Linux, macOS, Windows)
|
|
13
|
+
pgsqlpot stop -- stop a backgrounded honeypot
|
|
14
|
+
pgsqlpot restart -- restart (stop and start in the backgrounded) the honeypot
|
|
15
|
+
pgsqlpot status -- show running status
|
|
16
|
+
|
|
17
|
+
The working directory is resolved in this order:
|
|
18
|
+
1. DIR argument (for init) or --workdir flag (for other commands)
|
|
19
|
+
2. PGSLQPOT_WORKDIR environment variable
|
|
20
|
+
3. Current working directory
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import absolute_import, print_function
|
|
24
|
+
|
|
25
|
+
from argparse import ArgumentParser
|
|
26
|
+
from importlib import import_module
|
|
27
|
+
from os import (
|
|
28
|
+
chdir,
|
|
29
|
+
devnull,
|
|
30
|
+
dup2,
|
|
31
|
+
environ,
|
|
32
|
+
kill,
|
|
33
|
+
makedirs,
|
|
34
|
+
name,
|
|
35
|
+
remove,
|
|
36
|
+
walk,
|
|
37
|
+
O_RDWR,
|
|
38
|
+
)
|
|
39
|
+
from os import open as _open
|
|
40
|
+
from os.path import (
|
|
41
|
+
abspath,
|
|
42
|
+
dirname,
|
|
43
|
+
exists,
|
|
44
|
+
isdir,
|
|
45
|
+
join,
|
|
46
|
+
relpath,
|
|
47
|
+
)
|
|
48
|
+
from re import search, IGNORECASE
|
|
49
|
+
from shutil import copy2
|
|
50
|
+
import sys
|
|
51
|
+
|
|
52
|
+
from core.paths import bundled, get_workdir, workdir_path
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
# helpers
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
def _ensure_dir(p):
|
|
60
|
+
if not isdir(p):
|
|
61
|
+
makedirs(p)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _file_differs(a, b):
|
|
65
|
+
with open(a, 'r') as fa, open(b, 'r') as fb:
|
|
66
|
+
return fa.read() != fb.read()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _copy_if_missing(src, dst, label=None):
|
|
70
|
+
# Guard against copying a file onto itself (e.g. in a source checkout).
|
|
71
|
+
if abspath(src) == abspath(dst):
|
|
72
|
+
return False
|
|
73
|
+
if not exists(dst) or _file_differs(src, dst):
|
|
74
|
+
copy2(src, dst)
|
|
75
|
+
if label is not None:
|
|
76
|
+
print(' copied {}'.format(label))
|
|
77
|
+
return True
|
|
78
|
+
if label is not None:
|
|
79
|
+
print(' exists {}'.format(label))
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _set_workdir(path):
|
|
84
|
+
environ['PGSLQPOT_WORKDIR'] = abspath(path)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _pidfile():
|
|
88
|
+
return workdir_path('pgsqlpot.pid')
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _pid_running(pid):
|
|
92
|
+
if name == 'nt':
|
|
93
|
+
# os.kill(pid, 0) is a Unix idiom that doesn't work on Windows -
|
|
94
|
+
# it raises SystemError. Use OpenProcess instead: a non-NULL handle
|
|
95
|
+
# means the process exists; NULL means it doesn't (or we lack
|
|
96
|
+
# permission, but that also means it's not ours to manage).
|
|
97
|
+
import ctypes
|
|
98
|
+
SYNCHRONIZE = 0x00100000
|
|
99
|
+
handle = ctypes.windll.kernel32.OpenProcess(SYNCHRONIZE, False, pid)
|
|
100
|
+
if not handle:
|
|
101
|
+
return False
|
|
102
|
+
ctypes.windll.kernel32.CloseHandle(handle)
|
|
103
|
+
return True
|
|
104
|
+
try:
|
|
105
|
+
kill(pid, 0)
|
|
106
|
+
return True
|
|
107
|
+
except OSError:
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _locate_honeypot_script():
|
|
112
|
+
"""Return the absolute path to honeypot.py inside the installed package."""
|
|
113
|
+
# honeypot.py is bundled inside the pgsqlpot package directory alongside
|
|
114
|
+
# this file (cli.py). Using __file__ avoids any dependency on pkg_resources
|
|
115
|
+
# or importlib.resources and works on Python 2.7+.
|
|
116
|
+
here = dirname(abspath(__file__))
|
|
117
|
+
script_path = join(here, 'honeypot.py')
|
|
118
|
+
if not exists(script_path):
|
|
119
|
+
raise RuntimeError(
|
|
120
|
+
'Cannot locate honeypot.py. '
|
|
121
|
+
'Expected inside the pgsqlpot package directory: {}'.format(here)
|
|
122
|
+
)
|
|
123
|
+
return script_path
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _run_main():
|
|
127
|
+
"""
|
|
128
|
+
Import and run honeypot.main().
|
|
129
|
+
|
|
130
|
+
honeypot.py lives inside the pgsqlpot package directory. We import it
|
|
131
|
+
directly using importlib so it runs as a proper module rather than a
|
|
132
|
+
top-level script, avoiding any sys.path manipulation.
|
|
133
|
+
"""
|
|
134
|
+
script_path = _locate_honeypot_script()
|
|
135
|
+
script_dir = dirname(script_path)
|
|
136
|
+
|
|
137
|
+
# Ensure the package directory is on sys.path so that honeypot.py's own
|
|
138
|
+
# imports (from core.xxx, from output_plugins.xxx, etc.) resolve correctly.
|
|
139
|
+
if script_dir not in sys.path:
|
|
140
|
+
sys.path.insert(0, script_dir)
|
|
141
|
+
|
|
142
|
+
# Import as 'honeypot'; clear any stale cached version first.
|
|
143
|
+
if 'honeypot' in sys.modules:
|
|
144
|
+
del sys.modules['honeypot']
|
|
145
|
+
mod = import_module('honeypot')
|
|
146
|
+
mod.main()
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# ---------------------------------------------------------------------------
|
|
150
|
+
# init
|
|
151
|
+
# ---------------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
def cmd_init(args):
|
|
154
|
+
|
|
155
|
+
target = abspath(args.dir)
|
|
156
|
+
_set_workdir(target)
|
|
157
|
+
print('Initialising pgsqlpot working directory: {}'.format(target))
|
|
158
|
+
|
|
159
|
+
# Runtime directories
|
|
160
|
+
for d in ('etc', 'log', 'data'):
|
|
161
|
+
_ensure_dir(join(target, d))
|
|
162
|
+
print(' mkdir {}'.format(d))
|
|
163
|
+
|
|
164
|
+
# Config templates
|
|
165
|
+
for fname in ('honeypot.cfg.base',):
|
|
166
|
+
src = bundled('etc', fname)
|
|
167
|
+
dst = join(target, 'etc', fname)
|
|
168
|
+
_copy_if_missing(src, dst, join('etc', fname))
|
|
169
|
+
|
|
170
|
+
# docs/ tree - walk the entire bundled docs directory and copy everything.
|
|
171
|
+
docs_src = bundled('docs')
|
|
172
|
+
docs_dst = join(target, 'docs')
|
|
173
|
+
for dirpath, dirnames, filenames in walk(docs_src):
|
|
174
|
+
rel_dir = relpath(dirpath, docs_src)
|
|
175
|
+
dst_dir = join(docs_dst, rel_dir) if rel_dir != '.' else docs_dst
|
|
176
|
+
_ensure_dir(dst_dir)
|
|
177
|
+
for fname in filenames:
|
|
178
|
+
# geoipupdtask.ps1 is handled separately below (Windows only)
|
|
179
|
+
if fname == 'geoipupdtask.ps1':
|
|
180
|
+
continue
|
|
181
|
+
src = join(dirpath, fname)
|
|
182
|
+
dst = join(dst_dir, fname)
|
|
183
|
+
rel = join('docs', rel_dir, fname) if rel_dir != '.' else join('docs', fname)
|
|
184
|
+
_copy_if_missing(src, dst, rel)
|
|
185
|
+
|
|
186
|
+
# These files are copied silently to the working directory root.
|
|
187
|
+
_copy_if_missing(bundled('test', 'test.py'), join(target, 'test.py'))
|
|
188
|
+
_dockerfile = bundled('Dockerfile')
|
|
189
|
+
if exists(_dockerfile):
|
|
190
|
+
_copy_if_missing(_dockerfile, join(target, 'Dockerfile'))
|
|
191
|
+
# geoipupdtask.ps1 is a Windows Task Scheduler helper - copy on Windows only
|
|
192
|
+
if name == 'nt':
|
|
193
|
+
geo_src = bundled('docs', 'geoipupdtask.ps1')
|
|
194
|
+
if exists(geo_src):
|
|
195
|
+
_copy_if_missing(geo_src, join(target, 'geoipupdtask.ps1'))
|
|
196
|
+
|
|
197
|
+
print()
|
|
198
|
+
print('Next steps:')
|
|
199
|
+
print(' 1. Edit {} with your settings.'.format(join(target, 'etc', 'honeypot.cfg')))
|
|
200
|
+
print(' 2. Place GeoLite2-City.mmdb and GeoLite2-ASN.mmdb in {}.'.format(join(target, 'data')))
|
|
201
|
+
print(' 3. Run: pgsqlpot run (foreground)')
|
|
202
|
+
print(' or: pgsqlpot start (background)')
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
# ---------------------------------------------------------------------------
|
|
206
|
+
# run (foreground)
|
|
207
|
+
# ---------------------------------------------------------------------------
|
|
208
|
+
|
|
209
|
+
def cmd_run(args):
|
|
210
|
+
# honeypot.py has its own argparse parser and will see sys.argv directly.
|
|
211
|
+
# Keep only arguments that come after 'run' so honeypot.py's parser sees
|
|
212
|
+
# only the options intended for it (e.g. pgsqlpot run --port 5432).
|
|
213
|
+
try:
|
|
214
|
+
run_idx = sys.argv.index('run')
|
|
215
|
+
sys.argv = [sys.argv[0]] + sys.argv[run_idx + 1:]
|
|
216
|
+
except ValueError:
|
|
217
|
+
sys.argv = [sys.argv[0]]
|
|
218
|
+
|
|
219
|
+
# Output plugins use relative paths from CONFIG (e.g. 'log/pgsqlpot.json')
|
|
220
|
+
# without going through workdir_path(). Changing to the working directory
|
|
221
|
+
# before starting ensures these paths resolve correctly.
|
|
222
|
+
chdir(get_workdir())
|
|
223
|
+
|
|
224
|
+
_run_main()
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
# ---------------------------------------------------------------------------
|
|
228
|
+
# start (background) -- cross-platform
|
|
229
|
+
# ---------------------------------------------------------------------------
|
|
230
|
+
|
|
231
|
+
def cmd_start(args):
|
|
232
|
+
pidfile = _pidfile()
|
|
233
|
+
|
|
234
|
+
# Check if already running
|
|
235
|
+
if exists(pidfile):
|
|
236
|
+
with open(pidfile) as fh:
|
|
237
|
+
pid_str = fh.read().strip()
|
|
238
|
+
if pid_str:
|
|
239
|
+
try:
|
|
240
|
+
pid = int(pid_str)
|
|
241
|
+
if _pid_running(pid):
|
|
242
|
+
print('The honeypot is already running (PID {}).'.format(pid))
|
|
243
|
+
return
|
|
244
|
+
except ValueError:
|
|
245
|
+
pass
|
|
246
|
+
|
|
247
|
+
print('Starting the honeypot...')
|
|
248
|
+
if name == 'nt':
|
|
249
|
+
_start_windows(pidfile)
|
|
250
|
+
else:
|
|
251
|
+
_start_posix(pidfile)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _start_posix(pidfile):
|
|
255
|
+
"""Fork to background on POSIX (Linux, macOS)."""
|
|
256
|
+
from os import fork, setsid
|
|
257
|
+
pid = fork()
|
|
258
|
+
if pid > 0:
|
|
259
|
+
# Parent: record child PID and exit
|
|
260
|
+
with open(pidfile, 'w') as fh:
|
|
261
|
+
fh.write(str(pid))
|
|
262
|
+
print('The honeypot was started successfully (PID {}).'.format(pid))
|
|
263
|
+
return
|
|
264
|
+
|
|
265
|
+
# Child: detach from terminal
|
|
266
|
+
setsid()
|
|
267
|
+
_devnull = _open(devnull, O_RDWR)
|
|
268
|
+
for fd in (0, 1, 2):
|
|
269
|
+
dup2(_devnull, fd)
|
|
270
|
+
|
|
271
|
+
# Clear the 'start' subcommand from sys.argv before honeypot.py's parser runs
|
|
272
|
+
try:
|
|
273
|
+
start_idx = sys.argv.index('start')
|
|
274
|
+
sys.argv = [sys.argv[0]] + sys.argv[start_idx + 1:]
|
|
275
|
+
except ValueError:
|
|
276
|
+
sys.argv = [sys.argv[0]]
|
|
277
|
+
|
|
278
|
+
# Ensure relative paths in plugin configs resolve against the working directory
|
|
279
|
+
chdir(get_workdir())
|
|
280
|
+
|
|
281
|
+
_run_main()
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _start_windows(pidfile):
|
|
285
|
+
"""
|
|
286
|
+
Spawn a detached, hidden background process on Windows.
|
|
287
|
+
|
|
288
|
+
Uses pythonw.exe (the windowless interpreter) to avoid any console window.
|
|
289
|
+
DETACHED_PROCESS + CREATE_NO_WINDOW ensure the child survives parent exit
|
|
290
|
+
and has no console of its own.
|
|
291
|
+
"""
|
|
292
|
+
from subprocess import Popen, STARTF_USESHOWWINDOW, STARTUPINFO
|
|
293
|
+
|
|
294
|
+
# Defined as literals for Python 2.7/3.6 compatibility - these constants
|
|
295
|
+
# were only added to subprocess as named attributes in Python 3.7.
|
|
296
|
+
DETACHED_PROCESS = 0x00000008
|
|
297
|
+
CREATE_NO_WINDOW = 0x08000000
|
|
298
|
+
|
|
299
|
+
startupinfo = STARTUPINFO()
|
|
300
|
+
startupinfo.dwFlags |= STARTF_USESHOWWINDOW
|
|
301
|
+
startupinfo.wShowWindow = 0 # SW_HIDE
|
|
302
|
+
|
|
303
|
+
# pythonw.exe is the windowless interpreter Windows ships for background
|
|
304
|
+
# scripts; more reliable than CREATE_NO_WINDOW for Twisted-based processes.
|
|
305
|
+
pythonw = join(dirname(sys.executable), 'pythonw.exe')
|
|
306
|
+
interpreter = pythonw if exists(pythonw) else sys.executable
|
|
307
|
+
cmd = [interpreter, '-m', 'pgsqlpot.cli', 'run']
|
|
308
|
+
|
|
309
|
+
_devnull = open(devnull, 'wb')
|
|
310
|
+
try:
|
|
311
|
+
proc = Popen(
|
|
312
|
+
cmd,
|
|
313
|
+
creationflags=DETACHED_PROCESS | CREATE_NO_WINDOW,
|
|
314
|
+
startupinfo=startupinfo,
|
|
315
|
+
stdin=_devnull,
|
|
316
|
+
stdout=_devnull,
|
|
317
|
+
stderr=_devnull,
|
|
318
|
+
env=environ.copy(),
|
|
319
|
+
)
|
|
320
|
+
finally:
|
|
321
|
+
_devnull.close()
|
|
322
|
+
|
|
323
|
+
with open(pidfile, 'w') as fh:
|
|
324
|
+
fh.write(str(proc.pid))
|
|
325
|
+
print('The honeypot was started successfully (PID {}).'.format(proc.pid))
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
# ---------------------------------------------------------------------------
|
|
329
|
+
# stop
|
|
330
|
+
# ---------------------------------------------------------------------------
|
|
331
|
+
|
|
332
|
+
def cmd_stop(args):
|
|
333
|
+
pidfile = _pidfile()
|
|
334
|
+
if not exists(pidfile):
|
|
335
|
+
print('The honeypot is not running (no PID file).')
|
|
336
|
+
return
|
|
337
|
+
|
|
338
|
+
with open(pidfile) as fh:
|
|
339
|
+
pid_str = fh.read().strip()
|
|
340
|
+
|
|
341
|
+
if not pid_str:
|
|
342
|
+
remove(pidfile)
|
|
343
|
+
print('Empty PID file removed.')
|
|
344
|
+
return
|
|
345
|
+
|
|
346
|
+
try:
|
|
347
|
+
pid = int(pid_str)
|
|
348
|
+
except ValueError:
|
|
349
|
+
remove(pidfile)
|
|
350
|
+
print('Corrupt PID file removed.')
|
|
351
|
+
return
|
|
352
|
+
|
|
353
|
+
if not _pid_running(pid):
|
|
354
|
+
remove(pidfile)
|
|
355
|
+
print('Stale PID file (PID {} not running) removed.'.format(pid))
|
|
356
|
+
return
|
|
357
|
+
|
|
358
|
+
print('Stopping the honeypot (PID {})... '.format(pid), end='')
|
|
359
|
+
|
|
360
|
+
if name == 'nt':
|
|
361
|
+
_stop_windows(pid, pidfile)
|
|
362
|
+
else:
|
|
363
|
+
_stop_posix(pid, pidfile)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _stop_posix(pid, pidfile):
|
|
367
|
+
from signal import SIGKILL, SIGTERM
|
|
368
|
+
from time import sleep
|
|
369
|
+
kill(pid, SIGTERM)
|
|
370
|
+
for _ in range(60):
|
|
371
|
+
sleep(1)
|
|
372
|
+
if not _pid_running(pid):
|
|
373
|
+
print('Stopped.')
|
|
374
|
+
remove(pidfile)
|
|
375
|
+
return
|
|
376
|
+
print()
|
|
377
|
+
print('Did not stop gracefully; sending SIGKILL.')
|
|
378
|
+
kill(pid, SIGKILL)
|
|
379
|
+
remove(pidfile)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _stop_windows(pid, pidfile):
|
|
383
|
+
"""
|
|
384
|
+
Terminate the background honeypot process on Windows.
|
|
385
|
+
|
|
386
|
+
The process was launched with pythonw.exe + CREATE_NO_WINDOW so it has no
|
|
387
|
+
console. GenerateConsoleCtrlEvent only works for processes sharing a
|
|
388
|
+
console or in the same process group - neither applies here. We therefore
|
|
389
|
+
go straight to TerminateProcess via taskkill /F.
|
|
390
|
+
"""
|
|
391
|
+
from subprocess import call
|
|
392
|
+
|
|
393
|
+
_devnull = open(devnull, 'wb')
|
|
394
|
+
try:
|
|
395
|
+
ret = call(
|
|
396
|
+
['taskkill', '/F', '/PID', str(pid)],
|
|
397
|
+
stdout=_devnull, stderr=_devnull,
|
|
398
|
+
)
|
|
399
|
+
finally:
|
|
400
|
+
_devnull.close()
|
|
401
|
+
|
|
402
|
+
if ret == 0 or not _pid_running(pid):
|
|
403
|
+
print('Stopped.')
|
|
404
|
+
else:
|
|
405
|
+
print()
|
|
406
|
+
print('Warning: taskkill returned {}, process may still be running.'.format(ret))
|
|
407
|
+
remove(pidfile)
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
# ---------------------------------------------------------------------------
|
|
411
|
+
# restart
|
|
412
|
+
# ---------------------------------------------------------------------------
|
|
413
|
+
|
|
414
|
+
def cmd_restart(args):
|
|
415
|
+
cmd_stop(args)
|
|
416
|
+
cmd_start(args)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
# ---------------------------------------------------------------------------
|
|
420
|
+
# status
|
|
421
|
+
# ---------------------------------------------------------------------------
|
|
422
|
+
|
|
423
|
+
def cmd_status(args):
|
|
424
|
+
pidfile = _pidfile()
|
|
425
|
+
if not exists(pidfile):
|
|
426
|
+
print('The honeypot is not running.')
|
|
427
|
+
return
|
|
428
|
+
with open(pidfile) as fh:
|
|
429
|
+
pid_str = fh.read().strip()
|
|
430
|
+
if not pid_str:
|
|
431
|
+
remove(pidfile)
|
|
432
|
+
print('The honeypot is not running (empty PID file removed).')
|
|
433
|
+
return
|
|
434
|
+
try:
|
|
435
|
+
pid = int(pid_str)
|
|
436
|
+
except ValueError:
|
|
437
|
+
remove(pidfile)
|
|
438
|
+
print('Corrupt PID file removed.')
|
|
439
|
+
return
|
|
440
|
+
if _pid_running(pid):
|
|
441
|
+
print('The honeypot is running (PID: {}).'.format(pid))
|
|
442
|
+
else:
|
|
443
|
+
remove(pidfile)
|
|
444
|
+
print('The honeypot is not running (stale PID file for PID {} removed).'.format(pid))
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
# ---------------------------------------------------------------------------
|
|
448
|
+
# entry point
|
|
449
|
+
# ---------------------------------------------------------------------------
|
|
450
|
+
|
|
451
|
+
def main():
|
|
452
|
+
parser = ArgumentParser(
|
|
453
|
+
prog='pgsqlpot',
|
|
454
|
+
description='pgsqlpot - A PostgreSQL Honeypot',
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
# Read version from honeypot.py without importing it (avoids Twisted import
|
|
458
|
+
# at startup) and without using pkg_resources (deprecated in setuptools 81+).
|
|
459
|
+
try:
|
|
460
|
+
with open(_locate_honeypot_script()) as fh:
|
|
461
|
+
_content = fh.read()
|
|
462
|
+
_m = search(r"__VERSION__\s*=\s*['\"]([^'\"]+)['\"]", _content, IGNORECASE)
|
|
463
|
+
_version = _m.group(1) if _m else 'unknown'
|
|
464
|
+
except Exception:
|
|
465
|
+
_version = 'unknown'
|
|
466
|
+
|
|
467
|
+
parser.add_argument('-v', '--version', action='version', version=_version)
|
|
468
|
+
parser.add_argument(
|
|
469
|
+
'-w', '--workdir', default=None,
|
|
470
|
+
help='Working directory (overrides PGSLQPOT_WORKDIR and cwd)'
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
sub = parser.add_subparsers(dest='command', metavar='COMMAND')
|
|
474
|
+
sub.required = True
|
|
475
|
+
|
|
476
|
+
p_init = sub.add_parser('init', help='Scaffold a working directory')
|
|
477
|
+
p_init.add_argument(
|
|
478
|
+
'dir', nargs='?', default='.',
|
|
479
|
+
help='Directory to initialize (default: current directory)'
|
|
480
|
+
)
|
|
481
|
+
p_init.set_defaults(func=cmd_init)
|
|
482
|
+
|
|
483
|
+
p_run = sub.add_parser('run', help='Start the honeypot in the foreground')
|
|
484
|
+
p_run.set_defaults(func=cmd_run)
|
|
485
|
+
|
|
486
|
+
p_start = sub.add_parser('start', help='Start the honeypot in the background')
|
|
487
|
+
p_start.set_defaults(func=cmd_start)
|
|
488
|
+
|
|
489
|
+
sub.add_parser('stop', help='Stop the backgrounded honeypot').set_defaults(func=cmd_stop)
|
|
490
|
+
|
|
491
|
+
p_restart = sub.add_parser(
|
|
492
|
+
'restart', help='Restart (stop and start) the honeypot in the background'
|
|
493
|
+
)
|
|
494
|
+
p_restart.set_defaults(func=cmd_restart)
|
|
495
|
+
|
|
496
|
+
for _p in (p_run, p_start, p_restart):
|
|
497
|
+
_p.add_argument('-p', '--port', type=int, help='Port to listen on (default: 5432)')
|
|
498
|
+
_p.add_argument('-l', '--logfile', help='Log file (default: stdout)')
|
|
499
|
+
_p.add_argument('-s', '--sensor', help='Sensor name (default: hostname)')
|
|
500
|
+
|
|
501
|
+
sub.add_parser('status', help='Show running status').set_defaults(func=cmd_status)
|
|
502
|
+
|
|
503
|
+
args = parser.parse_args()
|
|
504
|
+
|
|
505
|
+
if args.workdir:
|
|
506
|
+
_set_workdir(args.workdir)
|
|
507
|
+
|
|
508
|
+
args.func(args)
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
if __name__ == '__main__':
|
|
512
|
+
main()
|
pgsqlpot/data/Dockerfile
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# syntax=docker/dockerfile:1
|
|
2
|
+
|
|
3
|
+
# ---------------------------------------------------------------------------
|
|
4
|
+
# Build stage: install pgsqlpot and its dependencies.
|
|
5
|
+
# The -dev variant has pip and build tools; the runtime variant does not.
|
|
6
|
+
# ---------------------------------------------------------------------------
|
|
7
|
+
FROM dhi.io/python:3-dev AS builder
|
|
8
|
+
|
|
9
|
+
ARG VERSION
|
|
10
|
+
ARG PLUGINS=all
|
|
11
|
+
|
|
12
|
+
# Install pgsqlpot (and selected plugin dependencies) into a prefix that
|
|
13
|
+
# we can copy wholesale into the runtime stage.
|
|
14
|
+
RUN pip install --prefix=/install --prefer-binary \
|
|
15
|
+
"pgsqlpot==${VERSION}${PLUGINS:+[${PLUGINS}]}"
|
|
16
|
+
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
# Runtime stage: minimal hardened image, no shell, no package manager.
|
|
19
|
+
#
|
|
20
|
+
# SECURITY NOTE: Running this container requires the host user to be a member
|
|
21
|
+
# of the 'docker' group, which grants effective root access on the host.
|
|
22
|
+
# This negates the benefit of running the honeypot as a restricted user.
|
|
23
|
+
# Consider using Podman instead, which does not require privileged access:
|
|
24
|
+
# https://podman.io/
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
FROM dhi.io/python:3
|
|
27
|
+
|
|
28
|
+
ARG VERSION
|
|
29
|
+
LABEL maintainer="Bontchev"
|
|
30
|
+
LABEL name="pgsqlpot"
|
|
31
|
+
LABEL version="${VERSION}"
|
|
32
|
+
|
|
33
|
+
# Copy the installed packages from the build stage
|
|
34
|
+
COPY --from=builder /install /usr/local
|
|
35
|
+
|
|
36
|
+
# Run as a non-root user inside the container.
|
|
37
|
+
# Note: this does not mitigate the host-level docker group privilege issue
|
|
38
|
+
# described above, but it limits post-exploitation options inside the container.
|
|
39
|
+
RUN useradd --no-create-home --shell /bin/false pgsqlpot
|
|
40
|
+
|
|
41
|
+
EXPOSE 5432
|
|
42
|
+
|
|
43
|
+
# Scaffold the working directory and hand ownership to the non-root user.
|
|
44
|
+
WORKDIR /pgsqlpot
|
|
45
|
+
RUN pgsqlpot init && chown -R pgsqlpot:pgsqlpot /pgsqlpot
|
|
46
|
+
|
|
47
|
+
USER pgsqlpot
|
|
48
|
+
|
|
49
|
+
# Mount your honeypot.cfg and GeoLite2 databases at runtime, e.g.:
|
|
50
|
+
#
|
|
51
|
+
# docker run \
|
|
52
|
+
# -v /path/to/honeypot.cfg:/pgsqlpot/etc/honeypot.cfg \
|
|
53
|
+
# -v /path/to/data:/pgsqlpot/data \
|
|
54
|
+
# -p 5432:5432 pgsqlpot
|
|
55
|
+
#
|
|
56
|
+
CMD ["pgsqlpot", "run"]
|