Skip to content

Commit 8009bc3

Browse files
authored
[DEV] Database model revamp
Merge pull request #15 from MatrixEditor/dev/db-revamp
2 parents 01d1a69 + 6354df1 commit 8009bc3

37 files changed

+937
-365
lines changed

dementor/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,5 @@
1818
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
1919
# SOFTWARE.
2020

21-
__version__ = "1.0.0.dev13"
21+
__version__ = "1.0.0.dev14"
2222
__author__ = "MatrixEditor"

dementor/assets/Dementor.toml

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -186,10 +186,10 @@ UPnP = true
186186
# These extra fields (e.g., `foo`, `bar`) will be passed to the final
187187
# FilterObj instance and may be used for protocol-specific handling.
188188

189-
# AnswerTo = []
189+
# Target = []
190190

191191
# Describes a list of hosts to *exclude* from poisoning (blacklist approach).
192-
# Uses the same filter structure as `AnswerTo`.
192+
# Uses the same filter structure as Target.
193193

194194
# Ignore = []
195195

@@ -205,16 +205,10 @@ UPnP = true
205205

206206
DuplicateCreds = true
207207

208-
# Custom database filename (overrides default "Dementor.db"). Do not put any
209-
# path information here.
210-
211-
# Name = "database.db"
212-
213-
# Custom path where the database file will be stored. If set, it overrides the
214-
# workspace directory.
215-
216-
# Directory = "/path/to/directory/"
217-
208+
# Dialect = "sqlite"
209+
# Driver = "pysqlite"
210+
# Url = "sqlite:///:memory:"
211+
# Path = "Dementor.db"
218212

219213
# =============================================================================
220214
# mDNS

dementor/config/session.py

Lines changed: 88 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,43 @@
1717
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
1818
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
1919
# SOFTWARE.
20+
# pyright: reportUninitializedInstanceVariable=false
2021
import asyncio
22+
import typing
2123

22-
from typing import Any, List
23-
24+
from typing import Any
2425
from pathlib import Path
2526

2627
from dementor.config.toml import TomlConfig, Attribute
2728
from dementor.config.util import is_true
2829
from dementor.paths import DEMENTOR_PATH
2930
from dementor import config
3031

32+
if typing.TYPE_CHECKING:
33+
from dementor.protocols import (
34+
kerberos,
35+
mdns,
36+
netbios,
37+
llmnr,
38+
ldap,
39+
smb,
40+
smtp,
41+
ftp,
42+
http,
43+
imap,
44+
ipp,
45+
mssql,
46+
mysql,
47+
pop3,
48+
quic,
49+
ssdp,
50+
upnp,
51+
x11,
52+
)
53+
from dementor.protocols.msrpc import rpc
54+
from dementor.db.model import DementorDB
55+
from dementor.db.connector import DatabaseConfig
56+
3157

3258
class SessionConfig(TomlConfig):
3359
_section_ = "Dementor"
@@ -64,14 +90,66 @@ class SessionConfig(TomlConfig):
6490
]
6591

