pgsqlpot 2.0.0__tar.gz → 2.0.2__tar.gz
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.
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/CHANGELOG.md +30 -0
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/MANIFEST.in +3 -4
- {pgsqlpot-2.0.0/pgsqlpot.egg-info → pgsqlpot-2.0.2}/PKG-INFO +6 -1
- pgsqlpot-2.0.2/core/httpclient.py +71 -0
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/core/tools.py +5 -6
- {pgsqlpot-2.0.0/pgsqlpot → pgsqlpot-2.0.2}/honeypot.py +1 -1
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/output_plugins/couch.py +5 -1
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/output_plugins/datadog.py +5 -9
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/output_plugins/discord.py +4 -9
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/output_plugins/elastic.py +2 -0
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/output_plugins/mysql.py +88 -32
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/output_plugins/nlcvapi.py +4 -10
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/output_plugins/postgres.py +9 -15
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/output_plugins/sqlite.py +7 -11
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/output_plugins/telegram.py +8 -12
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/output_plugins/xmpp.py +1 -1
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot/cli.py +44 -7
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot/data/Dockerfile +1 -0
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot/data/docs/INSTALL.md +2 -2
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot/data/docs/INSTALLWIN.md +3 -3
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot/data/docs/mysql/mysql.sql +6 -3
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot/data/docs/postgres/postgres.sql +16 -0
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot/data/docs/sqlite3/sqlite3.sql +5 -0
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot/data/etc/honeypot.cfg.base +46 -25
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2/pgsqlpot}/honeypot.py +1 -1
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2/pgsqlpot.egg-info}/PKG-INFO +6 -1
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot.egg-info/SOURCES.txt +1 -1
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot.egg-info/requires.txt +5 -0
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/setup.cfg +0 -3
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/setup.py +9 -2
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/LICENSE +0 -0
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/README.md +0 -0
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/core/__init__.py +0 -0
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/core/config.py +0 -0
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/core/logfile.py +0 -0
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/core/output.py +0 -0
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/core/paths.py +0 -0
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/core/protocol.py +0 -0
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/output_plugins/README.md +0 -0
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/output_plugins/__init__.py +0 -0
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/output_plugins/hpfeed.py +0 -0
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/output_plugins/influx2.py +0 -0
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/output_plugins/jsonlog.py +0 -0
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/output_plugins/kafka.py +0 -0
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/output_plugins/localsyslog.py +0 -0
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/output_plugins/mongodb.py +0 -0
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/output_plugins/redisdb.py +0 -0
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/output_plugins/rethinkdblog.py +0 -0
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/output_plugins/slack.py +0 -0
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/output_plugins/socketlog.py +0 -0
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/output_plugins/textlog.py +0 -0
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot/__init__.py +0 -0
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot/data/docs/PLUGINS.md +0 -0
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot/data/docs/TODO.md +0 -0
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot/data/docs/datadog/README.md +0 -0
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot/data/docs/discord/README.md +0 -0
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot/data/docs/geoipupdtask.ps1 +0 -0
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot/data/docs/mysql/README.md +0 -0
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot/data/docs/mysql/READMEWIN.md +0 -0
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot/data/docs/postgres/README.md +0 -0
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot/data/docs/postgres/READMEWIN.md +0 -0
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot/data/docs/slack/README.md +0 -0
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot/data/docs/sqlite3/README.md +0 -0
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot/data/docs/sqlite3/READMEWIN.md +0 -0
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot/data/docs/telegram/README.md +0 -0
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot/data/etc/honeypot.cfg +0 -0
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot/data/test/.gitignore +0 -0
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot/data/test/test.py +0 -0
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot.egg-info/dependency_links.txt +0 -0
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot.egg-info/entry_points.txt +0 -0
- {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot.egg-info/top_level.txt +0 -0
|
@@ -5,6 +5,36 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [2.0.2]
|
|
9
|
+
|
|
10
|
+
### Added in version 2.0.2
|
|
11
|
+
|
|
12
|
+
* Nothing
|
|
13
|
+
|
|
14
|
+
### Changed in version 2.0.2
|
|
15
|
+
|
|
16
|
+
* Increased the version number
|
|
17
|
+
* The `restart` command wasn't working correctly on Windows due to a race
|
|
18
|
+
condition. Fixed.
|
|
19
|
+
* Fixed a problem in the MySQL plugin that made it unresponsive under high
|
|
20
|
+
traffic
|
|
21
|
+
|
|
22
|
+
## [2.0.1]
|
|
23
|
+
|
|
24
|
+
### Added in version 2.0.1
|
|
25
|
+
|
|
26
|
+
* Nothing
|
|
27
|
+
|
|
28
|
+
### Changed in version 2.0.1
|
|
29
|
+
|
|
30
|
+
* Increased the version number
|
|
31
|
+
* The `datadog`, `discord`, `nlcvapi`, and `telegram` plugins now use a secure
|
|
32
|
+
connection (HTTPS) by default
|
|
33
|
+
* The `elastic` plugin now warns if the `ssl` is set while certificate
|
|
34
|
+
verification (`verify_certs`) is off
|
|
35
|
+
* The `couch` plugin now uses authentication mechanism that does not pass the
|
|
36
|
+
username and password in the URL
|
|
37
|
+
|
|
8
38
|
## [2.0.0]
|
|
9
39
|
|
|
10
40
|
### Added in version 2.0.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pgsqlpot
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.2
|
|
4
4
|
Summary: A PostgeSQL Honeypot
|
|
5
5
|
Home-page: https://gitlab.com/bontchev/pgsqlpot
|
|
6
6
|
Author: Vesselin Bontchev
|
|
@@ -43,11 +43,13 @@ Requires-Dist: twisted>=21; python_version >= "3"
|
|
|
43
43
|
Provides-Extra: couchdb
|
|
44
44
|
Requires-Dist: couchdb; extra == "couchdb"
|
|
45
45
|
Provides-Extra: datadog
|
|
46
|
+
Requires-Dist: certifi; extra == "datadog"
|
|
46
47
|
Requires-Dist: cryptography<=2.8; python_version < "3" and extra == "datadog"
|
|
47
48
|
Requires-Dist: pyOpenSSL<=18.0.0; python_version < "3" and extra == "datadog"
|
|
48
49
|
Requires-Dist: cryptography; python_version >= "3" and extra == "datadog"
|
|
49
50
|
Requires-Dist: pyOpenSSL; python_version >= "3" and extra == "datadog"
|
|
50
51
|
Provides-Extra: discord
|
|
52
|
+
Requires-Dist: certifi; extra == "discord"
|
|
51
53
|
Provides-Extra: elastic
|
|
52
54
|
Requires-Dist: elasticsearch<=7.13; python_version < "3" and extra == "elastic"
|
|
53
55
|
Requires-Dist: numpy<=1.16.6; python_version < "3" and extra == "elastic"
|
|
@@ -71,6 +73,7 @@ Provides-Extra: mysql
|
|
|
71
73
|
Requires-Dist: PyMySQL; python_version < "3" and extra == "mysql"
|
|
72
74
|
Requires-Dist: mysqlclient>=1.3.12; python_version >= "3" and extra == "mysql"
|
|
73
75
|
Provides-Extra: nlcvapi
|
|
76
|
+
Requires-Dist: certifi; extra == "nlcvapi"
|
|
74
77
|
Requires-Dist: pyOpenSSL<=18.0.0; python_version < "3" and extra == "nlcvapi"
|
|
75
78
|
Requires-Dist: pyOpenSSL; python_version >= "3" and extra == "nlcvapi"
|
|
76
79
|
Provides-Extra: postgres
|
|
@@ -87,12 +90,14 @@ Requires-Dist: slack-sdk; python_version >= "3" and extra == "slack"
|
|
|
87
90
|
Provides-Extra: socketlog
|
|
88
91
|
Provides-Extra: sqlite
|
|
89
92
|
Provides-Extra: telegram
|
|
93
|
+
Requires-Dist: certifi; extra == "telegram"
|
|
90
94
|
Provides-Extra: textlog
|
|
91
95
|
Provides-Extra: xmpp
|
|
92
96
|
Requires-Dist: xmpppy>=0.7.3; extra == "xmpp"
|
|
93
97
|
Provides-Extra: all
|
|
94
98
|
Requires-Dist: Automat<20; python_version < "3" and extra == "all"
|
|
95
99
|
Requires-Dist: PyMySQL; python_version < "3" and extra == "all"
|
|
100
|
+
Requires-Dist: certifi; extra == "all"
|
|
96
101
|
Requires-Dist: confluent-kafka; python_version >= "3" and extra == "all"
|
|
97
102
|
Requires-Dist: confluent-kafka<1.0; python_version < "3" and extra == "all"
|
|
98
103
|
Requires-Dist: couchdb; extra == "all"
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from __future__ import absolute_import
|
|
2
|
+
|
|
3
|
+
from io import open
|
|
4
|
+
|
|
5
|
+
from twisted.internet.ssl import Certificate, trustRootFromCertificates
|
|
6
|
+
from twisted.python.log import msg
|
|
7
|
+
from twisted.web.client import Agent, BrowserLikePolicyForHTTPS
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class _LegacyWebClientContextFactory(object):
|
|
11
|
+
@staticmethod
|
|
12
|
+
def build():
|
|
13
|
+
from twisted.internet.ssl import ClientContextFactory
|
|
14
|
+
|
|
15
|
+
class WebClientContextFactory(ClientContextFactory):
|
|
16
|
+
def getContext(self, hostname, port):
|
|
17
|
+
return ClientContextFactory.getContext(self)
|
|
18
|
+
|
|
19
|
+
return WebClientContextFactory()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _load_trust_root_from_pem_bundle(pem_path):
|
|
23
|
+
with open(pem_path, 'r', encoding='utf-8') as fh:
|
|
24
|
+
content = fh.read()
|
|
25
|
+
certs = []
|
|
26
|
+
end_marker = '-----END CERTIFICATE-----'
|
|
27
|
+
for chunk in content.split(end_marker):
|
|
28
|
+
chunk = chunk.strip()
|
|
29
|
+
if not chunk:
|
|
30
|
+
continue
|
|
31
|
+
pem = chunk + '\n' + end_marker + '\n'
|
|
32
|
+
certs.append(Certificate.loadPEM(pem))
|
|
33
|
+
if not certs:
|
|
34
|
+
return None
|
|
35
|
+
return trustRootFromCertificates(certs)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _trust_root(ca_certs):
|
|
39
|
+
if ca_certs:
|
|
40
|
+
try:
|
|
41
|
+
return _load_trust_root_from_pem_bundle(ca_certs)
|
|
42
|
+
except Exception as e:
|
|
43
|
+
msg('Failed to load CA bundle {}: {}'.format(ca_certs, e))
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
import certifi
|
|
48
|
+
return _load_trust_root_from_pem_bundle(certifi.where())
|
|
49
|
+
except Exception:
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def create_http_agent(reactor, pool, plugin_name, verify_tls=True, ca_certs=None):
|
|
54
|
+
"""
|
|
55
|
+
Build a Twisted Agent with sane HTTPS defaults.
|
|
56
|
+
verify_tls=True uses BrowserLikePolicyForHTTPS for cert + hostname checks.
|
|
57
|
+
verify_tls=False falls back to a legacy context factory for compatibility.
|
|
58
|
+
"""
|
|
59
|
+
if verify_tls:
|
|
60
|
+
return Agent(
|
|
61
|
+
reactor,
|
|
62
|
+
contextFactory=BrowserLikePolicyForHTTPS(trustRoot=_trust_root(ca_certs)),
|
|
63
|
+
pool=pool
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
msg('{}: TLS certificate verification is disabled.'.format(plugin_name))
|
|
67
|
+
return Agent(
|
|
68
|
+
reactor,
|
|
69
|
+
contextFactory=_LegacyWebClientContextFactory.build(),
|
|
70
|
+
pool=pool
|
|
71
|
+
)
|
|
@@ -15,12 +15,14 @@ from twisted.python.log import msg
|
|
|
15
15
|
try:
|
|
16
16
|
from urllib.request import urlopen
|
|
17
17
|
except ImportError:
|
|
18
|
-
from
|
|
18
|
+
from urllib2 import urlopen # type: ignore
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
if version_info[0] >= 3:
|
|
22
22
|
def decode(x):
|
|
23
|
-
|
|
23
|
+
if isinstance(x, bytes):
|
|
24
|
+
return x.decode('utf-8', errors='ignore')
|
|
25
|
+
return x
|
|
24
26
|
def encode(x):
|
|
25
27
|
return x.encode()
|
|
26
28
|
def ord(x):
|
|
@@ -85,10 +87,7 @@ def stop_plugins(cfg):
|
|
|
85
87
|
|
|
86
88
|
def get_public_ip(ip_reporter):
|
|
87
89
|
try:
|
|
88
|
-
|
|
89
|
-
return urlopen(ip_reporter).read().decode('latin1', errors='replace').encode('utf-8')
|
|
90
|
-
else:
|
|
91
|
-
return decode(urlopen(ip_reporter).read())
|
|
90
|
+
return decode(urlopen(ip_reporter, timeout=10).read())
|
|
92
91
|
except:
|
|
93
92
|
return None
|
|
94
93
|
|
|
@@ -23,7 +23,7 @@ from twisted.internet.reactor import listenTCP, run
|
|
|
23
23
|
from twisted.python.log import msg
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
__VERSION__ = '2.0.
|
|
26
|
+
__VERSION__ = '2.0.2'
|
|
27
27
|
__description__ = 'A PostgreSQL Honeypot'
|
|
28
28
|
__license__ = 'GPLv3'
|
|
29
29
|
__uri__ = 'https://gitlab.com/bontchev/pgsqlpot'
|
|
@@ -13,6 +13,7 @@ from twisted.python.log import msg
|
|
|
13
13
|
class Output(output.Output):
|
|
14
14
|
|
|
15
15
|
def start(self):
|
|
16
|
+
scheme = CONFIG.get('output_couch', 'scheme', fallback='http')
|
|
16
17
|
host = CONFIG.get('output_couch', 'host', fallback='localhost')
|
|
17
18
|
port = CONFIG.getint('output_couch', 'port', fallback=5984)
|
|
18
19
|
username = CONFIG.get('output_couch', 'username', fallback='pgsqlpot', raw=True)
|
|
@@ -20,7 +21,10 @@ class Output(output.Output):
|
|
|
20
21
|
db_name = CONFIG.get('output_couch', 'database', fallback='pgsqlpot')
|
|
21
22
|
|
|
22
23
|
try:
|
|
23
|
-
|
|
24
|
+
base_url = '{}://{}:{}'.format(scheme, host, port)
|
|
25
|
+
couchserver = Server(base_url)
|
|
26
|
+
if username:
|
|
27
|
+
couchserver.resource.credentials = (username, password)
|
|
24
28
|
|
|
25
29
|
if db_name in couchserver:
|
|
26
30
|
self.couch_db = couchserver[db_name]
|
|
@@ -10,18 +10,13 @@ from platform import node
|
|
|
10
10
|
|
|
11
11
|
from core import output
|
|
12
12
|
from core.config import CONFIG
|
|
13
|
+
from core.httpclient import create_http_agent
|
|
13
14
|
from core.tools import to_bytes
|
|
14
15
|
|
|
15
16
|
from twisted.internet import reactor
|
|
16
17
|
from twisted.python.log import msg
|
|
17
18
|
from twisted.web import client, http_headers
|
|
18
19
|
from twisted.web.client import FileBodyProducer
|
|
19
|
-
from twisted.internet.ssl import ClientContextFactory
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class WebClientContextFactory(ClientContextFactory):
|
|
23
|
-
def getContext(self, hostname, port):
|
|
24
|
-
return ClientContextFactory.getContext(self)
|
|
25
20
|
|
|
26
21
|
|
|
27
22
|
class QuietHTTP11ClientFactory(client._HTTP11ClientFactory):
|
|
@@ -33,16 +28,17 @@ class Output(output.Output):
|
|
|
33
28
|
self.url = CONFIG.get('output_datadog', 'url')
|
|
34
29
|
self.api_key = CONFIG.get('output_datadog', 'api_key', fallback='')
|
|
35
30
|
if len(self.api_key) == 0:
|
|
36
|
-
msg('
|
|
31
|
+
msg('output_datadog: API key is not defined.')
|
|
37
32
|
self.ddsource = CONFIG.get('output_datadog', 'ddsource', fallback='pgsqlpot')
|
|
38
33
|
self.ddtags = CONFIG.get('output_datadog', 'ddtags', fallback='env:dev')
|
|
39
34
|
self.service = CONFIG.get('output_datadog', 'service', fallback='honeypot')
|
|
40
35
|
self.hostname = CONFIG.get('output_datadog', 'hostname', fallback=node())
|
|
36
|
+
verify_tls = CONFIG.getboolean('output_datadog', 'verify_tls', fallback=True)
|
|
37
|
+
ca_certs = CONFIG.get('output_datadog', 'ca_certs', fallback=None)
|
|
41
38
|
|
|
42
|
-
contextFactory = WebClientContextFactory()
|
|
43
39
|
myQuietPool = client.HTTPConnectionPool(reactor)
|
|
44
40
|
myQuietPool._factory = QuietHTTP11ClientFactory
|
|
45
|
-
self.agent =
|
|
41
|
+
self.agent = create_http_agent(reactor, myQuietPool, 'output_datadog', verify_tls, ca_certs)
|
|
46
42
|
|
|
47
43
|
def stop(self):
|
|
48
44
|
pass
|
|
@@ -13,14 +13,13 @@ from time import gmtime, strftime, time
|
|
|
13
13
|
|
|
14
14
|
from core import output
|
|
15
15
|
from core.config import CONFIG
|
|
16
|
+
from core.httpclient import create_http_agent
|
|
16
17
|
from core.tools import decode, to_bytes
|
|
17
18
|
|
|
18
19
|
from twisted.internet import reactor
|
|
19
|
-
from twisted.internet.ssl import ClientContextFactory
|
|
20
20
|
from twisted.internet.task import deferLater
|
|
21
21
|
from twisted.python.log import msg
|
|
22
22
|
from twisted.web.client import (
|
|
23
|
-
Agent,
|
|
24
23
|
FileBodyProducer,
|
|
25
24
|
HTTPConnectionPool,
|
|
26
25
|
_HTTP11ClientFactory,
|
|
@@ -29,11 +28,6 @@ from twisted.web.client import (
|
|
|
29
28
|
from twisted.web.http_headers import Headers
|
|
30
29
|
|
|
31
30
|
|
|
32
|
-
class WebClientContextFactory(ClientContextFactory):
|
|
33
|
-
def getContext(self, hostname, port):
|
|
34
|
-
return ClientContextFactory.getContext(self)
|
|
35
|
-
|
|
36
|
-
|
|
37
31
|
class QuietHTTP11ClientFactory(_HTTP11ClientFactory):
|
|
38
32
|
noisy = False
|
|
39
33
|
|
|
@@ -43,10 +37,11 @@ class Output(output.Output):
|
|
|
43
37
|
def start(self):
|
|
44
38
|
self.url = to_bytes(CONFIG.get('output_discord', 'url'))
|
|
45
39
|
self.delay = CONFIG.getfloat('output_discord', 'delay', fallback=2.0)
|
|
46
|
-
|
|
40
|
+
verify_tls = CONFIG.getboolean('output_discord', 'verify_tls', fallback=True)
|
|
41
|
+
ca_certs = CONFIG.get('output_discord', 'ca_certs', fallback=None)
|
|
47
42
|
pool = HTTPConnectionPool(reactor)
|
|
48
43
|
pool._factory = QuietHTTP11ClientFactory
|
|
49
|
-
self.agent =
|
|
44
|
+
self.agent = create_http_agent(reactor, pool, 'output_discord', verify_tls, ca_certs)
|
|
50
45
|
self.last_sent = 0
|
|
51
46
|
self.requests_list = []
|
|
52
47
|
|
|
@@ -44,6 +44,8 @@ class Output(output.Output):
|
|
|
44
44
|
kwargs['verify_certs'] = verify_certs
|
|
45
45
|
if verify_certs and ca_certs:
|
|
46
46
|
kwargs['ca_certs'] = ca_certs
|
|
47
|
+
elif not verify_certs:
|
|
48
|
+
msg('output_elastic: SSL is enabled but certificate verification is disabled.')
|
|
47
49
|
|
|
48
50
|
# Create client
|
|
49
51
|
self.es = Elasticsearch(hosts=hosts, **kwargs)
|
|
@@ -24,9 +24,16 @@ except ImportError:
|
|
|
24
24
|
from _mysql_exceptions import (Error, OperationalError) # type: ignore
|
|
25
25
|
|
|
26
26
|
from twisted.enterprise.adbapi import ConnectionPool
|
|
27
|
-
from twisted.python.compat import reraise
|
|
28
27
|
from twisted.python.log import msg
|
|
29
28
|
|
|
29
|
+
if version_info[0] >= 3:
|
|
30
|
+
def _reraise(tp, value, tb):
|
|
31
|
+
raise value.with_traceback(tb)
|
|
32
|
+
else:
|
|
33
|
+
exec("""def _reraise(tp, value, tb):
|
|
34
|
+
raise tp, value, tb
|
|
35
|
+
""")
|
|
36
|
+
|
|
30
37
|
|
|
31
38
|
class ReconnectingConnectionPool(ConnectionPool):
|
|
32
39
|
"""
|
|
@@ -42,14 +49,27 @@ class ReconnectingConnectionPool(ConnectionPool):
|
|
|
42
49
|
def _runInteraction(self, interaction, *args, **kw):
|
|
43
50
|
|
|
44
51
|
def rerise_exception(conn):
|
|
45
|
-
|
|
52
|
+
tp, value, tb = exc_info()
|
|
46
53
|
try:
|
|
47
54
|
conn.rollback()
|
|
48
55
|
except Exception:
|
|
49
56
|
msg('Rollback failed')
|
|
50
|
-
|
|
57
|
+
_reraise(tp, value, tb)
|
|
58
|
+
|
|
59
|
+
conn = self.connect()
|
|
60
|
+
|
|
61
|
+
# Ping the connection before use. This transparently handles stale
|
|
62
|
+
# connections closed by MySQL's wait_timeout (default 8 hours): if the
|
|
63
|
+
# server closed our idle connection, ping(True) reconnects immediately.
|
|
64
|
+
# If MySQL is genuinely unreachable, ping(True) raises OperationalError
|
|
65
|
+
# within connect_timeout seconds, which is caught below and re-raised
|
|
66
|
+
# so the Deferred errback fires and the worker thread is freed promptly.
|
|
67
|
+
try:
|
|
68
|
+
conn.ping(True)
|
|
69
|
+
except Exception:
|
|
70
|
+
self.disconnect(conn)
|
|
71
|
+
raise
|
|
51
72
|
|
|
52
|
-
conn = self.connectionFactory(self)
|
|
53
73
|
trans = self.transactionFactory(self, conn)
|
|
54
74
|
try:
|
|
55
75
|
result = interaction(trans, *args, **kw)
|
|
@@ -57,12 +77,10 @@ class ReconnectingConnectionPool(ConnectionPool):
|
|
|
57
77
|
conn.commit()
|
|
58
78
|
return result
|
|
59
79
|
except OperationalError as e:
|
|
60
|
-
if e.args[0]
|
|
61
|
-
rerise_exception(conn)
|
|
62
|
-
else:
|
|
80
|
+
if e.args[0] in (2003, 2006, 2013):
|
|
63
81
|
conn = self.connections.get(self.threadID())
|
|
64
82
|
self.disconnect(conn)
|
|
65
|
-
|
|
83
|
+
rerise_exception(conn)
|
|
66
84
|
except Exception:
|
|
67
85
|
rerise_exception(conn)
|
|
68
86
|
|
|
@@ -73,6 +91,35 @@ class Output(output.Output):
|
|
|
73
91
|
if self.debug:
|
|
74
92
|
msg(message)
|
|
75
93
|
|
|
94
|
+
def _enqueue(self, label, d):
|
|
95
|
+
"""
|
|
96
|
+
Wrap a runInteraction() deferred with two targeted diagnostics:
|
|
97
|
+
|
|
98
|
+
1. Pending counter + threshold warning: if the worker threads are stuck
|
|
99
|
+
the counter climbs without ever coming back down. A warning fires
|
|
100
|
+
once per threshold crossing so the log stays quiet under normal load
|
|
101
|
+
but shouts when something is wrong.
|
|
102
|
+
|
|
103
|
+
2. Always-on errback: any DB error that would previously have been
|
|
104
|
+
silently swallowed now produces an explicit log line.
|
|
105
|
+
"""
|
|
106
|
+
self._pending += 1
|
|
107
|
+
|
|
108
|
+
def on_success(result):
|
|
109
|
+
self._pending -= 1
|
|
110
|
+
return result
|
|
111
|
+
|
|
112
|
+
def on_failure(failure):
|
|
113
|
+
self._pending -= 1
|
|
114
|
+
msg('output_mysql: runInteraction {} failed: {}'.format(label, failure))
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
d.addCallback(on_success)
|
|
118
|
+
d.addErrback(on_failure)
|
|
119
|
+
|
|
120
|
+
if self._pending > self._pending_warn_threshold:
|
|
121
|
+
msg('output_mysql: WARNING - {} interactions pending, worker thread may be stuck'.format(self._pending))
|
|
122
|
+
|
|
76
123
|
def start(self):
|
|
77
124
|
host = CONFIG.get('output_mysql', 'host', fallback='localhost')
|
|
78
125
|
database = CONFIG.get('output_mysql', 'database', fallback='pgsqlpot')
|
|
@@ -82,6 +129,13 @@ class Output(output.Output):
|
|
|
82
129
|
|
|
83
130
|
self.debug = CONFIG.getboolean('output_mysql', 'debug', fallback=False)
|
|
84
131
|
self.geoip = CONFIG.getboolean('output_mysql', 'geoip', fallback=True)
|
|
132
|
+
self._pending = 0
|
|
133
|
+
self._pending_warn_threshold = CONFIG.getint('output_mysql', 'pending_warn_threshold', fallback=100)
|
|
134
|
+
|
|
135
|
+
connect_timeout = CONFIG.getint('output_mysql', 'connect_timeout', fallback=10)
|
|
136
|
+
read_timeout = CONFIG.getint('output_mysql', 'read_timeout', fallback=30)
|
|
137
|
+
write_timeout = CONFIG.getint('output_mysql', 'write_timeout', fallback=30)
|
|
138
|
+
cp_max = CONFIG.getint('output_mysql', 'cp_max', fallback=5)
|
|
85
139
|
|
|
86
140
|
try:
|
|
87
141
|
self.dbh = ReconnectingConnectionPool(
|
|
@@ -93,11 +147,14 @@ class Output(output.Output):
|
|
|
93
147
|
port=port,
|
|
94
148
|
charset='utf8',
|
|
95
149
|
use_unicode=True,
|
|
150
|
+
connect_timeout=connect_timeout,
|
|
151
|
+
read_timeout=read_timeout,
|
|
152
|
+
write_timeout=write_timeout,
|
|
96
153
|
cp_min=1,
|
|
97
|
-
cp_max=
|
|
154
|
+
cp_max=cp_max,
|
|
98
155
|
)
|
|
99
156
|
except Error as e:
|
|
100
|
-
|
|
157
|
+
msg('output_mysql: MySQL Error {}: "{}"'.format(e.args[0], e.args[1]))
|
|
101
158
|
|
|
102
159
|
if self.geoip:
|
|
103
160
|
geoipdb_city_path = CONFIG.get('output_mysql', 'geoip_citydb', fallback='data/GeoLite2-City.mmdb')
|
|
@@ -106,13 +163,14 @@ class Output(output.Output):
|
|
|
106
163
|
self.reader_city = Reader(geoipdb_city_path)
|
|
107
164
|
except Exception:
|
|
108
165
|
self.reader_city = None
|
|
109
|
-
|
|
166
|
+
msg('output_mysql: Failed to open City GeoIP database {}'.format(geoipdb_city_path))
|
|
110
167
|
|
|
111
168
|
try:
|
|
112
169
|
self.reader_asn = Reader(geoipdb_asn_path)
|
|
113
170
|
except Exception:
|
|
114
171
|
self.reader_asn = None
|
|
115
|
-
|
|
172
|
+
msg('output_mysql: Failed to open ASN GeoIP database {}'.format(geoipdb_asn_path))
|
|
173
|
+
|
|
116
174
|
|
|
117
175
|
def stop(self):
|
|
118
176
|
if self.geoip:
|
|
@@ -120,7 +178,6 @@ class Output(output.Output):
|
|
|
120
178
|
self.reader_city.close()
|
|
121
179
|
if self.reader_asn is not None:
|
|
122
180
|
self.reader_asn.close()
|
|
123
|
-
|
|
124
181
|
def write(self, event):
|
|
125
182
|
"""
|
|
126
183
|
TODO: Check if the type (date, datetime or timestamp) of columns is appropriate for your needs and timezone
|
|
@@ -130,7 +187,8 @@ class Output(output.Output):
|
|
|
130
187
|
and back from UTC to the current time zone for retrieval.
|
|
131
188
|
(This does not occur for other types such as DATETIME.)"
|
|
132
189
|
"""
|
|
133
|
-
self.dbh.runInteraction(self.connect_event, event)
|
|
190
|
+
self._enqueue('connect_event', self.dbh.runInteraction(self.connect_event, event))
|
|
191
|
+
|
|
134
192
|
|
|
135
193
|
def simple_query(self, txn, sql, args):
|
|
136
194
|
if self.debug:
|
|
@@ -145,22 +203,19 @@ class Output(output.Output):
|
|
|
145
203
|
txn.execute(sql)
|
|
146
204
|
result = txn.fetchall()
|
|
147
205
|
except Exception as e:
|
|
148
|
-
|
|
206
|
+
msg('output_mysql: MySQL Error: {}'.format(e))
|
|
149
207
|
result = None
|
|
150
208
|
return result
|
|
151
209
|
|
|
152
210
|
def get_id(self, txn, table, column, entry):
|
|
211
|
+
# INSERT IGNORE silently skips the insert when a UNIQUE constraint would
|
|
212
|
+
# be violated, so the subsequent SELECT always finds exactly one row
|
|
213
|
+
# regardless of whether a concurrent call already inserted it.
|
|
214
|
+
self.simple_query(txn, "INSERT IGNORE INTO `{}` (`{}`) VALUES (%s)".format(table, column), (entry, ))
|
|
153
215
|
r = self.simple_query(txn, "SELECT `id` FROM `{}` WHERE `{}` = %s".format(table, column), (entry, ))
|
|
154
216
|
if r:
|
|
155
|
-
|
|
156
|
-
|
|
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
|
|
217
|
+
return r[0][0]
|
|
218
|
+
return 0
|
|
164
219
|
|
|
165
220
|
def connect_event(self, txn, event):
|
|
166
221
|
remote_ip = event['src_ip']
|
|
@@ -197,14 +252,15 @@ class Output(output.Output):
|
|
|
197
252
|
|
|
198
253
|
if self.geoip:
|
|
199
254
|
country, country_code, city, org, asn_num = geolocate(remote_ip, self.reader_city, self.reader_asn)
|
|
255
|
+
# INSERT IGNORE rather than ON DUPLICATE KEY UPDATE: geolocation
|
|
256
|
+
# data for an IP rarely changes, so skipping the update on
|
|
257
|
+
# subsequent hits is acceptable. More importantly, ON DUPLICATE
|
|
258
|
+
# KEY UPDATE takes an exclusive row lock on the existing row,
|
|
259
|
+
# causing InnoDB lock contention when multiple threads process
|
|
260
|
+
# connections from the same IP simultaneously.
|
|
261
|
+
# INSERT IGNORE avoids that lock entirely.
|
|
200
262
|
self.simple_query(txn, """
|
|
201
|
-
INSERT INTO `geolocation` (`ip`, `country_name`, `country_iso_code`, `city_name`, `org`, `org_asn`)
|
|
263
|
+
INSERT IGNORE INTO `geolocation` (`ip`, `country_name`, `country_iso_code`, `city_name`, `org`, `org_asn`)
|
|
202
264
|
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
265
|
""",
|
|
210
|
-
(remote_ip, country, country_code, city, org, asn_num,
|
|
266
|
+
(remote_ip, country, country_code, city, org, asn_num, ))
|
|
@@ -8,15 +8,14 @@ from json import dumps, loads
|
|
|
8
8
|
|
|
9
9
|
from core import output
|
|
10
10
|
from core.config import CONFIG
|
|
11
|
+
from core.httpclient import create_http_agent
|
|
11
12
|
from core.tools import decode, geolocate, to_bytes
|
|
12
13
|
|
|
13
14
|
from geoip2.database import Reader
|
|
14
15
|
|
|
15
16
|
from twisted.internet import reactor
|
|
16
|
-
from twisted.internet.ssl import ClientContextFactory
|
|
17
17
|
from twisted.python.log import msg
|
|
18
18
|
from twisted.web.client import (
|
|
19
|
-
Agent,
|
|
20
19
|
FileBodyProducer,
|
|
21
20
|
HTTPConnectionPool,
|
|
22
21
|
_HTTP11ClientFactory,
|
|
@@ -25,12 +24,6 @@ from twisted.web.client import (
|
|
|
25
24
|
from twisted.web.http_headers import Headers
|
|
26
25
|
|
|
27
26
|
|
|
28
|
-
class WebClientContextFactory(ClientContextFactory):
|
|
29
|
-
|
|
30
|
-
def getContext(self, hostname, port):
|
|
31
|
-
return ClientContextFactory.getContext(self)
|
|
32
|
-
|
|
33
|
-
|
|
34
27
|
class QuietHTTP11ClientFactory(_HTTP11ClientFactory):
|
|
35
28
|
noisy = False
|
|
36
29
|
|
|
@@ -40,10 +33,11 @@ class Output(output.Output):
|
|
|
40
33
|
def start(self):
|
|
41
34
|
self.host = CONFIG.get('output_nlcvapi', 'host', fallback='https://api.nlcv.bas.bg/v1.0/honeypot')
|
|
42
35
|
self.geoip = CONFIG.getboolean('output_nlcvapi', 'geoip', fallback=True)
|
|
43
|
-
|
|
36
|
+
verify_tls = CONFIG.getboolean('output_nlcvapi', 'verify_tls', fallback=True)
|
|
37
|
+
ca_certs = CONFIG.get('output_nlcvapi', 'ca_certs', fallback=None)
|
|
44
38
|
pool = HTTPConnectionPool(reactor)
|
|
45
39
|
pool._factory = QuietHTTP11ClientFactory
|
|
46
|
-
self.agent =
|
|
40
|
+
self.agent = create_http_agent(reactor, pool, 'output_nlcvapi', verify_tls, ca_certs)
|
|
47
41
|
|
|
48
42
|
if self.geoip:
|
|
49
43
|
geoipdb_city_path = CONFIG.get('output_nlcvapi', 'geoip_citydb', fallback='data/GeoLite2-City.mmdb')
|
|
@@ -74,24 +74,18 @@ class Output(output.Output):
|
|
|
74
74
|
return result
|
|
75
75
|
|
|
76
76
|
def get_id(self, txn, table, column, entry):
|
|
77
|
+
# ON CONFLICT ... DO UPDATE is a deliberate no-op that makes RETURNING
|
|
78
|
+
# yield the existing row's id even when the INSERT is skipped due to a
|
|
79
|
+
# unique-constraint conflict. DO NOTHING would leave RETURNING empty.
|
|
77
80
|
r = self.simple_query(
|
|
78
81
|
txn,
|
|
79
|
-
"
|
|
80
|
-
(
|
|
81
|
-
|
|
82
|
+
"INSERT INTO {} ({}) VALUES (%s) "
|
|
83
|
+
"ON CONFLICT ({}) DO UPDATE SET {} = EXCLUDED.{} "
|
|
84
|
+
"RETURNING id".format(table, column, column, column, column),
|
|
85
|
+
(entry, ))
|
|
82
86
|
if r:
|
|
83
|
-
|
|
84
|
-
|
|
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
|
|
87
|
+
return int(r[0])
|
|
88
|
+
return 0
|
|
95
89
|
|
|
96
90
|
def connect_event(self, txn, event):
|
|
97
91
|
remote_ip = event['src_ip']
|
|
@@ -15,7 +15,7 @@ from twisted.python.log import msg
|
|
|
15
15
|
class Output(output.Output):
|
|
16
16
|
|
|
17
17
|
def start(self):
|
|
18
|
-
db_name = CONFIG.get('output_sqlite', 'db_file', fallback='
|
|
18
|
+
db_name = CONFIG.get('output_sqlite', 'db_file', fallback='log/pgsqlpot.db')
|
|
19
19
|
self.debug = CONFIG.getboolean('output_sqlite', 'debug', fallback=False)
|
|
20
20
|
self.geoip = CONFIG.getboolean('output_sqlite', 'geoip', fallback=True)
|
|
21
21
|
|
|
@@ -75,17 +75,13 @@ class Output(output.Output):
|
|
|
75
75
|
return result
|
|
76
76
|
|
|
77
77
|
def get_id(self, txn, table, column, entry):
|
|
78
|
+
# INSERT OR IGNORE silently skips the insert when a UNIQUE constraint
|
|
79
|
+
# would be violated, so the subsequent SELECT always finds one row.
|
|
80
|
+
self.simple_query(txn, "INSERT OR IGNORE INTO `{}` (`{}`) VALUES (?)".format(table, column), (entry, ))
|
|
78
81
|
r = self.simple_query(txn, "SELECT `id` FROM `{}` WHERE `{}` = ?".format(table, column), (entry, ))
|
|
79
82
|
if r:
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
self.simple_query(txn, "INSERT INTO `{}` (`{}`) VALUES (?)".format(table, column), (entry, ))
|
|
83
|
-
r = self.simple_query(txn, 'SELECT LAST_INSERT_ROWID()', ())
|
|
84
|
-
if r:
|
|
85
|
-
id = int(r[0][0])
|
|
86
|
-
else:
|
|
87
|
-
id = 0
|
|
88
|
-
return id
|
|
83
|
+
return r[0][0]
|
|
84
|
+
return 0
|
|
89
85
|
|
|
90
86
|
def connect_event(self, txn, event):
|
|
91
87
|
remote_ip = event['src_ip']
|
|
@@ -100,7 +96,7 @@ class Output(output.Output):
|
|
|
100
96
|
""",
|
|
101
97
|
(event['session'], event['unixtime'], operation_id, remote_ip, event['src_port'],
|
|
102
98
|
event['dst_ip'], event['dst_port'], sensor_id, ))
|
|
103
|
-
|
|
99
|
+
|
|
104
100
|
if event['operation'].lower() == 'login':
|
|
105
101
|
usr_id = self.get_id(txn, 'usernames', 'username', event['username'])
|
|
106
102
|
pwd_id = self.get_id(txn, 'passwords', 'password', event['password'])
|