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