6692
# TODO: move into .pyi
67-
db: Any
68-
db_config: Any
69-
krb5_config: Any
70-
mdns_config: Any
71-
llmnr_config: Any
72-
quic_config: Any
73-
netbiosns_config: Any
74-
ldap_config: List[Any]
93+
if typing.TYPE_CHECKING:
94+
workspace_path: str
95+
extra_modules: list[str]
96+
ipv6: str | None
97+
ipv4: str | None
98+
interface: str | None
99+
protocols: dict[str, Any]
100+
101+
db: DementorDB
102+
db_config: DatabaseConfig
103+
krb5_config: kerberos.KerberosConfig
104+
mdns_config: mdns.MDNSConfig
105+
llmnr_config: llmnr.LLMNRConfig
106+
netbiosns_config: netbios.NBTNSConfig
107+
ldap_config: list[ldap.LDAPServerConfig]
108+
smtp_servers: list[smtp.SMTPServerConfig]
109+
smb_config: list[smb.SMBServerConfig]
110+
ftp_config: list[ftp.FTPServerConfig]
111+
proxy_config: http.ProxyAutoConfig
112+
http_config: list[http.HTTPServerConfig]
113+
winrm_config: list[http.HTTPServerConfig]
114+
imap_config: list[imap.IMAPServerConfig]
115+
ipp_config: ipp.IPPConfig
116+
rpc_config: rpc.RPCConfig
117+
mssql_config: mssql.MSSQLConfig
118+
ssrp_config: mssql.SSRPConfig
119+
mysql_config: mysql.MySQLConfig
120+
pop3_config: list[pop3.POP3ServerConfig]
121+
quic_config: quic.QuicServerConfig
122+
ssdp_config: ssdp.SSDPConfig
123+
upnp_config: upnp.UPNPConfig
124+
x11_config: x11.X11Config
125+
126+
ntlm_challange: bytes
127+
ntlm_ess: bool
128+
analysis: bool
129+
loop: asyncio.AbstractEventLoop
130+
131+
llmnr_enabled: bool
132+
nbtns_enabled: bool
133+
nbtds_enabled: bool
134+
smtp_enabled: bool
135+
smb_enabled: bool
136+
ftp_enabled: bool
137+
kdc_enabled: bool
138+
ldap_enabled: bool
139+
quic_enabled: bool
140+
mdns_enabled: bool
141+
http_enabled: bool
142+
rpc_enabled: bool
143+
winrm_enabled: bool
144+
mssql_enabled: bool
145+
ssrp_enabled: bool
146+
imap_enabled: bool
147+
pop3_enabled: bool
148+
mysql_enabled: bool
149+
x11_enabled: bool
150+
ipp_enabled: bool
151+
ssdp_enabled: bool
152+
upnp_enabled: bool
75153

76154
def __init__(self) -> None:
77155
super().__init__(config._get_global_config().get("Dementor", {}))
@@ -90,9 +168,6 @@ def __init__(self) -> None:
90168
self.ntlm_challange = b"1337LEET"
91169
self.ntlm_ess = True
92170

93-
# SMB configuration
94-
self.smb_server_config = []
95-
96171
def is_bound_to_all(self) -> bool:
97172
# REVISIT: this should raise an exception
98173
return self.interface == "ALL"

dementor/config/toml.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,12 @@
1717
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
1818
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
1919
# SOFTWARE.
20-
from typing import NamedTuple, Callable, Any
20+
from typing import NamedTuple, Callable, Any, TypeVar
2121

2222
from dementor.config.util import get_value
2323

2424
_LOCAL = object()
25+
_T = TypeVar("_T", bound="TomlConfig")
2526

2627

2728
class Attribute(NamedTuple):
@@ -36,7 +37,7 @@ class TomlConfig:
3637
_section_: str | None
3738
_fields_: list[Attribute]
3839

