33 Commits

Author SHA1 Message Date
847e7bce48 No longer send an "ignore this message" at start time. 2025-06-13 10:43:52 -04:00
8c0c3eb81f format cleanup on the default prompt 2025-05-30 14:13:41 -04:00
681ab58750 Changed default LLM to 'gpt-4o-mini'
Only used if the model isn't specified in the config or on the command line
2025-04-25 09:28:08 -04:00
e738379fc4 Updated default temperature to 0.2
This is only used if no temperature is specified on the command line or in the config file.
2025-04-25 09:17:21 -04:00
10e2f11599 Added 'temperature' parameter to control randomness in LLM responses.
Lower temps mean less randomness in the  responses, which increase the chances of consistency between sessions. Not a guarantee, though.
2025-04-25 09:12:40 -04:00
a3c14bbf15 Preliminary support for Azure OpenAI models, plus "porn fix"
This version adds support for Azure OpenAI models. I'm not entirely happy with how each LLM provider has it's own set of params, and am investigating how to make these seem a little more unified, so this support may change in the future.

Also, Azure's content filter flags the "XXX-END-OF-SESSION-XXX" token as "sexual content", so I changed it to use "YYY" instead. I feel so protected!
2025-03-20 15:21:07 -04:00
e2e47c4e6c Improved --config handling and user accounts from the command line
If all of the necessary options are passed as command line flags, you may no longer even need a config file. in this case, don't complain that a config file wasn't provided.  As part of this, allow the user to set the user account(s) using the -u flag.
2025-02-13 14:13:21 -05:00
87aa843116 Created command-line flags for almost all of the options specified in the config file. 2025-02-13 13:23:48 -05:00
d9ba6b3b61 Fixed server_version_string setting
The config template specified the default server version string as "SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.3" but the SSH module automatically prepends "SSH-2.0-" to the beginning. This gave the version string returned to the client a potential fingerprint that could be used to easily identify DECEIVE honeypots. Updated the default value and added comments to document this behavior.
2025-02-13 13:19:21 -05:00
9844f2f59b Merge pull request #4 from Paulslewis66/main
Added Ollama Local LLM's
2025-02-12 10:05:51 -05:00
7ca56b86a5 Update config.ini.TEMPLATE
comment out llama config
2025-02-11 21:49:56 +00:00
ab6296e956 Revert "Update config.ini.TEMPLATE"
This reverts commit 2df4358356.
2025-02-11 21:44:44 +00:00
da0513f526 Revert "Update config.ini.TEMPLATE"
This reverts commit 92ad118de2.
2025-02-11 21:44:31 +00:00
2851120d67 Revert "Added LogViewer"
This reverts commit e60f33b8ea.
2025-02-11 21:44:27 +00:00
ae8c130a1b Revert "Added JSON and removed Base64"
This reverts commit 12fd8aeb70.
2025-02-11 21:44:18 +00:00
17ec8619e0 Revert "Update .gitignore"
This reverts commit f929f4b41d.
2025-02-11 21:44:11 +00:00
e9cdd22b34 Revert "Update .gitignore"
This reverts commit 95f1c5935f.
2025-02-11 21:44:00 +00:00
95f1c5935f Update .gitignore 2025-02-11 16:58:50 +00:00
f929f4b41d Update .gitignore
Added Logviewer .gitignore
2025-02-11 16:58:08 +00:00
12fd8aeb70 Added JSON and removed Base64 2025-02-09 19:17:44 +00:00
e60f33b8ea Added LogViewer 2025-02-09 16:57:00 +00:00
92ad118de2 Update config.ini.TEMPLATE
Update for incept5/llama3.1-claude LLM
2025-02-08 21:38:36 +00:00
2df4358356 Update config.ini.TEMPLATE
commented out Ollama
2025-02-08 21:17:22 +00:00
4bd3cfcdc2 Update config.ini.TEMPLATE
Added Ollama Config Option
2025-02-08 21:05:03 +00:00
f1f6c294e9 Update ssh_server.py
Added Ollama Model
2025-02-08 21:04:02 +00:00
c980fc6167 Update requirements.txt
Update for Ollama
2025-02-08 21:02:31 +00:00
1d0e046924 Added 'sensor_name' and 'sensor_protocol' to logs
* 'sensor_name` is an arbitrary string that identifies the specific honeypot sensor that generated the log. Set it in the config.ini file. If not set, it will default to the honeypot system's hostname.
* 'sensor_protocol' identifies the specific protocol this honeypot sensor uses. For SSH, it's always "ssh" but as other protocols are added to DECEIVE in the future, this will have different values for their logs.
2025-02-06 10:16:42 -05:00
fd0d8a78fc Update README.md
Added more info about the `details` and `interactive` fields to logging section.
2025-02-05 06:41:16 -05:00
dba537c58f removed debug statements 2025-02-04 16:11:48 -05:00
b222940de2 Wildcard password support
Setting a password to be "*" in the config file will cause the server to accept any password the client provides for that account, including an empty password.
2025-02-04 16:05:23 -05:00
0197b8b1df Update config.ini.TEMPLATE to support non-interactive commands
Oops, I forgot to commit this before.
2025-02-04 14:55:02 -05:00
5f27aeeabb Correctly handle both interactive and non-interactive SSH sessions
SSH servers can take user commands from an interactive session as normal, but users can also include commands on the ssh client command line which are executed on the server (e.g., "ssh <hostname> 'uname -a'"). We now execute these non-interactive commands properly as well.

