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
5 changes: 4 additions & 1 deletion backend/cases/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from .models import Case
from django.contrib.auth.models import User
from core.serializers import UserSerializer
from core.validators import validate_upload_filename

# class UserSerializer(serializers.ModelSerializer):
# class Meta:
Expand All @@ -18,7 +19,9 @@ class Meta:


class InitiateUploadSerializer(serializers.Serializer):
filename = serializers.CharField(max_length=255)
filename = serializers.CharField(
max_length=255, validators=[validate_upload_filename]
)
os = serializers.CharField(max_length=255)
case_id = serializers.IntegerField()

Expand Down
10 changes: 7 additions & 3 deletions backend/cases/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from evidences.models import Evidence
from .serializers import CaseSerializer, InitiateUploadSerializer, UploadChunkSerializer, CompleteUploadSerializer
from core.permissions import get_accessible_cases, check_case_access
from core.validators import safe_media_path
import os
import shutil

Expand Down Expand Up @@ -69,8 +70,11 @@ def post(self, request):
except ValueError:
return Response({'error': 'Invalid chunk filenames.'}, status=status.HTTP_400_BAD_REQUEST)

final_filename = upload_session.filename
final_file_path = os.path.join(settings.MEDIA_ROOT, 'evidences', final_filename)
try:
final_file_path = safe_media_path('evidences', upload_session.filename)
except ValueError:
return Response({'error': 'Invalid filename.'}, status=status.HTTP_400_BAD_REQUEST)
final_filename = os.path.basename(final_file_path)
os.makedirs(os.path.dirname(final_file_path), exist_ok=True)

# Assemble the chunks into the final file
Expand All @@ -85,7 +89,7 @@ def post(self, request):
# Create the Evidence record
evidence = Evidence.objects.create(
name=final_filename,
url=f"file://{os.path.join(settings.MEDIA_ROOT, 'evidences', final_filename)}",
url=f"file://{final_file_path}",
linked_case=upload_session.case,
os=upload_session.os,
etag=upload_session.upload_id,
Expand Down
44 changes: 44 additions & 0 deletions backend/core/validators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import os

from django.conf import settings
from rest_framework import serializers


def is_safe_upload_filename(value: str) -> bool:
"""
Return True only if ``value`` is a plain filename that cannot escape its
directory: no path separators (POSIX or Windows), no parent-directory
references, no null bytes, and equal to its own basename.
"""
if not value or value in (".", ".."):
return False
if "/" in value or "\\" in value or "\x00" in value:
return False
return value == os.path.basename(value)


def validate_upload_filename(value: str) -> str:
"""DRF field validator wrapper around :func:`is_safe_upload_filename`."""
if not is_safe_upload_filename(value):
raise serializers.ValidationError("Invalid filename.")
return value


def safe_media_path(subdir: str, filename: str) -> str:
"""
Resolve ``MEDIA_ROOT/<subdir>/<filename>`` and guarantee the result stays
inside ``MEDIA_ROOT/<subdir>``. Raises ``ValueError`` on any unsafe filename
or escape attempt.

The filename is validated here so callers cannot forget to; this is the
single safe sink for assembling user-named uploads. ``subdir`` MUST be a
trusted, hardcoded value (never user-controlled) — the guarantee only covers
the filename, not the subdir.
"""
if not is_safe_upload_filename(filename):
raise ValueError("Invalid upload filename.")
base_dir = os.path.realpath(os.path.join(settings.MEDIA_ROOT, subdir))
target = os.path.realpath(os.path.join(base_dir, filename))
if target == base_dir or os.path.commonpath([base_dir, target]) != base_dir:
raise ValueError("Resolved upload path escapes the target directory.")
return target
64 changes: 35 additions & 29 deletions backend/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,38 +16,44 @@ if [ "$DATABASE" = "postgresql" ] || [ "$DATABASE" = "postgres" ]; then
echo "PostgreSQL is ready!"
fi

echo "=== STARTING MIGRATIONS ==="
echo "Current working directory: $(pwd)"
echo "Current user: $(whoami)"
echo "Python version: $(python --version)"

echo "Checking migration status..."
if ! python manage.py showmigrations --list > /dev/null 2>&1; then
echo "Migration table appears to be corrupted. Attempting to fix..."

python manage.py migrate --run-syncdb --verbosity=2 || {
echo "Syncdb failed, trying fake-initial migration..."
python manage.py migrate --fake-initial --verbosity=2
}
# Only one service runs migrations/static/admin; running them in every worker
# would make concurrent migrate runs race on the same DDL.
if [ "${RUN_MIGRATIONS:-false}" = "true" ]; then
echo "=== STARTING MIGRATIONS ==="
echo "Current working directory: $(pwd)"
echo "Current user: $(whoami)"
echo "Python version: $(python --version)"

echo "Checking migration status..."
if ! python manage.py showmigrations --list > /dev/null 2>&1; then
echo "Migration table appears to be corrupted. Attempting to fix..."

python manage.py migrate --run-syncdb --verbosity=2 || {
echo "Syncdb failed, trying fake-initial migration..."
python manage.py migrate --fake-initial --verbosity=2
}
else
echo "Migration table exists, proceeding normally..."
echo "Django apps: $(python manage.py showmigrations --list)"
fi

echo "Making migrations..."
python manage.py makemigrations cases evidences yararulesets yararules volatility_engine symbols --noinput --verbosity=2

echo "Applying migrations..."
python manage.py migrate --noinput --verbosity=2

echo "Migrations completed successfully!"

echo "Collecting static files..."
python manage.py collectstatic --noinput --verbosity=2

echo "Creating admin user if not exists..."
python manage.py initadmin
else
echo "Migration table exists, proceeding normally..."
echo "Django apps: $(python manage.py showmigrations --list)"
echo "=== SKIPPING MIGRATIONS (RUN_MIGRATIONS not set) ==="
fi

echo "Making migrations..."
python manage.py makemigrations cases evidences yararulesets yararules volatility_engine symbols --noinput --verbosity=2

echo "Applying migrations..."
python manage.py migrate --noinput --verbosity=2

echo "Migrations completed successfully!"

echo "Collecting static files..."
python manage.py collectstatic --noinput --verbosity=2

echo "Creating admin user if not exists..."
python manage.py initadmin

echo "=== ENTRYPOINT COMPLETED ==="

exec "$@"
37 changes: 36 additions & 1 deletion backend/evidences/signals.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,33 @@
import logging
import os
import shutil

from django.conf import settings
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
from evidences.models import Evidence
from evidences.serializers import EvidenceSerializer
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer

logger = logging.getLogger(__name__)


@receiver(post_save, sender=Evidence)
def send_evidence_created(sender, instance, created, **kwargs):


# Kick off automatic Linux ISF resolution so the analyst doesn't have to
# upload kernel symbols manually before running plugins.
if created and instance.os == "linux":
try:
from volatility_engine.tasks import generate_linux_symbols
generate_linux_symbols.delay(instance.id)
except Exception:
import logging
logging.getLogger(__name__).exception(
"Failed to enqueue Linux ISF resolution for evidence %s", instance.id
)

channel_layer = get_channel_layer()
serializer = EvidenceSerializer(instance)

Expand All @@ -26,6 +45,22 @@ def send_evidence_created(sender, instance, created, **kwargs):
)