39-
def __init__(self, config: dict | None = None) -> None:
40+
def __init__(self, config: dict[str, Any] | None = None) -> None:
4041
for field in self._fields_:
4142
self._set_field(
4243
config or {},
@@ -62,8 +63,12 @@ def __getitem__(self, key: str) -> Any:
6263
raise KeyError(f"Could not find config with key {key!r}")
6364

6465
@staticmethod
65-
def build_config(cls_ty, section: str | None = None) -> Any:
66-
return cls_ty(get_value(section or cls_ty._section_, key=None, default={}))
66+
def build_config(cls_ty: type[_T], section: str | None = None) -> _T:
67+
section_name = section or cls_ty._section_
68+
if section_name is None:
69+
raise ValueError("section cannot be None")
70+
71+
return cls_ty(get_value(section_name, key=None, default={}))
6772

6873
def _set_field(
6974
self,

dementor/config/util.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,10 @@ def is_true(value: str) -> bool:
5353

5454

5555
class BytesValue:
56-
def __init__(self, length=None) -> None:
57-
self.length = length
56+
def __init__(self, length: int | None = None) -> None:
57+
self.length: int | None = length
5858

59-
def __call__(self, value) -> Any:
59+
def __call__(self, value: Any) -> bytes:
6060
match value:
6161
case None:
6262
return secrets.token_bytes(self.length or 1)
@@ -78,7 +78,7 @@ def random_value(size: int) -> str:
7878
return "".join(random.choice(string.ascii_letters) for _ in range(size))
7979

8080

81-
def format_string(value: str, locals: dict | None = None) -> str:
81+
def format_string(value: str, locals: dict[str, Any] | None = None) -> str:
8282
config = _get_global_config()
8383
try:
8484
template = _SANDBOX.from_string(value)

dementor/db/__init__.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Copyright (c) 2025-Present MatrixEditor
2+
#
3+
# Permission is hereby granted, free of charge, to any person obtaining a copy
4+
# of this software and associated documentation files (the "Software"), to deal
5+
# in the Software without restriction, including without limitation the rights
6+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
# copies of the Software, and to permit persons to whom the Software is
8+
# furnished to do so, subject to the following conditions:
9+
#
10+
# The above copyright notice and this permission notice shall be included in all
11+
# copies or substantial portions of the Software.
12+
#
13+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19+
# SOFTWARE.
20+
21+
_CLEARTEXT = "Cleartext"
22+
_NO_USER = "<missing-user>"
23+
24+
25+
def normalize_client_address(client: str) -> str:
26+
return client.removeprefix("::ffff:")
27+

dementor/db/connector.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# Copyright (c) 2025-Present MatrixEditor
2+
#
3+
# Permission is hereby granted, free of charge, to any person obtaining a copy
4+
# of this software and associated documentation files (the "Software"), to deal
5+
# in the Software without restriction, including without limitation the rights
6+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
# copies of the Software, and to permit persons to whom the Software is
8+
# furnished to do so, subject to the following conditions:
9+
#
10+
# The above copyright notice and this permission notice shall be included in all
11+
# copies or substantial portions of the Software.
12+
#
13+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19+
# SOFTWARE.
20+
import sqlite3
21+
from sqlalchemy import Engine, create_engine
22+
23+
from dementor.config.session import SessionConfig
24+
from dementor.db.model import DementorDB, ModelBase
25+
from dementor.log.logger import dm_logger
26+
from dementor.config.toml import TomlConfig, Attribute as A
27+
28+
29+
class DatabaseConfig(TomlConfig):
30+
_section_ = "DB"
31+
_fields_ = [
32+
A("db_raw_path", "Url", None),
33+
A("db_path", "Path", "Dementor.db"),
34+
A("db_duplicate_creds", "DuplicateCreds", False),
35+
A("db_dialect", "Dialect", None),
36+
A("db_driver", "Driver", None),
37+
]
38+
39+
40+
def init_dementor_db(session: SessionConfig) -> Engine | None:
41+
engine = init_engine(session)
42+
if engine is not None:
43+
ModelBase.metadata.create_all(engine)
44+
return engine
45+
46+
47+
def init_engine(session: SessionConfig) -> Engine | None:
48+
# based on dialect and driver configuration
49+
raw_path = session.db_config.db_raw_path
50+
if raw_path is None:
51+
# fall back to constructing the path manually
52+
dialect = session.db_config.db_dialect or "sqlite"
53+
driver = session.db_config.db_driver or "pysqlite"
54+
path = session.db_config.db_path
55+
if not path:
56+
return dm_logger.error("Database path not specified!")
57+
58+
if dialect == "sqlite":
59+
if path != ":memory:":
60+
path = f"/{session.resolve_path(path)}"
61+
62+
# see https://docs.sqlalchemy.org/en/20/core/engines.html#database-urls
63+
raw_path = f"{dialect}+{driver}://{path}"
64+
else:
65+
sql_type, path = raw_path.split("://")
66+
if sql_type.count("+") > 0:
67+
dialect, driver = sql_type.split("+")
68+
else:
69+
dialect = sql_type
70+
driver = "<default>"
71+
72+
if dialect != "sqlite":
73+
first_element, *parts = path.split("/")
74+
if "@" in first_element:
75+
first_element = first_element.split("@")[1]
76+
path = "***:***@" + "/".join([first_element] + list(parts))
77+
78+
dm_logger.debug("Using database [%s:%s] at: %s", dialect, driver, path)
79+
return create_engine(raw_path, isolation_level="AUTOCOMMIT", future=True)
80+
81+
82+
def create_db(session: SessionConfig) -> DementorDB:
83+
# TODO: add support for custom database implementations
84+
engine = init_engine(session)
85+
if not engine:
86+
raise Exception("Failed to create database engine")
87+
return DementorDB(engine, session)

0 commit comments

Comments
 (0)