Files
CoM-c2itall/heartbeat/heartbeat_ingest.py
T
n0mad1k 1d95a868a0 Fix 11 bugs found in full codebase review
- deploy_phishing.py: undefined extra_vars on orchestration playbook
- freebird/main_menu.py: set_variable() called as function, is a method; fix to config.prompt_set_variable()
- freebird/tool_manager.py: shell=True with f-string paths; replace with direct pip binary call
- certipy-enum.py: password exposed in process list and logs; pass via env var
- cleanup_engine.py: load_vars_file() called with path string, expects provider name
- ioc-u.py: scapy import not guarded; tool crashed if scapy missing
- um_ops.py + um-vault.py: SQL table names f-stringed directly; quote identifiers
- ops-logger.sh: 63-line duplicate function block; removed second copy; quote config_script var
- recon_tools.py: ir-sandbox + link-sandbox not in TOOLS dict; forensics menu option 5 hidden from user
- um-crack.py: missing --scope flag inconsistent with all other Umbra tools
- heartbeat_ingest.py: openssl subprocess not catching FileNotFoundError; wrap with clear error
2026-04-01 22:11:01 -04:00

168 lines
5.7 KiB
Python

#!/usr/bin/env python3
"""heartbeat_ingest.py — HTTP(S) server that receives heartbeat POSTs and writes to engagement state.
Writes heartbeats to: ~/.umbra/engagements/{engagement}/heartbeats/{hostname}.json
Usage:
python heartbeat_ingest.py <engagement> [--port 8443] [--bind 127.0.0.1]
python heartbeat_ingest.py <engagement> --tls --cert cert.pem --key key.pem
python heartbeat_ingest.py <engagement> --auth-token SECRET
"""
import argparse
import hashlib
import hmac
import json
import os
import ssl
import subprocess
import sys
import tempfile
from http.server import HTTPServer, BaseHTTPRequestHandler
UMBRA_HOME = os.path.expanduser("~/.umbra")
ENGAGEMENTS_DIR = os.path.join(UMBRA_HOME, "engagements")
# Will be set by main()
_engagement_name = ""
_heartbeat_dir = ""
_auth_token = ""
class HeartbeatHandler(BaseHTTPRequestHandler):
"""Handle POST /hb with JSON heartbeat payload."""
def _check_auth(self):
"""Validate auth token if configured."""
if not _auth_token:
return True
token = self.headers.get("X-Auth-Token", "")
return hmac.compare_digest(token, _auth_token)
def do_POST(self):
if not self._check_auth():
self.send_response(403)
self.end_headers()
self.wfile.write(b"forbidden")
return
try:
length = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(length)
data = json.loads(body)
hostname = data.get("hostname", "unknown").replace("/", "_").replace("..", "_")
hb_path = os.path.join(_heartbeat_dir, f"{hostname}.json")
with open(hb_path, "w") as f:
json.dump(data, f, indent=2)
self.send_response(200)
self.end_headers()
self.wfile.write(b"ok")
print(f" [+] {data.get('ts', '?')} heartbeat from {hostname} ({data.get('ip', '?')})")
except (json.JSONDecodeError, KeyError) as e:
self.send_response(400)
self.end_headers()
self.wfile.write(f"bad request: {e}".encode())
except Exception as e:
self.send_response(500)
self.end_headers()
self.wfile.write(f"error: {e}".encode())
def do_GET(self):
"""Health check endpoint."""
if not self._check_auth():
self.send_response(403)
self.end_headers()
self.wfile.write(b"forbidden")
return
self.send_response(200)
self.end_headers()
self.wfile.write(json.dumps({"status": "ok", "engagement": _engagement_name}).encode())
def log_message(self, fmt, *args):
"""Suppress default access logging."""
pass
def generate_self_signed_cert(cert_path, key_path):
"""Generate a self-signed certificate for TLS."""
try:
subprocess.run([
"openssl", "req", "-x509", "-newkey", "rsa:2048",
"-keyout", key_path, "-out", cert_path,
"-days", "365", "-nodes",
"-subj", "/CN=heartbeat-ingest",
], check=True, capture_output=True)
except FileNotFoundError:
raise RuntimeError("openssl not found — install it or provide --cert/--key manually")
print(f"[*] Generated self-signed cert: {cert_path}")
def main():
global _engagement_name, _heartbeat_dir, _auth_token
parser = argparse.ArgumentParser(description="Heartbeat ingest server")
parser.add_argument("engagement", help="Engagement name")
parser.add_argument("--port", "-p", type=int, default=8443, help="Listen port (default: 8443)")
parser.add_argument("--bind", "-b", default="127.0.0.1", help="Bind address (default: 127.0.0.1)")
parser.add_argument("--auth-token", default=os.environ.get("UMBRA_HB_TOKEN", ""),
help="Shared secret for auth (X-Auth-Token header)")
parser.add_argument("--tls", action="store_true", help="Enable TLS")
parser.add_argument("--cert", default="", help="TLS certificate path (PEM)")
parser.add_argument("--key", default="", help="TLS private key path (PEM)")
args = parser.parse_args()
_engagement_name = args.engagement.strip().replace(" ", "_")
_heartbeat_dir = os.path.join(ENGAGEMENTS_DIR, _engagement_name, "heartbeats")
os.makedirs(_heartbeat_dir, exist_ok=True)
_auth_token = args.auth_token
# Also ensure engagement dir exists
eng_dir = os.path.join(ENGAGEMENTS_DIR, _engagement_name)
os.makedirs(eng_dir, exist_ok=True)
server = HTTPServer((args.bind, args.port), HeartbeatHandler)
# TLS setup
if args.tls:
cert_path = args.cert
key_path = args.key
# Auto-generate self-signed cert if none provided
if not cert_path or not key_path:
cert_dir = os.path.join(eng_dir, "tls")
os.makedirs(cert_dir, exist_ok=True)
cert_path = os.path.join(cert_dir, "heartbeat.crt")
key_path = os.path.join(cert_dir, "heartbeat.key")
if not os.path.exists(cert_path):
generate_self_signed_cert(cert_path, key_path)
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ctx.load_cert_chain(cert_path, key_path)
server.socket = ctx.wrap_socket(server.socket, server_side=True)
proto = "https"
else:
proto = "http"
print(f"[*] Heartbeat ingest listening on {args.bind}:{args.port} ({proto})")
print(f"[*] Engagement: {_engagement_name}")
print(f"[*] Writing to: {_heartbeat_dir}")
print(f"[*] Auth: {'enabled' if _auth_token else 'disabled'}")
print(f"[*] Endpoint: POST {proto}://<this-host>:{args.port}/hb")
print()
try:
server.serve_forever()
except KeyboardInterrupt:
print("\n[*] Stopped")
server.server_close()
if __name__ == "__main__":
main()