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:
@@ -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)
|
||||
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user