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
output_plugins/mysql.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
from __future__ import absolute_import
|
|
5
|
+
|
|
6
|
+
from sys import exc_info, version_info
|
|
7
|
+
|
|
8
|
+
from core import output
|
|
9
|
+
from core.config import CONFIG
|
|
10
|
+
from core.tools import geolocate
|
|
11
|
+
|
|
12
|
+
from geoip2.database import Reader
|
|
13
|
+
|
|
14
|
+
if version_info[0] < 3:
|
|
15
|
+
import pymysql
|
|
16
|
+
pymysql.install_as_MySQLdb()
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
from MySQLdb import (Error, OperationalError)
|
|
20
|
+
except ImportError:
|
|
21
|
+
try:
|
|
22
|
+
from MySQLdb._exceptions import (Error, OperationalError)
|
|
23
|
+
except ImportError:
|
|
24
|
+
from _mysql_exceptions import (Error, OperationalError) # type: ignore
|
|
25
|
+
|
|
26
|
+
from twisted.enterprise.adbapi import ConnectionPool
|
|
27
|
+
from twisted.python.compat import reraise
|
|
28
|
+
from twisted.python.log import msg
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ReconnectingConnectionPool(ConnectionPool):
|
|
32
|
+
"""
|
|
33
|
+
Reconnecting adbapi connection pool for MySQL.
|
|
34
|
+
This class improves on the solution posted at
|
|
35
|
+
http://www.gelens.org/2008/09/12/reinitializing-twisted-connectionpool/
|
|
36
|
+
by checking exceptions by error code and only disconnecting the current
|
|
37
|
+
connection instead of all of them.
|
|
38
|
+
Also see:
|
|
39
|
+
http://twistedmatrix.com/pipermail/twisted-python/2009-July/020007.html
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def _runInteraction(self, interaction, *args, **kw):
|
|
43
|
+
|
|
44
|
+
def rerise_exception(conn):
|
|
45
|
+
_, excValue, excTraceback = exc_info()
|
|
46
|
+
try:
|
|
47
|
+
conn.rollback()
|
|
48
|
+
except Exception:
|
|
49
|
+
msg('Rollback failed')
|
|
50
|
+
reraise(excValue, excTraceback)
|
|
51
|
+
|
|
52
|
+
conn = self.connectionFactory(self)
|
|
53
|
+
trans = self.transactionFactory(self, conn)
|
|
54
|
+
try:
|
|
55
|
+
result = interaction(trans, *args, **kw)
|
|
56
|
+
trans.close()
|
|
57
|
+
conn.commit()
|
|
58
|
+
return result
|
|
59
|
+
except OperationalError as e:
|
|
60
|
+
if e.args[0] not in (2003, 2006, 2013):
|
|
61
|
+
rerise_exception(conn)
|
|
62
|
+
else:
|
|
63
|
+
conn = self.connections.get(self.threadID())
|
|
64
|
+
self.disconnect(conn)
|
|
65
|
+
return ConnectionPool._runInteraction(self, interaction, *args, **kw)
|
|
66
|
+
except Exception:
|
|
67
|
+
rerise_exception(conn)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class Output(output.Output):
|
|
71
|
+
|
|
72
|
+
def local_log(self, message):
|
|
73
|
+
if self.debug:
|
|
74
|
+
msg(message)
|
|
75
|
+
|
|
76
|
+
def start(self):
|
|
77
|
+
host = CONFIG.get('output_mysql', 'host', fallback='localhost')
|
|
78
|
+
database = CONFIG.get('output_mysql', 'database', fallback='pgsqlpot')
|
|
79
|
+
user = CONFIG.get('output_mysql', 'username', fallback='pgsqlpot', raw=True)
|
|
80
|
+
password = CONFIG.get('output_mysql', 'password', fallback='', raw=True)
|
|
81
|
+
port = CONFIG.getint('output_mysql', 'port', fallback=3306)
|
|
82
|
+
|
|
83
|
+
self.debug = CONFIG.getboolean('output_mysql', 'debug', fallback=False)
|
|
84
|
+
self.geoip = CONFIG.getboolean('output_mysql', 'geoip', fallback=True)
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
self.dbh = ReconnectingConnectionPool(
|
|
88
|
+
'MySQLdb',
|
|
89
|
+
host=host,
|
|
90
|
+
db=database,
|
|
91
|
+
user=user,
|
|
92
|
+
passwd=password,
|
|
93
|
+
port=port,
|
|
94
|
+
charset='utf8',
|
|
95
|
+
use_unicode=True,
|
|
96
|
+
cp_min=1,
|
|
97
|
+
cp_max=1
|
|
98
|
+
)
|
|
99
|
+
except Error as e:
|
|
100
|
+
self.local_log('output_mysql: MySQL Error {}: "{}"'.format(e.args[0], e.args[1]))
|
|
101
|
+
|
|
102
|
+
if self.geoip:
|
|
103
|
+
geoipdb_city_path = CONFIG.get('output_mysql', 'geoip_citydb', fallback='data/GeoLite2-City.mmdb')
|
|
104
|
+
geoipdb_asn_path = CONFIG.get('output_mysql', 'geoip_asndb', fallback='data/GeoLite2-ASN.mmdb')
|
|
105
|
+
try:
|
|
106
|
+
self.reader_city = Reader(geoipdb_city_path)
|
|
107
|
+
except Exception:
|
|
108
|
+
self.reader_city = None
|
|
109
|
+
self.local_log('Failed to open City GeoIP database {}'.format(geoipdb_city_path))
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
self.reader_asn = Reader(geoipdb_asn_path)
|
|
113
|
+
except Exception:
|
|
114
|
+
self.reader_asn = None
|
|
115
|
+
self.local_log('Failed to open ASN GeoIP database {}'.format(geoipdb_asn_path))
|
|
116
|
+
|
|
117
|
+
def stop(self):
|
|
118
|
+
if self.geoip:
|
|
119
|
+
if self.reader_city is not None:
|
|
120
|
+
self.reader_city.close()
|
|
121
|
+
if self.reader_asn is not None:
|
|
122
|
+
self.reader_asn.close()
|
|
123
|
+
|
|
124
|
+
def write(self, event):
|
|
125
|
+
"""
|
|
126
|
+
TODO: Check if the type (date, datetime or timestamp) of columns is appropriate for your needs and timezone
|
|
127
|
+
- MySQL Documentation - The DATE, DATETIME, and TIMESTAMP Types
|
|
128
|
+
(https://dev.mysql.com/doc/refman/5.7/en/datetime.html):
|
|
129
|
+
"MySQL converts TIMESTAMP values from the current time zone to UTC for storage,
|
|
130
|
+
and back from UTC to the current time zone for retrieval.
|
|
131
|
+
(This does not occur for other types such as DATETIME.)"
|
|
132
|
+
"""
|
|
133
|
+
self.dbh.runInteraction(self.connect_event, event)
|
|
134
|
+
|
|
135
|
+
def simple_query(self, txn, sql, args):
|
|
136
|
+
if self.debug:
|
|
137
|
+
if len(args):
|
|
138
|
+
self.local_log("output_mysql: MySQL query: {} {}".format(sql, repr(args)))
|
|
139
|
+
else:
|
|
140
|
+
self.local_log("output_mysql: MySQL query: {}".format(sql))
|
|
141
|
+
try:
|
|
142
|
+
if len(args):
|
|
143
|
+
txn.execute(sql, args)
|
|
144
|
+
else:
|
|
145
|
+
txn.execute(sql)
|
|
146
|
+
result = txn.fetchall()
|
|
147
|
+
except Exception as e:
|
|
148
|
+
self.local_log('output_mysql: MySQL Error: {}'.format(e))
|
|
149
|
+
result = None
|
|
150
|
+
return result
|
|
151
|
+
|
|
152
|
+
def get_id(self, txn, table, column, entry):
|
|
153
|
+
r = self.simple_query(txn, "SELECT `id` FROM `{}` WHERE `{}` = %s".format(table, column), (entry, ))
|
|
154
|
+
if r:
|
|
155
|
+
id = r[0][0]
|
|
156
|
+
else:
|
|
157
|
+
self.simple_query(txn, "INSERT INTO `{}` (`{}`) VALUES (%s)".format(table, column), (entry, ))
|
|
158
|
+
r = self.simple_query(txn, 'SELECT LAST_INSERT_ID()', ())
|
|
159
|
+
if r:
|
|
160
|
+
id = int(r[0][0])
|
|
161
|
+
else:
|
|
162
|
+
id = 0
|
|
163
|
+
return id
|
|
164
|
+
|
|
165
|
+
def connect_event(self, txn, event):
|
|
166
|
+
remote_ip = event['src_ip']
|
|
167
|
+
operation_id = self.get_id(txn, 'operations', 'op_name', event['operation'])
|
|
168
|
+
sensor_id = self.get_id(txn, 'sensors', 'name', event['sensor'])
|
|
169
|
+
|
|
170
|
+
self.simple_query(txn,
|
|
171
|
+
"""
|
|
172
|
+
INSERT INTO `connections` (
|
|
173
|
+
`session`, `timestamp`, `operation`, `ip`,
|
|
174
|
+
`remote_port`, `local_host`, `local_port`, `sensor`)
|
|
175
|
+
VALUES (%s, FROM_UNIXTIME(%s), %s, %s, %s, %s, %s, %s)
|
|
176
|
+
""",
|
|
177
|
+
(event['session'], event['unixtime'], operation_id, remote_ip,
|
|
178
|
+
event['src_port'], event['dst_ip'], event['dst_port'], sensor_id, ))
|
|
179
|
+
|
|
180
|
+
if event['operation'].lower() == 'login':
|
|
181
|
+
usr_id = self.get_id(txn, 'usernames', 'username', event['username'])
|
|
182
|
+
pwd_id = self.get_id(txn, 'passwords', 'password', event['password'])
|
|
183
|
+
self.simple_query(txn,
|
|
184
|
+
"""
|
|
185
|
+
INSERT INTO `credentials` (`session`, `username`, `password`) VALUES(%s, %s, %s)
|
|
186
|
+
""",
|
|
187
|
+
(event['session'], usr_id, pwd_id, ))
|
|
188
|
+
if 'variables' in event:
|
|
189
|
+
for key, value in event['variables'].items():
|
|
190
|
+
var_id = self.get_id(txn, 'vars', 'var_name', key)
|
|
191
|
+
val_id = self.get_id(txn, 'var_values', 'var_value', value)
|
|
192
|
+
self.simple_query(txn,
|
|
193
|
+
"""
|
|
194
|
+
INSERT INTO `variables` (`session`, `var`, `val`) VALUES(%s, %s, %s)
|
|
195
|
+
""",
|
|
196
|
+
(event['session'], var_id, val_id, ))
|
|
197
|
+
|
|
198
|
+
if self.geoip:
|
|
199
|
+
country, country_code, city, org, asn_num = geolocate(remote_ip, self.reader_city, self.reader_asn)
|
|
200
|
+
self.simple_query(txn, """
|
|
201
|
+
INSERT INTO `geolocation` (`ip`, `country_name`, `country_iso_code`, `city_name`, `org`, `org_asn`)
|
|
202
|
+
VALUES (%s, %s, %s, %s, %s, %s)
|
|
203
|
+
ON DUPLICATE KEY UPDATE
|
|
204
|
+
`country_name` = %s,
|
|
205
|
+
`country_iso_code` = %s,
|
|
206
|
+
`city_name` = %s,
|
|
207
|
+
`org` = %s,
|
|
208
|
+
`org_asn` = %s
|
|
209
|
+
""",
|
|
210
|
+
(remote_ip, country, country_code, city, org, asn_num, country, country_code, city, org, asn_num, ))
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
from __future__ import absolute_import
|
|
5
|
+
|
|
6
|
+
from io import BytesIO
|
|
7
|
+
from json import dumps, loads
|
|
8
|
+
|
|
9
|
+
from core import output
|
|
10
|
+
from core.config import CONFIG
|
|
11
|
+
from core.tools import decode, geolocate, to_bytes
|
|
12
|
+
|
|
13
|
+
from geoip2.database import Reader
|
|
14
|
+
|
|
15
|
+
from twisted.internet import reactor
|
|
16
|
+
from twisted.internet.ssl import ClientContextFactory
|
|
17
|
+
from twisted.python.log import msg
|
|
18
|
+
from twisted.web.client import (
|
|
19
|
+
Agent,
|
|
20
|
+
FileBodyProducer,
|
|
21
|
+
HTTPConnectionPool,
|
|
22
|
+
_HTTP11ClientFactory,
|
|
23
|
+
readBody,
|
|
24
|
+
)
|
|
25
|
+
from twisted.web.http_headers import Headers
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class WebClientContextFactory(ClientContextFactory):
|
|
29
|
+
|
|
30
|
+
def getContext(self, hostname, port):
|
|
31
|
+
return ClientContextFactory.getContext(self)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class QuietHTTP11ClientFactory(_HTTP11ClientFactory):
|
|
35
|
+
noisy = False
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Output(output.Output):
|
|
39
|
+
|
|
40
|
+
def start(self):
|
|
41
|
+
self.host = CONFIG.get('output_nlcvapi', 'host', fallback='https://api.nlcv.bas.bg/v1.0/honeypot')
|
|
42
|
+
self.geoip = CONFIG.getboolean('output_nlcvapi', 'geoip', fallback=True)
|
|
43
|
+
contextFactory = WebClientContextFactory()
|
|
44
|
+
pool = HTTPConnectionPool(reactor)
|
|
45
|
+
pool._factory = QuietHTTP11ClientFactory
|
|
46
|
+
self.agent = Agent(reactor, contextFactory=contextFactory, pool=pool)
|
|
47
|
+
|
|
48
|
+
if self.geoip:
|
|
49
|
+
geoipdb_city_path = CONFIG.get('output_nlcvapi', 'geoip_citydb', fallback='data/GeoLite2-City.mmdb')
|
|
50
|
+
geoipdb_asn_path = CONFIG.get('output_nlcvapi', 'geoip_asndb', fallback='data/GeoLite2-ASN.mmdb')
|
|
51
|
+
try:
|
|
52
|
+
self.reader_city = Reader(geoipdb_city_path)
|
|
53
|
+
except Exception:
|
|
54
|
+
self.reader_city = None
|
|
55
|
+
msg('Failed to open City GeoIP database {}'.format(geoipdb_city_path))
|
|
56
|
+
try:
|
|
57
|
+
self.reader_asn = Reader(geoipdb_asn_path)
|
|
58
|
+
except Exception:
|
|
59
|
+
self.reader_asn = None
|
|
60
|
+
msg('Failed to open ASN GeoIP database {}'.format(geoipdb_asn_path))
|
|
61
|
+
|
|
62
|
+
def stop(self):
|
|
63
|
+
if self.geoip:
|
|
64
|
+
if getattr(self, 'reader_city', None) is not None:
|
|
65
|
+
self.reader_city.close()
|
|
66
|
+
if getattr(self, 'reader_asn', None) is not None:
|
|
67
|
+
self.reader_asn.close()
|
|
68
|
+
|
|
69
|
+
def write(self, event):
|
|
70
|
+
event['honeypot'] = 'pgsqlpot'
|
|
71
|
+
if self.geoip:
|
|
72
|
+
country, country_code, city, org, asn_num = geolocate(event['src_ip'], self.reader_city, self.reader_asn)
|
|
73
|
+
event['country'] = country
|
|
74
|
+
event['country_code'] = country_code
|
|
75
|
+
event['city'] = city
|
|
76
|
+
event['org'] = org
|
|
77
|
+
event['asn'] = asn_num
|
|
78
|
+
self._postentry(event)
|
|
79
|
+
|
|
80
|
+
def _postentry(self, entry):
|
|
81
|
+
|
|
82
|
+
def cbBody(body):
|
|
83
|
+
return processResult(body)
|
|
84
|
+
|
|
85
|
+
def cbPartial(failure):
|
|
86
|
+
"""
|
|
87
|
+
Google HTTP Server does not set Content-Length. Twisted marks it as partial
|
|
88
|
+
"""
|
|
89
|
+
failure.printTraceback()
|
|
90
|
+
return processResult(failure.value.response)
|
|
91
|
+
|
|
92
|
+
def cbResponse(response):
|
|
93
|
+
if response.code in (200, 201):
|
|
94
|
+
return
|
|
95
|
+
msg('NLCVAPI response: {}: "{}"'.format(response.code, decode(response.phrase)))
|
|
96
|
+
d = readBody(response)
|
|
97
|
+
d.addCallback(cbBody)
|
|
98
|
+
d.addErrback(cbPartial)
|
|
99
|
+
return d
|
|
100
|
+
|
|
101
|
+
def cbError(failure):
|
|
102
|
+
failure.printTraceback()
|
|
103
|
+
|
|
104
|
+
def processResult(result):
|
|
105
|
+
try:
|
|
106
|
+
j = loads(result)
|
|
107
|
+
msg('NLCVAPI response: {}'.format(j.get('message', '')))
|
|
108
|
+
except Exception:
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
headers = Headers({
|
|
112
|
+
b'User-Agent': [b'PGSQLPot'],
|
|
113
|
+
b'Content-Type': [b'application/json'],
|
|
114
|
+
})
|
|
115
|
+
body = FileBodyProducer(BytesIO(to_bytes(dumps(entry, sort_keys=True))))
|
|
116
|
+
d = self.agent.request(b'POST', to_bytes(self.host), headers, body)
|
|
117
|
+
d.addCallback(cbResponse)
|
|
118
|
+
d.addErrback(cbError)
|
|
119
|
+
return d
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
|
|
2
|
+
from __future__ import absolute_import
|
|
3
|
+
|
|
4
|
+
from core import output
|
|
5
|
+
from core.config import CONFIG
|
|
6
|
+
from core.tools import geolocate
|
|
7
|
+
|
|
8
|
+
from geoip2.database import Reader
|
|
9
|
+
from psycopg2 import OperationalError
|
|
10
|
+
|
|
11
|
+
from twisted.enterprise.adbapi import ConnectionPool
|
|
12
|
+
from twisted.python.log import msg
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Output(output.Output):
|
|
16
|
+
|
|
17
|
+
def start(self):
|
|
18
|
+
host = CONFIG.get('output_postgres', 'host', fallback='localhost')
|
|
19
|
+
port = CONFIG.getint('output_postgres', 'port', fallback=5432)
|
|
20
|
+
username = CONFIG.get('output_postgres', 'username', fallback='pgsqlpot')
|
|
21
|
+
password = CONFIG.get('output_postgres', 'password')
|
|
22
|
+
database = CONFIG.get('output_postgres', 'database', fallback='pgsqlpot')
|
|
23
|
+
self.debug = CONFIG.getboolean('output_postgres', 'debug', fallback=False)
|
|
24
|
+
self.geoip = CONFIG.getboolean('output_postgres', 'geoip', fallback=True)
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
self.dbh = ConnectionPool(
|
|
28
|
+
'psycopg2',
|
|
29
|
+
database=database,
|
|
30
|
+
user=username,
|
|
31
|
+
password=password,
|
|
32
|
+
host=host,
|
|
33
|
+
port=port,
|
|
34
|
+
cp_min=1,
|
|
35
|
+
cp_max=1
|
|
36
|
+
)
|
|
37
|
+
except OperationalError as e:
|
|
38
|
+
msg('output_postgres: postgres Error: {}'.format(e))
|
|
39
|
+
|
|
40
|
+
if self.geoip:
|
|
41
|
+
geoipdb_city_path = CONFIG.get('output_postgres', 'geoip_citydb', fallback='data/GeoLite2-City.mmdb')
|
|
42
|
+
geoipdb_asn_path = CONFIG.get('output_postgres', 'geoip_asndb', fallback='data/GeoLite2-ASN.mmdb')
|
|
43
|
+
try:
|
|
44
|
+
self.reader_city = Reader(geoipdb_city_path)
|
|
45
|
+
except Exception:
|
|
46
|
+
self.reader_city = None
|
|
47
|
+
msg('Failed to open City GeoIP database {}'.format(geoipdb_city_path))
|
|
48
|
+
try:
|
|
49
|
+
self.reader_asn = Reader(geoipdb_asn_path)
|
|
50
|
+
except Exception:
|
|
51
|
+
self.reader_asn = None
|
|
52
|
+
msg('Failed to open ASN GeoIP database {}'.format(geoipdb_asn_path))
|
|
53
|
+
|
|
54
|
+
def stop(self):
|
|
55
|
+
if self.geoip:
|
|
56
|
+
if self.reader_city is not None:
|
|
57
|
+
self.reader_city.close()
|
|
58
|
+
if self.reader_asn is not None:
|
|
59
|
+
self.reader_asn.close()
|
|
60
|
+
|
|
61
|
+
def write(self, event):
|
|
62
|
+
self.dbh.runInteraction(self.connect_event, event)
|
|
63
|
+
|
|
64
|
+
def simple_query(self, txn, sql, args, returns_value=True):
|
|
65
|
+
if self.debug:
|
|
66
|
+
msg('output_postgres: postgres query: {} {}'.format(sql, repr(args)))
|
|
67
|
+
result = None
|
|
68
|
+
try:
|
|
69
|
+
txn.execute(sql, args)
|
|
70
|
+
if returns_value:
|
|
71
|
+
result = txn.fetchone()
|
|
72
|
+
except Exception as e:
|
|
73
|
+
msg('output_postgres: postgres Error: {}'.format(e))
|
|
74
|
+
return result
|
|
75
|
+
|
|
76
|
+
def get_id(self, txn, table, column, entry):
|
|
77
|
+
r = self.simple_query(
|
|
78
|
+
txn,
|
|
79
|
+
"SELECT id FROM {} WHERE {} = %s".format(table, column),
|
|
80
|
+
(entry, )
|
|
81
|
+
)
|
|
82
|
+
if r:
|
|
83
|
+
id = int(r[0])
|
|
84
|
+
else:
|
|
85
|
+
r = self.simple_query(
|
|
86
|
+
txn,
|
|
87
|
+
"INSERT INTO {} ({}) VALUES (%s) RETURNING id".format(table, column),
|
|
88
|
+
(entry, )
|
|
89
|
+
)
|
|
90
|
+
if r:
|
|
91
|
+
id = int(r[0])
|
|
92
|
+
else:
|
|
93
|
+
id = 0
|
|
94
|
+
return id
|
|
95
|
+
|
|
96
|
+
def connect_event(self, txn, event):
|
|
97
|
+
remote_ip = event['src_ip']
|
|
98
|
+
operation_id = self.get_id(txn, 'operations', 'op_name', event['operation'])
|
|
99
|
+
sensor_id = self.get_id(txn, 'sensors', 'sname', event['sensor'])
|
|
100
|
+
|
|
101
|
+
self.simple_query(
|
|
102
|
+
txn,
|
|
103
|
+
"""
|
|
104
|
+
INSERT INTO connections (
|
|
105
|
+
sess_no, time_stamp, ip, remote_port, operation,
|
|
106
|
+
local_host, local_port, sensor)
|
|
107
|
+
VALUES (%s, to_timestamp(%s), %s, %s, %s, %s, %s, %s)
|
|
108
|
+
""",
|
|
109
|
+
(event['session'], event['unixtime'], remote_ip, event['src_port'], operation_id,
|
|
110
|
+
event['dst_ip'], event['dst_port'], sensor_id, ),
|
|
111
|
+
False
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
if event['operation'].lower() == 'login':
|
|
115
|
+
usr_id = self.get_id(txn, 'usernames', 'username', event['username'])
|
|
116
|
+
pwd_id = self.get_id(txn, 'passwords', 'passwd', event['password'])
|
|
117
|
+
self.simple_query(
|
|
118
|
+
txn,
|
|
119
|
+
"""
|
|
120
|
+
INSERT INTO credentials (sess_no, username, passwd) VALUES(%s, %s, %s)
|
|
121
|
+
""",
|
|
122
|
+
(event['session'], usr_id, pwd_id, ),
|
|
123
|
+
False
|
|
124
|
+
)
|
|
125
|
+
if 'variables' in event:
|
|
126
|
+
for key, value in event['variables'].items():
|
|
127
|
+
var_id = self.get_id(txn, 'vars', 'var_name', key)
|
|
128
|
+
val_id = self.get_id(txn, 'var_values', 'var_value', value)
|
|
129
|
+
self.simple_query(
|
|
130
|
+
txn,
|
|
131
|
+
"""
|
|
132
|
+
INSERT INTO variables (sess_no, var, val) VALUES(%s, %s, %s)
|
|
133
|
+
""",
|
|
134
|
+
(event['session'], var_id, val_id, ),
|
|
135
|
+
False
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
if self.geoip:
|
|
139
|
+
country, country_code, city, org, asn_num = geolocate(remote_ip, self.reader_city, self.reader_asn)
|
|
140
|
+
self.simple_query(
|
|
141
|
+
txn,
|
|
142
|
+
"""
|
|
143
|
+
INSERT INTO geolocation (ip, country_name, country_iso_code, city_name, org, org_asn)
|
|
144
|
+
VALUES (%s, %s, %s, %s, %s, %s)
|
|
145
|
+
ON CONFLICT (ip) DO UPDATE SET
|
|
146
|
+
country_name = EXCLUDED.country_name,
|
|
147
|
+
country_iso_code = EXCLUDED.country_iso_code,
|
|
148
|
+
city_name = EXCLUDED.city_name,
|
|
149
|
+
org = EXCLUDED.org,
|
|
150
|
+
org_asn = EXCLUDED.org_asn
|
|
151
|
+
""",
|
|
152
|
+
(remote_ip, country, country_code, city, org, asn_num, ),
|
|
153
|
+
False
|
|
154
|
+
)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
from __future__ import absolute_import
|
|
5
|
+
|
|
6
|
+
from json import dumps
|
|
7
|
+
|
|
8
|
+
from core import output
|
|
9
|
+
from core.config import CONFIG
|
|
10
|
+
|
|
11
|
+
from requests import post
|
|
12
|
+
|
|
13
|
+
from twisted.internet.threads import deferToThread
|
|
14
|
+
from twisted.python.log import msg
|
|
15
|
+
|
|
16
|
+
class Output(output.Output):
|
|
17
|
+
|
|
18
|
+
def start(self):
|
|
19
|
+
self.debug = CONFIG.getboolean('output_redisdb', 'debug', fallback=False)
|
|
20
|
+
host = CONFIG.get('output_redisdb', 'host')
|
|
21
|
+
self.password = CONFIG.get('output_redisdb', 'password')
|
|
22
|
+
keyname = CONFIG.get('output_redisdb', 'keyname', fallback='pgsqlpot')
|
|
23
|
+
self.url = 'https://{}/lpush/{}'.format(host, keyname)
|
|
24
|
+
|
|
25
|
+
def stop(self):
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
def write(self, event):
|
|
29
|
+
payload = dumps(event, sort_keys=True)
|
|
30
|
+
headers = {
|
|
31
|
+
b'Authorization': 'Bearer {}'.format(self.password),
|
|
32
|
+
b'Content-Type': b'application/json'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if self.debug:
|
|
36
|
+
msg(payload)
|
|
37
|
+
|
|
38
|
+
d = deferToThread(post, self.url, data=payload, headers=headers, timeout=5)
|
|
39
|
+
d.addCallback(self._on_response)
|
|
40
|
+
d.addErrback(self._on_error)
|
|
41
|
+
|
|
42
|
+
def _on_response(self, response):
|
|
43
|
+
if self.debug and response.status_code != 200:
|
|
44
|
+
msg('[REST] Response code: {}'.format(response.status_code))
|
|
45
|
+
|
|
46
|
+
def _on_error(self, failure):
|
|
47
|
+
msg('[REST] REST error: {}'.format(failure))
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
|
|
2
|
+
from __future__ import absolute_import
|
|
3
|
+
|
|
4
|
+
from time import sleep
|
|
5
|
+
|
|
6
|
+
from core import output
|
|
7
|
+
from core.config import CONFIG
|
|
8
|
+
|
|
9
|
+
from rethinkdb import r
|
|
10
|
+
|
|
11
|
+
from twisted.python.log import msg
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Output(output.Output):
|
|
15
|
+
|
|
16
|
+
def start(self):
|
|
17
|
+
host = CONFIG.get('output_rethinkdblog', 'host', fallback='localhost')
|
|
18
|
+
port = CONFIG.getint('output_rethinkdblog', 'port', fallback=28015)
|
|
19
|
+
self.db = CONFIG.get('output_rethinkdblog', 'db', fallback='pgsqlpot')
|
|
20
|
+
password = CONFIG.get('output_rethinkdblog', 'password', raw=True)
|
|
21
|
+
self.table = CONFIG.get('output_rethinkdblog', 'table', fallback='events')
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
self.connection = r.connect(host=host, port=port, password=password)
|
|
25
|
+
|
|
26
|
+
if self.db not in r.db_list().run(self.connection):
|
|
27
|
+
r.db_create(self.db).run(self.connection)
|
|
28
|
+
for _ in range(50): # ~5 seconds max
|
|
29
|
+
if self.db in r.db_list().run(self.connection):
|
|
30
|
+
break
|
|
31
|
+
sleep(0.1)
|
|
32
|
+
if self.table not in r.db(self.db).table_list().run(self.connection):
|
|
33
|
+
r.db(self.db).table_create(self.table).run(self.connection)
|
|
34
|
+
|
|
35
|
+
except r.RqlError as err:
|
|
36
|
+
msg('output_rethinkdblog: Error: {}'.format(err.message))
|
|
37
|
+
|
|
38
|
+
def stop(self):
|
|
39
|
+
if getattr(self, 'connection', None):
|
|
40
|
+
self.connection.close()
|
|
41
|
+
|
|
42
|
+
def write(self, event):
|
|
43
|
+
try:
|
|
44
|
+
r.db(self.db).table(self.table).insert(event).run(self.connection)
|
|
45
|
+
except r.RqlRuntimeError as err:
|
|
46
|
+
msg('output_rethinkdblog: Error: {}'.format(err.message))
|
output_plugins/slack.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
from __future__ import absolute_import
|
|
5
|
+
|
|
6
|
+
from sys import version_info
|
|
7
|
+
from time import gmtime, strftime, time
|
|
8
|
+
|
|
9
|
+
from core import output
|
|
10
|
+
from core.config import CONFIG
|
|
11
|
+
|
|
12
|
+
from twisted.internet import reactor
|
|
13
|
+
from twisted.internet.task import deferLater
|
|
14
|
+
|
|
15
|
+
# --- Slack compatibility wrapper ---
|
|
16
|
+
if version_info[0] >= 3:
|
|
17
|
+
# Python 3: slack-sdk
|
|
18
|
+
from slack_sdk import WebClient as SlackClientWrapper # type: ignore
|
|
19
|
+
else:
|
|
20
|
+
# Python 2.7: slackclient 1.x
|
|
21
|
+
from slackclient import SlackClient as BaseSlackClient # type: ignore
|
|
22
|
+
|
|
23
|
+
class SlackResponse(object):
|
|
24
|
+
"""Wrap Python 2 SlackClient dict to mimic SlackResponse"""
|
|
25
|
+
def __init__(self, data):
|
|
26
|
+
self.data = data
|
|
27
|
+
|
|
28
|
+
class SlackClientWrapper(object):
|
|
29
|
+
def __init__(self, token):
|
|
30
|
+
self._client = BaseSlackClient(token)
|
|
31
|
+
|
|
32
|
+
def chat_postMessage(self, **kwargs):
|
|
33
|
+
result = self._client.api_call("chat.postMessage", **kwargs)
|
|
34
|
+
return SlackResponse(result)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Output(output.Output):
|
|
38
|
+
"""
|
|
39
|
+
Slack webhook output plugin.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def start(self):
|
|
43
|
+
self.slack_channel = CONFIG.get('output_slack', 'channel')
|
|
44
|
+
self.slack_token = CONFIG.get('output_slack', 'token')
|
|
45
|
+
self.delay = CONFIG.getfloat('output_slack', 'delay', fallback=1.2)
|
|
46
|
+
self.sc = SlackClientWrapper(self.slack_token)
|
|
47
|
+
self.last_sent = 0
|
|
48
|
+
self.requests_list = []
|
|
49
|
+
|
|
50
|
+
def stop(self):
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
def write(self, event):
|
|
54
|
+
operation = event['operation'].lower()
|
|
55
|
+
|
|
56
|
+
message = '[{} UTC] [PGSQLPot on {} ({})]: {}'.format(
|
|
57
|
+
strftime('%Y-%m-%d %H:%M:%S', gmtime(event['unixtime'])),
|
|
58
|
+
event['sensor'], event['session'], operation.capitalize()
|
|
59
|
+
)
|
|
60
|
+
if operation == 'unknown':
|
|
61
|
+
message += ' operation: "{}"'.format(event['username'])
|
|
62
|
+
elif operation == 'command':
|
|
63
|
+
message += ' "{}'.format(event['command'])
|
|
64
|
+
if event['args']:
|
|
65
|
+
message += ' {}'.format(event['args'])
|
|
66
|
+
message += '"'
|
|
67
|
+
message += ' from {}:{}'.format(event['src_ip'], event['dst_port'])
|
|
68
|
+
if operation == 'login':
|
|
69
|
+
message += ', username: "{}", password: "{}"'.format(
|
|
70
|
+
event['username'], event['password']
|
|
71
|
+
)
|
|
72
|
+
message += '.'
|
|
73
|
+
|
|
74
|
+
self.requests_list.append(message)
|
|
75
|
+
self._drain()
|
|
76
|
+
|
|
77
|
+
def _drain(self):
|
|
78
|
+
"""
|
|
79
|
+
Send the next queued message if the rate-limit window has elapsed.
|
|
80
|
+
If messages remain, schedule another drain after self.delay seconds.
|
|
81
|
+
"""
|
|
82
|
+
if not self.requests_list:
|
|
83
|
+
return
|
|
84
|
+
now = time()
|
|
85
|
+
elapsed = now - self.last_sent
|
|
86
|
+
if elapsed < self.delay:
|
|
87
|
+
# Re-schedule after the remainder of the delay window
|
|
88
|
+
deferLater(reactor, self.delay - elapsed, self._drain)
|
|
89
|
+
return
|
|
90
|
+
self.last_sent = now
|
|
91
|
+
message = self.requests_list.pop(0)
|
|
92
|
+
self.sc.chat_postMessage(channel=self.slack_channel, text=message)
|
|
93
|
+
if self.requests_list:
|
|
94
|
+
deferLater(reactor, self.delay, self._drain)
|