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.
Files changed (71) hide show
  1. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/CHANGELOG.md +30 -0
  2. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/MANIFEST.in +3 -4
  3. {pgsqlpot-2.0.0/pgsqlpot.egg-info → pgsqlpot-2.0.2}/PKG-INFO +6 -1
  4. pgsqlpot-2.0.2/core/httpclient.py +71 -0
  5. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/core/tools.py +5 -6
  6. {pgsqlpot-2.0.0/pgsqlpot → pgsqlpot-2.0.2}/honeypot.py +1 -1
  7. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/output_plugins/couch.py +5 -1
  8. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/output_plugins/datadog.py +5 -9
  9. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/output_plugins/discord.py +4 -9
  10. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/output_plugins/elastic.py +2 -0
  11. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/output_plugins/mysql.py +88 -32
  12. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/output_plugins/nlcvapi.py +4 -10
  13. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/output_plugins/postgres.py +9 -15
  14. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/output_plugins/sqlite.py +7 -11
  15. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/output_plugins/telegram.py +8 -12
  16. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/output_plugins/xmpp.py +1 -1
  17. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot/cli.py +44 -7
  18. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot/data/Dockerfile +1 -0
  19. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot/data/docs/INSTALL.md +2 -2
  20. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot/data/docs/INSTALLWIN.md +3 -3
  21. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot/data/docs/mysql/mysql.sql +6 -3
  22. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot/data/docs/postgres/postgres.sql +16 -0
  23. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot/data/docs/sqlite3/sqlite3.sql +5 -0
  24. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot/data/etc/honeypot.cfg.base +46 -25
  25. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2/pgsqlpot}/honeypot.py +1 -1
  26. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2/pgsqlpot.egg-info}/PKG-INFO +6 -1
  27. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot.egg-info/SOURCES.txt +1 -1
  28. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot.egg-info/requires.txt +5 -0
  29. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/setup.cfg +0 -3
  30. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/setup.py +9 -2
  31. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/LICENSE +0 -0
  32. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/README.md +0 -0
  33. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/core/__init__.py +0 -0
  34. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/core/config.py +0 -0
  35. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/core/logfile.py +0 -0
  36. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/core/output.py +0 -0
  37. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/core/paths.py +0 -0
  38. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/core/protocol.py +0 -0
  39. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/output_plugins/README.md +0 -0
  40. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/output_plugins/__init__.py +0 -0
  41. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/output_plugins/hpfeed.py +0 -0
  42. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/output_plugins/influx2.py +0 -0
  43. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/output_plugins/jsonlog.py +0 -0
  44. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/output_plugins/kafka.py +0 -0
  45. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/output_plugins/localsyslog.py +0 -0
  46. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/output_plugins/mongodb.py +0 -0
  47. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/output_plugins/redisdb.py +0 -0
  48. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/output_plugins/rethinkdblog.py +0 -0
  49. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/output_plugins/slack.py +0 -0
  50. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/output_plugins/socketlog.py +0 -0
  51. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/output_plugins/textlog.py +0 -0
  52. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot/__init__.py +0 -0
  53. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot/data/docs/PLUGINS.md +0 -0
  54. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot/data/docs/TODO.md +0 -0
  55. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot/data/docs/datadog/README.md +0 -0
  56. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot/data/docs/discord/README.md +0 -0
  57. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot/data/docs/geoipupdtask.ps1 +0 -0
  58. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot/data/docs/mysql/README.md +0 -0
  59. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot/data/docs/mysql/READMEWIN.md +0 -0
  60. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot/data/docs/postgres/README.md +0 -0
  61. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot/data/docs/postgres/READMEWIN.md +0 -0
  62. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot/data/docs/slack/README.md +0 -0
  63. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot/data/docs/sqlite3/README.md +0 -0
  64. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot/data/docs/sqlite3/READMEWIN.md +0 -0
  65. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot/data/docs/telegram/README.md +0 -0
  66. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot/data/etc/honeypot.cfg +0 -0
  67. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot/data/test/.gitignore +0 -0
  68. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot/data/test/test.py +0 -0
  69. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot.egg-info/dependency_links.txt +0 -0
  70. {pgsqlpot-2.0.0 → pgsqlpot-2.0.2}/pgsqlpot.egg-info/entry_points.txt +0 -0
  71. {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,9 +1,8 @@
1
- include honeypot.py
2
- include setup.cfg
1
+ include CHANGELOG.md
3
2
  include LICENSE
4
3
  include README.md
5
- include CHANGELOG.md
6
- graft pgsqlpot
4
+ include honeypot.py
7
5
  graft core
8
6
  graft output_plugins
7
+ graft pgsqlpot
9
8
  prune pgsqlpot/data/test/develop
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pgsqlpot
3
- Version: 2.0.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 urllib import urlopen
18
+ from urllib2 import urlopen # type: ignore
19
19
 
20
20
 
21
21
  if version_info[0] >= 3:
22
22
  def decode(x):
23
- return x.decode('utf-8', errors='ignore')
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
- if version_info[0] < 3:
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.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
- couchserver = Server('http://{}:{}@{}:{}'.format(username, password, host, port))
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('Datadog output module: API key is not defined.')
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 = client.Agent(reactor, contextFactory=contextFactory, pool=myQuietPool)
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
- contextFactory = WebClientContextFactory()
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 = Agent(reactor, contextFactory=contextFactory, pool=pool)
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
- _, excValue, excTraceback = exc_info()
52
+ tp, value, tb = exc_info()
46
53
  try:
47
54
  conn.rollback()
48
55
  except Exception:
49
56
  msg('Rollback failed')
50
- reraise(excValue, excTraceback)
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] not in (2003, 2006, 2013):
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
- return ConnectionPool._runInteraction(self, interaction, *args, **kw)
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=1
154
+ cp_max=cp_max,
98
155
  )
99
156
  except Error as e:
100
- self.local_log('output_mysql: MySQL Error {}: "{}"'.format(e.args[0], e.args[1]))
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
- self.local_log('Failed to open City GeoIP database {}'.format(geoipdb_city_path))
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
- self.local_log('Failed to open ASN GeoIP database {}'.format(geoipdb_asn_path))
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
- self.local_log('output_mysql: MySQL Error: {}'.format(e))
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
- 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
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, 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
- contextFactory = WebClientContextFactory()
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 = Agent(reactor, contextFactory=contextFactory, pool=pool)
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
- "SELECT id FROM {} WHERE {} = %s".format(table, column),
80
- (entry, )
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
- 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
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='data/pgsqlpot.db')
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
- id = r[0][0]
81
- else:
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'])