CitrixPot: NetScaler / Citrix ADC Gateway honeypot (initial public release)

This commit is contained in:
CitrixPot committed 2026-09-28 19:44:37 +02:00
commit 4491b42714
54 files changed
+4532

No files matched your search

+92
View File
@@ -0,0 +1,92 @@
"""Local append-only event journal and inert, content-addressed evidence."""
import datetime
import hashlib
import json
import os
import pathlib
import threading
import time
class EvidenceError(OSError):
pass
class Evidence:
def __init__(self, directory, quota=1024 ** 3):
self.directory = pathlib.Path(directory)
self.directory.mkdir(parents=True, exist_ok=True, mode=0o700)
self.artifacts = self.directory / 'artifacts'
self.artifacts.mkdir(exist_ok=True, mode=0o700)
os.chmod(self.directory, 0o700)
os.chmod(self.artifacts, 0o700)
self.lock = threading.RLock()
self.quota = quota
self.used = sum(p.stat().st_size for p in self.directory.rglob('*') if p.is_file())
self.journal = os.open(self.directory / 'events.jsonl', os.O_WRONLY | os.O_APPEND | os.O_CREAT | os.O_NOFOLLOW, 0o600)
os.fchmod(self.journal, 0o600)
def reserve(self, size):
if self.used + size > self.quota:
raise OSError('Evidence quota reached; preserve/archive evidence before restarting')
def emit(self, event, **data):
now = time.time_ns()
entry = dict(schema_version=2, event=event, time_ns=now,
timestamp=datetime.datetime.fromtimestamp(now / 1e9, datetime.timezone.utc).isoformat(timespec='microseconds'),
**data)
raw = (json.dumps(entry, ensure_ascii=True) + '\n').encode()
with self.lock:
self.reserve(len(raw))
with os.fdopen(os.dup(self.journal), 'ab') as output:
output.write(raw)
output.flush()
os.fsync(output.fileno())
self.used += len(raw)
def save(self, data):
digest = hashlib.sha256(data).hexdigest()
path = self.artifacts / (digest + '.bin')
with self.lock:
if not path.exists():
self.reserve(len(data))
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
try:
with os.fdopen(fd, 'wb') as output:
output.write(data)
output.flush()
os.fsync(output.fileno())
except BaseException:
path.unlink(missing_ok=True) # do not leave a partial blob with a full-content hash
raise
self.used += len(data)
return dict(sha256=digest, bytes=len(data), artifact='artifacts/' + path.name)
def close(self):
os.close(self.journal)
class RecordingReader:
"""Retain only bytes consumed by the HTTP parser, after TLS decryption."""
def __init__(self, source, limit):
self.source, self.limit = source, limit
self.data = bytearray()
self.truncated = False
def record(self, data):
room = max(0, self.limit - len(self.data))
self.data.extend(data[:room])
self.truncated |= len(data) > room
return data
def read(self, *args):
return self.record(self.source.read(*args))
def read1(self, *args):
return self.record(self.source.read1(*args))
def readline(self, *args):
return self.record(self.source.readline(*args))
def close(self):
self.source.close()