@receiver(post_delete, sender=Evidence)
def cleanup_evidence_media(sender, instance, **kwargs):
"""
Remove the evidence's dumped-artefact directory (media/{id}/) from disk.
"""
artefact_dir = os.path.join(settings.MEDIA_ROOT, str(instance.id))
if os.path.isdir(artefact_dir):
try:
shutil.rmtree(artefact_dir, ignore_errors=True)
logger.info("Removed media directory for deleted evidence %s", instance.id)
except Exception:
logger.exception(
"Failed to remove media directory for deleted evidence %s", instance.id
)


@receiver(post_delete, sender=Evidence)
def send_evidence_deleted(sender, instance, **kwargs):
channel_layer = get_channel_layer()
Expand Down
34 changes: 34 additions & 0 deletions backend/symbols/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,23 @@ def send_symbol_created(sender, instance, created, **kwargs):
{"type": "send_notification", "status": "created", "message": serializer.data},
)

# A newly uploaded Linux ISF may satisfy evidences still waiting for symbols:
# re-verify the ones not yet ready so the extraction gate can open.
if created and instance.os == "Linux":
try:
from volatility_engine.models import LinuxSymbolResolution
from volatility_engine.tasks import reverify_linux_symbols
pending = LinuxSymbolResolution.objects.exclude(status="ready").values_list(
"evidence_id", flat=True
)
for evidence_id in pending:
reverify_linux_symbols.delay(evidence_id)
except Exception:
import logging
logging.getLogger(__name__).exception(
"Failed to trigger ISF re-verification after symbol upload"
)


@receiver(post_delete, sender=Symbol)
def send_symbol_deleted(sender, instance, **kwargs):
Expand All @@ -24,3 +41,20 @@ def send_symbol_deleted(sender, instance, **kwargs):
"symbols",
{"type": "send_notification", "status": "deleted", "message": serializer.data},
)

# Deleting a Linux ISF may break evidences that were relying on it:
# re-validate the currently-ready ones so the extraction gate re-closes.
if instance.os == "Linux":
try:
from volatility_engine.models import LinuxSymbolResolution
from volatility_engine.tasks import reverify_linux_symbols
ready = LinuxSymbolResolution.objects.filter(status="ready").values_list(
"evidence_id", flat=True
)
for evidence_id in ready:
reverify_linux_symbols.delay(evidence_id)
except Exception:
import logging
logging.getLogger(__name__).exception(
"Failed to trigger ISF re-validation after symbol deletion"
)
Loading