Refactor: implemented Repository Factory and Async Mutator Engine. Decoupled storage logic and enforced Dependency Injection across CLI and Web API. Updated documentation.
Some checks failed
CI / Lint (ruff) (push) Successful in 12s
CI / SAST (bandit) (push) Successful in 13s
CI / Dependency audit (pip-audit) (push) Successful in 22s
CI / Test (Standard) (3.11) (push) Failing after 54s
CI / Test (Standard) (3.12) (push) Successful in 1m35s
CI / Test (Live) (3.11) (push) Has been skipped
CI / Test (Fuzz) (3.11) (push) Has been skipped
CI / Merge dev → testing (push) Has been skipped
CI / Prepare Merge to Main (push) Has been skipped
CI / Finalize Merge to Main (push) Has been skipped
Some checks failed
CI / Lint (ruff) (push) Successful in 12s
CI / SAST (bandit) (push) Successful in 13s
CI / Dependency audit (pip-audit) (push) Successful in 22s
CI / Test (Standard) (3.11) (push) Failing after 54s
CI / Test (Standard) (3.12) (push) Successful in 1m35s
CI / Test (Live) (3.11) (push) Has been skipped
CI / Test (Fuzz) (3.11) (push) Has been skipped
CI / Merge dev → testing (push) Has been skipped
CI / Prepare Merge to Main (push) Has been skipped
CI / Finalize Merge to Main (push) Has been skipped
This commit is contained in:
@@ -29,12 +29,12 @@ def _log(event_type: str, severity: int = 6, **kwargs) -> None:
|
||||
def _setup_bait_fs() -> str:
|
||||
bait_dir = Path("/tmp/ftp_bait")
|
||||
bait_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
(bait_dir / "backup.tar.gz").write_bytes(b"\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x03\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00")
|
||||
(bait_dir / "db_dump.sql").write_text("CREATE TABLE users (id INT, username VARCHAR(50), password VARCHAR(50));\nINSERT INTO users VALUES (1, 'admin', 'pbkdf2:sha256:5000$...');\n")
|
||||
(bait_dir / "config.ini").write_text("[database]\nuser = dbadmin\npassword = db_super_admin_pass_!\nhost = localhost\n")
|
||||
(bait_dir / "credentials.txt").write_text("admin:super_secret_admin_pw\nroot:toor\nalice:wonderland\n")
|
||||
|
||||
|
||||
return str(bait_dir)
|
||||
|
||||
class ServerFTP(FTP):
|
||||
|
||||
@@ -97,7 +97,7 @@ def _publish(topic: str, value: str, retain: bool = True) -> bytes:
|
||||
payload = str(value).encode()
|
||||
fixed = 0x31 if retain else 0x30
|
||||
remaining = len(topic_len) + len(topic_bytes) + len(payload)
|
||||
|
||||
|
||||
# variable length encoding
|
||||
rem_bytes = []
|
||||
while remaining > 0:
|
||||
@@ -108,7 +108,7 @@ def _publish(topic: str, value: str, retain: bool = True) -> bytes:
|
||||
rem_bytes.append(encoded)
|
||||
if not rem_bytes:
|
||||
rem_bytes = [0]
|
||||
|
||||
|
||||
return bytes([fixed]) + bytes(rem_bytes) + topic_len + topic_bytes + payload
|
||||
|
||||
|
||||
@@ -132,7 +132,7 @@ def _generate_topics() -> dict:
|
||||
return topics
|
||||
except Exception as e:
|
||||
_log("config_error", severity=4, error=str(e))
|
||||
|
||||
|
||||
if MQTT_PERSONA == "water_plant":
|
||||
topics.update({
|
||||
"plant/water/tank1/level": f"{random.uniform(60.0, 80.0):.1f}",
|
||||
@@ -186,7 +186,7 @@ class MQTTProtocol(asyncio.Protocol):
|
||||
pkt_type = (pkt_byte >> 4) & 0x0f
|
||||
flags = pkt_byte & 0x0f
|
||||
qos = (flags >> 1) & 0x03
|
||||
|
||||
|
||||
# Decode remaining length (variable-length encoding)
|
||||
pos = 1
|
||||
remaining = 0
|
||||
@@ -225,7 +225,7 @@ class MQTTProtocol(asyncio.Protocol):
|
||||
packet_id, subs = _parse_subscribe(payload)
|
||||
granted_qos = [1] * len(subs) # grant QoS 1 for all
|
||||
self._transport.write(_suback(packet_id, granted_qos))
|
||||
|
||||
|
||||
# Immediately send retained publishes matching topics
|
||||
for sub_topic, _ in subs:
|
||||
_log("subscribe", src=self._peer[0], topics=[sub_topic])
|
||||
@@ -245,11 +245,11 @@ class MQTTProtocol(asyncio.Protocol):
|
||||
topic, packet_id, data = _parse_publish(payload, qos)
|
||||
# Attacker command received!
|
||||
_log("publish", src=self._peer[0], topic=topic, payload=data.decode(errors="replace"))
|
||||
|
||||
|
||||
if qos == 1:
|
||||
puback = bytes([0x40, 0x02]) + struct.pack(">H", packet_id)
|
||||
self._transport.write(puback)
|
||||
|
||||
|
||||
elif pkt_type == 12: # PINGREQ
|
||||
self._transport.write(b"\xd0\x00") # PINGRESP
|
||||
elif pkt_type == 14: # DISCONNECT
|
||||
|
||||
@@ -156,7 +156,7 @@ class RedisProtocol(asyncio.Protocol):
|
||||
elif pattern != '*':
|
||||
pat = pattern.encode()
|
||||
keys = [k for k in keys if k == pat]
|
||||
|
||||
|
||||
resp = f"*{len(keys)}\r\n".encode() + b"".join(_bulk(k.decode()) for k in keys)
|
||||
self._transport.write(resp)
|
||||
elif verb == "GET":
|
||||
|
||||
@@ -45,7 +45,7 @@ def _log(event_type: str, severity: int = 6, **kwargs) -> None:
|
||||
def _rand_msg_id() -> str:
|
||||
"""Return a Postfix-style 12-char alphanumeric queue ID."""
|
||||
chars = string.ascii_uppercase + string.digits
|
||||
return "".join(random.choices(chars, k=12)) # noqa: S311
|
||||
return "".join(random.choices(chars, k=12))
|
||||
|
||||
|
||||
def _decode_auth_plain(blob: str) -> tuple[str, str]:
|
||||
|
||||
@@ -153,11 +153,11 @@ def _parse_snmp(data: bytes):
|
||||
# PDU type (0xa0 = GetRequest, 0xa1 = GetNextRequest)
|
||||
if pos >= len(data):
|
||||
raise ValueError("Missing PDU type")
|
||||
|
||||
|
||||
pdu_type = data[pos]
|
||||
if pdu_type not in (0xa0, 0xa1):
|
||||
raise ValueError(f"Invalid PDU type {pdu_type}")
|
||||
|
||||
|
||||
pos += 1
|
||||
_, pos = _read_ber_length(data, pos)
|
||||
# request-id
|
||||
|
||||
@@ -155,13 +155,13 @@ def write_syslog_file(line: str) -> None:
|
||||
"""Append a syslog line to the rotating log file."""
|
||||
try:
|
||||
_get_file_logger().info(line)
|
||||
|
||||
|
||||
# Also parse and write JSON log
|
||||
import json
|
||||
import re
|
||||
from datetime import datetime
|
||||
from typing import Optional, Any
|
||||
|
||||
from typing import Optional
|
||||
|
||||
_RFC5424_RE: re.Pattern = re.compile(
|
||||
r"^<\d+>1 "
|
||||
r"(\S+) " # 1: TIMESTAMP
|
||||
@@ -174,7 +174,7 @@ def write_syslog_file(line: str) -> None:
|
||||
_SD_BLOCK_RE: re.Pattern = re.compile(r'\[decnet@55555\s+(.*?)\]', re.DOTALL)
|
||||
_PARAM_RE: re.Pattern = re.compile(r'(\w+)="((?:[^"\\]|\\.)*)"')
|
||||
_IP_FIELDS: tuple[str, ...] = ("src_ip", "src", "client_ip", "remote_ip", "ip")
|
||||
|
||||
|
||||
_m: Optional[re.Match] = _RFC5424_RE.match(line)
|
||||
if _m:
|
||||
_ts_raw: str
|
||||
@@ -183,10 +183,10 @@ def write_syslog_file(line: str) -> None:
|
||||
_event_type: str
|
||||
_sd_rest: str
|
||||
_ts_raw, _decky, _service, _event_type, _sd_rest = _m.groups()
|
||||
|
||||
|
||||
_fields: dict[str, str] = {}
|
||||
_msg: str = ""
|
||||
|
||||
|
||||
if _sd_rest.startswith("-"):
|
||||
_msg = _sd_rest[1:].lstrip()
|
||||
elif _sd_rest.startswith("["):
|
||||
@@ -194,27 +194,27 @@ def write_syslog_file(line: str) -> None:
|
||||
if _block:
|
||||
for _k, _v in _PARAM_RE.findall(_block.group(1)):
|
||||
_fields[_k] = _v.replace('\\"', '"').replace("\\\\", "\\").replace("\\]", "]")
|
||||
|
||||
|
||||
# extract msg after the block
|
||||
_msg_match: Optional[re.Match] = re.search(r'\]\s+(.+)$', _sd_rest)
|
||||
if _msg_match:
|
||||
_msg = _msg_match.group(1).strip()
|
||||
else:
|
||||
_msg = _sd_rest
|
||||
|
||||
|
||||
_attacker_ip: str = "Unknown"
|
||||
for _fname in _IP_FIELDS:
|
||||
if _fname in _fields:
|
||||
_attacker_ip = _fields[_fname]
|
||||
break
|
||||
|
||||
|
||||
# Parse timestamp to normalize it
|
||||
_ts_formatted: str
|
||||
try:
|
||||
_ts_formatted = datetime.fromisoformat(_ts_raw).strftime("%Y-%m-%d %H:%M:%S")
|
||||
except ValueError:
|
||||
_ts_formatted = _ts_raw
|
||||
|
||||
|
||||
_payload: dict[str, Any] = {
|
||||
"timestamp": _ts_formatted,
|
||||
"decky": _decky,
|
||||
@@ -226,7 +226,7 @@ def write_syslog_file(line: str) -> None:
|
||||
"raw_line": line
|
||||
}
|
||||
_get_json_logger().info(json.dumps(_payload))
|
||||
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
Reference in New Issue
Block a user