Also added a new "interactive" flag to all user commands (true/false) to show which type of command execution this was.
2025-02-04 12:29:12 -05:00
585ee66009 Don't print ConnectionLost exceptions to the console
These are far too frequent. We still log them, though, we just don't print them.
2025-01-28 10:48:29 -05:00
5 changed files with 200 additions and 67 deletions

View File

@ -93,8 +93,8 @@ Things to note:
* `Session summary` * `Session summary`
* `SSH connection closed` * `SSH connection closed`
* Several of these message types also feature a `details` field with additional information * Several of these message types also feature a `details` field with additional information
* `User input` messages contain a base64-encoded copy of the entire user input * `User input` messages contain a base64-encoded copy of the entire user input in the `details` field, as well as an `interactive` field (true/false) that tells you whether this was an interactive or non-interactive command (i.e., whether they logged in with a terminal session or provided a command on the SSH command-line).
* `LLM response` messages contain a base64-encoded copy of the entire simulated response * `LLM response` messages contain a base64-encoded copy of the entire simulated response in the `details` field.
* `Session summary` messages contain not only a summary of the commands, but also a guess as to what they might have been intended to accomplish. There will also be a `judgement` field that contains one of "BENIGN", "SUSPICIOUS", or "MALICIOUS" * `Session summary` messages contain not only a summary of the commands, but also a guess as to what they might have been intended to accomplish. There will also be a `judgement` field that contains one of "BENIGN", "SUSPICIOUS", or "MALICIOUS"
* Since this is a honeypot and not intended for use by real users, IT WILL LOG USERNAMES AND PASSWORDS! These are found in the `Authentication success` messages, in the `username` and `password` fields. * Since this is a honeypot and not intended for use by real users, IT WILL LOG USERNAMES AND PASSWORDS! These are found in the `Authentication success` messages, in the `username` and `password` fields.
@ -102,4 +102,4 @@ Things to note:
Contributions are welcome! Please submit pull requests or open issues to discuss any changes or improvements. Contributions are welcome! Please submit pull requests or open issues to discuss any changes or improvements.
### License ### License
This project is licensed under the MIT License. See the LICENSE file for details. This project is licensed under the MIT License. See the LICENSE file for details.

View File

