CitrixPot: NetScaler / Citrix ADC Gateway honeypot (initial public release)
This commit is contained in:
commit
4491b42714
54 files changed
+4532
No files matched your search
+92
@@ -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()
|
||||
Reference in new issue
Block a user