Encrypt sensitive credential fields before event bus emission (#215)

- Add encrypt_credential_dict() and decrypt_credential_field() to utils/crypto.py
- Create utils/credential_encryption.py with encryption/decryption helpers
- Add get_credential_encryption_key() to retrieve key from Infisical at runtime
- Add emit_credential_found() wrapper for modules to use instead of direct bus.emit()
- Update all 15 modules emitting CREDENTIAL_FOUND to use encrypted wrapper
- Update 4 modules consuming CREDENTIAL_FOUND to decrypt payload before processing
- Sensitive fields (username, password, hash, token, etc.) encrypted with AES-256-GCM
- Falls back to plaintext if encryption key unavailable or encryption fails
This commit is contained in:
Cobra
2026-04-06 11:43:46 -04:00
parent f15b8994cd
commit edf2ea7c36
18 changed files with 256 additions and 63 deletions
+2 -1
View File
@@ -20,6 +20,7 @@ from typing import Optional
from modules.base import BaseModule
from utils.bettercap_api import BettercapAPI
from utils.credential_encryption import emit_credential_found
logger = logging.getLogger("sensor.active.bettercap_mgr")
@@ -477,7 +478,7 @@ class BettercapManager(BaseModule):
"credential_value": data.get("password", data.get("hash", "")),
"raw_data": data,
}
self.bus.emit("CREDENTIAL_FOUND", payload, source_module=self.name)
emit_credential_found(self.bus, self.name, payload)
logger.info(
"Credential captured: %s@%s (%s)",
payload["username"], payload["target_ip"], payload["target_service"],
+1
View File
@@ -23,6 +23,7 @@ from pathlib import Path
from typing import Optional
from modules.base import BaseModule
from utils.credential_encryption import emit_credential_found
logger = logging.getLogger("sensor.active.evil_twin")
+1
View File
@@ -20,6 +20,7 @@ from pathlib import Path
from typing import Optional
from modules.base import BaseModule
from utils.credential_encryption import emit_credential_found
logger = logging.getLogger("sensor.active.mitmproxy_mgr")
+1
View File
@@ -18,6 +18,7 @@ import time
from typing import Optional
from modules.base import BaseModule
from utils.credential_encryption import emit_credential_found
logger = logging.getLogger("sensor.active.ntlm_relay")
+17 -24
View File
@@ -20,6 +20,7 @@ from pathlib import Path
from typing import Optional
from modules.base import BaseModule
from utils.credential_encryption import emit_credential_found
logger = logging.getLogger("sensor.active.responder_mgr")
@@ -403,31 +404,23 @@ class ResponderManager(BaseModule):
with self._hashes_lock:
self._captured_hashes.append(hash_entry)
self.bus.emit(
"CREDENTIAL_FOUND",
{
"source_module": self.name,
"source_ip": "",
"target_service": "responder",
"username": username,
"domain": domain,
"credential_type": hash_type,
"credential_value": line,
"hashcat_mode": hashcat_mode,
},
source_module=self.name,
)
emit_credential_found(self.bus, self.name, {
"source_module": self.name,
"source_ip": "",
"target_service": "responder",
"username": username,
"domain": domain,
"credential_type": hash_type,
"credential_value": line,
"hashcat_mode": hashcat_mode,
})
logger.info("Hash captured: %s\\%s (%s)", domain, username, hash_type)
def _process_cleartext_line(self, line: str) -> None:
"""Process a cleartext credential line from Responder session log."""
self.bus.emit(
"CREDENTIAL_FOUND",
{
"source_module": self.name,
"target_service": "responder_cleartext",
"credential_type": "cleartext",
"credential_value": line,
},
source_module=self.name,
)
emit_credential_found(self.bus, self.name, {
"source_module": self.name,
"target_service": "responder_cleartext",
"credential_type": "cleartext",
"credential_value": line,
})
+1
View File
@@ -29,6 +29,7 @@ from typing import Optional
from modules.base import BaseModule
from core.bus import Event
from utils.credential_encryption import emit_credential_found
logger = logging.getLogger("sensor.connectivity.data_exfil")
+3
View File
@@ -18,6 +18,7 @@ from collections import defaultdict
from typing import Optional
from modules.base import BaseModule
from utils.credential_encryption import emit_credential_found
logger = logging.getLogger("sensor.intel.change_detector")
@@ -362,6 +363,8 @@ class ChangeDetector(BaseModule):
def _on_credential_found(self, event) -> None:
"""Monitor for unusual authentication patterns."""
p = event.payload
from utils.credential_encryption import decrypt_credential_payload
p = decrypt_credential_payload(p)
# Track rapid auth failures from unknown sources as investigation indicator
if p.get("cred_type") == "auth_failure":
src = p.get("source_ip", "")
+3
View File
@@ -17,6 +17,7 @@ import time
from typing import Optional
from modules.base import BaseModule
from utils.credential_encryption import emit_credential_found
logger = logging.getLogger("sensor.intel.credential_db")
@@ -179,6 +180,8 @@ class CredentialDB(BaseModule):
def _on_credential_found(self, event) -> None:
"""Handle CREDENTIAL_FOUND events from any capture module."""
p = event.payload
from utils.credential_encryption import decrypt_credential_payload
p = decrypt_credential_payload(p)
self._ingest_credential(
source_module=event.source_module or p.get("source_module", "unknown"),
source_ip=p.get("source_ip", ""),
+23 -22
View File
@@ -17,6 +17,7 @@ import time
from typing import Optional
from modules.base import BaseModule
from utils.credential_encryption import emit_credential_found
logger = logging.getLogger("sensor.intel.tool_output_parser")
@@ -255,7 +256,7 @@ class ToolOutputParser(BaseModule):
self._emitted_hashes.add(hash_key)
self._creds_parsed += 1
self.bus.emit("CREDENTIAL_FOUND", {
emit_credential_found(self.bus, self.name, {
"service": "smb",
"username": username,
"domain": domain,
@@ -265,7 +266,7 @@ class ToolOutputParser(BaseModule):
"source_ip": "",
"target_ip": "",
"notes": f"Captured by Responder from {os.path.basename(path)}",
}, source_module=self.name)
})
def _parse_responder_session(self, path: str) -> None:
"""Parse Responder-Session.log for cleartext credentials."""
@@ -287,7 +288,7 @@ class ToolOutputParser(BaseModule):
self._emitted_hashes.add(hash_key)
self._creds_parsed += 1
self.bus.emit("CREDENTIAL_FOUND", {
emit_credential_found(self.bus, self.name, {
"service": protocol,
"username": username,
"domain": "",
@@ -297,7 +298,7 @@ class ToolOutputParser(BaseModule):
"source_ip": source_ip,
"target_ip": "",
"notes": "Cleartext capture by Responder",
}, source_module=self.name)
})
# Also check for HTTP basic auth in session log
for match in _RESPONDER_HTTP_RE.finditer(new_data):
@@ -310,7 +311,7 @@ class ToolOutputParser(BaseModule):
self._emitted_hashes.add(hash_key)
self._creds_parsed += 1
self.bus.emit("CREDENTIAL_FOUND", {
emit_credential_found(self.bus, self.name, {
"service": "http",
"username": username,
"domain": "",
@@ -320,7 +321,7 @@ class ToolOutputParser(BaseModule):
"source_ip": "",
"target_ip": "",
"notes": "HTTP Basic Auth captured by Responder",
}, source_module=self.name)
})
# ------------------------------------------------------------------
# bettercap parser
@@ -441,7 +442,7 @@ class ToolOutputParser(BaseModule):
self._emitted_hashes.add(hash_key)
self._creds_parsed += 1
self.bus.emit("CREDENTIAL_FOUND", {
emit_credential_found(self.bus, self.name, {
"service": protocol,
"username": "",
"domain": "",
@@ -450,7 +451,7 @@ class ToolOutputParser(BaseModule):
"source_ip": source,
"target_ip": target,
"notes": "Captured by bettercap",
}, source_module=self.name)
})
return
hash_key = f"bettercap:{protocol}:{username}:{password[:16]}"
@@ -459,7 +460,7 @@ class ToolOutputParser(BaseModule):
self._emitted_hashes.add(hash_key)
self._creds_parsed += 1
self.bus.emit("CREDENTIAL_FOUND", {
emit_credential_found(self.bus, self.name, {
"service": protocol,
"username": username,
"domain": "",
@@ -469,7 +470,7 @@ class ToolOutputParser(BaseModule):
"source_ip": source,
"target_ip": target,
"notes": "Captured by bettercap sniffer",
}, source_module=self.name)
})
def _handle_bettercap_host(self, data: dict) -> None:
"""Extract host info from a bettercap endpoint event."""
@@ -526,7 +527,7 @@ class ToolOutputParser(BaseModule):
if hash_key not in self._emitted_hashes:
self._emitted_hashes.add(hash_key)
self._creds_parsed += 1
self.bus.emit("CREDENTIAL_FOUND", {
emit_credential_found(self.bus, self.name, {
"service": f"http://{host}",
"username": username,
"domain": "",
@@ -536,7 +537,7 @@ class ToolOutputParser(BaseModule):
"source_ip": source_ip,
"target_ip": host,
"notes": "HTTP Basic Auth from traffic",
}, source_module=self.name)
})
except Exception:
pass
return
@@ -553,7 +554,7 @@ class ToolOutputParser(BaseModule):
# Check if it looks like a JWT
cred_type = "jwt" if token.count(".") == 2 else "bearer_token"
self.bus.emit("CREDENTIAL_FOUND", {
emit_credential_found(self.bus, self.name, {
"service": f"http://{host}",
"username": "",
"domain": "",
@@ -563,7 +564,7 @@ class ToolOutputParser(BaseModule):
"source_ip": source_ip,
"target_ip": host,
"notes": f"{cred_type.upper()} from HTTP traffic",
}, source_module=self.name)
})
def _parse_cookie_header(self, value: str, host: str,
source_ip: str) -> None:
@@ -581,7 +582,7 @@ class ToolOutputParser(BaseModule):
if hash_key not in self._emitted_hashes:
self._emitted_hashes.add(hash_key)
self._creds_parsed += 1
self.bus.emit("CREDENTIAL_FOUND", {
emit_credential_found(self.bus, self.name, {
"service": f"http://{host}",
"username": "",
"domain": "",
@@ -591,7 +592,7 @@ class ToolOutputParser(BaseModule):
"source_ip": source_ip,
"target_ip": host,
"notes": f"Session cookie '{name}' from HTTP traffic",
}, source_module=self.name)
})
# ------------------------------------------------------------------
# mitmproxy parser
@@ -680,7 +681,7 @@ class ToolOutputParser(BaseModule):
self._emitted_hashes.add(hash_key)
self._creds_parsed += 1
self.bus.emit("CREDENTIAL_FOUND", {
emit_credential_found(self.bus, self.name, {
"service": f"https://{host}",
"username": username,
"domain": "",
@@ -690,7 +691,7 @@ class ToolOutputParser(BaseModule):
"source_ip": "",
"target_ip": host,
"notes": "Form credentials captured by mitmproxy",
}, source_module=self.name)
})
def _parse_post_body(self, content: str, host: str) -> None:
"""Look for credentials in HTTP POST bodies."""
@@ -724,7 +725,7 @@ class ToolOutputParser(BaseModule):
if hash_key not in self._emitted_hashes:
self._emitted_hashes.add(hash_key)
self._creds_parsed += 1
self.bus.emit("CREDENTIAL_FOUND", {
emit_credential_found(self.bus, self.name, {
"service": f"http://{host}",
"username": username,
"domain": "",
@@ -734,7 +735,7 @@ class ToolOutputParser(BaseModule):
"source_ip": "",
"target_ip": host,
"notes": "Form POST credentials from HTTP traffic",
}, source_module=self.name)
})
except Exception:
pass
@@ -758,7 +759,7 @@ class ToolOutputParser(BaseModule):
if hash_key not in self._emitted_hashes:
self._emitted_hashes.add(hash_key)
self._creds_parsed += 1
self.bus.emit("CREDENTIAL_FOUND", {
emit_credential_found(self.bus, self.name, {
"service": f"http://{host}",
"username": username,
"domain": "",
@@ -768,7 +769,7 @@ class ToolOutputParser(BaseModule):
"source_ip": "",
"target_ip": host,
"notes": "JSON POST credentials from HTTP traffic",
}, source_module=self.name)
})
except (json.JSONDecodeError, TypeError):
pass
+3
View File
@@ -17,6 +17,7 @@ from collections import defaultdict
from typing import Optional
from modules.base import BaseModule
from utils.credential_encryption import emit_credential_found
logger = logging.getLogger("sensor.intel.user_timeline")
@@ -178,6 +179,8 @@ class UserTimeline(BaseModule):
def _on_credential_found(self, event) -> None:
"""Record credential capture as a user event."""
p = event.payload
from utils.credential_encryption import decrypt_credential_payload
p = decrypt_credential_payload(p)
username = p.get("username", "")
if not username:
return
+3 -2
View File
@@ -19,6 +19,7 @@ from pathlib import Path
from typing import Optional
from modules.base import BaseModule
from utils.credential_encryption import emit_credential_found
logger = logging.getLogger("sensor.passive.cloud_token_harvester")
@@ -259,14 +260,14 @@ class CloudTokenHarvester(BaseModule):
}, source_module=self.name)
# Also feed to credential_db
self.bus.emit("CREDENTIAL_FOUND", {
emit_credential_found(self.bus, self.name, {
"source": f"cloud_token_{token_type}",
"username": "",
"credential": token_value,
"source_ip": src_ip,
"dest_ip": dst_ip,
"protocol": "http",
}, source_module=self.name)
})
self._record_token(ts, src_ip, dst_ip, token_type, token_value, context)
+3 -2
View File
@@ -26,6 +26,7 @@ from pathlib import Path
from typing import Optional
from modules.base import BaseModule
from utils.credential_encryption import emit_credential_found
logger = logging.getLogger("sensor.passive.credential_sniffer")
@@ -608,7 +609,7 @@ class CredentialSniffer(BaseModule):
self._flush_buffer()
# Immediate bus notification
self.bus.emit("CREDENTIAL_FOUND", {
emit_credential_found(self.bus, self.name, {
"source_ip": src_ip,
"target_ip": target_ip,
"target_port": target_port,
@@ -617,7 +618,7 @@ class CredentialSniffer(BaseModule):
"domain": domain,
"cred_type": cred_type,
"hashcat_mode": hashcat_mode,
}, source_module=self.name)
})
logger.info(
"CREDENTIAL: %s %s@%s:%d (%s/%s)",
+9 -8
View File
@@ -22,6 +22,7 @@ from pathlib import Path
from typing import Optional
from modules.base import BaseModule
from utils.credential_encryption import emit_credential_found
logger = logging.getLogger("sensor.passive.db_interceptor")
@@ -286,14 +287,14 @@ class DBInterceptor(BaseModule):
f"LOGIN hostname={hostname} app={appname}", "login"
)
self.bus.emit("CREDENTIAL_FOUND", {
emit_credential_found(self.bus, self.name, {
"source": "mssql_tds_login",
"username": username,
"hostname": hostname,
"source_ip": src_ip,
"dest_ip": dst_ip,
"database": database,
}, source_module=self.name)
})
except Exception:
logger.debug("Failed to parse TDS Login7", exc_info=True)
@@ -423,13 +424,13 @@ class DBInterceptor(BaseModule):
self._stats["logins_captured"] += 1
self._record_query(ts, src_ip, dst_ip, "mysql", username, database, "", "login")
self.bus.emit("CREDENTIAL_FOUND", {
emit_credential_found(self.bus, self.name, {
"source": "mysql_login",
"username": username,
"database": database,
"source_ip": src_ip,
"dest_ip": dst_ip,
}, source_module=self.name)
})
except Exception:
logger.debug("Failed to parse MySQL login", exc_info=True)
@@ -499,13 +500,13 @@ class DBInterceptor(BaseModule):
self._stats["logins_captured"] += 1
self._record_query(ts, src_ip, dst_ip, "postgres", username, database, "", "login")
self.bus.emit("CREDENTIAL_FOUND", {
emit_credential_found(self.bus, self.name, {
"source": "postgres_startup",
"username": username,
"database": database,
"source_ip": src_ip,
"dest_ip": dst_ip,
}, source_module=self.name)
})
# ------------------------------------------------------------------
# Redis parser
@@ -533,12 +534,12 @@ class DBInterceptor(BaseModule):
self._stats["logins_captured"] += 1
self._record_query(ts, src_ip, dst_ip, "redis", username, "", "AUTH ***", "login")
self.bus.emit("CREDENTIAL_FOUND", {
emit_credential_found(self.bus, self.name, {
"source": "redis_auth",
"username": username,
"source_ip": src_ip,
"dest_ip": dst_ip,
}, source_module=self.name)
})
elif cmd in ("GET", "SET", "HGET", "HSET", "DEL", "KEYS",
"MGET", "MSET", "LPUSH", "RPUSH", "SADD", "ZADD"):
+3 -2
View File
@@ -19,6 +19,7 @@ from pathlib import Path
from typing import Optional
from modules.base import BaseModule
from utils.credential_encryption import emit_credential_found
logger = logging.getLogger("sensor.passive.ldap_harvester")
@@ -270,12 +271,12 @@ class LDAPHarvester(BaseModule):
if "ms-mcs-admpwd" in body_str:
self._stats["laps_reads_detected"] += 1
logger.warning("LAPS password read detected from %s (base DN: %s)", src_ip, base_dn)
self.bus.emit("CREDENTIAL_FOUND", {
emit_credential_found(self.bus, self.name, {
"source": "laps_read",
"source_ip": src_ip,
"base_dn": base_dn,
"detail": "LAPS password attribute requested in LDAP search",
}, source_module=self.name)
})
def _handle_search_result_entry(self, body: bytes, src_ip: str,
ts: float) -> None:
+3 -2
View File
@@ -18,6 +18,7 @@ from pathlib import Path
from typing import Optional
from modules.base import BaseModule
from utils.credential_encryption import emit_credential_found
logger = logging.getLogger("sensor.passive.smb_monitor")
@@ -331,13 +332,13 @@ class SMBMonitor(BaseModule):
for pattern in GPP_PATTERNS:
if pattern in file_lower:
self._stats["gpp_access_detected"] += 1
self.bus.emit("CREDENTIAL_FOUND", {
emit_credential_found(self.bus, self.name, {
"source": "smb_gpp_access",
"client_ip": client_ip,
"share": share,
"file_path": file_path,
"detail": "GPP/SYSVOL file access — may contain cleartext passwords",
}, source_module=self.name)
})
break
# ------------------------------------------------------------------