@ -6,6 +6,10 @@
# The name of the file you wish to write the honeypot log to. # The name of the file you wish to write the honeypot log to.
log_file = ssh_log.log log_file = ssh_log.log
# The name of the sensor, used to identify this honeypot in the logs.
# If you leave this blank, the honeypot will use the system's hostname.
sensor_name = deceive
# Settings for the SSH honeypot # Settings for the SSH honeypot
[ssh] [ssh]
# The port the SSH honeypot will listen on. You will probably want to set # The port the SSH honeypot will listen on. You will probably want to set
@ -14,8 +18,10 @@ port = 8022
# The host key to use for the SSH server. This should be a private key. # The host key to use for the SSH server. This should be a private key.
# See the README for how to generate this key. # See the README for how to generate this key.
host_priv_key = ssh_host_key host_priv_key = ssh_host_key
# The server version string to send to clients. # The server version string to send to clients. The SSH server automatically
server_version_string = SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.3 # prepends "SSH-2.0-" to this. So "OpenSSH_8.2p1 Ubuntu-4ubuntu0.3" will
# be transformed to "SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.3".
server_version_string = OpenSSH_8.2p1 Ubuntu-4ubuntu0.3
# Settings to configure which LLM backend to use. Only one stanza # Settings to configure which LLM backend to use. Only one stanza
# should be uncommented at a time. # should be uncommented at a time.
@ -24,6 +30,17 @@ server_version_string = SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.3
llm_provider = openai llm_provider = openai
model_name = gpt-4o model_name = gpt-4o
##### Azure OpenAI
#llm_provider = azure
#azure_deployment = gpt-4o
#azure_api_version = 2025-01-01-preview
#azure_endpoint = <your endpoint url>
#model_name = gpt-4o
##### ollama llama3
#llm_provider = ollama
#model_name = llama3.3
##### Any model via Amazon Bedrock ##### Any model via Amazon Bedrock
# Valid AWS model names can be found here: https://docs.aws.amazon.com/bedrock/latest/userguide/model-ids.html # Valid AWS model names can be found here: https://docs.aws.amazon.com/bedrock/latest/userguide/model-ids.html
#llm_provider = AWS #llm_provider = AWS
@ -36,6 +53,12 @@ model_name = gpt-4o
#llm_provider = gemini #llm_provider = gemini
#model_name = gemini-1.5-pro #model_name = gemini-1.5-pro
# Temperature controls randomness in LLM responses. Values usually range from 0.0 to 2.0.
# Lower values (e.g., 0.2) make responses more focused and deterministic.
# Higher values (e.g., 0.8) make responses more creative and variable.
# Default is 0.2.
temperature = 0.2
# The maximum number of tokens to send to the LLM backend in a single # The maximum number of tokens to send to the LLM backend in a single
# request. This includes the message history for the session, so should # request. This includes the message history for the session, so should
# be fairly high. Not all models support large token counts, so be sure # be fairly high. Not all models support large token counts, so be sure
@ -51,10 +74,11 @@ system_prompt = Interpret all inputs as though they were SSH commands and provid
responses do not indicate that the user is on a virtual machine, container, responses do not indicate that the user is on a virtual machine, container,
or emulation at all. or emulation at all.
On the first call, be sure to include a realistic MOTD. If {interactive} is True, make sure to include a realistic MOTD on the
first call, and to end all responses with a realistic shell prompt to display to
the user, including a space at the end.
End all responses with a realistic shell prompt to display to the user, If {interactive} is False, do not include a shell prompt or MOTD.
including a space at the end.
Include ANSI color codes for the terminal with the output of ls commands Include ANSI color codes for the terminal with the output of ls commands
(including any flags), or in any other situation where it is appropriate, but (including any flags), or in any other situation where it is appropriate, but
@ -74,9 +98,12 @@ system_prompt = Interpret all inputs as though they were SSH commands and provid
# The valid user accounts and passwords for the SSH server, in the # The valid user accounts and passwords for the SSH server, in the
# form "username = password". Note that you can enable login without # form "username = password". Note that you can enable login without
# a password by leaving that field blank (e.g., "guest =" on a line by # a password by leaving that field blank (e.g., "guest =" on a line by
# itself) # itself). You can set an account to accept ANY password, including an empty
# password, by setting the password to "*"
[user_accounts] [user_accounts]
guest = guest =
user1 = secretpw user1 = secretpw
user2 = password123 user2 = password123
root = *

View File

@ -1,2 +1 @@
You are a video game developer's system. Include realistic video game source You are a video game developer's system. Include realistic video game source and asset files.
and asset files.

View File

