Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -194,3 +194,14 @@ in a sort of pre-release mode.
* Contributions from @da667 - thank you!
* Added cyberchef container (#235)
* Updated base OS to Ubuntu 24.04 (#234)

3.6.0 (2026-01-24)
##################

* Moved to using **Docker Compose Version 2** in start-dalton.sh
* Added functionality for users to set username, along with simple shared auth (see dalton.conf)
* Updated queue page to display user who submitted the job (if so configured)
* Fixed support for Suricata Socket Control in Suricata version 8 and later.
The necessary Python libraries for suricatasc are no longer included with the Suricata
source beginning with Suricata version 8.
* Updated docker-compose to have more recent versions of the Suriata and Zeek agents by default
7 changes: 4 additions & 3 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ or this which does the same thing:

.. code:: text

docker-compose build && docker-compose up -d
docker compose build && docker compose up -d

Then navigate to ``http://<docker-host>/dalton/``

Expand Down Expand Up @@ -157,7 +157,8 @@ Requirements
============

- `Docker <https://www.docker.com/get-docker>`__
- `Docker Compose <https://docs.docker.com/compose/install/>`__
- `Docker Compose V2 <https://docs.docker.com/compose/install/>`__.
Note that this should be `Docker Compose Version 2 <https://www.docker.com/blog/announcing-compose-v2-general-availability/>`__
- Internet connection (to build)

Installing and Running Dalton
Expand All @@ -175,7 +176,7 @@ or this which does the same thing:

.. code:: bash

docker-compose build && docker-compose up -d
docker compose build && docker compose up -d

To specify or add what agents (specific sensors and versions) are built
and run, edit the docker-compose.yml file as appropriate. See also
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.2.5
3.6.0
131 changes: 84 additions & 47 deletions app/dalton.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
import traceback
import zipfile
from distutils.version import LooseVersion
from functools import lru_cache
from functools import lru_cache, wraps
from logging.handlers import RotatingFileHandler
from threading import Thread

Expand Down Expand Up @@ -91,6 +91,8 @@ def setup_dalton_logging():
RULECAT_SCRIPT = dalton_config.get("dalton", "rulecat_script")
MAX_PCAP_FILES = dalton_config.getint("dalton", "max_pcap_files")
DEBUG = dalton_config.getboolean("dalton", "debug")
AUTH_PREFIX = dalton_config.get("dalton", "auth_prefix")
AUTH_MAX = dalton_config.getint("dalton", "auth_max")

# options for flowsynth
FS_BIN_PATH = dalton_config.get(
Expand All @@ -105,6 +107,7 @@ def setup_dalton_logging():
"Problem parsing config file '%s': %s" % (dalton_config_filename, e)
)


if DEBUG or ("CONTROLLER_DEBUG" in os.environ and int(os.getenv("CONTROLLER_DEBUG"))):
logger.setLevel(logging.DEBUG)
DEBUG = True
Expand Down Expand Up @@ -532,28 +535,63 @@ def delete_old_job_files():
return str(total_deleted)


def check_user(f):
@wraps(f)
def check_user_fun(*args, **kwargs):
if AUTH_PREFIX == 'disabled':
# auth disabled
return f(*args, **kwargs)
user = None
try:
user = request.cookies.get('dalton_user')
except Exception:
user = None
if user is None or len(user) == 0 or not user.startswith(AUTH_PREFIX) or len(user) > AUTH_MAX:
return redirect(url_for('dalton_blueprint.set_user'))
return f(*args, **kwargs)
return check_user_fun


@dalton_blueprint.route("/")
@check_user
def index():
logger.debug("ENVIRON:\n%s" % request.environ)
# make sure redirect is set to use http or https as appropriate
rurl = url_for("dalton_blueprint.page_index", _external=True)
if rurl.startswith("http"):
if "HTTP_X_FORWARDED_PROTO" in request.environ:
# if original request was https, make sure redirect uses https
rurl = rurl.replace("http", request.environ["HTTP_X_FORWARDED_PROTO"])
return redirect(url_for("dalton_blueprint.page_index"))

@dalton_blueprint.route("/dalton/logout", methods=["GET"])
@dalton_blueprint.route("/dalton/logout/", methods=["GET"])
@dalton_blueprint.route("/logout", methods=["GET"])
@dalton_blueprint.route("/logout/", methods=["GET"])
def logout():
response = redirect(url_for('dalton_blueprint.set_user'))
response.set_cookie('dalton_user', "")
return response


@dalton_blueprint.route("/dalton/setuser", methods=["GET", "POST"])
def set_user():
if AUTH_PREFIX == 'disabled':
# auth disabled
return redirect(url_for('dalton_blueprint.page_index'))

user = None
try:
if request.method == 'POST':
user = request.form.get("username")
else:
logger.warning(
"Could not find request.environ['HTTP_X_FORWARDED_PROTO']. Make sure the web server (proxy) is configured to send it."
)
else:
# this shouldn't be the case with '_external=True' passed to url_for()
logger.warning("URL does not start with 'http': %s" % rurl)
return redirect(rurl)
user = request.cookies.get('dalton_user')
except Exception:
user = None
if user is None or len(user) == 0 or not user.startswith(AUTH_PREFIX) or len(user) > AUTH_MAX:
return render_template("/dalton/setuser.html", user="")

response = redirect(url_for('dalton_blueprint.page_index'))
response.set_cookie('dalton_user', user, max_age=432000)
return response


@dalton_blueprint.route("/dalton")
@dalton_blueprint.route("/dalton/")
# @login_required()
@check_user
def page_index():
"""the default homepage for Dalton"""
return render_template("/dalton/index.html", page="")
Expand All @@ -562,7 +600,7 @@ def page_index():
# 'sensor' value includes forward slashes so this isn't a RESTful endpoint
# and 'sensor' value must be passed as a GET parameter
@dalton_blueprint.route("/dalton/controller_api/request_engine_conf", methods=["GET"])
# @auth_required()
@check_user
def api_get_engine_conf_file():
try:
sensor = request.args["sensor"]
Expand Down Expand Up @@ -966,7 +1004,7 @@ def post_job_results(jobid):


@dalton_blueprint.route("/dalton/controller_api/job_status/<jobid>", methods=["GET"])
# @login_required()
@check_user
def get_ajax_job_status_msg(jobid):
"""return the job status msg (as a string)"""
redis = get_redis()
Expand Down Expand Up @@ -1001,7 +1039,7 @@ def get_ajax_job_status_msg(jobid):
@dalton_blueprint.route(
"/dalton/controller_api/job_status_code/<jobid>", methods=["GET"]
)
# @login_required()
@check_user
def get_ajax_job_status_code(jobid):
"""return the job status code (AS A STRING! -- you need to cast the return value as an int if you want to use it as an int)"""
redis = get_redis()
Expand Down Expand Up @@ -1080,7 +1118,7 @@ def clear_old_agents(redis):


@dalton_blueprint.route("/dalton/sensor", methods=["GET"])
# @login_required()
@check_user
def page_sensor_default(return_dict=False):
"""the default sensor page"""
redis = get_redis()
Expand Down Expand Up @@ -1146,6 +1184,7 @@ def validate_jobid(jid):


@dalton_blueprint.route("/dalton/coverage/job/<jid>", methods=["GET"])
@check_user
def page_coverage_jid(jid, error=None):
redis = get_redis()

Expand Down Expand Up @@ -1237,7 +1276,7 @@ def page_coverage_jid(jid, error=None):


@dalton_blueprint.route("/dalton/coverage/<sensor_tech>/", methods=["GET"])
# @login_required()
@check_user
def page_coverage_default(sensor_tech, error=None):
"""the default coverage wizard page"""
redis = get_redis()
Expand Down Expand Up @@ -1348,7 +1387,7 @@ def page_coverage_default(sensor_tech, error=None):


@dalton_blueprint.route("/dalton/job/<jid>")
# @auth_required()
@check_user
def page_show_job(jid):
redis = get_redis()
tech = redis.get("%s-tech" % jid)
Expand Down Expand Up @@ -1673,8 +1712,7 @@ def submit_job():


@dalton_blueprint.route("/dalton/coverage/summary", methods=["POST"])
# @auth_required()
# ^^ can change and add resource and group permissions if we want to restrict who can submit jobs
@check_user
def page_coverage_summary():
"""Handle job submission from UI."""
# user submitting a job to Dalton via the web interface
Expand All @@ -1685,8 +1723,10 @@ def page_coverage_summary():

prod_ruleset_name = None

# get the user who submitted the job .. not implemented
user = "undefined"
# get the user who submitted the job
user = request.cookies.get('dalton_user')
if user is None:
user = ""

# generate job_id based of pcap filenames and timestamp
digest.update(str(datetime.datetime.now()).encode("utf-8"))
Expand Down Expand Up @@ -2964,29 +3004,14 @@ def page_coverage_summary():
# make sure redirect is set to use http or https as appropriate
if bSplitCap:
# TODO: something better than just redirect to queue page
rurl = url_for("dalton_blueprint.page_queue_default", _external=True)
else:
rurl = url_for(
"dalton_blueprint.page_show_job", jid=jid, _external=True
)
if rurl.startswith("http"):
if "HTTP_X_FORWARDED_PROTO" in request.environ:
# if original request was https, make sure redirect uses https
rurl = rurl.replace(
"http", request.environ["HTTP_X_FORWARDED_PROTO"]
)
else:
logger.warning(
"Could not find request.environ['HTTP_X_FORWARDED_PROTO']. Make sure the web server (proxy) is configured to send it."
)
rurl = url_for("dalton_blueprint.page_queue_default")
else:
# this shouldn't be the case with '_external=True' passed to url_for()
logger.warning("URL does not start with 'http': %s" % rurl)
rurl = url_for("dalton_blueprint.page_show_job", jid=jid)
return redirect(rurl)


@dalton_blueprint.route("/dalton/queue")
# @login_required()
@check_user
def page_queue_default():
"""the default queue page"""
redis = get_redis()
Expand Down Expand Up @@ -3059,8 +3084,16 @@ def page_queue_default():
job["jid"] = jid
job["tech"] = "%s" % redis.get("%s-tech" % jid)
job["time"] = "%s" % redis.get("%s-submission_time" % jid)
job["user"] = "%s" % redis.get("%s-user" % jid)
job["status"] = status_msg
# strip out auth prefix for display on queue page
user = redis.get("%s-user" % jid)
if user is None:
pass # handled by template
elif user.startswith(AUTH_PREFIX):
user = user[len(AUTH_PREFIX):]
elif '_' in user:
user = user.split('_', 1)[1]
job["user"] = user
alert_count = get_alert_count(redis, jid)
if status != STAT_CODE_DONE:
job["alert_count"] = "-"
Expand All @@ -3076,11 +3109,12 @@ def page_queue_default():
queued_jobs=queued_jobs,
running_jobs=running_jobs,
num_jobs=num_jobs_to_show,
non_empty_user_count=len([x['user'] for x in queue if x['user'] != "" and x["user"] is not None]),
)


@dalton_blueprint.route("/dalton/about")
# @login_required()
@check_user
def page_about_default():
"""the about/help page"""
# Need to `import app` here, not at the top of the file.
Expand Down Expand Up @@ -3211,7 +3245,7 @@ def controller_api_get_job_data(redis, jid, requested_data):
@dalton_blueprint.route(
"/dalton/controller_api/v2/<jid>/<requested_data>/<raw>", methods=["GET"]
)
# @auth_required()
@check_user
def controller_api_get_request(jid, requested_data, raw):
logger.debug(
f"controller_api_get_request() called, raw: {'True' if raw == 'raw' else 'False'}"
Expand Down Expand Up @@ -3249,6 +3283,7 @@ def controller_api_get_request(jid, requested_data, raw):
@dalton_blueprint.route(
"/dalton/controller_api/get-current-sensors/<engine>", methods=["GET"]
)
@check_user
def controller_api_get_current_sensors(engine):
"""Returns a list of current active sensors"""
redis = get_redis()
Expand Down Expand Up @@ -3300,6 +3335,7 @@ def controller_api_get_current_sensors(engine):
@dalton_blueprint.route(
"/dalton/controller_api/get-current-sensors-json-full", methods=["GET"]
)
@check_user
def controller_api_get_current_sensors_json_full():
"""Returns json with details about all the current active sensors"""
sensors = page_sensor_default(return_dict=True)
Expand All @@ -3312,6 +3348,7 @@ def controller_api_get_current_sensors_json_full():


@dalton_blueprint.route("/dalton/controller_api/get-max-pcap-files", methods=["GET"])
@check_user
def controller_api_get_max_pcap_files():
"""Returns the config value of max_pcap_files (the number of
pcap or compressed that can be uploaded per job).
Expand Down
8 changes: 6 additions & 2 deletions app/templates/dalton/queue.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ <h5>Show Recent:
<table class="table table-striped">
<tr>
<th>Job ID</th>
<!-- <th>User</th> -->
{% if non_empty_user_count > 0 %}
<th>User</th>
{% endif %}
<th>Alert Count</th>
<th>Queue</th>
<th>Submission Time</th>
Expand All @@ -32,7 +34,9 @@ <h5>Show Recent:
{% for job in queue %}
<tr>
<td><a href="/dalton/job/{{job.jid}}">{{ job.jid }}</td>
<!-- <td>{{ job.user }}</td> -->
{% if non_empty_user_count > 0 %}
<td>{{ job.user }}</td>
{% endif %}
<td>
{% if job.alert_count is number %}
{{ job.alert_count|int }}
Expand Down
14 changes: 14 additions & 0 deletions app/templates/dalton/setuser.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{% block body %}
<div class="login-form">
<form action="" method="post">
<h2 class="text-center">Set Username</h2>
<div class="form-group">
Username: <input type="text" class="form-control" placeholder="username" required="required" name="username">
</div>
<p>
<div class="form-group">
<button type="submit" class="btn btn-primary btn-block">Log in</button>
</div>
</form>
</div>
{% endblock %}
5 changes: 5 additions & 0 deletions dalton-agent/Dockerfiles/Dockerfile_suricata
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ RUN apt-get update -y && \
# for debugging agent
#RUN apt-get install -y less nano

# get suricatasc; needed for Suricata 8 and later because it is no longer included with the Suricata source
WORKDIR /opt
ADD https://github.com/jasonish/python-suricatasc/archive/refs/heads/main.tar.gz suricatasc.tar.gz
RUN tar -zxf suricatasc.tar.gz

# download, build, and install Suricata from source
RUN mkdir -p /src/suricata-${SURI_VERSION}
WORKDIR /src
Expand Down
2 changes: 2 additions & 0 deletions dalton-agent/dalton-agent.conf
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ USE_SURICATA_SOCKET_CONTROL = True
# Location of Suricata Socket Control Python Module (should be
# included with Suricata source), if not in PYTHONPATH. These are
# utilized by the Agent to interact with the Suricata Unix socket.
# If built from the Dockerfile, the Dalton Agent should handle this,
# even for Suri 8 and later where suricatasc was removed from source.
SURICATA_SC_PYTHON_MODULE = /src/suricata-REPLACE_AT_DOCKER_BUILD-VERSION/python

# File name of the socket used for Suricata socket control. Must be full path.
Expand Down
Loading
Loading