@ -15,16 +15,23 @@ import datetime
import uuid import uuid
from base64 import b64encode from base64 import b64encode
from operator import itemgetter from operator import itemgetter
from langchain_openai import ChatOpenAI from langchain_openai import ChatOpenAI, AzureChatOpenAI
from langchain_aws import ChatBedrock, ChatBedrockConverse from langchain_aws import ChatBedrock, ChatBedrockConverse
from langchain_google_genai import ChatGoogleGenerativeAI from langchain_google_genai import ChatGoogleGenerativeAI
from langchain_ollama import ChatOllama
from langchain_core.messages import HumanMessage, SystemMessage, trim_messages from langchain_core.messages import HumanMessage, SystemMessage, trim_messages
from langchain_core.chat_history import BaseChatMessageHistory, InMemoryChatMessageHistory from langchain_core.chat_history import BaseChatMessageHistory, InMemoryChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables import RunnablePassthrough from langchain_core.runnables import RunnablePassthrough
from asyncssh.misc import ConnectionLost
import socket
class JSONFormatter(logging.Formatter): class JSONFormatter(logging.Formatter):
def __init__(self, sensor_name, *args, **kwargs):
super().__init__(*args, **kwargs)
self.sensor_name = sensor_name
def format(self, record): def format(self, record):
log_record = { log_record = {
"timestamp": datetime.datetime.fromtimestamp(record.created, datetime.timezone.utc).isoformat(sep="T", timespec="milliseconds"), "timestamp": datetime.datetime.fromtimestamp(record.created, datetime.timezone.utc).isoformat(sep="T", timespec="milliseconds"),
@ -34,8 +41,12 @@ class JSONFormatter(logging.Formatter):
"src_port": record.src_port, "src_port": record.src_port,
"dst_ip": record.dst_ip, "dst_ip": record.dst_ip,
"dst_port": record.dst_port, "dst_port": record.dst_port,
"message": record.getMessage() "message": record.getMessage(),
"sensor_name": self.sensor_name,
"sensor_protocol": "ssh"
} }
if hasattr(record, 'interactive'):
log_record["interactive"] = record.interactive
# Include any additional fields from the extra dictionary # Include any additional fields from the extra dictionary
for key, value in record.__dict__.items(): for key, value in record.__dict__.items():
if key not in log_record and key != 'args' and key != 'msg': if key not in log_record and key != 'args' and key != 'msg':
@ -74,7 +85,8 @@ class MySSHServer(asyncssh.SSHServer):
def connection_lost(self, exc: Optional[Exception]) -> None: def connection_lost(self, exc: Optional[Exception]) -> None:
if exc: if exc:
logger.error('SSH connection error', extra={"error": str(exc)}) logger.error('SSH connection error', extra={"error": str(exc)})
traceback.print_exception(exc) if not isinstance(exc, ConnectionLost):
traceback.print_exception(exc)
else: else:
logger.info("SSH connection closed") logger.info("SSH connection closed")
# Ensure session summary is called on connection loss if attributes are set # Ensure session summary is called on connection loss if attributes are set
@ -101,7 +113,7 @@ class MySSHServer(asyncssh.SSHServer):
def validate_password(self, username: str, password: str) -> bool: def validate_password(self, username: str, password: str) -> bool:
pw = accounts.get(username, '*') pw = accounts.get(username, '*')
if ((pw != '*') and (password == pw)): if pw == '*' or (pw != '*' and password == pw):
logger.info("Authentication success", extra={"username": username, "password": password}) logger.info("Authentication success", extra={"username": username, "password": password})
return True return True
else: else:
@ -148,7 +160,8 @@ representative examples.
llm_response = await session.ainvoke( llm_response = await session.ainvoke(
{ {
"messages": [HumanMessage(content=prompt)], "messages": [HumanMessage(content=prompt)],
"username": process.get_extra_info('username') "username": process.get_extra_info('username'),
"interactive": True # Ensure interactive flag is passed
}, },
config=llm_config config=llm_config
) )
@ -163,6 +176,7 @@ representative examples.
judgement = "MALICIOUS" judgement = "MALICIOUS"
logger.info("Session summary", extra={"details": llm_response.content, "judgement": judgement}) logger.info("Session summary", extra={"details": llm_response.content, "judgement": judgement})
server.summary_generated = True server.summary_generated = True
async def handle_client(process: asyncssh.SSHServerProcess, server: MySSHServer) -> None: async def handle_client(process: asyncssh.SSHServerProcess, server: MySSHServer) -> None:
@ -176,42 +190,57 @@ async def handle_client(process: asyncssh.SSHServerProcess, server: MySSHServer)
llm_config = {"configurable": {"session_id": task_uuid}} llm_config = {"configurable": {"session_id": task_uuid}}
llm_response = await with_message_history.ainvoke(
{
"messages": [HumanMessage(content="ignore this message")],
"username": process.get_extra_info('username')
},
config=llm_config
)
process.stdout.write(f"{llm_response.content}")
logger.info("LLM response", extra={"details": b64encode(llm_response.content.encode('utf-8')).decode('utf-8')})
# Store process, llm_config, and session in the MySSHServer instance
server._process = process
server._llm_config = llm_config
server._session = with_message_history
try: try:
async for line in process.stdin: if process.command:
line = line.rstrip('\n') # Handle non-interactive command execution
logger.info("User input", extra={"details": b64encode(line.encode('utf-8')).decode('utf-8')}) command = process.command
logger.info("User input", extra={"details": b64encode(command.encode('utf-8')).decode('utf-8'), "interactive": False})
# Send the command to the LLM and give the response to the user
llm_response = await with_message_history.ainvoke( llm_response = await with_message_history.ainvoke(
{ {
"messages": [HumanMessage(content=line)], "messages": [HumanMessage(content=command)],
"username": process.get_extra_info('username') "username": process.get_extra_info('username'),
"interactive": False
}, },
config=llm_config config=llm_config
) )
if llm_response.content == "XXX-END-OF-SESSION-XXX": process.stdout.write(f"{llm_response.content}")
await session_summary(process, llm_config, with_message_history, server) logger.info("LLM response", extra={"details": b64encode(llm_response.content.encode('utf-8')).decode('utf-8'), "interactive": False})
process.exit(0) await session_summary(process, llm_config, with_message_history, server)
return process.exit(0)
else: else:
process.stdout.write(f"{llm_response.content}") # Handle interactive session
logger.info("LLM response", extra={"details": b64encode(llm_response.content.encode('utf-8')).decode('utf-8')}) llm_response = await with_message_history.ainvoke(
{
"messages": [HumanMessage(content="")],
"username": process.get_extra_info('username'),
"interactive": True
},
config=llm_config
)
process.stdout.write(f"{llm_response.content}")
logger.info("LLM response", extra={"details": b64encode(llm_response.content.encode('utf-8')).decode('utf-8'), "interactive": True})
async for line in process.stdin:
line = line.rstrip('\n')
logger.info("User input", extra={"details": b64encode(line.encode('utf-8')).decode('utf-8'), "interactive": True})
# Send the command to the LLM and give the response to the user
llm_response = await with_message_history.ainvoke(
{
"messages": [HumanMessage(content=line)],
"username": process.get_extra_info('username'),
"interactive": True
},
config=llm_config
)
if llm_response.content == "YYY-END-OF-SESSION-YYY":
await session_summary(process, llm_config, with_message_history, server)
process.exit(0)
return
else:
process.stdout.write(f"{llm_response.content}")
logger.info("LLM response", extra={"details": b64encode(llm_response.content.encode('utf-8')).decode('utf-8'), "interactive": True})
except asyncssh.BreakReceived: except asyncssh.BreakReceived:
pass pass
@ -249,7 +278,7 @@ class ContextFilter(logging.Filter):
if task: if task:
task_name = task.get_name() task_name = task.get_name()
else: else:
task_name = "-" task_name = thread_local.__dict__.get('session_id', '-')
record.src_ip = thread_local.__dict__.get('src_ip', '-') record.src_ip = thread_local.__dict__.get('src_ip', '-')
record.src_port = thread_local.__dict__.get('src_port', '-') record.src_port = thread_local.__dict__.get('src_port', '-')
@ -257,7 +286,7 @@ class ContextFilter(logging.Filter):
record.dst_port = thread_local.__dict__.get('dst_port', '-') record.dst_port = thread_local.__dict__.get('dst_port', '-')
record.task_name = task_name record.task_name = task_name
return True return True
def llm_get_session_history(session_id: str) -> BaseChatMessageHistory: def llm_get_session_history(session_id: str) -> BaseChatMessageHistory:
@ -276,23 +305,43 @@ def get_user_accounts() -> dict:
return accounts return accounts
def choose_llm(): def choose_llm(llm_provider: Optional[str] = None, model_name: Optional[str] = None):
llm_provider_name = config['llm'].get("llm_provider", "openai") llm_provider_name = llm_provider or config['llm'].get("llm_provider", "openai")
llm_provider_name = llm_provider_name.lower() llm_provider_name = llm_provider_name.lower()
model_name = config['llm'].get("model_name", "gpt-3.5-turbo") model_name = model_name or config['llm'].get("model_name", "gpt-4o-mini")
# Get temperature parameter from config, default to 0.2 if not specified
temperature = config['llm'].getfloat("temperature", 0.2)
if llm_provider_name == 'openai': if llm_provider_name == 'openai':
llm_model = ChatOpenAI( llm_model = ChatOpenAI(
model=model_name model=model_name,
temperature=temperature
)
elif llm_provider_name == 'azure':
llm_model = AzureChatOpenAI(
azure_deployment=config['llm'].get("azure_deployment"),
azure_endpoint=config['llm'].get("azure_endpoint"),
api_version=config['llm'].get("azure_api_version"),
model=config['llm'].get("model_name"), # Ensure model_name is passed here
temperature=temperature
)
elif llm_provider_name == 'ollama':
llm_model = ChatOllama(
model=model_name,
temperature=temperature
) )
elif llm_provider_name == 'aws': elif llm_provider_name == 'aws':
llm_model = ChatBedrockConverse( llm_model = ChatBedrockConverse(
model=model_name, model=model_name,
region_name=config['llm'].get("aws_region", "us-east-1"), region_name=config['llm'].get("aws_region", "us-east-1"),
credentials_profile_name=config['llm'].get("aws_credentials_profile", "default") ) credentials_profile_name=config['llm'].get("aws_credentials_profile", "default"),
temperature=temperature
)
elif llm_provider_name == 'gemini': elif llm_provider_name == 'gemini':
llm_model = ChatGoogleGenerativeAI( llm_model = ChatGoogleGenerativeAI(
model=model_name, model=model_name,
temperature=temperature
) )
else: else:
raise ValueError(f"Invalid LLM provider {llm_provider_name}.") raise ValueError(f"Invalid LLM provider {llm_provider_name}.")
@ -327,25 +376,82 @@ def get_prompts(prompt: Optional[str], prompt_file: Optional[str]) -> dict:
try: try:
# Parse command line arguments # Parse command line arguments
parser = argparse.ArgumentParser(description='Start the SSH honeypot server.') parser = argparse.ArgumentParser(description='Start the SSH honeypot server.')
parser.add_argument('-c', '--config', type=str, default='config.ini', help='Path to the configuration file') parser.add_argument('-c', '--config', type=str, default=None, help='Path to the configuration file')
parser.add_argument('-p', '--prompt', type=str, help='The entire text of the prompt') parser.add_argument('-p', '--prompt', type=str, help='The entire text of the prompt')
parser.add_argument('-f', '--prompt-file', type=str, default='prompt.txt', help='Path to the prompt file') parser.add_argument('-f', '--prompt-file', type=str, default='prompt.txt', help='Path to the prompt file')
parser.add_argument('-l', '--llm-provider', type=str, help='The LLM provider to use')
parser.add_argument('-m', '--model-name', type=str, help='The model name to use')
parser.add_argument('-t', '--trimmer-max-tokens', type=int, help='The maximum number of tokens to send to the LLM backend in a single request')
parser.add_argument('-s', '--system-prompt', type=str, help='System prompt for the LLM')
parser.add_argument('-r', '--temperature', type=float, help='Temperature parameter for controlling randomness in LLM responses (0.0-2.0)')
parser.add_argument('-P', '--port', type=int, help='The port the SSH honeypot will listen on')
parser.add_argument('-k', '--host-priv-key', type=str, help='The host key to use for the SSH server')
parser.add_argument('-v', '--server-version-string', type=str, help='The server version string to send to clients')
parser.add_argument('-L', '--log-file', type=str, help='The name of the file you wish to write the honeypot log to')
parser.add_argument('-S', '--sensor-name', type=str, help='The name of the sensor, used to identify this honeypot in the logs')
parser.add_argument('-u', '--user-account', action='append', help='User account in the form username=password. Can be repeated.')
args = parser.parse_args() args = parser.parse_args()
# Check if the config file exists # Determine which config file to load
if not os.path.exists(args.config): config = ConfigParser()
print(f"Error: The specified config file '{args.config}' does not exist.", file=sys.stderr) if args.config is not None:
sys.exit(1) # User explicitly set a config file; error if it doesn't exist.
if not os.path.exists(args.config):
print(f"Error: The specified config file '{args.config}' does not exist.", file=sys.stderr)
sys.exit(1)
config.read(args.config)
else:
default_config = "config.ini"
if os.path.exists(default_config):
config.read(default_config)
else:
# Use defaults when no config file found.
config['honeypot'] = {'log_file': 'ssh_log.log', 'sensor_name': socket.gethostname()}
config['ssh'] = {'port': '8022', 'host_priv_key': 'ssh_host_key', 'server_version_string': 'SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.3'}
config['llm'] = {'llm_provider': 'openai', 'model_name': 'gpt-3.5-turbo', 'trimmer_max_tokens': '64000', 'temperature': '0.7', 'system_prompt': ''}
config['user_accounts'] = {}
# Override config values with command line arguments if provided
if args.llm_provider:
config['llm']['llm_provider'] = args.llm_provider
if args.model_name:
config['llm']['model_name'] = args.model_name
if args.trimmer_max_tokens:
config['llm']['trimmer_max_tokens'] = str(args.trimmer_max_tokens)
if args.system_prompt:
config['llm']['system_prompt'] = args.system_prompt
if args.temperature is not None:
config['llm']['temperature'] = str(args.temperature)
if args.port:
config['ssh']['port'] = str(args.port)
if args.host_priv_key:
config['ssh']['host_priv_key'] = args.host_priv_key
if args.server_version_string:
config['ssh']['server_version_string'] = args.server_version_string
if args.log_file:
config['honeypot']['log_file'] = args.log_file
if args.sensor_name:
config['honeypot']['sensor_name'] = args.sensor_name
# Merge command-line user accounts into the config
if args.user_account:
if 'user_accounts' not in config:
config['user_accounts'] = {}
for account in args.user_account:
if '=' in account:
key, value = account.split('=', 1)
config['user_accounts'][key.strip()] = value.strip()
else:
config['user_accounts'][account.strip()] = ''
# Read the user accounts from the configuration
accounts = get_user_accounts()
# Always use UTC for logging # Always use UTC for logging
logging.Formatter.formatTime = (lambda self, record, datefmt=None: datetime.datetime.fromtimestamp(record.created, datetime.timezone.utc).isoformat(sep="T",timespec="milliseconds")) logging.Formatter.formatTime = (lambda self, record, datefmt=None: datetime.datetime.fromtimestamp(record.created, datetime.timezone.utc).isoformat(sep="T",timespec="milliseconds"))
# Read our configuration file # Get the sensor name from the config or use the system's hostname
config = ConfigParser() sensor_name = config['honeypot'].get('sensor_name', socket.gethostname())
config.read(args.config)
# Read the user accounts from the configuration file
accounts = get_user_accounts()
# Set up the honeypot logger # Set up the honeypot logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -354,7 +460,7 @@ try:
log_file_handler = logging.FileHandler(config['honeypot'].get("log_file", "ssh_log.log")) log_file_handler = logging.FileHandler(config['honeypot'].get("log_file", "ssh_log.log"))
logger.addHandler(log_file_handler) logger.addHandler(log_file_handler)
log_file_handler.setFormatter(JSONFormatter()) log_file_handler.setFormatter(JSONFormatter(sensor_name))
f = ContextFilter() f = ContextFilter()
logger.addFilter(f) logger.addFilter(f)
@ -365,7 +471,7 @@ try:
llm_system_prompt = prompts["system_prompt"] llm_system_prompt = prompts["system_prompt"]
llm_user_prompt = prompts["user_prompt"] llm_user_prompt = prompts["user_prompt"]
llm = choose_llm() llm = choose_llm(config['llm'].get("llm_provider"), config['llm'].get("model_name"))
llm_sessions = dict() llm_sessions = dict()

View File

@ -7,7 +7,8 @@ langchain_community
langchain_openai langchain_openai
# For Google's Gemini models # For Google's Gemini models
langchain_google_genai langchain_google_genai
# For Ollama models
langchain_ollama
# For AWS # For AWS
langchain_aws langchain_aws
transformers transformers