├── nginx
├── ssl
│ └── .gitkeep
└── nginx.conf
├── backend
├── cases
│ ├── __init__.py
│ ├── migrations
│ │ ├── __init__.py
│ │ └── 0001_initial.py
│ ├── tests.py
│ ├── admin.py
│ ├── apps.py
│ ├── urls.py
│ ├── models.py
│ ├── serializers.py
│ └── signals.py
├── core
│ ├── __init__.py
│ ├── migrations
│ │ ├── __init__.py
│ │ └── 0001_initial.py
│ ├── tests.py
│ ├── admin.py
│ ├── apps.py
│ ├── serializers.py
│ ├── urls.py
│ ├── models.py
│ ├── management
│ │ └── commands
│ │ │ └── initadmin.py
│ └── stix.py
├── symbols
│ ├── __init__.py
│ ├── migrations
│ │ ├── __init__.py
│ │ └── 0001_initial.py
│ ├── tests.py
│ ├── admin.py
│ ├── serializers.py
│ ├── apps.py
│ ├── urls.py
│ ├── models.py
│ ├── signals.py
│ └── views.py
├── evidences
│ ├── __init__.py
│ ├── migrations
│ │ ├── __init__.py
│ │ └── 0001_initial.py
│ ├── tests.py
│ ├── admin.py
│ ├── apps.py
│ ├── urls.py
│ ├── serializers.py
│ ├── models.py
│ └── signals.py
├── volatility_engine
│ ├── __init__.py
│ ├── migrations
│ │ ├── __init__.py
│ │ └── 0001_initial.py
│ ├── tests.py
│ ├── admin.py
│ ├── apps.py
│ ├── volweb_misc.json
│ ├── signals.py
│ ├── serializers.py
│ ├── models.py
│ ├── urls.py
│ ├── plugins
│ │ ├── linux
│ │ │ ├── volweb_misc.py
│ │ │ └── volweb_main.py
│ │ └── windows
│ │ │ ├── volweb_misc.py
│ │ │ └── volweb_main.py
│ └── tasks.py
├── .dockerignore
├── backend
│ ├── __init__.py
│ ├── celery.py
│ ├── wsgi.py
│ ├── asgi.py
│ ├── keyconfig.py
│ ├── routing.py
│ └── urls.py
├── .gitignore
├── entrypoint.sh
├── manage.py
├── Dockerfile
└── requirements.txt
├── .gitignore
├── frontend
├── .dockerignore
├── src
│ ├── vite-env.d.ts
│ ├── components
│ │ ├── Investigate
│ │ │ ├── Loot.tsx
│ │ │ ├── Linux
│ │ │ │ ├── Buttons
│ │ │ │ │ ├── DumpPslistButton.tsx
│ │ │ │ │ ├── FileScanButton.tsx
│ │ │ │ │ └── DumpMapsButton.tsx
│ │ │ │ └── Components
│ │ │ │ │ └── ProcessMetadata.tsx
│ │ │ ├── Windows
│ │ │ │ ├── Buttons
│ │ │ │ │ ├── DumpPslistButton.tsx
│ │ │ │ │ ├── FileScanButton.tsx
│ │ │ │ │ └── ComputeHandlesButton.tsx
│ │ │ │ └── Components
│ │ │ │ │ └── ProcessMetadata.tsx
│ │ │ └── PluginDataGrid.tsx
│ │ ├── LinearProgressBar.tsx
│ │ ├── Statistics
│ │ │ └── StatisticsCard.tsx
│ │ ├── RecentItems
│ │ │ ├── RecentCases.tsx
│ │ │ └── RecentISF.tsx
│ │ ├── Explore
│ │ │ ├── Windows
│ │ │ │ ├── FilteredPlugins.tsx
│ │ │ │ ├── Explore.tsx
│ │ │ │ ├── GraphDataController.tsx
│ │ │ │ ├── ProcessGraph.tsx
│ │ │ │ └── GraphEventsController.tsx
│ │ │ ├── Linux
│ │ │ │ ├── FilteredPlugins.tsx
│ │ │ │ ├── GraphDataController.tsx
│ │ │ │ ├── Explore.tsx
│ │ │ │ ├── ProcessGraph.tsx
│ │ │ │ └── GraphEventsController.tsx
│ │ │ └── EnrichedDataGrid.tsx
│ │ ├── StixButton.tsx
│ │ ├── Charts
│ │ │ ├── DonutChart.tsx
│ │ │ └── LineChart.tsx
│ │ ├── InvestigatorSelect.tsx
│ │ ├── SnackbarProvider.tsx
│ │ ├── MenuBar.tsx
│ │ ├── Lists
│ │ │ └── PluginList.tsx
│ │ ├── Dialogs
│ │ │ ├── SymbolCreationDialog.tsx
│ │ │ └── CaseCreationDialog.tsx
│ │ └── EvidenceMetadata.tsx
│ ├── main.tsx
│ ├── pages
│ │ ├── cases
│ │ │ ├── Cases.tsx
│ │ │ └── CaseDetail.tsx
│ │ ├── evidences
│ │ │ └── Evidences.tsx
│ │ ├── symbols
│ │ │ └── Symbols.tsx
│ │ └── dashboard
│ │ │ └── Dashboard.tsx
│ ├── utils
│ │ ├── countTasksByDate.ts
│ │ ├── downloadFile.ts
│ │ ├── axiosInstance.ts
│ │ └── processAnalysis.ts
│ ├── App.tsx
│ ├── index.css
│ ├── types.tsx
│ └── assets
│ │ └── react.svg
├── tsconfig.json
├── .gitignore
├── index.html
├── Dockerfile
├── tsconfig.node.json
├── vite.config.ts
├── tsconfig.app.json
├── eslint.config.js
├── nginx.conf
├── package.json
└── README.md
├── .env.dev
├── .env.example
├── docker-compose-dev.yaml
├── docker-compose.yaml
├── docker-compose-prod.yaml
└── README.md
/nginx/ssl/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/cases/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/core/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/symbols/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/evidences/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/cases/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/core/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/symbols/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/volatility_engine/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/evidences/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/.dockerignore:
--------------------------------------------------------------------------------
1 | media/*
2 | venv/*
3 |
--------------------------------------------------------------------------------
/backend/volatility_engine/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.DS_Store
2 | venv/*
3 | .env
4 | *.pem
5 |
--------------------------------------------------------------------------------
/frontend/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | eslint.config.js
3 |
--------------------------------------------------------------------------------
/frontend/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/backend/core/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | # Create your tests here.
4 |
--------------------------------------------------------------------------------
/backend/cases/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | # Create your tests here.
4 |
--------------------------------------------------------------------------------
/backend/core/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | # Register your models here.
4 |
--------------------------------------------------------------------------------
/backend/evidences/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | # Create your tests here.
4 |
--------------------------------------------------------------------------------
/backend/symbols/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | # Create your tests here.
4 |
--------------------------------------------------------------------------------
/backend/backend/__init__.py:
--------------------------------------------------------------------------------
1 | from .celery import app as celery_app
2 |
3 | __all__ = ["celery_app"]
4 |
--------------------------------------------------------------------------------
/backend/volatility_engine/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | # Create your tests here.
4 |
--------------------------------------------------------------------------------
/.env.dev:
--------------------------------------------------------------------------------
1 | CSRF_TRUSTED_ORIGINS=http://localhost:5173
2 | DEBUG=True
3 | BROKER_HOST=localhost
4 | BROKER_PORT=6379
5 |
--------------------------------------------------------------------------------
/frontend/src/components/Investigate/Loot.tsx:
--------------------------------------------------------------------------------
1 | // This should display the list of dumped artefacts (registry, process, files, ...)
2 |
--------------------------------------------------------------------------------
/backend/cases/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from .models import *
3 |
4 | # Register your models here.
5 | admin.site.register(Case)
6 |
--------------------------------------------------------------------------------
/backend/.gitignore:
--------------------------------------------------------------------------------
1 | *.pem
2 | db.sqlite3
3 | *.DS_Store
4 | docker/.env
5 | venv/*
6 | minio_storage
7 | *__pycache__
8 | media/*
9 | /media/symbols/*
10 |
--------------------------------------------------------------------------------
/backend/evidences/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from .models import *
3 |
4 | # Register your models here.
5 | admin.site.register(Evidence)
6 |
--------------------------------------------------------------------------------
/backend/symbols/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from .models import Symbol
3 |
4 | # Register your models here.
5 | admin.site.register(Symbol)
6 |
--------------------------------------------------------------------------------
/frontend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "references": [
4 | { "path": "./tsconfig.app.json" },
5 | { "path": "./tsconfig.node.json" }
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/backend/core/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class CoreConfig(AppConfig):
5 | default_auto_field = "django.db.models.BigAutoField"
6 | name = "core"
7 |
--------------------------------------------------------------------------------
/backend/symbols/serializers.py:
--------------------------------------------------------------------------------
1 | from rest_framework import serializers
2 | from .models import Symbol
3 |
4 |
5 | class SymbolSerializer(serializers.ModelSerializer):
6 | class Meta:
7 | model = Symbol
8 | fields = "__all__"
9 |
--------------------------------------------------------------------------------
/backend/volatility_engine/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | # Register your models here.
4 | from .models import VolatilityPlugin, EnrichedProcess
5 |
6 | admin.site.register(VolatilityPlugin)
7 | admin.site.register(EnrichedProcess)
8 |
--------------------------------------------------------------------------------
/backend/cases/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class CasesConfig(AppConfig):
5 | default_auto_field = "django.db.models.BigAutoField"
6 | name = "cases"
7 |
8 | def ready(self):
9 | import cases.signals
10 |
--------------------------------------------------------------------------------
/backend/symbols/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class SymbolsConfig(AppConfig):
5 | default_auto_field = "django.db.models.BigAutoField"
6 | name = "symbols"
7 |
8 | def ready(self):
9 | import symbols.signals
10 |
--------------------------------------------------------------------------------
/backend/evidences/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class EvidencesConfig(AppConfig):
5 | default_auto_field = "django.db.models.BigAutoField"
6 | name = "evidences"
7 |
8 | def ready(self):
9 | import evidences.signals
10 |
--------------------------------------------------------------------------------
/frontend/src/main.tsx:
--------------------------------------------------------------------------------
1 | import { StrictMode } from 'react'
2 | import { createRoot } from 'react-dom/client'
3 | import App from './App.tsx'
4 | import './index.css'
5 |
6 | createRoot(document.getElementById('root')!).render(
7 |
8 |
9 | ,
10 | )
11 |
--------------------------------------------------------------------------------
/backend/volatility_engine/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class VolatilityEngineConfig(AppConfig):
5 | default_auto_field = "django.db.models.BigAutoField"
6 | name = "volatility_engine"
7 |
8 | def ready(self):
9 | import volatility_engine.signals
10 |
--------------------------------------------------------------------------------
/frontend/src/pages/cases/Cases.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Box } from "@mui/material";
3 | import CaseList from "../../components/Lists/CaseList";
4 |
5 | const Cases: React.FC = () => {
6 | return (
7 |
8 |
9 |
10 | );
11 | };
12 |
13 | export default Cases;
14 |
--------------------------------------------------------------------------------
/frontend/src/pages/evidences/Evidences.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import EvidenceList from "../../components/Lists/EvidenceList";
3 | import Box from "@mui/material/Box";
4 |
5 | const EvidencePage: React.FC = () => {
6 | return (
7 |
8 |
9 |
10 | );
11 | };
12 | export default EvidencePage;
13 |
--------------------------------------------------------------------------------
/frontend/src/pages/symbols/Symbols.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import SymbolsList from "../../components/Lists/SymbolsList";
3 | import Box from "@mui/material/Box";
4 |
5 | const SymbolsPage: React.FC = () => {
6 | return (
7 |
8 |
9 |
10 | );
11 | };
12 |
13 | export default SymbolsPage;
14 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | DEBUG=False
2 |
3 | DJANGO_SECRET=change_me_or_die
4 | CSRF_TRUSTED_ORIGINS=http://localhost:3000
5 |
6 | POSTGRES_USER=volweb # Change me
7 | POSTGRES_PASSWORD=volweb # Change me
8 | POSTGRES_DB=volweb
9 |
10 | DATABASE=postgres
11 | DATABASE_HOST=volweb-postgresdb
12 | DATABASE_PORT=5432
13 |
14 |
15 | BROKER_HOST=volweb-redis
16 | BROKER_PORT=6379
17 |
--------------------------------------------------------------------------------
/backend/backend/celery.py:
--------------------------------------------------------------------------------
1 | from celery import Celery
2 | import os
3 |
4 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings")
5 | app = Celery("backend")
6 | app.config_from_object("django.conf:settings", namespace="CELERY")
7 |
8 | app.conf.update(
9 | result_expires=3600 * 24,
10 | )
11 | app.conf.task_track_started = True
12 | app.autodiscover_tasks()
13 |
--------------------------------------------------------------------------------
/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/frontend/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | VolWeb
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/backend/symbols/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path, include
2 | from rest_framework import routers
3 | from .views import SymbolViewSet, SymbolUploadView
4 |
5 | router = routers.DefaultRouter()
6 | router.register(r"symbols", SymbolViewSet, basename="symbol")
7 |
8 | urlpatterns = [
9 | path(
10 | "upload_symbols/",
11 | SymbolUploadView.as_view(),
12 | name="upload-symbols",
13 | ),
14 | path("", include(router.urls)),
15 | ]
16 |
--------------------------------------------------------------------------------
/backend/backend/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for backend project.
3 |
4 | It exposes the WSGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.wsgi import get_wsgi_application
13 |
14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings")
15 |
16 | application = get_wsgi_application()
17 |
--------------------------------------------------------------------------------
/backend/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | if [ "$DATABASE" = "postgres" ]; then
4 | echo "Waiting for postgres..."
5 |
6 | while ! nc -z $DATABASE_HOST $DATABASE_PORT; do
7 | sleep 0.1
8 | done
9 |
10 | echo "PostgreSQL started"
11 | fi
12 | echo "Making migrations and migrating the database. "
13 | python manage.py makemigrations --noinput
14 | python manage.py migrate --noinput
15 | python manage.py collectstatic --noinput
16 | python manage.py initadmin
17 | exec "$@"
18 |
--------------------------------------------------------------------------------
/backend/backend/asgi.py:
--------------------------------------------------------------------------------
1 | import os
2 | from django.core.asgi import get_asgi_application
3 | from channels.routing import ProtocolTypeRouter, URLRouter
4 | from channels.auth import AuthMiddlewareStack
5 | from backend.routing import websockets_urlpatterns
6 |
7 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings")
8 |
9 | application = ProtocolTypeRouter(
10 | {
11 | "http": get_asgi_application(),
12 | "websocket": AuthMiddlewareStack(URLRouter(websockets_urlpatterns)),
13 | }
14 | )
15 |
--------------------------------------------------------------------------------
/frontend/Dockerfile:
--------------------------------------------------------------------------------
1 | # Stage 1: Build the frontend using a Node.js image
2 | FROM node:18-alpine AS builder
3 |
4 | WORKDIR /app
5 |
6 | COPY package.json package-lock.json ./
7 | RUN npm ci
8 |
9 | COPY . .
10 |
11 | RUN npm run build
12 |
13 | FROM nginx:1.23-alpine
14 |
15 | # Remove default Nginx static assets
16 | RUN rm -rf /usr/share/nginx/html/*
17 |
18 | COPY --from=builder /app/dist /usr/share/nginx/html
19 | COPY nginx.conf /etc/nginx/conf.d/default.conf
20 |
21 | EXPOSE 3000
22 |
23 | CMD ["nginx", "-g", "daemon off;"]
24 |
--------------------------------------------------------------------------------
/backend/backend/keyconfig.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 |
4 | class Database:
5 | NAME = os.getenv("POSTGRES_DB", None)
6 | USER = os.getenv("POSTGRES_USER", None)
7 | PASSWORD = os.getenv("POSTGRES_PASSWORD", None)
8 | HOST = os.getenv("DATABASE_HOST", None)
9 | PORT = os.getenv("DATABASE_PORT", None)
10 |
11 |
12 | class Secrets:
13 | SECRET_KEY = os.getenv("DJANGO_SECRET", None)
14 | BROKER_HOST = os.getenv("BROKER_HOST", "127.0.0.1")
15 | BROKER_PORT = os.getenv("BROKER_PORT", "6379")
16 | WEBSOCKET_URL = os.getenv("WEBSOCKET_URL", None)
17 |
--------------------------------------------------------------------------------
/backend/backend/routing.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 | from .consumers import (
3 | VolatilityTaskConsumer,
4 | CasesTaskConsumer,
5 | EvidencesTaskConsumer,
6 | SymbolsTaskConsumer,
7 | )
8 |
9 | websockets_urlpatterns = [
10 | path("ws/cases/", CasesTaskConsumer.as_asgi()),
11 | path("ws/evidences/", EvidencesTaskConsumer.as_asgi()),
12 | path("ws/evidences//", EvidencesTaskConsumer.as_asgi()),
13 | path("ws/symbols/", SymbolsTaskConsumer.as_asgi()),
14 | path("ws/engine//", VolatilityTaskConsumer.as_asgi()),
15 | ]
16 |
--------------------------------------------------------------------------------
/frontend/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "lib": ["ES2023"],
5 | "module": "ESNext",
6 | "skipLibCheck": true,
7 |
8 | /* Bundler mode */
9 | "moduleResolution": "bundler",
10 | "allowImportingTsExtensions": true,
11 | "isolatedModules": true,
12 | "moduleDetection": "force",
13 | "noEmit": true,
14 |
15 | /* Linting */
16 | "strict": true,
17 | "noUnusedLocals": true,
18 | "noUnusedParameters": true,
19 | "noFallthroughCasesInSwitch": true
20 | },
21 | "include": ["vite.config.ts"]
22 | }
23 |
--------------------------------------------------------------------------------
/backend/evidences/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path, include
2 | from rest_framework import routers
3 | from .views import EvidenceViewSet, EvidenceStatisticsApiView, BindEvidenceViewSet
4 |
5 | router = routers.DefaultRouter()
6 | router.register(r"evidences", EvidenceViewSet, basename="evidence")
7 |
8 | urlpatterns = [
9 | path(
10 | "evidence-statistics//",
11 | EvidenceStatisticsApiView.as_view(),
12 | name="statistics",
13 | ),
14 | path("evidences/bind/", BindEvidenceViewSet.as_view(), name="bind"),
15 | path("", include(router.urls)),
16 | ]
17 |
--------------------------------------------------------------------------------
/frontend/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import react from "@vitejs/plugin-react-swc";
3 |
4 | export default defineConfig({
5 | plugins: [react()],
6 | server: {
7 | proxy: {
8 | "/api": "http://localhost:8000",
9 | "/core": "http://localhost:8000",
10 | "/media": "http://localhost:8000",
11 | "/admin": "http://localhost:8000",
12 | "/swagger": "http://localhost:8000",
13 | "/static": "http://localhost:8000",
14 | "/ws": {
15 | target: "ws://localhost:8000",
16 | ws: true,
17 | },
18 | },
19 | },
20 | });
21 |
--------------------------------------------------------------------------------
/frontend/src/utils/countTasksByDate.ts:
--------------------------------------------------------------------------------
1 | interface Task {
2 | date_created: string;
3 | }
4 |
5 | interface TaskStats {
6 | dates: string[];
7 | counts: number[];
8 | }
9 |
10 | export const countTasksByDate = (taskArray: Task[]): TaskStats => {
11 | const dateCounts: { [key: string]: number } = {};
12 | taskArray.forEach((task) => {
13 | const date = task.date_created.split("T")[0];
14 | dateCounts[date] = (dateCounts[date] || 0) + 1;
15 | });
16 | const dates = Object.keys(dateCounts).sort();
17 | const counts = dates.map((date) => dateCounts[date]);
18 |
19 | return { dates, counts };
20 | };
21 |
--------------------------------------------------------------------------------
/backend/volatility_engine/volweb_misc.json:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": {
3 | "windows": {
4 | "volatility3.plugins.windows.mftscan.ADS": {
5 | "icon": "AccountTree",
6 | "description": "Retrive the Alternative Data Stream from the MFT records.",
7 | "category": "Filesystem",
8 | "display": "True"
9 | },
10 | "volatility3.plugins.windows.mftscan.ResidentData": {
11 | "icon": "DataObject",
12 | "description": "Scans for MFT Records with Resident Data.",
13 | "category": "Filesystem",
14 | "display": "True"
15 | }
16 | },
17 | "linux": {}
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/backend/evidences/serializers.py:
--------------------------------------------------------------------------------
1 | from rest_framework import serializers
2 | from .models import Evidence
3 |
4 |
5 | class EvidenceSerializer(serializers.ModelSerializer):
6 | class Meta:
7 | model = Evidence
8 | fields = "__all__"
9 |
10 |
11 | class BindEvidenceSerializer(serializers.ModelSerializer):
12 | class Meta:
13 | model = Evidence
14 | fields = "__all__"
15 | extra_kwargs = {
16 | "access_key_id": {"write_only": True},
17 | "access_key": {"write_only": True},
18 | "etag": {"read_only": True},
19 | "name": {"read_only": True},
20 | }
21 |
--------------------------------------------------------------------------------
/backend/volatility_engine/signals.py:
--------------------------------------------------------------------------------
1 | from celery import states
2 | from celery.signals import before_task_publish
3 | from django_celery_results.models import TaskResult
4 |
5 |
6 | @before_task_publish.connect
7 | def create_task_result_on_publish(sender=None, headers=None, body=None, **kwargs):
8 | if "task" not in headers:
9 | return
10 |
11 | TaskResult.objects.store_result(
12 | "application/json",
13 | "utf-8",
14 | headers["id"],
15 | None,
16 | states.PENDING,
17 | task_name=headers["task"],
18 | task_args=headers["argsrepr"],
19 | task_kwargs=headers["kwargsrepr"],
20 | )
21 |
--------------------------------------------------------------------------------
/frontend/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "isolatedModules": true,
13 | "moduleDetection": "force",
14 | "noEmit": true,
15 | "jsx": "react-jsx",
16 |
17 | /* Linting */
18 | "strict": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "noFallthroughCasesInSwitch": true
22 | },
23 | "include": ["src"]
24 | }
25 |
--------------------------------------------------------------------------------
/docker-compose-dev.yaml:
--------------------------------------------------------------------------------
1 | services:
2 | volweb-redis:
3 | container_name: volweb-redis
4 | image: "redis:latest"
5 | command: ["redis-server", "--appendonly", "yes"]
6 | volumes:
7 | - "redis-data-dev:/data"
8 | ports:
9 | - "${BROKER_PORT}:${BROKER_PORT}"
10 |
11 | volweb-minio-dev:
12 | container_name: volweb-minio-dev
13 | network_mode: "host"
14 | image: minio/minio
15 | volumes:
16 | - minio-storage-dev:/data
17 | environment:
18 | - MINIO_ROOT_USER=user
19 | - MINIO_ROOT_PASSWORD=password
20 | command: server --console-address ":9001" /data
21 | volumes:
22 | minio-storage-dev:
23 | postgres-data-dev:
24 | redis-data-dev:
25 |
--------------------------------------------------------------------------------
/backend/cases/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path, include
2 | from rest_framework.routers import DefaultRouter
3 | from cases.views import (
4 | CaseViewSet,
5 | InitiateUploadView,
6 | UploadChunkView,
7 | CompleteUploadView,
8 | )
9 |
10 | router = DefaultRouter()
11 | router.register(r"cases", CaseViewSet)
12 |
13 | urlpatterns = [
14 | path(
15 | "cases/upload/initiate/", InitiateUploadView.as_view(), name="initiate_upload"
16 | ),
17 | path("cases/upload/chunk/", UploadChunkView.as_view(), name="upload_chunk"),
18 | path(
19 | "cases/upload/complete/", CompleteUploadView.as_view(), name="complete_upload"
20 | ),
21 | path("", include(router.urls)),
22 | ]
23 |
--------------------------------------------------------------------------------
/frontend/src/utils/downloadFile.ts:
--------------------------------------------------------------------------------
1 | import axiosInstance from "./axiosInstance";
2 |
3 | export const downloadFile = async (fileUrl: string, fileName: string) => {
4 | try {
5 | const response = await axiosInstance.get(fileUrl, {
6 | responseType: "blob",
7 | });
8 | const url = window.URL.createObjectURL(new Blob([response.data]));
9 | const link = document.createElement("a");
10 | link.href = url;
11 | link.setAttribute("download", fileName);
12 | document.body.appendChild(link);
13 | link.click();
14 |
15 | link.parentNode?.removeChild(link);
16 | window.URL.revokeObjectURL(url);
17 | } catch (error) {
18 | console.error("Error downloading file:", error);
19 | }
20 | };
21 |
--------------------------------------------------------------------------------
/backend/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """Django's command-line utility for administrative tasks."""
3 | import os
4 | import sys
5 |
6 |
7 | def main():
8 | """Run administrative tasks."""
9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings")
10 | try:
11 | from django.core.management import execute_from_command_line
12 | except ImportError as exc:
13 | raise ImportError(
14 | "Couldn't import Django. Are you sure it's installed and "
15 | "available on your PYTHONPATH environment variable? Did you "
16 | "forget to activate a virtual environment?"
17 | ) from exc
18 | execute_from_command_line(sys.argv)
19 |
20 |
21 | if __name__ == "__main__":
22 | main()
23 |
--------------------------------------------------------------------------------
/frontend/src/components/LinearProgressBar.tsx:
--------------------------------------------------------------------------------
1 | import LinearProgress, {
2 | LinearProgressProps,
3 | } from "@mui/material/LinearProgress";
4 | import Typography from "@mui/material/Typography";
5 | import Box from "@mui/material/Box";
6 |
7 | export default function LinearProgressWithLabel(
8 | props: LinearProgressProps & { value: number },
9 | ) {
10 | return (
11 |
12 |
13 |
14 |
15 |
16 | {`${Math.round(props.value)}%`}
20 |
21 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/frontend/eslint.config.js:
--------------------------------------------------------------------------------
1 | import js from '@eslint/js'
2 | import globals from 'globals'
3 | import reactHooks from 'eslint-plugin-react-hooks'
4 | import reactRefresh from 'eslint-plugin-react-refresh'
5 | import tseslint from 'typescript-eslint'
6 |
7 | export default tseslint.config(
8 | { ignores: ['dist'] },
9 | {
10 | extends: [js.configs.recommended, ...tseslint.configs.recommended],
11 | files: ['**/*.{ts,tsx}'],
12 | languageOptions: {
13 | ecmaVersion: 2020,
14 | globals: globals.browser,
15 | },
16 | plugins: {
17 | 'react-hooks': reactHooks,
18 | 'react-refresh': reactRefresh,
19 | },
20 | rules: {
21 | ...reactHooks.configs.recommended.rules,
22 | 'react-refresh/only-export-components': [
23 | 'warn',
24 | { allowConstantExport: true },
25 | ],
26 | },
27 | },
28 | )
29 |
--------------------------------------------------------------------------------
/frontend/src/components/Statistics/StatisticsCard.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Card, CardContent, Typography, Box } from "@mui/material";
3 |
4 | interface StatisticsCardProps {
5 | title: string;
6 | value: number;
7 | icon: React.ReactNode; // Accepts a React node for the icon
8 | }
9 |
10 | const StatisticsCard: React.FC = ({
11 | title,
12 | value,
13 | icon,
14 | }) => {
15 | return (
16 |
17 |
18 |
19 | {icon}
20 |
21 | {title}
22 |
23 |
24 | {value}
25 |
26 |
27 | );
28 | };
29 |
30 | export default StatisticsCard;
31 |
--------------------------------------------------------------------------------
/backend/symbols/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | import os
3 |
4 | CHOICES = (
5 | ("Windows", "Windows"),
6 | ("Linux", "Linux"),
7 | # ('MacOs', 'MacOs'), <- not implemented yet
8 | )
9 |
10 | UPLOAD_PATH = "symbols/"
11 |
12 |
13 | class Symbol(models.Model):
14 | id = models.AutoField(primary_key=True)
15 | name = models.CharField(max_length=100)
16 | os = models.CharField(max_length=50, choices=CHOICES)
17 | description = models.TextField(max_length=500)
18 | symbols_file = models.FileField(upload_to=UPLOAD_PATH)
19 |
20 | def __str__(self):
21 | return str(self.name)
22 |
23 | def save(self, *args, **kwargs):
24 | super(Symbol, self).save(*args, **kwargs)
25 |
26 | def delete(self, *args, **kwargs):
27 | if self.symbols_file:
28 | self.symbols_file.delete()
29 | super(Symbol, self).delete(*args, **kwargs)
30 |
--------------------------------------------------------------------------------
/backend/volatility_engine/serializers.py:
--------------------------------------------------------------------------------
1 | from rest_framework import serializers
2 | from .models import VolatilityPlugin, EnrichedProcess
3 | from django_celery_results.models import TaskResult
4 |
5 |
6 | class VolatilityPluginNameSerializer(serializers.ModelSerializer):
7 | class Meta:
8 | model = VolatilityPlugin
9 | fields = ["name", "description", "icon", "category", "display", "results"]
10 |
11 |
12 | class VolatilityPluginDetailSerializer(serializers.ModelSerializer):
13 | class Meta:
14 | model = VolatilityPlugin
15 | fields = ["name", "artefacts"]
16 |
17 |
18 | class TasksSerializer(serializers.ModelSerializer):
19 | task_kwargs = serializers.JSONField
20 |
21 | class Meta:
22 | model = TaskResult
23 | fields = "__all__"
24 |
25 |
26 | class EnrichedProcessSerializer(serializers.ModelSerializer):
27 | class Meta:
28 | model = EnrichedProcess
29 | fields = "__all__"
30 |
--------------------------------------------------------------------------------
/backend/cases/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from django.contrib.auth.models import User
3 | import uuid
4 |
5 |
6 | class Case(models.Model):
7 | id = models.AutoField(primary_key=True)
8 | name = models.CharField(max_length=500, unique=True)
9 | description = models.TextField()
10 | linked_users = models.ManyToManyField(User)
11 | last_update = models.DateField(auto_now=True)
12 |
13 | def __str__(self):
14 | return self.name
15 |
16 |
17 | class UploadSession(models.Model):
18 | upload_id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
19 | filename = models.CharField(max_length=255)
20 | case = models.ForeignKey(Case, on_delete=models.CASCADE)
21 | user = models.ForeignKey(User, on_delete=models.CASCADE)
22 | os = models.CharField(max_length=50, blank=True)
23 | created_at = models.DateTimeField(auto_now_add=True)
24 |
25 | def __str__(self):
26 | return str(self.upload_id)
27 |
--------------------------------------------------------------------------------
/backend/symbols/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 5.1.1 on 2024-11-26 22:31
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | initial = True
9 |
10 | dependencies = []
11 |
12 | operations = [
13 | migrations.CreateModel(
14 | name="Symbol",
15 | fields=[
16 | ("id", models.AutoField(primary_key=True, serialize=False)),
17 | ("name", models.CharField(max_length=100)),
18 | (
19 | "os",
20 | models.CharField(
21 | choices=[("Windows", "Windows"), ("Linux", "Linux")],
22 | max_length=50,
23 | ),
24 | ),
25 | ("description", models.TextField(max_length=500)),
26 | ("symbols_file", models.FileField(upload_to="symbols/")),
27 | ],
28 | ),
29 | ]
30 |
--------------------------------------------------------------------------------
/backend/cases/serializers.py:
--------------------------------------------------------------------------------
1 | from rest_framework import serializers
2 | from .models import Case
3 | from django.contrib.auth.models import User
4 | from core.serializers import UserSerializer
5 |
6 | # class UserSerializer(serializers.ModelSerializer):
7 | # class Meta:
8 | # model = User
9 | # fields = ["username", "id"]
10 |
11 |
12 | class CaseSerializer(serializers.ModelSerializer):
13 | linked_users = UserSerializer(many=True)
14 |
15 | class Meta:
16 | model = Case
17 | fields = "__all__"
18 |
19 |
20 | class InitiateUploadSerializer(serializers.Serializer):
21 | filename = serializers.CharField(max_length=255)
22 | os = serializers.CharField(max_length=255)
23 | case_id = serializers.IntegerField()
24 |
25 |
26 | class UploadChunkSerializer(serializers.Serializer):
27 | upload_id = serializers.UUIDField()
28 | part_number = serializers.IntegerField()
29 | chunk = serializers.FileField()
30 |
31 |
32 | class CompleteUploadSerializer(serializers.Serializer):
33 | upload_id = serializers.UUIDField()
34 |
--------------------------------------------------------------------------------
/frontend/src/components/RecentItems/RecentCases.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {
3 | Card,
4 | CardContent,
5 | Typography,
6 | List,
7 | ListItem,
8 | ListItemText,
9 | ListItemIcon,
10 | Divider,
11 | } from "@mui/material";
12 | import WorkIcon from "@mui/icons-material/Work";
13 | import { Case } from "../../types";
14 |
15 | interface RecentCasesProps {
16 | cases: Case[];
17 | }
18 |
19 | const RecentCases: React.FC = ({ cases }) => {
20 | return (
21 |
22 |
23 |
24 | Recent Cases
25 |
26 |
27 |
28 | {cases.map((caseItem, index) => (
29 |
30 |
31 |
32 |
33 |
34 |
35 | ))}
36 |
37 |
38 |
39 | );
40 | };
41 |
42 | export default RecentCases;
43 |
--------------------------------------------------------------------------------
/backend/cases/signals.py:
--------------------------------------------------------------------------------
1 | from django.db.models.signals import post_save, post_delete
2 | from django.dispatch import receiver
3 | from cases.models import Case
4 | from cases.serializers import CaseSerializer
5 | from volatility_engine.tasks import start_extraction, start_timeliner
6 | from asgiref.sync import async_to_sync
7 | from channels.layers import get_channel_layer
8 |
9 |
10 | @receiver(post_save, sender=Case)
11 | def send_case_created(sender, instance, created, **kwargs):
12 | channel_layer = get_channel_layer()
13 | serializer = CaseSerializer(instance)
14 | async_to_sync(channel_layer.group_send)(
15 | "cases",
16 | {"type": "send_notification", "status": "created", "message": serializer.data},
17 | )
18 |
19 |
20 | @receiver(
21 | post_delete,
22 | sender=Case,
23 | )
24 | def send_case_deleted(sender, instance, **kwargs):
25 | channel_layer = get_channel_layer()
26 | serializer = CaseSerializer(instance)
27 | async_to_sync(channel_layer.group_send)(
28 | "cases",
29 | {"type": "send_notification", "status": "deleted", "message": serializer.data},
30 | )
31 |
--------------------------------------------------------------------------------
/backend/symbols/signals.py:
--------------------------------------------------------------------------------
1 | from django.db.models.signals import post_save, post_delete
2 | from django.dispatch import receiver
3 | from symbols.models import Symbol
4 | from symbols.serializers import SymbolSerializer
5 | from volatility_engine.tasks import start_extraction, start_timeliner
6 | from asgiref.sync import async_to_sync
7 | from channels.layers import get_channel_layer
8 |
9 |
10 | @receiver(post_save, sender=Symbol)
11 | def send_symbol_created(sender, instance, created, **kwargs):
12 | channel_layer = get_channel_layer()
13 | serializer = SymbolSerializer(instance)
14 | async_to_sync(channel_layer.group_send)(
15 | "symbols",
16 | {"type": "send_notification", "status": "created", "message": serializer.data},
17 | )
18 |
19 |
20 | @receiver(post_delete, sender=Symbol)
21 | def send_symbol_deleted(sender, instance, **kwargs):
22 | channel_layer = get_channel_layer()
23 | serializer = SymbolSerializer(instance)
24 | async_to_sync(channel_layer.group_send)(
25 | "symbols",
26 | {"type": "send_notification", "status": "deleted", "message": serializer.data},
27 | )
28 |
--------------------------------------------------------------------------------
/frontend/src/components/RecentItems/RecentISF.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {
3 | Card,
4 | CardContent,
5 | Typography,
6 | List,
7 | ListItem,
8 | ListItemText,
9 | ListItemIcon,
10 | Divider,
11 | } from "@mui/material";
12 | import BackupTableIcon from "@mui/icons-material/BackupTable";
13 | interface ISFItem {
14 | name: string;
15 | }
16 |
17 | interface RecentISFProps {
18 | isfList: ISFItem[];
19 | }
20 |
21 | const RecentISF: React.FC = ({ isfList }) => {
22 | return (
23 |
24 |
25 |
26 | Recent ISF
27 |
28 |
29 |
30 | {isfList.map((isfItem, index) => (
31 |
32 |
33 |
34 |
35 |
36 |
37 | ))}
38 |
39 |
40 |
41 | );
42 | };
43 |
44 | export default RecentISF;
45 |
--------------------------------------------------------------------------------
/backend/core/serializers.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth.models import User
2 | from rest_framework import serializers
3 | from .models import Indicator
4 | from django_celery_results.models import TaskResult
5 |
6 |
7 | class UserSerializer(serializers.ModelSerializer):
8 | class Meta:
9 | model = User
10 | fields = ("id", "username", "email", "first_name", "last_name")
11 |
12 |
13 | class IndicatorSerializer(serializers.ModelSerializer):
14 | dump_linked_dump_name = serializers.SerializerMethodField()
15 |
16 | class Meta:
17 | model = Indicator
18 | fields = "__all__"
19 | extra_fields = ["dump_linked_dump_name"]
20 |
21 | def get_dump_linked_dump_name(self, obj):
22 | # Return the name of the linked case instead of the id
23 | return obj.evidence.name
24 |
25 |
26 | class TypeSerializer(serializers.Serializer):
27 | value = serializers.CharField()
28 | display = serializers.CharField()
29 |
30 |
31 | class TasksSerializer(serializers.ModelSerializer):
32 | task_kwargs = serializers.JSONField
33 |
34 | class Meta:
35 | model = TaskResult
36 | fields = "__all__"
37 |
--------------------------------------------------------------------------------
/backend/volatility_engine/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from evidences.models import Evidence
3 |
4 |
5 | class VolatilityPlugin(models.Model):
6 | """
7 | Django model of a volatility3 plugin
8 | Each plugin as a name a linked evidence and the extracted artefacts
9 | """
10 |
11 | name = models.CharField(max_length=100)
12 | icon = models.CharField(max_length=30, null=True)
13 | description = models.TextField(null=True)
14 | evidence = models.ForeignKey(Evidence, on_delete=models.CASCADE)
15 | artefacts = models.JSONField(null=True)
16 | category = models.CharField(max_length=100)
17 | display = models.CharField(max_length=10)
18 | results = models.BooleanField(default=False)
19 |
20 | def __str__(self):
21 | return str(self.name)
22 |
23 |
24 | class EnrichedProcess(models.Model):
25 | """
26 | Model to store enriched process information.
27 | Combines data from multiple plugins for processes.
28 | """
29 |
30 | evidence = models.ForeignKey(Evidence, on_delete=models.CASCADE)
31 | pid = models.IntegerField()
32 | data = models.JSONField()
33 |
34 | def __str__(self):
35 | return str(self.pid)
36 |
--------------------------------------------------------------------------------
/backend/evidences/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from cases.models import Case
3 |
4 | OS = (
5 | ("windows", "windows"),
6 | ("linux", "linux"),
7 | # ('MacOs', 'MacOs'), <- not implemented yet
8 | )
9 |
10 | SOURCES = (
11 | ("AWS", "AWS"),
12 | ("MINIO", "MINIO"),
13 | ("FILESYSTEM", "FILESYSTEM"),
14 | )
15 |
16 |
17 | class Evidence(models.Model):
18 | """
19 | Evidence Model
20 | Holds the important metadata about the memory image.
21 | """
22 |
23 | id = models.AutoField(primary_key=True)
24 | name = models.CharField(max_length=250)
25 | etag = models.CharField(max_length=256, unique=True)
26 | os = models.CharField(max_length=10, choices=OS)
27 | linked_case = models.ForeignKey(Case, on_delete=models.CASCADE, null=False)
28 | status = models.IntegerField(default=0)
29 | access_key_id = models.TextField(null=True)
30 | access_key = models.TextField(null=True)
31 | url = models.TextField(null=True)
32 | region = models.TextField(null=True)
33 | endpoint = models.TextField(null=True)
34 | source = models.CharField(max_length=10, choices=SOURCES, null=True)
35 |
36 | def __str__(self):
37 | return str(self.name)
38 |
--------------------------------------------------------------------------------
/backend/evidences/signals.py:
--------------------------------------------------------------------------------
1 | from django.db.models.signals import post_save, post_delete
2 | from django.dispatch import receiver
3 | from evidences.models import Evidence
4 | from evidences.serializers import EvidenceSerializer
5 | from volatility_engine.tasks import start_extraction, start_timeliner
6 | from asgiref.sync import async_to_sync
7 | from channels.layers import get_channel_layer
8 |
9 |
10 | @receiver(post_save, sender=Evidence)
11 | def send_evidence_created(sender, instance, created, **kwargs):
12 | if created:
13 | start_extraction.apply_async(args=[instance.id])
14 | channel_layer = get_channel_layer()
15 | serializer = EvidenceSerializer(instance)
16 | async_to_sync(channel_layer.group_send)(
17 | "evidences",
18 | {"type": "send_notification", "status": "created", "message": serializer.data},
19 | )
20 |
21 |
22 | @receiver(post_delete, sender=Evidence)
23 | def send_evidence_deleted(sender, instance, **kwargs):
24 | channel_layer = get_channel_layer()
25 | serializer = EvidenceSerializer(instance)
26 | async_to_sync(channel_layer.group_send)(
27 | "evidences",
28 | {"type": "send_notification", "status": "deleted", "message": serializer.data},
29 | )
30 |
--------------------------------------------------------------------------------
/backend/backend/urls.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from django.urls import path, include
3 | from django.conf.urls.static import static
4 | from django.conf import settings
5 | from django.urls import re_path
6 | from rest_framework import permissions
7 | from drf_yasg.views import get_schema_view
8 | from drf_yasg import openapi
9 |
10 | schema_view = get_schema_view(
11 | openapi.Info(
12 | title="VolWeb API",
13 | default_version='v1',
14 | description="Documentation",
15 | terms_of_service="https://github.com/k1nd0ne/VolWeb/",
16 | contact=openapi.Contact(email="k1nd0ne@mail.com"),
17 | license=openapi.License(name="GPL v3 License"),
18 | ),
19 | public=True,
20 | permission_classes=(permissions.AllowAny,)
21 | ,)
22 |
23 |
24 | urlpatterns = [
25 | path("admin/", admin.site.urls),
26 | path('swagger/', schema_view.without_ui(cache_timeout=0), name='schema-json'),
27 | path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
28 | path("api/", include("cases.urls")),
29 | path("api/", include("evidences.urls")),
30 | path("api/", include("symbols.urls")),
31 | path("api/", include("volatility_engine.urls")),
32 | path("core/", include("core.urls")),
33 | ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
34 |
--------------------------------------------------------------------------------
/backend/core/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 | from rest_framework_simplejwt import views as jwt_views
3 | from core.views import (
4 | LogoutView,
5 | UserList,
6 | IndicatorApiView,
7 | IndicatorCaseApiView,
8 | IndicatorExportApiView,
9 | IndicatorEvidenceApiView,
10 | IndicatorTypeListAPIView,
11 | StatisticsApiView,
12 | )
13 |
14 | urlpatterns = [
15 | path("logout/", LogoutView.as_view(), name="logout"),
16 | path("token/", jwt_views.TokenObtainPairView.as_view(), name="token_obtain_pair"),
17 | path("token/refresh/", jwt_views.TokenRefreshView.as_view(), name="token_refresh"),
18 | path("users/", UserList.as_view(), name="users"),
19 | path("stix/indicators/", IndicatorApiView.as_view()),
20 | path("stix/indicators//", IndicatorApiView.as_view()),
21 | path("stix/indicators/case//", IndicatorCaseApiView.as_view()),
22 | path(
23 | "stix/indicators/evidence//",
24 | IndicatorEvidenceApiView.as_view(),
25 | ),
26 | path("stix/export//", IndicatorExportApiView.as_view()),
27 | path(
28 | "stix/indicator-types/",
29 | IndicatorTypeListAPIView.as_view(),
30 | name="indicator_types",
31 | ),
32 | path("statistics/", StatisticsApiView.as_view(), name="statistics"),
33 | ]
34 |
--------------------------------------------------------------------------------
/backend/symbols/views.py:
--------------------------------------------------------------------------------
1 | from rest_framework import viewsets
2 | from .models import Symbol
3 | from .serializers import SymbolSerializer
4 | from rest_framework.permissions import IsAuthenticated
5 | from rest_framework.views import APIView
6 | from rest_framework.response import Response
7 | from rest_framework import status
8 | from django.core.files.storage import default_storage
9 | from django.core.files.base import ContentFile
10 |
11 |
12 | class SymbolViewSet(viewsets.ModelViewSet):
13 | permission_classes = (IsAuthenticated,)
14 | queryset = Symbol.objects.all()
15 | serializer_class = SymbolSerializer
16 |
17 |
18 | class SymbolUploadView(APIView):
19 | permission_classes = [IsAuthenticated]
20 |
21 | def post(self, request, format=None):
22 | serializer = SymbolSerializer(data=request.data)
23 | if serializer.is_valid():
24 | symbol = serializer.save()
25 |
26 | # Serialize the symbol object before returning it in the response
27 | symbol_data = SymbolSerializer(symbol).data
28 |
29 | return Response(
30 | {"detail": "File uploaded successfully.", "symbol": symbol_data},
31 | status=status.HTTP_201_CREATED,
32 | )
33 | else:
34 | return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
35 |
--------------------------------------------------------------------------------
/frontend/src/components/Explore/Windows/FilteredPlugins.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {
3 | AccordionSummary,
4 | Accordion,
5 | Typography,
6 | AccordionDetails,
7 | Box,
8 | } from "@mui/material";
9 | import { ExpandMore } from "@mui/icons-material";
10 | import EnrichedDataGrid from "../EnrichedDataGrid";
11 | import { ProcessInfo, EnrichedProcessData } from "../../../types";
12 |
13 | interface FilteredPluginsProps {
14 | process: ProcessInfo;
15 | enrichedData: EnrichedProcessData | null;
16 | show: boolean;
17 | }
18 |
19 | const FilteredPlugins: React.FC = ({ enrichedData }) => {
20 | return (
21 |
22 | {enrichedData &&
23 | Object.keys(enrichedData).map(
24 | (key, index) =>
25 | key !== "pslist" && (
26 |
27 | }>
28 | {key}
29 |
30 |
31 | []}
33 | />
34 |
35 |
36 | ),
37 | )}
38 |
39 | );
40 | };
41 |
42 | export default FilteredPlugins;
43 |
--------------------------------------------------------------------------------
/frontend/src/components/Explore/Linux/FilteredPlugins.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {
3 | AccordionSummary,
4 | Accordion,
5 | Typography,
6 | AccordionDetails,
7 | Box,
8 | } from "@mui/material";
9 | import { ExpandMore } from "@mui/icons-material";
10 | import EnrichedDataGrid from "../EnrichedDataGrid";
11 | import { LinuxProcessInfo, EnrichedProcessData } from "../../../types";
12 |
13 | interface FilteredPluginsProps {
14 | process: LinuxProcessInfo;
15 | enrichedData: EnrichedProcessData | null;
16 | show: boolean;
17 | }
18 |
19 | const FilteredPlugins: React.FC = ({ enrichedData }) => {
20 | return (
21 |
22 | {enrichedData &&
23 | Object.keys(enrichedData).map(
24 | (key, index) =>
25 | key !== "pslist" && (
26 |
27 | }>
28 | {key}
29 |
30 |
31 | []}
33 | />
34 |
35 |
36 | ),
37 | )}
38 |
39 | );
40 | };
41 |
42 | export default FilteredPlugins;
43 |
--------------------------------------------------------------------------------
/frontend/src/components/StixButton.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Button from "@mui/material/Button";
3 | import GetAppIcon from "@mui/icons-material/GetApp";
4 | import axiosInstance from "../utils/axiosInstance";
5 |
6 | interface StixButtonProps {
7 | caseId: number;
8 | }
9 |
10 | const StixButton: React.FC = ({ caseId }) => {
11 | const exportStixBundle = async () => {
12 | try {
13 | const response = await axiosInstance.get(`/core/stix/export/${caseId}/`, {
14 | responseType: "blob",
15 | });
16 |
17 | const url = window.URL.createObjectURL(new Blob([response.data]));
18 | const link = document.createElement("a");
19 | link.href = url;
20 | link.setAttribute("download", `stix_bundle_${caseId}.json`);
21 | document.body.appendChild(link);
22 | link.click();
23 |
24 | // Check if parentNode exists before removing the link
25 | if (link.parentNode) {
26 | link.parentNode.removeChild(link);
27 | }
28 | } catch (error) {
29 | console.error("Error exporting STIX bundle", error);
30 | }
31 | };
32 |
33 | return (
34 | }
38 | onClick={exportStixBundle}
39 | >
40 | STIX Bundle
41 |
42 | );
43 | };
44 |
45 | export default StixButton;
46 |
--------------------------------------------------------------------------------
/backend/Dockerfile:
--------------------------------------------------------------------------------
1 | # Stage 1: Install dependencies
2 | FROM python:3.12.6 AS builder
3 |
4 | ENV PYTHONUNBUFFERED=1 \
5 | PYTHONDONTWRITEBYTECODE=1 \
6 | PATH="/home/app/.local/bin:$PATH"
7 |
8 | RUN apt-get update \
9 | && apt-get install -y --no-install-recommends netcat-traditional \
10 | && rm -rf /var/lib/apt/lists/*
11 |
12 | RUN useradd --user-group --create-home --no-log-init --shell /bin/bash app
13 | USER app
14 |
15 | WORKDIR /home/app/web
16 | ENV PATH="/home/app/.local/bin:$PATH"
17 |
18 | COPY requirements.txt .
19 |
20 | RUN pip install --upgrade pip \
21 | && pip install --user --no-cache-dir -r requirements.txt
22 |
23 | # Stage 2: Copy only the necessary parts from builder
24 | FROM python:3.12.6
25 |
26 | ENV PYTHONUNBUFFERED=1 \
27 | PYTHONDONTWRITEBYTECODE=1 \
28 | PATH="/home/app/.local/bin:$PATH" \
29 | APP_HOME=/home/app/web
30 |
31 | RUN apt-get update \
32 | && apt-get install -y --no-install-recommends netcat-traditional \
33 | && rm -rf /var/lib/apt/lists/*
34 |
35 | RUN useradd --user-group --create-home --no-log-init --shell /bin/bash app \
36 | && mkdir -p $APP_HOME/staticfiles \
37 | && chown -R app:app $APP_HOME
38 |
39 | USER app
40 | WORKDIR $APP_HOME
41 | ENV PATH="/home/app/.local/bin:$PATH"
42 |
43 | COPY --chown=app:app --from=builder /home/app/.local /home/app/.local
44 | COPY --chown=app:app . $APP_HOME
45 |
46 | ENTRYPOINT ["./entrypoint.sh"]
47 |
--------------------------------------------------------------------------------
/frontend/src/components/Investigate/Linux/Buttons/DumpPslistButton.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Button, CircularProgress } from "@mui/material";
3 | import { Download } from "@mui/icons-material";
4 | import axiosInstance from "../../../../utils/axiosInstance";
5 |
6 | interface DumpButtonProps {
7 | evidenceId: string | undefined;
8 | pid: number | undefined;
9 | loading: boolean;
10 | setLoading: (loading: boolean) => void;
11 | }
12 |
13 | const DumpButton: React.FC = ({
14 | evidenceId,
15 | pid,
16 | loading,
17 | setLoading,
18 | }) => {
19 | const handleDump = async () => {
20 | setLoading(true);
21 | try {
22 | await axiosInstance.post(`/api/evidence/tasks/dump/process/pslist/`, {
23 | pid,
24 | evidenceId,
25 | });
26 | // Loading remains true until task completes and WebSocket updates it
27 | } catch (error) {
28 | // Handle the error appropriately
29 | console.error("Error dumping process:", error);
30 | setLoading(false);
31 | }
32 | };
33 |
34 | return (
35 | : }
43 | >
44 | {loading ? "Dumping..." : "Dump (pslist)"}
45 |
46 | );
47 | };
48 |
49 | export default DumpButton;
50 |
--------------------------------------------------------------------------------
/backend/core/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from evidences.models import Evidence
3 |
4 | TYPES = (
5 | ("artifact-sha1", "Artifact - SHA-1"),
6 | ("artifact-sha256", "Artifact - SHA-256"),
7 | ("artifact-md5", "Artifact - MD5"),
8 | ("artifact-url", "Artifact - URL"),
9 | ("autonomous-system", "Autonomous System"),
10 | ("directory", "Directory"),
11 | ("domain-name", "Domain Name"),
12 | ("email-addr", "Email Address"),
13 | ("file-path", "File Path"),
14 | ("file-sha256", "File SHA-256"),
15 | ("file-sha1", "File SHA-1"),
16 | ("file-md5", "File MD5"),
17 | ("ipv4-addr", "IPv4 Address"),
18 | ("ipv6-addr", "IPv6 Address"),
19 | ("mac-addr", "MAC Address"),
20 | ("mutex", "Mutex"),
21 | ("network-traffic", "Network Traffic"),
22 | ("process-name", "Process - Name"),
23 | ("process-cwd", "Process - CWD"),
24 | ("process-cmdline", "Process - Command Line"),
25 | ("software", "Software"),
26 | ("url", "URL"),
27 | ("user-account", "User Account"),
28 | ("windows-registry-key", "Windows Registry Key"),
29 | ("x509-certificate", "X.509 Certificate SHA1"),
30 | )
31 |
32 |
33 | class Indicator(models.Model):
34 | id = models.AutoField(primary_key=True)
35 | evidence = models.ForeignKey(Evidence, on_delete=models.CASCADE)
36 | name = models.TextField()
37 | type = models.CharField(max_length=100, choices=TYPES)
38 | description = models.TextField()
39 | value = models.TextField()
40 |
--------------------------------------------------------------------------------
/frontend/src/components/Charts/DonutChart.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactApexChart from "react-apexcharts";
3 | import { Card, CardContent } from "@mui/material";
4 |
5 | interface DonutChartProps {
6 | totalWindows: number;
7 | totalLinux: number;
8 | theme: "light" | "dark";
9 | }
10 |
11 | const DonutChart: React.FC = ({
12 | totalWindows,
13 | totalLinux,
14 | theme,
15 | }) => {
16 | const state = {
17 | series: [totalWindows, totalLinux],
18 | options: {
19 | chart: {
20 | width: 380,
21 | background: "transparent",
22 | foreColor: theme !== "dark" ? "#121212" : "#fff",
23 | },
24 | labels: ["Windows", "Linux"],
25 | dataLabels: {
26 | enabled: true,
27 | },
28 | fill: {
29 | type: "gradient",
30 | gradient: {
31 | gradientToColors: ["#790909", "#670979"],
32 | },
33 | colors: ["#790909", "#670979"],
34 | },
35 | title: {
36 | text: "Operating System Repartition",
37 | style: {
38 | color: theme !== "dark" ? "#101418" : "#fff",
39 | },
40 | },
41 | },
42 | };
43 |
44 | return (
45 |
46 |
47 |
53 |
54 |
55 | );
56 | };
57 |
58 | export default DonutChart;
59 |
--------------------------------------------------------------------------------
/backend/volatility_engine/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 | from .views import (
3 | EvidencePluginsView,
4 | PluginArtefactsView,
5 | TimelinerArtefactsView,
6 | TimelinerTask,
7 | HandlesTask,
8 | ProcessDumpPslistTask,
9 | ProcessDumpMapsTask,
10 | FileDumpTask,
11 | TasksApiView,
12 | EnrichedProcessView,
13 | RestartAnalysisTask,
14 | )
15 |
16 | urlpatterns = [
17 | path(
18 | "evidence//plugins/",
19 | EvidencePluginsView.as_view(),
20 | name="evidence-plugins",
21 | ),
22 | path(
23 | "evidence//plugin//",
24 | PluginArtefactsView.as_view(),
25 | name="evidence-plugin-artefacts",
26 | ),
27 | path(
28 | "evidence//plugin//artefacts/",
29 | TimelinerArtefactsView.as_view(),
30 | ),
31 | path("evidence/tasks/timeliner/", TimelinerTask.as_view()),
32 | path("evidence/tasks/handles/", HandlesTask.as_view()),
33 | path("evidence/tasks/dump/process/pslist/", ProcessDumpPslistTask.as_view()),
34 | path("evidence/tasks/dump/process/maps/", ProcessDumpMapsTask.as_view()),
35 | path("evidence/tasks/dump/file/", FileDumpTask.as_view()),
36 | path("evidence//tasks/", TasksApiView.as_view()),
37 | path(
38 | "evidence//process//enriched/",
39 | EnrichedProcessView.as_view(),
40 | ),
41 | path("evidence/tasks/restart/", RestartAnalysisTask.as_view()),
42 | ]
43 |
--------------------------------------------------------------------------------
/frontend/src/components/Investigate/Windows/Buttons/DumpPslistButton.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Button, CircularProgress } from "@mui/material";
3 | import { Download } from "@mui/icons-material";
4 | import axiosInstance from "../../../../utils/axiosInstance";
5 | import { useSnackbar } from "../../../SnackbarProvider";
6 |
7 | interface DumpPslistButtonProps {
8 | evidenceId: string | undefined;
9 | pid: number | undefined;
10 | loading: boolean;
11 | setLoading: (loading: boolean) => void;
12 | }
13 |
14 | const DumpPslistButton: React.FC = ({
15 | evidenceId,
16 | pid,
17 | loading,
18 | setLoading,
19 | }) => {
20 | const { display_message } = useSnackbar();
21 |
22 | const handleDump = async () => {
23 | setLoading(true);
24 | try {
25 | await axiosInstance.post(`/api/evidence/tasks/dump/process/pslist/`, {
26 | pid,
27 | evidenceId,
28 | });
29 | } catch (error) {
30 | display_message("error", `Error dumping process: ${error}`);
31 | console.error("Error dumping process:", error);
32 | setLoading(false);
33 | }
34 | };
35 |
36 | return (
37 | : }
45 | >
46 | {loading ? "Dumping..." : "Dump (pslist)"}
47 |
48 | );
49 | };
50 |
51 | export default DumpPslistButton;
52 |
--------------------------------------------------------------------------------
/frontend/src/components/Explore/Windows/Explore.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import axiosInstance from "../../../utils/axiosInstance";
3 | import ProcessGraph from "./ProcessGraph";
4 | import { ProcessInfo, Evidence } from "../../../types";
5 | import { Box, CircularProgress } from "@mui/material";
6 | import { annotateProcessData } from "../../../utils/processAnalysis";
7 | interface ExploreProps {
8 | evidence: Evidence;
9 | }
10 | import { useSnackbar } from "../../SnackbarProvider";
11 |
12 | const ExploreWin: React.FC = ({ evidence }) => {
13 | const [data, setData] = useState([]); // Initialize with an empty array
14 | const { display_message } = useSnackbar();
15 |
16 | useEffect(() => {
17 | const fetchTree = async () => {
18 | try {
19 | const response = await axiosInstance.get(
20 | `/api/evidence/${evidence.id}/plugin/volatility3.plugins.windows.pstree.PsTree/`,
21 | );
22 | annotateProcessData(response.data.artefacts);
23 | setData(response.data.artefacts);
24 | } catch (error) {
25 | display_message(
26 | "error",
27 | `The pstree data could not be retreived: ${error}`,
28 | );
29 | console.error("Error fetching pstree data", error);
30 | }
31 | };
32 |
33 | fetchTree();
34 | }, [evidence.id, display_message]);
35 |
36 | return (
37 |
38 | {data.length > 0 ? : }
39 |
40 | );
41 | };
42 |
43 | export default ExploreWin;
44 |
--------------------------------------------------------------------------------
/frontend/src/components/Explore/Linux/GraphDataController.tsx:
--------------------------------------------------------------------------------
1 | import { FC, useEffect } from "react";
2 | import Graph from "graphology";
3 | import { useLoadGraph } from "@react-sigma/core";
4 | import { LinuxProcessInfo } from "../../../types";
5 | import ForceSupervisor from "graphology-layout-force/worker";
6 |
7 | interface GraphDataControllerProps {
8 | data: LinuxProcessInfo[];
9 | }
10 |
11 | const GraphDataController: FC = ({ data }) => {
12 | const loadGraph = useLoadGraph();
13 |
14 | useEffect(() => {
15 | const graph = new Graph();
16 |
17 | // Number of root nodes
18 | const N = data.length;
19 |
20 | // Add root nodes with positions
21 | data.forEach((process, i) => {
22 | const nodeId = process.PID.toString();
23 | const angle = (i * 2 * Math.PI) / N;
24 | const x = 100 * Math.cos(angle);
25 | const y = 100 * Math.sin(angle);
26 |
27 | graph.addNode(nodeId, {
28 | label: `${process.COMM || "Unknown"} - ${process.PID} (${process.__children.length})`,
29 | size: 10,
30 | color:
31 | process.anomalies && process.anomalies.length > 0
32 | ? "#ffa726"
33 | : "#FFFFFF",
34 | x: x,
35 | y: y,
36 | });
37 | });
38 |
39 | loadGraph(graph);
40 |
41 | const layout = new ForceSupervisor(graph);
42 | layout.start();
43 | setTimeout(() => {
44 | layout.stop();
45 | }, 1000);
46 |
47 | return () => {
48 | layout.stop();
49 | };
50 | }, [data, loadGraph]);
51 |
52 | return null;
53 | };
54 |
55 | export default GraphDataController;
56 |
--------------------------------------------------------------------------------
/frontend/src/components/Explore/Windows/GraphDataController.tsx:
--------------------------------------------------------------------------------
1 | import { FC, useEffect } from "react";
2 | import Graph from "graphology";
3 | import { useLoadGraph } from "@react-sigma/core";
4 | import { ProcessInfo } from "../../../types";
5 | import ForceSupervisor from "graphology-layout-force/worker";
6 |
7 | interface GraphDataControllerProps {
8 | data: ProcessInfo[];
9 | }
10 |
11 | const GraphDataController: FC = ({ data }) => {
12 | const loadGraph = useLoadGraph();
13 |
14 | useEffect(() => {
15 | const graph = new Graph();
16 |
17 | // Number of root nodes
18 | const N = data.length;
19 |
20 | // Add root nodes with positions
21 | data.forEach((process, i) => {
22 | const nodeId = process.PID.toString();
23 | const angle = (i * 2 * Math.PI) / N;
24 | const x = 100 * Math.cos(angle);
25 | const y = 100 * Math.sin(angle);
26 |
27 | graph.addNode(nodeId, {
28 | label: `${process.ImageFileName || "Unknown"} - ${process.PID} (${process.__children.length})`,
29 | size: 10,
30 | color:
31 | process.anomalies && process.anomalies.length > 0
32 | ? "#ffa726"
33 | : "#FFFFFF",
34 | x: x,
35 | y: y,
36 | });
37 | });
38 |
39 | loadGraph(graph);
40 |
41 | const layout = new ForceSupervisor(graph);
42 | layout.start();
43 | setTimeout(() => {
44 | layout.stop();
45 | }, 1000);
46 |
47 | return () => {
48 | layout.stop();
49 | };
50 | }, [data, loadGraph]);
51 |
52 | return null;
53 | };
54 |
55 | export default GraphDataController;
56 |
--------------------------------------------------------------------------------
/frontend/src/components/Explore/EnrichedDataGrid.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { DataGrid, GridColDef } from "@mui/x-data-grid";
3 |
4 | interface EnrichedDataGridProps {
5 | data: Record[];
6 | }
7 |
8 | const EnrichedDataGrid: React.FC = ({ data }) => {
9 | const [columns, setColumns] = useState([]);
10 |
11 | useEffect(() => {
12 | if (data && data.length > 0) {
13 | const generatedColumns: GridColDef[] = Object.keys(data[0] ?? {})
14 | .filter((key) => key !== "__children" && key !== "id")
15 | .map((key) => ({
16 | field: key,
17 | headerName: key,
18 | flex: 1,
19 | renderCell: (params) => {
20 | const value = params.value;
21 | if (typeof value === "boolean") {
22 | return value ? "True" : "False";
23 | } else if (value === null || value === undefined) {
24 | return "";
25 | } else if (typeof value === "object") {
26 | return JSON.stringify(value, null, 2);
27 | } else {
28 | return value;
29 | }
30 | },
31 | }));
32 | setColumns(generatedColumns);
33 | }
34 | }, [data]);
35 |
36 | return (
37 | ({ id: index, ...row }))}
40 | columns={columns}
41 | density="compact"
42 | showToolbar={true}
43 | getRowId={(row) => row.id as string | number}
44 | />
45 | );
46 | };
47 |
48 | export default EnrichedDataGrid;
49 |
--------------------------------------------------------------------------------
/frontend/src/components/Explore/Linux/Explore.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import axiosInstance from "../../../utils/axiosInstance";
3 | import ProcessGraph from "./ProcessGraph";
4 | import { LinuxProcessInfo, Evidence } from "../../../types";
5 | import { Box, CircularProgress } from "@mui/material";
6 | import { annotateProcessData } from "../../../utils/processAnalysis";
7 | interface ExploreProps {
8 | evidence: Evidence;
9 | }
10 | import { useSnackbar } from "../../SnackbarProvider";
11 |
12 | const ExploreLinux: React.FC = ({ evidence }) => {
13 | const [data, setData] = useState([]); // Initialize with an empty array
14 | const { display_message } = useSnackbar();
15 |
16 | useEffect(() => {
17 | const fetchTree = async () => {
18 | try {
19 | const response = await axiosInstance.get(
20 | `/api/evidence/${evidence.id}/plugin/volatility3.plugins.linux.pstree.PsTree/`,
21 | );
22 | annotateProcessData(response.data.artefacts);
23 | setData(response.data.artefacts);
24 | console.log(response.data.artefacts);
25 | } catch (error) {
26 | display_message(
27 | "error",
28 | `The pstree data could not be retreived: ${error}`,
29 | );
30 | console.error("Error fetching pstree data", error);
31 | }
32 | };
33 |
34 | fetchTree();
35 | }, [evidence.id, display_message]);
36 |
37 | return (
38 |
39 | {data.length > 0 ? : }
40 |
41 | );
42 | };
43 |
44 | export default ExploreLinux;
45 |
--------------------------------------------------------------------------------
/frontend/nginx.conf:
--------------------------------------------------------------------------------
1 | server {
2 | listen 3000;
3 | server_name localhost;
4 |
5 | # Allow special characters in headers
6 | ignore_invalid_headers off;
7 | # Allow any size file to be uploaded.
8 | # Set to a value such as 1000m; to restrict file size to a specific value
9 | client_max_body_size 0;
10 | # Disable buffering
11 | proxy_buffering off;
12 | proxy_request_buffering off;
13 |
14 | # Serve static files
15 | root /usr/share/nginx/html;
16 | index index.html index.htm;
17 |
18 | location /api/ {
19 | proxy_pass http://volweb-backend:8000;
20 | proxy_set_header Host $host;
21 | }
22 |
23 | location /core/ {
24 | proxy_pass http://volweb-backend:8000;
25 | proxy_set_header Host $host;
26 | }
27 |
28 | location /admin/ {
29 | proxy_pass http://volweb-backend:8000;
30 | proxy_set_header Host $host;
31 | }
32 |
33 | location /static/ {
34 | alias /home/app/web/staticfiles/;
35 | }
36 |
37 | location /media/ {
38 | alias /home/app/web/media/;
39 | autoindex on;
40 | }
41 |
42 | location /swagger/ {
43 | proxy_pass http://volweb-backend:8000;
44 | proxy_set_header Host $host;
45 | }
46 |
47 | # WebSocket proxy
48 | location /ws/ {
49 | proxy_pass http://volweb-backend:8000;
50 | proxy_http_version 1.1;
51 | proxy_set_header Upgrade $http_upgrade;
52 | proxy_set_header Connection "Upgrade";
53 | proxy_set_header Host $host;
54 | }
55 |
56 | location / {
57 | try_files $uri $uri/ /index.html;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "private": true,
4 | "version": "3.15.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc -b && vite build",
9 | "lint": "eslint .",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "@aws-sdk/client-s3": "^3.664.0",
14 | "@aws-sdk/lib-storage": "^3.664.0",
15 | "@emotion/react": "^11.13.3",
16 | "@emotion/styled": "^11.13.0",
17 | "@fontsource/roboto": "^5.1.0",
18 | "@mui/icons-material": "^7.1.0",
19 | "@mui/material": "^7.1.1",
20 | "@mui/x-data-grid": "^8.5.0",
21 | "@mui/x-tree-view": "^8.5.0",
22 | "@react-sigma/core": "^5.0.2",
23 | "@sigma/node-image": "^3.0.0-beta.16",
24 | "apexcharts": "^4.1.0",
25 | "axios": "^1.7.7",
26 | "graphology": "^0.26.0",
27 | "graphology-layout-force": "^0.2.4",
28 | "graphology-layout-forceatlas2": "^0.10.1",
29 | "graphology-layout-noverlap": "^0.4.2",
30 | "graphology-types": "^0.24.8",
31 | "react": "^19.0.0",
32 | "react-apexcharts": "^1.6.0",
33 | "react-dom": "^19.0.0",
34 | "react-router-dom": "^6.26.2",
35 | "sigma": "^3.0.1",
36 | "socket.io-client": "^4.8.1"
37 | },
38 | "devDependencies": {
39 | "@eslint/js": "^9.9.0",
40 | "@types/react": "^18.3.3",
41 | "@types/react-dom": "^18.3.0",
42 | "@vitejs/plugin-react-swc": "^3.5.0",
43 | "eslint": "^9.9.0",
44 | "eslint-plugin-react-hooks": "^5.1.0-rc.0",
45 | "eslint-plugin-react-refresh": "^0.4.9",
46 | "globals": "^15.9.0",
47 | "typescript": "^5.5.3",
48 | "typescript-eslint": "^8.0.1",
49 | "vite": "^6.3.5"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/frontend/src/components/Charts/LineChart.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactApexChart from "react-apexcharts";
3 | import { Card, CardContent } from "@mui/material";
4 |
5 | interface LineChartProps {
6 | dates: string[];
7 | counts: number[];
8 | theme: "light" | "dark";
9 | }
10 |
11 | const LineChart: React.FC = ({ dates, counts, theme }) => {
12 | const state = {
13 | series: [
14 | {
15 | name: "Analysis Started",
16 | data: counts,
17 | },
18 | ],
19 | options: {
20 | theme: {
21 | mode: "dark" as const,
22 | palette: "palette1",
23 | monochrome: {
24 | enabled: true,
25 | color: "#9a0000",
26 | shadeTo: "light" as const,
27 | shadeIntensity: 0.65,
28 | },
29 | },
30 | labels: dates,
31 |
32 | dataLabels: { enabled: false },
33 | chart: {
34 | background: "#121212",
35 | stacked: false,
36 | zoom: {
37 | enabled: true,
38 | autoScaleYaxis: true,
39 | },
40 | stroke: {
41 | curve: "smooth",
42 | },
43 | yaxis: {
44 | opposite: true,
45 | labels: {
46 | style: {
47 | colors: theme !== "dark" ? "#101418" : "#fff",
48 | },
49 | },
50 | },
51 | },
52 | },
53 | };
54 |
55 | return (
56 |
57 |
58 |
64 |
65 |
66 | );
67 | };
68 |
69 | export default LineChart;
70 |
--------------------------------------------------------------------------------
/frontend/README.md:
--------------------------------------------------------------------------------
1 | # React + TypeScript + Vite
2 |
3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4 |
5 | Currently, two official plugins are available:
6 |
7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9 |
10 | ## Expanding the ESLint configuration
11 |
12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
13 |
14 | - Configure the top-level `parserOptions` property like this:
15 |
16 | ```js
17 | export default tseslint.config({
18 | languageOptions: {
19 | // other options...
20 | parserOptions: {
21 | project: ['./tsconfig.node.json', './tsconfig.app.json'],
22 | tsconfigRootDir: import.meta.dirname,
23 | },
24 | },
25 | })
26 | ```
27 |
28 | - Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
29 | - Optionally add `...tseslint.configs.stylisticTypeChecked`
30 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
31 |
32 | ```js
33 | // eslint.config.js
34 | import react from 'eslint-plugin-react'
35 |
36 | export default tseslint.config({
37 | // Set the react version
38 | settings: { react: { version: '18.3' } },
39 | plugins: {
40 | // Add the react plugin
41 | react,
42 | },
43 | rules: {
44 | // other rules...
45 | // Enable its recommended rules
46 | ...react.configs.recommended.rules,
47 | ...react.configs['jsx-runtime'].rules,
48 | },
49 | })
50 | ```
51 |
--------------------------------------------------------------------------------
/frontend/src/utils/axiosInstance.ts:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 |
3 | // Create an Axios instance
4 | const axiosInstance = axios.create();
5 |
6 | // Add a request interceptor to include auth headers
7 | axiosInstance.interceptors.request.use(
8 | (config) => {
9 | const token = localStorage.getItem("access_token");
10 | if (token) {
11 | config.headers = config.headers ?? new axios.AxiosHeaders();
12 | config.headers.set("Authorization", `Bearer ${token}`);
13 | }
14 | return config;
15 | },
16 | (error) => Promise.reject(error),
17 | );
18 |
19 | axiosInstance.interceptors.response.use(
20 | (response) => response,
21 | async (error) => {
22 | const originalRequest = error.config;
23 | if (
24 | error.response &&
25 | error.response.status === 401 &&
26 | !originalRequest._retry
27 | ) {
28 | originalRequest._retry = true;
29 | const refreshToken = localStorage.getItem("refresh_token");
30 |
31 | if (refreshToken) {
32 | try {
33 | const response = await axios.post("/core/token/refresh/", {
34 | refresh: refreshToken,
35 | });
36 |
37 | if (response.status === 200) {
38 | const { access, refresh } = response.data;
39 | localStorage.setItem("access_token", access);
40 |
41 | if (refresh) {
42 | localStorage.setItem("refresh_token", refresh);
43 | }
44 |
45 | originalRequest.headers["Authorization"] = `Bearer ${access}`;
46 | return axiosInstance(originalRequest);
47 | }
48 | } catch (refreshError) {
49 | console.error("Token refresh failed", refreshError);
50 | window.location.href = "/login";
51 | }
52 | }
53 | }
54 | return Promise.reject(error);
55 | },
56 | );
57 |
58 | export default axiosInstance;
59 |
--------------------------------------------------------------------------------
/backend/core/management/commands/initadmin.py:
--------------------------------------------------------------------------------
1 | from django.core.management.base import BaseCommand
2 | from django.contrib.auth import get_user_model
3 | from django.contrib.auth.models import User
4 |
5 |
6 | def createSuperUser(username, password, email="", firstName="", lastName=""):
7 | invalidInputs = ["", None]
8 |
9 | if username.strip() in invalidInputs or password.strip() in invalidInputs:
10 | return None
11 |
12 | user = User(
13 | username=username,
14 | email=email,
15 | first_name=firstName,
16 | last_name=lastName,
17 | )
18 | user.set_password(password)
19 | user.is_superuser = True
20 | user.is_staff = True
21 | user.save()
22 |
23 | return user
24 |
25 |
26 | def createSimpleUser(username, password, email="", firstName="", lastName=""):
27 | invalidInputs = ["", None]
28 |
29 | if username.strip() in invalidInputs or password.strip() in invalidInputs:
30 | return None
31 |
32 | user = User(
33 | username=username,
34 | email=email,
35 | first_name=firstName,
36 | last_name=lastName,
37 | )
38 | user.set_password(password)
39 | user.is_superuser = False
40 | user.is_staff = False
41 | user.save()
42 |
43 | return user
44 |
45 |
46 | class Command(BaseCommand):
47 | def handle(self, *args, **options):
48 | Account = get_user_model()
49 | if Account.objects.count() == 0:
50 | try:
51 | createSuperUser(
52 | "admin", "password", email="", firstName="NAME", lastName="SURNAME"
53 | )
54 | createSimpleUser(
55 | "user", "password", email="", firstName="NAME", lastName="SURNAME"
56 | )
57 | except:
58 | print("Could not create the admin and user accounts")
59 | else:
60 | print("User&Admin accounts can only be initialized if no Accounts exist")
61 |
--------------------------------------------------------------------------------
/nginx/nginx.conf:
--------------------------------------------------------------------------------
1 | upstream volweb-platform {
2 | server volweb-frontend:3000;
3 | }
4 |
5 |
6 | map $http_upgrade $connection_upgrade {
7 | default upgrade;
8 | '' close;
9 | }
10 |
11 |
12 | upstream websocket {
13 | server unix:/tmp/daphne.sock;
14 | }
15 |
16 | server {
17 | listen 80;
18 | server_name volweb.example.com;
19 | location / {
20 | if ($request_method = GET) {
21 | rewrite ^ https://$host$request_uri? permanent;
22 | }
23 | return 405;
24 | }
25 | }
26 |
27 |
28 | server {
29 |
30 | listen 443 ssl http2;
31 |
32 | server_name volweb.example.com;
33 | ssl_certificate /etc/nginx/certs/fullchain.pem;
34 | ssl_certificate_key /etc/nginx/certs/privkey.pem;
35 |
36 | ssl_protocols TLSv1.2;
37 | ssl_prefer_server_ciphers on;
38 | ssl_ciphers "EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EECDH+ECDSA+SHA256 EECDH+aRSA+SHA384 EECDH+aRSA+SHA256 EECDH+aRSA+RC4 EECDH EDH+aRSA HIGH !RC4 !aNULL !eNULL !LOW !3DES !MD5 !EXP !PSK !SRP !DSS";
39 | ssl_ecdh_curve secp384r1;
40 | # OSCP stapling
41 | ssl_stapling on;
42 | ssl_stapling_verify on;
43 |
44 | # Force DH key exchange
45 | ssl_session_tickets off;
46 |
47 | # SSL optimization
48 | ssl_session_cache shared:SSL:1m;
49 | ssl_session_timeout 10m;
50 | ssl_buffer_size 4k;
51 | client_max_body_size 1G;
52 | location / {
53 | proxy_pass http://volweb-platform;
54 | add_header Strict-Transport-Security "max-age=31536000";
55 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
56 | proxy_set_header Host $host;
57 | proxy_set_header X-Real-IP $remote_addr;
58 | proxy_set_header X-Forwarded-Proto $scheme;
59 | proxy_redirect off;
60 |
61 |
62 | # WebSocket support
63 | proxy_http_version 1.1;
64 | proxy_set_header Upgrade $http_upgrade;
65 | proxy_set_header Connection "upgrade";
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/backend/evidences/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 5.1.1 on 2024-11-26 22:31
2 |
3 | import django.db.models.deletion
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | initial = True
10 |
11 | dependencies = [
12 | ("cases", "0001_initial"),
13 | ]
14 |
15 | operations = [
16 | migrations.CreateModel(
17 | name="Evidence",
18 | fields=[
19 | ("id", models.AutoField(primary_key=True, serialize=False)),
20 | ("name", models.CharField(max_length=250)),
21 | ("etag", models.CharField(max_length=256, unique=True)),
22 | (
23 | "os",
24 | models.CharField(
25 | choices=[("windows", "windows"), ("linux", "linux")],
26 | max_length=10,
27 | ),
28 | ),
29 | ("status", models.IntegerField(default=0)),
30 | ("access_key_id", models.TextField(null=True)),
31 | ("access_key", models.TextField(null=True)),
32 | ("url", models.TextField(null=True)),
33 | ("region", models.TextField(null=True)),
34 | ("endpoint", models.TextField(null=True)),
35 | (
36 | "source",
37 | models.CharField(
38 | choices=[
39 | ("AWS", "AWS"),
40 | ("MINIO", "MINIO"),
41 | ("FILESYSTEM", "FILESYSTEM"),
42 | ],
43 | max_length=10,
44 | null=True,
45 | ),
46 | ),
47 | (
48 | "linked_case",
49 | models.ForeignKey(
50 | on_delete=django.db.models.deletion.CASCADE, to="cases.case"
51 | ),
52 | ),
53 | ],
54 | ),
55 | ]
56 |
--------------------------------------------------------------------------------
/frontend/src/components/InvestigatorSelect.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { TextField, CircularProgress } from "@mui/material";
3 | import Autocomplete from "@mui/material/Autocomplete";
4 | import axiosInstance from "../utils/axiosInstance";
5 | import { User } from "../types";
6 |
7 | const InvestigatorSelect: React.FC<{
8 | selectedUsers: User[];
9 | setSelectedUsers: (users: User[]) => void;
10 | }> = ({ selectedUsers, setSelectedUsers }) => {
11 | const [users, setUsers] = useState([]);
12 | const [loading, setLoading] = useState(true);
13 |
14 | useEffect(() => {
15 | const fetchUsers = async () => {
16 | try {
17 | const response = await axiosInstance.get("/core/users/");
18 | setUsers(response.data);
19 | } catch (error) {
20 | console.error("Error fetching users", error);
21 | } finally {
22 | setLoading(false);
23 | }
24 | };
25 |
26 | fetchUsers();
27 | }, []);
28 |
29 | const availableUsers = users.filter(
30 | (user) =>
31 | !selectedUsers.some((selectedUser) => selectedUser.id === user.id),
32 | );
33 |
34 | return (
35 |
39 | `${user.first_name} ${user.last_name} (${user.username})`
40 | }
41 | value={selectedUsers}
42 | onChange={(_event, newValue) => setSelectedUsers(newValue)}
43 | renderInput={(params) => (
44 |
55 | {loading ? (
56 |
57 | ) : null}
58 | {params.InputProps.endAdornment}
59 | >
60 | ),
61 | }}
62 | />
63 | )}
64 | />
65 | );
66 | };
67 |
68 | export default InvestigatorSelect;
69 |
--------------------------------------------------------------------------------
/frontend/src/App.tsx:
--------------------------------------------------------------------------------
1 | import "@fontsource/roboto/300.css";
2 | import "@fontsource/roboto/400.css";
3 | import "@fontsource/roboto/500.css";
4 | import "@fontsource/roboto/700.css";
5 | import React from "react";
6 | import {
7 | BrowserRouter as Router,
8 | Routes,
9 | Route,
10 | Navigate,
11 | } from "react-router-dom";
12 | import { ThemeProvider, createTheme } from "@mui/material/styles";
13 | import CssBaseline from "@mui/material/CssBaseline";
14 | import MiniDrawer from "./components/SideBar";
15 | import Cases from "./pages/cases/Cases";
16 | import Dashboard from "./pages/dashboard/Dashboard";
17 | import Evidences from "./pages/evidences/Evidences";
18 | import Login from "./pages/auth/Login";
19 | import CaseDetail from "./pages/cases/CaseDetail";
20 | import Symbols from "./pages/symbols/Symbols";
21 | import EvidenceDetail from "./pages/evidences/EvidenceDetails";
22 | import { SnackbarProvider } from "./components/SnackbarProvider";
23 | const darkTheme = createTheme({
24 | palette: {
25 | mode: "dark",
26 | },
27 | });
28 |
29 | const PrivateRoute = ({ children }: { children: JSX.Element }) => {
30 | const isAuthenticated = !!localStorage.getItem("access_token");
31 | return isAuthenticated ? children : ;
32 | };
33 |
34 | const App: React.FC = () => {
35 | return (
36 |
37 |
38 |
39 |
40 | } />
41 |
45 |
46 |
47 |
48 |
49 | }
50 | >
51 | } />
52 | } />
53 | } />
54 | } />
55 | } />
56 | } />
57 |
58 |
59 |
60 |
61 | );
62 | };
63 |
64 | export default App;
65 |
--------------------------------------------------------------------------------
/frontend/src/components/SnackbarProvider.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | createContext,
3 | useState,
4 | useContext,
5 | ReactNode,
6 | useMemo,
7 | useCallback,
8 | } from "react";
9 | import { Snackbar, Alert, AlertColor } from "@mui/material";
10 |
11 | interface SnackbarContextValue {
12 | display_message: (severity: AlertColor, message: string) => void;
13 | }
14 |
15 | const SnackbarContext = createContext(
16 | undefined,
17 | );
18 |
19 | export const useSnackbar = (): SnackbarContextValue => {
20 | const context = useContext(SnackbarContext);
21 | if (!context) {
22 | throw new Error("useSnackbar must be used within a SnackbarProvider");
23 | }
24 | return context;
25 | };
26 |
27 | interface SnackbarProviderProps {
28 | children: ReactNode;
29 | }
30 |
31 | export const SnackbarProvider: React.FC = ({
32 | children,
33 | }) => {
34 | const [open, setOpen] = useState(false);
35 | const [severity, setSeverity] = useState("success");
36 | const [message, setMessage] = useState("");
37 |
38 | // Memoize the display_message function
39 | const display_message = useCallback(
40 | (severity: AlertColor, message: string) => {
41 | setSeverity(severity);
42 | setMessage(message);
43 | setOpen(true);
44 | },
45 | [],
46 | );
47 |
48 | const contextValue = useMemo(() => ({ display_message }), [display_message]);
49 |
50 | const handleClose = (
51 | _event?: React.SyntheticEvent | Event,
52 | reason?: string,
53 | ) => {
54 | if (reason === "clickaway") {
55 | return;
56 | }
57 | setOpen(false);
58 | };
59 |
60 | return (
61 |
62 | {children}
63 |
69 |
76 | {message}
77 |
78 |
79 |
80 | );
81 | };
82 |
--------------------------------------------------------------------------------
/backend/cases/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 5.1.1 on 2024-11-26 22:31
2 |
3 | import django.db.models.deletion
4 | import uuid
5 | from django.conf import settings
6 | from django.db import migrations, models
7 |
8 |
9 | class Migration(migrations.Migration):
10 |
11 | initial = True
12 |
13 | dependencies = [
14 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
15 | ]
16 |
17 | operations = [
18 | migrations.CreateModel(
19 | name="Case",
20 | fields=[
21 | ("id", models.AutoField(primary_key=True, serialize=False)),
22 | ("name", models.CharField(max_length=500, unique=True)),
23 | ("description", models.TextField()),
24 | ("last_update", models.DateField(auto_now=True)),
25 | ("linked_users", models.ManyToManyField(to=settings.AUTH_USER_MODEL)),
26 | ],
27 | ),
28 | migrations.CreateModel(
29 | name="UploadSession",
30 | fields=[
31 | (
32 | "id",
33 | models.BigAutoField(
34 | auto_created=True,
35 | primary_key=True,
36 | serialize=False,
37 | verbose_name="ID",
38 | ),
39 | ),
40 | (
41 | "upload_id",
42 | models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
43 | ),
44 | ("filename", models.CharField(max_length=255)),
45 | ("os", models.CharField(blank=True, max_length=50)),
46 | ("created_at", models.DateTimeField(auto_now_add=True)),
47 | (
48 | "case",
49 | models.ForeignKey(
50 | on_delete=django.db.models.deletion.CASCADE, to="cases.case"
51 | ),
52 | ),
53 | (
54 | "user",
55 | models.ForeignKey(
56 | on_delete=django.db.models.deletion.CASCADE,
57 | to=settings.AUTH_USER_MODEL,
58 | ),
59 | ),
60 | ],
61 | ),
62 | ]
63 |
--------------------------------------------------------------------------------
/backend/volatility_engine/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 5.1.1 on 2024-11-26 22:31
2 |
3 | import django.db.models.deletion
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | initial = True
10 |
11 | dependencies = [
12 | ("evidences", "0001_initial"),
13 | ]
14 |
15 | operations = [
16 | migrations.CreateModel(
17 | name="EnrichedProcess",
18 | fields=[
19 | (
20 | "id",
21 | models.BigAutoField(
22 | auto_created=True,
23 | primary_key=True,
24 | serialize=False,
25 | verbose_name="ID",
26 | ),
27 | ),
28 | ("pid", models.IntegerField()),
29 | ("data", models.JSONField()),
30 | (
31 | "evidence",
32 | models.ForeignKey(
33 | on_delete=django.db.models.deletion.CASCADE,
34 | to="evidences.evidence",
35 | ),
36 | ),
37 | ],
38 | ),
39 | migrations.CreateModel(
40 | name="VolatilityPlugin",
41 | fields=[
42 | (
43 | "id",
44 | models.BigAutoField(
45 | auto_created=True,
46 | primary_key=True,
47 | serialize=False,
48 | verbose_name="ID",
49 | ),
50 | ),
51 | ("name", models.CharField(max_length=100)),
52 | ("icon", models.CharField(max_length=30, null=True)),
53 | ("description", models.TextField(null=True)),
54 | ("artefacts", models.JSONField(null=True)),
55 | ("category", models.CharField(max_length=100)),
56 | ("display", models.CharField(max_length=10)),
57 | ("results", models.BooleanField(default=False)),
58 | (
59 | "evidence",
60 | models.ForeignKey(
61 | on_delete=django.db.models.deletion.CASCADE,
62 | to="evidences.evidence",
63 | ),
64 | ),
65 | ],
66 | ),
67 | ]
68 |
--------------------------------------------------------------------------------
/backend/volatility_engine/plugins/linux/volweb_misc.py:
--------------------------------------------------------------------------------
1 | import json
2 | import logging
3 | import importlib
4 | from volatility3.framework.interfaces import plugins
5 | from volatility3.framework.configuration import requirements
6 | from volatility3.framework.renderers import TreeGrid
7 | from volatility_engine.utils import DjangoRenderer
8 |
9 | vollog = logging.getLogger(__name__)
10 |
11 |
12 | class VolWebMisc(plugins.PluginInterface):
13 | _required_framework_version = (2, 0, 0)
14 | _version = (1, 0, 0)
15 |
16 | def load_plugin_info(self, json_file_path):
17 | with open(json_file_path, "r") as file:
18 | return json.load(file).get("plugins", {}).get("linux", [])
19 |
20 | @classmethod
21 | def get_requirements(cls):
22 | return [
23 | requirements.TranslationLayerRequirement(
24 | name="primary",
25 | description="Memory layer for the kernel",
26 | architectures=["Intel32", "Intel64"],
27 | )
28 | ]
29 |
30 | def dynamic_import(self, module_name):
31 | module_path, class_name = module_name.rsplit(".", 1)
32 | module = importlib.import_module(module_path)
33 | return getattr(module, class_name)
34 |
35 | def run_all(self):
36 | volweb_plugins = self.load_plugin_info("volatility_engine/volweb_misc.json")
37 | instances = {}
38 | for plugin, details in volweb_plugins.items():
39 | try:
40 | plugin_class = self.dynamic_import(plugin)
41 | instances[plugin] = {
42 | "class": plugin_class(self.context, self.config_path),
43 | "details": details,
44 | }
45 | instances[plugin]["details"]["name"] = plugin
46 | except ImportError as e:
47 | vollog.error(f"Could not import {plugin}: {e}")
48 |
49 | for name, plugin in instances.items():
50 | vollog.info(f"RUNNING: {name}")
51 | self._grid = plugin["class"].run()
52 | renderer = DjangoRenderer(
53 | evidence_id=self.context.config["VolWeb.Evidence"],
54 | plugin=plugin["details"],
55 | )
56 | renderer.render(self._grid)
57 |
58 | def _generator(self):
59 | yield (0, ("Success",))
60 |
61 | def run(self):
62 | self.run_all()
63 | return TreeGrid(
64 | [("Status", str)],
65 | self._generator(),
66 | )
67 |
--------------------------------------------------------------------------------
/backend/requirements.txt:
--------------------------------------------------------------------------------
1 | aiobotocore==2.15.2
2 | aiohappyeyeballs==2.4.3
3 | aiohttp==3.10.11
4 | aioitertools==0.12.0
5 | aiosignal==1.3.1
6 | amqp==5.2.0
7 | antlr4-python3-runtime==4.9.3
8 | argon2-cffi==23.1.0
9 | argon2-cffi-bindings==21.2.0
10 | asgiref==3.8.1
11 | attrs==24.2.0
12 | autobahn==24.4.2
13 | Automat==24.8.1
14 | billiard==4.2.1
15 | boto3==1.35.16
16 | botocore==1.35.16
17 | cachetools==5.5.0
18 | capstone==5.0.3
19 | celery==5.4.0
20 | certifi==2024.8.30
21 | cffi==1.17.1
22 | channels==4.1.0
23 | channels-redis==4.2.0
24 | charset-normalizer==3.4.0
25 | click==8.1.7
26 | click-didyoumean==0.3.1
27 | click-plugins==1.1.1
28 | click-repl==0.3.0
29 | constantly==23.10.4
30 | cryptography==43.0.3
31 | daphne==4.1.2
32 | decorator==5.1.1
33 | Django==5.1.5
34 | django-celery-results==2.5.1
35 | django-cors-headers==4.4.0
36 | django-filter==24.3
37 | djangorestframework==3.15.2
38 | djangorestframework-simplejwt==5.3.1
39 | drf-yasg==1.21.8
40 | frozenlist==1.4.1
41 | fsspec==2024.9.0
42 | gcsfs==2024.9.0.post1
43 | google-api-core==2.21.0
44 | google-auth==2.35.0
45 | google-auth-oauthlib==1.2.1
46 | google-cloud-core==2.4.1
47 | google-cloud-storage==2.18.2
48 | google-crc32c==1.6.0
49 | google-resumable-media==2.7.2
50 | googleapis-common-protos==1.65.0
51 | h2==4.1.0
52 | hpack==4.0.0
53 | hyperframe==6.0.1
54 | hyperlink==21.0.0
55 | idna==3.10
56 | importlib_metadata==8.5.0
57 | incremental==24.7.2
58 | inflection==0.5.1
59 | jmespath==1.0.1
60 | kombu==5.4.2
61 | Markdown==3.7
62 | minio==7.2.9
63 | msgpack==1.1.0
64 | multidict==6.1.0
65 | oauthlib==3.2.2
66 | packaging==24.2
67 | pefile==2024.8.26
68 | priority==1.3.0
69 | prompt_toolkit==3.0.48
70 | propcache==0.2.0
71 | proto-plus==1.24.0
72 | protobuf==5.28.2
73 | psycopg2-binary==2.9.10
74 | pyasn1==0.6.1
75 | pyasn1_modules==0.4.1
76 | pycparser==2.22
77 | pycryptodome==3.20.0
78 | PyJWT==2.9.0
79 | pyOpenSSL==24.2.1
80 | python-dateutil==2.9.0.post0
81 | pytz==2024.2
82 | PyYAML==6.0.2
83 | redis==5.1.1
84 | requests==2.32.3
85 | requests-oauthlib==2.0.0
86 | rsa==4.9
87 | s3fs==2024.9.0
88 | s3transfer==0.10.3
89 | service-identity==24.2.0
90 | setuptools==75.3.0
91 | simplejson==3.19.3
92 | six==1.16.0
93 | sqlparse==0.5.1
94 | stix2==3.0.1
95 | stix2-patterns==2.0.0
96 | Twisted==24.10.0
97 | txaio==23.1.1
98 | typing_extensions==4.12.2
99 | tzdata==2024.2
100 | uritemplate==4.1.1
101 | urllib3==2.2.3
102 | vine==5.1.0
103 | volatility3==2.26.0
104 | wcwidth==0.2.13
105 | wrapt==1.16.0
106 | yara-python==4.5.1
107 | yarl==1.14.0
108 | zipp==3.20.2
109 | zope.interface==7.1.1
110 | pillow==11.2.1
111 |
--------------------------------------------------------------------------------
/backend/core/stix.py:
--------------------------------------------------------------------------------
1 | from stix2 import Indicator as StixIndicator, Bundle
2 |
3 |
4 | def create_indicator(indicator):
5 | pattern_mapping = {
6 | "artifact-sha1": "[artifact:hashes.'SHA-1' = '{}']",
7 | "artifact-sha256": "[artifact:hashes.'SHA-256' = '{}']",
8 | "artifact-md5": "[artifact:hashes.'MD5' = '{}']",
9 | "artifact-url": "[artifact:url = '{}']",
10 | "autonomous-system": "[autonomous-system:number = {}]",
11 | "directory": "[directory:path = '{}']",
12 | "domain-name": "[domain-name:value = '{}']",
13 | "email-addr": "[email-addr:value = '{}']",
14 | "file-sha256": "[file:hashes.'SHA-256' = '{}']",
15 | "file-sha1": "[file:hashes.'SHA-1' = '{}']",
16 | "file-md5": "[file:hashes.'MD5' = '{}']",
17 | "file-path": "[file:path = '{}']",
18 | "ipv4-addr": "[ipv4-addr:value = '{}']",
19 | "ipv6-addr": "[ipv6-addr:value = '{}']",
20 | "mac-addr": "[mac-addr:value = '{}']",
21 | "mutex": "[mutex:name = '{}']",
22 | "network-traffic": "[network-traffic:src_port = {}]",
23 | "process-cmdline": "[process:command_line = '{}']",
24 | "process-name": "[process:name = '{}']",
25 | "process-cwd": "[process:cwd = '{}']",
26 | "software": "[software:name = '{}']",
27 | "url": "[url:value = '{}']",
28 | "user-account": "[user-account:user_id = '{}']",
29 | "windows-registry-key": "[windows-registry-key:key = '{}']",
30 | "x509-certificate": "[x509-certificate:hashes.'SHA-1' = '{}']",
31 | }
32 |
33 | pattern_template = pattern_mapping.get(indicator.type)
34 | if pattern_template:
35 | value = (
36 | str(indicator.value).replace("\\", "\\\\")
37 | if "path" in indicator.type
38 | or "cmdline" in indicator.type
39 | or "registry-key" in indicator.type
40 | else indicator.value
41 | )
42 | pattern = pattern_template.format(value)
43 | stix_indicator = StixIndicator(
44 | pattern_type="stix",
45 | pattern=pattern,
46 | valid_from=indicator.evidence.linked_case.last_update,
47 | description=indicator.description,
48 | )
49 | return stix_indicator
50 | return None
51 |
52 |
53 | def export_bundle(indicators):
54 | stix_indicators = []
55 | for indicator in indicators:
56 | result = create_indicator(indicator=indicator)
57 | if result:
58 | stix_indicators.append(result)
59 |
60 | bundle = Bundle(objects=stix_indicators)
61 | bundle_json = bundle.serialize(pretty=True)
62 | return bundle_json
63 |
--------------------------------------------------------------------------------
/frontend/src/index.css:
--------------------------------------------------------------------------------
1 | a:hover {
2 | color: #f44336;
3 | }
4 |
5 | body {
6 | min-height: 100vh;
7 | }
8 |
9 | .controls {
10 | position: absolute;
11 | bottom: var(--stage-padding);
12 | left: var(--stage-padding);
13 | }
14 |
15 | :root {
16 | --ruby: #e22653;
17 | --grey: #999;
18 | --dark-grey: #666;
19 | --light-grey: #ccc;
20 | --cream: #f9f7ed;
21 | --transparent-white: #ffffffcc;
22 | --transition: all ease-out 300ms;
23 | --shadow: 0 1px 5px var(--dark-grey);
24 | --hover-opacity: 0.7;
25 | --stage-padding: 8px;
26 | --panels-width: 350px;
27 | --border-radius: 3px;
28 | }
29 |
30 | div.ico {
31 | --sigma-controls-size: 2rem;
32 | }
33 |
34 | .panels {
35 | position: absolute;
36 | font-size: 0.08rem;
37 | top: 0;
38 | right: 0;
39 | max-width: 400px;
40 | width: 400px;
41 | max-height: calc(100vh - 2 * var(--stage-padding));
42 | overflow-y: auto;
43 | padding: var(--stage-padding);
44 | scrollbar-width: thin;
45 | }
46 |
47 | .panel {
48 | padding: 1em;
49 | border-radius: var(--border-radius);
50 | box-shadow: var(--shadow);
51 | }
52 | .panel:not(:last-child) {
53 | margin-bottom: 0.5em;
54 | }
55 | .panel h2 button {
56 | float: right;
57 | border: 1px solid black;
58 | border-radius: var(--border-radius);
59 | font-size: 1.2em;
60 | height: 1em;
61 | width: 1em;
62 | text-align: center;
63 | padding: 0;
64 | cursor: pointer;
65 | display: flex;
66 | align-items: center;
67 | justify-content: center;
68 | }
69 | .panel h2 button:hover {
70 | opacity: var(--hover-opacity);
71 | }
72 |
73 | .panels-2 {
74 | position: absolute;
75 | top: 0;
76 | left: 0;
77 | width: 70%;
78 | max-height: calc(100vh - 2 * var(--stage-padding));
79 | overflow-y: auto;
80 | padding: var(--stage-padding);
81 | scrollbar-width: thin;
82 | }
83 |
84 | .panel-2 {
85 | padding: 1em;
86 | border-radius: var(--border-radius);
87 |
88 | box-shadow: var(--shadow);
89 | }
90 | .panel-2:not(:last-child) {
91 | margin-bottom: 0.5em;
92 | }
93 | .panel-2 h2 button {
94 | float: right;
95 | border: 1px solid black;
96 | border-radius: var(--border-radius);
97 | font-size: 1.2em;
98 | height: 1em;
99 | width: 1em;
100 | text-align: center;
101 | padding: 0;
102 | cursor: pointer;
103 | display: flex;
104 | align-items: center;
105 | justify-content: center;
106 | }
107 | .panel-2 h2 button:hover {
108 | opacity: var(--hover-opacity);
109 | }
110 |
--------------------------------------------------------------------------------
/backend/volatility_engine/plugins/windows/volweb_misc.py:
--------------------------------------------------------------------------------
1 | import json
2 | import logging
3 | import importlib
4 | from typing import Dict, Any, List, Tuple, Optional
5 | from volatility3.framework import interfaces
6 | from volatility3.framework.interfaces import plugins
7 | from volatility3.framework.configuration import requirements
8 | from volatility3.framework.renderers import TreeGrid
9 | from volatility_engine.utils import DjangoRenderer
10 | from volatility_engine.models import VolatilityPlugin
11 | from volatility3.plugins import yarascan
12 |
13 | vollog = logging.getLogger(__name__)
14 |
15 |
16 | class VolWebMisc(plugins.PluginInterface):
17 | _required_framework_version = (2, 0, 0)
18 | _version = (1, 0, 0)
19 |
20 | def load_plugin_info(self, json_file_path):
21 | with open(json_file_path, "r") as file:
22 | return json.load(file).get("plugins", {}).get("windows", [])
23 |
24 | @classmethod
25 | def get_requirements(cls):
26 | return [
27 | requirements.TranslationLayerRequirement(
28 | name="primary",
29 | description="Memory layer for the kernel",
30 | architectures=["Intel32", "Intel64"],
31 | )
32 | ]
33 |
34 | def dynamic_import(self, module_name):
35 | module_path, class_name = module_name.rsplit(".", 1)
36 | module = importlib.import_module(module_path)
37 | return getattr(module, class_name)
38 |
39 | def run_all(self):
40 | volweb_plugins = self.load_plugin_info("volatility_engine/volweb_misc.json")
41 | instances = {}
42 | for plugin, details in volweb_plugins.items():
43 | try:
44 | plugin_class = self.dynamic_import(plugin)
45 | instances[plugin] = {
46 | "class": plugin_class(self.context, self.config_path),
47 | "details": details,
48 | }
49 | instances[plugin]["details"]["name"] = plugin
50 | except ImportError as e:
51 | vollog.error(f"Could not import {plugin}: {e}")
52 |
53 | for name, plugin in instances.items():
54 | vollog.info(f"RUNNING: {name}")
55 | self._grid = plugin["class"].run()
56 | renderer = DjangoRenderer(
57 | evidence_id=self.context.config["VolWeb.Evidence"],
58 | plugin=plugin["details"],
59 | )
60 | renderer.render(self._grid)
61 |
62 | def _generator(self):
63 | yield (0, ("Success",))
64 |
65 | def run(self):
66 | self.run_all()
67 | return TreeGrid(
68 | [("Status", str)],
69 | self._generator(),
70 | )
71 |
--------------------------------------------------------------------------------
/docker-compose.yaml:
--------------------------------------------------------------------------------
1 | services:
2 | volweb-postgresdb:
3 | container_name: volweb-postgresdb
4 | environment:
5 | - POSTGRES_USER=${POSTGRES_USER}
6 | - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
7 | - POSTGRES_DB=${POSTGRES_DB}
8 | image: postgres:14.1
9 | restart: always
10 | ports:
11 | - 5432:5432
12 | volumes:
13 | - postgres-data:/var/lib/postgresql/data
14 |
15 | volweb-redis:
16 | container_name: volweb-redis
17 | image: "redis:latest"
18 | restart: always
19 | command: ["redis-server", "--appendonly", "yes"]
20 | volumes:
21 | - "redis-data:/data"
22 | ports:
23 | - "6379:6379"
24 |
25 | volweb-backend:
26 | image: "forensicxlab/volweb-backend:latest"
27 | container_name: volweb-backend
28 | environment:
29 | - DATABASE=${DATABASE}
30 | - DATABASE_HOST=${DATABASE_HOST}
31 | - CSRF_TRUSTED_ORIGINS=${CSRF_TRUSTED_ORIGINS}
32 | - DATABASE_PORT=${DATABASE_PORT}
33 | - POSTGRES_USER=${POSTGRES_USER}
34 | - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
35 | - POSTGRES_DB=${POSTGRES_DB}
36 | - DJANGO_SECRET=${DJANGO_SECRET}
37 | - BROKER_HOST=${BROKER_HOST}
38 | - BROKER_PORT=${BROKER_PORT}
39 | command: daphne -b 0.0.0.0 -p 8000 backend.asgi:application
40 | ports:
41 | - "8000:8000"
42 | depends_on:
43 | - volweb-postgresdb
44 | - volweb-redis
45 | restart: always
46 | volumes:
47 | - staticfiles:/home/app/web/staticfiles
48 | - media:/home/app/web/media
49 |
50 | volweb-workers:
51 | image: "forensicxlab/volweb-backend:latest"
52 | container_name: volweb-workers
53 | environment:
54 | - DATABASE=${DATABASE}
55 | - DATABASE_HOST=${DATABASE_HOST}
56 | - CSRF_TRUSTED_ORIGINS=${CSRF_TRUSTED_ORIGINS}
57 | - DATABASE_PORT=${DATABASE_PORT}
58 | - POSTGRES_USER=${POSTGRES_USER}
59 | - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
60 | - POSTGRES_DB=${POSTGRES_DB}
61 | - DJANGO_SECRET=${DJANGO_SECRET}
62 | - BROKER_HOST=${BROKER_HOST}
63 | - BROKER_PORT=${BROKER_PORT}
64 | command: celery -A backend worker --loglevel=INFO
65 | depends_on:
66 | - volweb-redis
67 | - volweb-postgresdb
68 | restart: always
69 | volumes:
70 | - media:/home/app/web/media
71 | deploy:
72 | mode: replicated
73 | replicas: 1
74 |
75 | volweb-frontend:
76 | image: "forensicxlab/volweb-frontend:latest"
77 | container_name: volweb-frontend
78 | ports:
79 | - "3000:3000"
80 | depends_on:
81 | - volweb-backend
82 | restart: always
83 | volumes:
84 | - staticfiles:/home/app/web/staticfiles
85 | - media:/home/app/web/media
86 |
87 | volumes:
88 | postgres-data:
89 | redis-data:
90 | staticfiles:
91 | media:
92 |
--------------------------------------------------------------------------------
/frontend/src/components/Investigate/Windows/Buttons/FileScanButton.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { useParams } from "react-router-dom";
3 | import axiosInstance from "../../../../utils/axiosInstance";
4 | import {
5 | Button,
6 | Tooltip,
7 | Dialog,
8 | DialogContent,
9 | DialogTitle,
10 | Divider,
11 | } from "@mui/material";
12 | import { FolderOpen } from "@mui/icons-material";
13 | import { Artefact } from "../../../../types";
14 | import FileScan from "../Components/FileScan";
15 | import { useSnackbar } from "../../../SnackbarProvider";
16 |
17 | const WindowsFileScanButton: React.FC = () => {
18 | const { id } = useParams<{ id: string }>();
19 | const [open, setOpen] = useState(false);
20 | const [data, setData] = useState([]);
21 | const { display_message } = useSnackbar();
22 |
23 | const fetchFileScan = async () => {
24 | try {
25 | const response = await axiosInstance.get(
26 | `/api/evidence/${id}/plugin/volatility3.plugins.windows.filescan.FileScan`,
27 | );
28 |
29 | const artefactsWithId: Artefact[] = [];
30 | response.data.artefacts.forEach((artefact: Artefact, index: number) => {
31 | artefactsWithId.push({ ...artefact, id: index });
32 | if (Array.isArray(artefact.__children) && artefact.__children.length) {
33 | artefact.__children.forEach((child: Artefact, idx: number) => {
34 | artefactsWithId.push({ ...child, id: `${index}-${idx}` });
35 | });
36 | }
37 | });
38 | setData(artefactsWithId);
39 | } catch (error) {
40 | display_message("error", `Error fetching filescan details: ${error}`);
41 | console.error("Error fetching filescan details", error);
42 | }
43 | };
44 |
45 | const handleOpen = () => {
46 | fetchFileScan();
47 | setOpen(true);
48 | };
49 |
50 | const handleClose = () => {
51 | setOpen(false);
52 | };
53 |
54 | return (
55 | <>
56 |
57 |
58 | }
64 | sx={{
65 | marginRight: 1,
66 | marginBottom: 1,
67 | }}
68 | disabled={false}
69 | >
70 | {"FileScan"}
71 |
72 |
73 |
74 |
75 | FileScan
76 |
77 |
78 |
79 |
80 |
81 |
82 | >
83 | );
84 | };
85 |
86 | export default WindowsFileScanButton;
87 |
--------------------------------------------------------------------------------
/backend/core/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 5.1.1 on 2024-11-26 22:31
2 |
3 | import django.db.models.deletion
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | initial = True
10 |
11 | dependencies = [
12 | ("evidences", "0001_initial"),
13 | ]
14 |
15 | operations = [
16 | migrations.CreateModel(
17 | name="Indicator",
18 | fields=[
19 | ("id", models.AutoField(primary_key=True, serialize=False)),
20 | ("name", models.TextField()),
21 | (
22 | "type",
23 | models.CharField(
24 | choices=[
25 | ("artifact-sha1", "Artifact - SHA-1"),
26 | ("artifact-sha256", "Artifact - SHA-256"),
27 | ("artifact-md5", "Artifact - MD5"),
28 | ("artifact-url", "Artifact - URL"),
29 | ("autonomous-system", "Autonomous System"),
30 | ("directory", "Directory"),
31 | ("domain-name", "Domain Name"),
32 | ("email-addr", "Email Address"),
33 | ("file-path", "File Path"),
34 | ("file-sha256", "File SHA-256"),
35 | ("file-sha1", "File SHA-1"),
36 | ("file-md5", "File MD5"),
37 | ("ipv4-addr", "IPv4 Address"),
38 | ("ipv6-addr", "IPv6 Address"),
39 | ("mac-addr", "MAC Address"),
40 | ("mutex", "Mutex"),
41 | ("network-traffic", "Network Traffic"),
42 | ("process-name", "Process - Name"),
43 | ("process-cwd", "Process - CWD"),
44 | ("process-cmdline", "Process - Command Line"),
45 | ("software", "Software"),
46 | ("url", "URL"),
47 | ("user-account", "User Account"),
48 | ("windows-registry-key", "Windows Registry Key"),
49 | ("x509-certificate", "X.509 Certificate SHA1"),
50 | ],
51 | max_length=100,
52 | ),
53 | ),
54 | ("description", models.TextField()),
55 | ("value", models.TextField()),
56 | (
57 | "evidence",
58 | models.ForeignKey(
59 | on_delete=django.db.models.deletion.CASCADE,
60 | to="evidences.evidence",
61 | ),
62 | ),
63 | ],
64 | ),
65 | ]
66 |
--------------------------------------------------------------------------------
/frontend/src/components/MenuBar.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { useNavigate } from "react-router-dom";
3 | import AppBar from "@mui/material/AppBar";
4 | import Box from "@mui/material/Box";
5 | import Toolbar from "@mui/material/Toolbar";
6 | import IconButton from "@mui/material/IconButton";
7 | import AccountCircle from "@mui/icons-material/AccountCircle";
8 | import MenuItem from "@mui/material/MenuItem";
9 | import Menu from "@mui/material/Menu";
10 |
11 | function ResponsiveAppBar() {
12 | const [anchorEl, setAnchorEl] = React.useState(null);
13 | const username = localStorage.getItem("username");
14 | const navigate = useNavigate();
15 |
16 | const handleMenu = (event: React.MouseEvent) => {
17 | setAnchorEl(event.currentTarget);
18 | };
19 |
20 | const handleClose = () => {
21 | setAnchorEl(null);
22 | };
23 |
24 | const handleLogout = () => {
25 | localStorage.removeItem("access_token");
26 | localStorage.removeItem("refresh_token");
27 | localStorage.removeItem("username");
28 | navigate("/login");
29 | handleClose();
30 | };
31 |
32 | return (
33 |
34 |
41 |
50 |
51 |
52 |
53 | {username}
54 |
62 |
63 |
64 |
87 |
88 |
89 |
90 | );
91 | }
92 |
93 | export default ResponsiveAppBar;
94 |
--------------------------------------------------------------------------------
/frontend/src/components/Lists/PluginList.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import axiosInstance from "../../utils/axiosInstance";
3 | import {
4 | List,
5 | ListItem,
6 | ListItemIcon,
7 | ListItemText,
8 | Typography,
9 | Box,
10 | } from "@mui/material";
11 | import * as Icons from "@mui/icons-material";
12 | import { Plugin } from "../../types";
13 | import { useSnackbar } from "../SnackbarProvider";
14 |
15 | interface PluginListProps {
16 | evidenceId?: string;
17 | }
18 |
19 | const PluginList: React.FC = ({ evidenceId }) => {
20 | const [plugins, setPlugins] = useState([]);
21 | const { display_message } = useSnackbar();
22 |
23 | useEffect(() => {
24 | axiosInstance
25 | .get(`/api/evidence/${evidenceId}/plugins/`)
26 | .then((response) => {
27 | setPlugins(response.data);
28 | })
29 | .catch((error) => {
30 | display_message("error", `Error fetching plugins: ${error}`);
31 | console.error("Error fetching plugins", error);
32 | });
33 | }, [evidenceId, display_message]);
34 |
35 | // Function to get the icon component by name
36 | const getIcon = (iconName: string) => {
37 | const IconComponent = Icons[iconName as keyof typeof Icons];
38 | if (IconComponent) {
39 | return ;
40 | }
41 | return ;
42 | };
43 |
44 | return (
45 |
46 |
47 | {plugins.map((plugin) => (
48 |
49 | {getIcon(plugin.icon)}
50 |
54 |
59 | Category: {plugin.category}
60 |
61 |
66 | {plugin.description}
67 |
68 |
76 | {plugin.results ? "Available" : "Unavailable"}
77 |
78 |
79 | }
80 | />
81 |
82 | ))}
83 |
84 |
85 | );
86 | };
87 |
88 | export default PluginList;
89 |
--------------------------------------------------------------------------------
/docker-compose-prod.yaml:
--------------------------------------------------------------------------------
1 | services:
2 | volweb-postgresdb:
3 | container_name: volweb-postgresdb
4 | environment:
5 | - POSTGRES_USER=${POSTGRES_USER}
6 | - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
7 | - POSTGRES_DB=${POSTGRES_DB}
8 | image: postgres:14.1
9 | restart: always
10 | ports:
11 | - 5432:5432
12 | volumes:
13 | - postgres-data:/var/lib/postgresql/data
14 |
15 | volweb-redis:
16 | container_name: volweb-redis
17 | image: "redis:latest"
18 | restart: always
19 | command: ["redis-server", "--appendonly", "yes"]
20 | volumes:
21 | - "redis-data:/data"
22 | ports:
23 | - "6379:6379"
24 |
25 | volweb-backend:
26 | image: "forensicxlab/volweb-backend:latest"
27 | container_name: volweb-backend
28 | environment:
29 | - DATABASE=${DATABASE}
30 | - DATABASE_HOST=${DATABASE_HOST}
31 | - CSRF_TRUSTED_ORIGINS=${CSRF_TRUSTED_ORIGINS}
32 | - DATABASE_PORT=${DATABASE_PORT}
33 | - POSTGRES_USER=${POSTGRES_USER}
34 | - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
35 | - POSTGRES_DB=${POSTGRES_DB}
36 | - DJANGO_SECRET=${DJANGO_SECRET}
37 | - BROKER_HOST=${BROKER_HOST}
38 | - BROKER_PORT=${BROKER_PORT}
39 | command: daphne -b 0.0.0.0 -p 8000 backend.asgi:application
40 | ports:
41 | - "8000:8000"
42 | depends_on:
43 | - volweb-postgresdb
44 | - volweb-redis
45 | restart: always
46 | volumes:
47 | - staticfiles:/home/app/web/staticfiles
48 | - media:/home/app/web/media
49 |
50 | volweb-workers:
51 | image: "forensicxlab/volweb-backend:latest"
52 | container_name: volweb-workers
53 | environment:
54 | - DATABASE=${DATABASE}
55 | - DATABASE_HOST=${DATABASE_HOST}
56 | - CSRF_TRUSTED_ORIGINS=${CSRF_TRUSTED_ORIGINS}
57 | - DATABASE_PORT=${DATABASE_PORT}
58 | - POSTGRES_USER=${POSTGRES_USER}
59 | - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
60 | - POSTGRES_DB=${POSTGRES_DB}
61 | - DJANGO_SECRET=${DJANGO_SECRET}
62 | - BROKER_HOST=${BROKER_HOST}
63 | - BROKER_PORT=${BROKER_PORT}
64 | command: celery -A backend worker --loglevel=INFO
65 | depends_on:
66 | - volweb-redis
67 | - volweb-postgresdb
68 | restart: always
69 | volumes:
70 | - media:/home/app/web/media
71 | deploy:
72 | mode: replicated
73 | replicas: 1
74 |
75 | volweb-frontend:
76 | image: "forensicxlab/volweb-frontend:latest"
77 | container_name: volweb-frontend
78 | expose:
79 | - 3000
80 | depends_on:
81 | - volweb-backend
82 | restart: always
83 | volumes:
84 | - staticfiles:/home/app/web/staticfiles
85 | - media:/home/app/web/media
86 |
87 | volweb-nginx:
88 | container_name: volweb_nginx
89 | image: nginx:mainline-alpine
90 | restart: always
91 | ports:
92 | - "80:80"
93 | - "443:443"
94 | volumes:
95 | - ./nginx:/etc/nginx/conf.d
96 | - ./nginx/ssl/:/etc/nginx/certs/
97 | depends_on:
98 | - volweb-frontend
99 | - volweb-backend
100 | - volweb-workers
101 |
102 | volumes:
103 | postgres-data:
104 | redis-data:
105 | staticfiles:
106 | media:
107 |
--------------------------------------------------------------------------------
/frontend/src/components/Investigate/Linux/Buttons/FileScanButton.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { useParams } from "react-router-dom";
3 | import axiosInstance from "../../../../utils/axiosInstance";
4 | import {
5 | Button,
6 | Tooltip,
7 | Dialog,
8 | DialogContent,
9 | DialogTitle,
10 | IconButton,
11 | Divider,
12 | Paper,
13 | } from "@mui/material";
14 | import CloseIcon from "@mui/icons-material/Close";
15 | import { FolderOpen } from "@mui/icons-material";
16 | import { Artefact } from "../../../../types";
17 | import FileScan from "../Components/FileScan";
18 | import { useSnackbar } from "../../../SnackbarProvider";
19 |
20 | const LinuxFileScanButton: React.FC = () => {
21 | const { id } = useParams<{ id: string }>();
22 | const [open, setOpen] = useState(false);
23 | const [data, setData] = useState([]);
24 | const { display_message } = useSnackbar();
25 |
26 | const fetchFileScan = async () => {
27 | try {
28 | const response = await axiosInstance.get(
29 | `/api/evidence/${id}/plugin/volatility3.plugins.linux.pagecache.Files`,
30 | );
31 |
32 | const artefactsWithId: Artefact[] = [];
33 | response.data.artefacts.forEach((artefact: Artefact, index: number) => {
34 | artefactsWithId.push({ ...artefact, id: index });
35 | if (Array.isArray(artefact.__children) && artefact.__children.length) {
36 | artefact.__children.forEach((child: Artefact, idx: number) => {
37 | artefactsWithId.push({ ...child, id: `${index}-${idx}` });
38 | });
39 | }
40 | });
41 | setData(artefactsWithId);
42 | } catch (error) {
43 | display_message("error", `Error fetching filescan details: ${error}`);
44 | console.error("Error fetching filescan details", error);
45 | }
46 | };
47 |
48 | const handleOpen = () => {
49 | fetchFileScan();
50 | setOpen(true);
51 | };
52 |
53 | const handleClose = () => {
54 | setOpen(false);
55 | };
56 |
57 | return (
58 | <>
59 |
60 |
61 | }
67 | sx={{
68 | marginRight: 1,
69 | marginBottom: 1,
70 | }}
71 | disabled={false}
72 | >
73 | {"FileScan"}
74 |
75 |
76 |
77 |
78 |
79 | FileScan
80 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 | >
99 | );
100 | };
101 |
102 | export default LinuxFileScanButton;
103 |
--------------------------------------------------------------------------------
/backend/volatility_engine/plugins/linux/volweb_main.py:
--------------------------------------------------------------------------------
1 | import json
2 | import logging
3 | import importlib
4 | from volatility3.framework.interfaces import plugins
5 | from volatility3.framework.configuration import requirements
6 | from volatility3.framework.renderers import TreeGrid
7 | from volatility_engine.utils import DjangoRenderer, file_handler
8 | from evidences.models import Evidence
9 |
10 | vollog = logging.getLogger(__name__)
11 |
12 |
13 | class VolWebMain(plugins.PluginInterface):
14 | _required_framework_version = (2, 0, 0)
15 | _version = (1, 0, 0)
16 |
17 | def load_plugin_info(self, json_file_path):
18 | with open(json_file_path, "r") as file:
19 | return json.load(file).get("plugins", {}).get("linux", [])
20 |
21 | @classmethod
22 | def get_requirements(cls):
23 | return [
24 | requirements.ModuleRequirement(
25 | name="kernel",
26 | description="Linux kernel",
27 | architectures=["Intel32", "Intel64"],
28 | ),
29 | ]
30 |
31 | def dynamic_import(self, module_name):
32 | module_path, class_name = module_name.rsplit(".", 1)
33 | module = importlib.import_module(module_path)
34 | return getattr(module, class_name)
35 |
36 | def run_all(self):
37 | volweb_plugins = self.load_plugin_info("volatility_engine/volweb_plugins.json")
38 |
39 | instances = {}
40 | for plugin, details in volweb_plugins.items():
41 | try:
42 | plugin_class = self.dynamic_import(plugin)
43 | instances[plugin] = {
44 | "class": plugin_class(self.context, self.config_path),
45 | "details": details,
46 | }
47 | instances[plugin]["details"]["name"] = plugin
48 | except ImportError as e:
49 | vollog.error(f"Could not import {plugin}: {e}")
50 |
51 | evidence_id = self.context.config["VolWeb.Evidence"]
52 | evidence = Evidence.objects.get(id=evidence_id)
53 | count = 0
54 | total = len(instances.items())
55 | for name, plugin in instances.items():
56 | try:
57 | vollog.info(f"RUNNING: {name}")
58 | self.context.config["plugins.VolWebMain.dump"] = (
59 | False # No dump by default
60 | )
61 | if name == "volatility3.plugins.windows.registry.hivelist.HiveList":
62 | self.context.config["plugins.VolWebMain.dump"] = (
63 | True # We want to dump the hivelist
64 | )
65 | plugin["class"]._file_handler = file_handler(
66 | f"media/{evidence_id}/"
67 | ) # Our file_handler need to be passed to the sub-plugin
68 | self._grid = plugin["class"].run()
69 | renderer = DjangoRenderer(
70 | evidence_id=evidence_id, plugin=plugin["details"]
71 | ) # Render the output of each plugin in the django database
72 | renderer.render(self._grid)
73 | evidence.status = (count * 100) / total
74 | count += 1
75 | evidence.save()
76 | except:
77 | pass
78 |
79 | def _generator(self):
80 | yield (0, ("Success",))
81 |
82 | def run(self):
83 | self.run_all()
84 | return TreeGrid(
85 | [("Status", str)],
86 | self._generator(),
87 | )
88 |
--------------------------------------------------------------------------------
/frontend/src/components/Investigate/Linux/Components/ProcessMetadata.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Box from "@mui/material/Box";
3 | import {
4 | Card,
5 | CardContent,
6 | Divider,
7 | Typography,
8 | List,
9 | ListItem,
10 | ListItemText,
11 | } from "@mui/material";
12 | import InfoIcon from "@mui/icons-material/Info";
13 | import { LinuxProcessInfo } from "../../../../types";
14 | import { styled } from "@mui/material/styles";
15 | import DumpPslistButton from "../Buttons/DumpPslistButton";
16 | import DumpMapsButton from "../Buttons/DumpMapsButton";
17 |
18 | interface ProcessMetadataProps {
19 | processMetadata: LinuxProcessInfo;
20 | id: string | undefined;
21 | loadingDumpPslist: boolean;
22 | setLoadingDumpPslist: (loading: boolean) => void;
23 | loadingDumpMaps: boolean;
24 | setLoadingDumpMaps: (loading: boolean) => void;
25 | }
26 |
27 | const ValueText = styled("span")(({ theme }) => ({
28 | color: theme.palette.primary.main,
29 | // In case we identify some anomalies in the future
30 | // "&.wow64": {
31 | // color: "red",
32 | // },
33 | }));
34 |
35 | const ProcessMetadata: React.FC = ({
36 | processMetadata,
37 | id,
38 | loadingDumpPslist,
39 | setLoadingDumpPslist,
40 | loadingDumpMaps,
41 | setLoadingDumpMaps,
42 | }) => {
43 | return (
44 |
45 |
46 |
47 |
56 |
57 | Metadata
58 |
59 |
60 |
61 | {processMetadata ? (
62 | Object.entries(processMetadata).map(
63 | ([key, value]) =>
64 | key !== "__children" &&
65 | key !== "File output" && (
66 |
67 |
70 | {`${key}: `}
71 |
72 | {value ? value.toString() : "N/A"}
73 |
74 | >
75 | }
76 | />
77 |
78 | ),
79 | )
80 | ) : (
81 | <>No Metadata>
82 | )}
83 |
84 |
85 |
91 |
97 |
98 |
99 |
100 |
101 | );
102 | };
103 |
104 | export default ProcessMetadata;
105 |
--------------------------------------------------------------------------------
/frontend/src/pages/cases/CaseDetail.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { useParams } from "react-router-dom";
3 | import { Case } from "../../types";
4 | import axiosInstance from "../../utils/axiosInstance";
5 | import {
6 | Typography,
7 | CircularProgress,
8 | Card,
9 | CardContent,
10 | Divider,
11 | Stack,
12 | Container,
13 | } from "@mui/material";
14 | import Grid from "@mui/material/Grid";
15 | import EvidenceList from "../../components/Lists/EvidenceList";
16 | import CaseIndicatorsList from "../../components/Lists/CaseIndicatorsList";
17 | import StixButton from "../../components/StixButton";
18 | import { useSnackbar } from "../../components/SnackbarProvider";
19 | const CaseDetail: React.FC = () => {
20 | const { display_message } = useSnackbar();
21 |
22 | const { id } = useParams<{ id: string }>();
23 | const [caseDetail, setCaseDetail] = useState(null);
24 | const [loading, setLoading] = useState(true);
25 |
26 | useEffect(() => {
27 | const fetchCaseDetail = async () => {
28 | try {
29 | const response = await axiosInstance.get(`/api/cases/${id}/`);
30 | setCaseDetail(response.data);
31 | setLoading(false);
32 | } catch (error) {
33 | display_message("error", `An error fetching case details: ${error}`);
34 | console.error("Error fetching case details", error);
35 | }
36 | };
37 |
38 | fetchCaseDetail();
39 | }, [id, display_message]);
40 |
41 | if (loading) {
42 | return ;
43 | }
44 |
45 | return (
46 | caseDetail && (
47 |
48 |
49 |
50 |
51 |
52 | {caseDetail.name}
53 |
54 |
55 | {caseDetail.description}
56 |
57 |
58 | Last Update: {caseDetail.last_update}
59 |
60 |
65 | Investigator(s):{" "}
66 | {caseDetail.linked_users
67 | .map((user) => user.username)
68 | .join(", ")}
69 |
70 |
71 |
72 |
73 |
74 |
75 | Linked evidences
76 |
77 |
78 |
79 |
80 |
81 |
86 |
87 | Indicators
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 | )
96 | );
97 | };
98 |
99 | export default CaseDetail;
100 |
--------------------------------------------------------------------------------
/frontend/src/components/Investigate/Windows/Components/ProcessMetadata.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Box from "@mui/material/Box";
3 | import {
4 | Card,
5 | CardContent,
6 | Divider,
7 | Typography,
8 | List,
9 | ListItem,
10 | ListItemText,
11 | } from "@mui/material";
12 | import InfoIcon from "@mui/icons-material/Info";
13 | import { ProcessInfo } from "../../../../types";
14 | import { styled } from "@mui/material/styles";
15 | import DumpPslistButton from "../Buttons/DumpPslistButton";
16 | import ComputeHandlesButton from "../Buttons/ComputeHandlesButton";
17 |
18 | interface ProcessMetadataProps {
19 | processMetadata: ProcessInfo;
20 | id: string | undefined;
21 | loadingDump: boolean;
22 | setLoadingDump: (loading: boolean) => void;
23 | loadingHandles: boolean;
24 | setLoadingHandles: (loading: boolean) => void;
25 | }
26 |
27 | const ValueText = styled("span")(({ theme }) => ({
28 | color: theme.palette.primary.main,
29 | "&.wow64": {
30 | color: "red",
31 | },
32 | }));
33 |
34 | const ProcessMetadata: React.FC = ({
35 | processMetadata,
36 | id,
37 | loadingDump,
38 | setLoadingDump,
39 | loadingHandles,
40 | setLoadingHandles,
41 | }) => {
42 | return (
43 |
44 |
45 |
46 |
55 |
56 | Metadata
57 |
58 |
59 |
60 | {processMetadata ? (
61 | Object.entries(processMetadata).map(
62 | ([key, value]) =>
63 | key !== "__children" &&
64 | key !== "File output" && (
65 |
66 |
69 | {`${key}: `}
70 |
76 | {value ? value.toString() : "N/A"}
77 |
78 | >
79 | }
80 | />
81 |
82 | ),
83 | )
84 | ) : (
85 | <>>
86 | )}
87 |
88 |
89 |
95 |
101 |
102 |
103 |
104 |
105 | );
106 | };
107 |
108 | export default ProcessMetadata;
109 |
--------------------------------------------------------------------------------
/backend/volatility_engine/plugins/windows/volweb_main.py:
--------------------------------------------------------------------------------
1 | import json
2 | import logging
3 | import importlib
4 | from typing import Dict, Any, List, Tuple, Optional
5 | from volatility3.framework import interfaces
6 | from volatility3.framework.interfaces import plugins
7 | from volatility3.framework.configuration import requirements
8 | from volatility3.framework.renderers import TreeGrid
9 | from volatility_engine.utils import DjangoRenderer, file_handler
10 | from volatility_engine.models import VolatilityPlugin
11 | from evidences.models import Evidence
12 |
13 | vollog = logging.getLogger(__name__)
14 |
15 |
16 | class VolWebMain(plugins.PluginInterface):
17 | _required_framework_version = (2, 0, 0)
18 | _version = (1, 0, 0)
19 |
20 | def load_plugin_info(self, json_file_path):
21 | with open(json_file_path, "r") as file:
22 | return json.load(file).get("plugins", {}).get("windows", [])
23 |
24 | @classmethod
25 | def get_requirements(cls):
26 | return [
27 | requirements.ModuleRequirement(
28 | name="kernel",
29 | description="Windows kernel",
30 | architectures=["Intel32", "Intel64"],
31 | ),
32 | ]
33 |
34 | def dynamic_import(self, module_name):
35 | module_path, class_name = module_name.rsplit(".", 1)
36 | module = importlib.import_module(module_path)
37 | return getattr(module, class_name)
38 |
39 | def run_all(self):
40 | volweb_plugins = self.load_plugin_info("volatility_engine/volweb_plugins.json")
41 |
42 | instances = {}
43 | for plugin, details in volweb_plugins.items():
44 | try:
45 | plugin_class = self.dynamic_import(plugin)
46 | instances[plugin] = {
47 | "class": plugin_class(self.context, self.config_path),
48 | "details": details,
49 | }
50 | instances[plugin]["details"]["name"] = plugin
51 | except ImportError as e:
52 | vollog.error(f"Could not import {plugin}: {e}")
53 |
54 | evidence_id = self.context.config["VolWeb.Evidence"]
55 | evidence = Evidence.objects.get(id=evidence_id)
56 | count = 0
57 | total = len(instances.items())
58 | for name, plugin in instances.items():
59 | try:
60 | vollog.info(f"RUNNING: {name}")
61 | self.context.config["plugins.VolWebMain.dump"] = (
62 | False # No dump by default
63 | )
64 | if name == "volatility3.plugins.windows.registry.hivelist.HiveList":
65 | self.context.config["plugins.VolWebMain.dump"] = (
66 | True # We want to dump the hivelist
67 | )
68 | plugin["class"]._file_handler = file_handler(
69 | f"media/{evidence_id}/"
70 | ) # Our file_handler need to be passed to the sub-plugin
71 | self._grid = plugin["class"].run()
72 | renderer = DjangoRenderer(
73 | evidence_id=evidence_id, plugin=plugin["details"]
74 | ) # Render the output of each plugin in the django database
75 | renderer.render(self._grid)
76 | evidence.status = (count * 100) / total
77 | count += 1
78 | evidence.save()
79 | except:
80 | pass
81 |
82 | def _generator(self):
83 | yield (0, ("Success",))
84 |
85 | def run(self):
86 | self.run_all()
87 | return TreeGrid(
88 | [("Status", str)],
89 | self._generator(),
90 | )
91 |
--------------------------------------------------------------------------------
/frontend/src/types.tsx:
--------------------------------------------------------------------------------
1 | export interface User {
2 | id: number;
3 | username: string;
4 | first_name: string;
5 | last_name: string;
6 | }
7 |
8 | export interface Indicator {
9 | id: number;
10 | type: string;
11 | name: string;
12 | description: string;
13 | value: string;
14 | dump_linked_dump_name: string;
15 | }
16 |
17 | export interface TaskData {
18 | task_name: string;
19 | status: string;
20 | task_args: string;
21 | }
22 |
23 | export interface Case {
24 | id: number;
25 | name: string;
26 | description: string;
27 | bucket_id: string;
28 | linked_users: Array;
29 | last_update: string;
30 | }
31 |
32 | export interface Evidence {
33 | id: number;
34 | name: string;
35 | os: string;
36 | status: number;
37 | }
38 |
39 | export interface CloudStorage {
40 | endpoint: string;
41 | access_key: string;
42 | secret_key: string;
43 | region: string;
44 | }
45 |
46 | export interface ProcessInfo {
47 | PID: number;
48 | PPID: number;
49 | ImageFileName: string | null;
50 | OffsetV: number | null;
51 | Threads: number | null;
52 | Handles: number | null;
53 | SessionId: number | null;
54 | Wow64: boolean | null;
55 | CreateTime: string | null;
56 | ExitTime: string | null;
57 | __children: ProcessInfo[];
58 | anomalies: string[] | undefined;
59 | }
60 |
61 | export interface LinuxProcessInfo {
62 | PID: number;
63 | PPID: number;
64 | TID: number;
65 | COMM: string;
66 | __children: LinuxProcessInfo[];
67 | anomalies: string[] | undefined;
68 | }
69 |
70 | export interface NetworkInfo {
71 | __children: string[];
72 | Offset: number;
73 | Proto: string;
74 | LocalAddr: string;
75 | LocalPort: number;
76 | ForeignAddr: string;
77 | ForeignPort: number;
78 | State: string;
79 | PID: number;
80 | Owner: string;
81 | Created: string;
82 | id: number;
83 | }
84 |
85 | interface KnownEnrichedData {
86 | pslist: ProcessInfo;
87 | "volatility3.plugins.windows.cmdline.CmdLine"?: { Args: string }[];
88 | "volatility3.plugins.windows.sessions.Sessions"?: {
89 | "Session ID": number;
90 | Process: string;
91 | "User Name": string;
92 | "Create Time": string;
93 | }[];
94 | "volatility3.plugins.windows.netscan.NetScan"?: NetworkInfo[];
95 | "volatility3.plugins.windows.netstat.NetStat"?: NetworkInfo[];
96 | }
97 |
98 | export interface EnrichedProcessData extends KnownEnrichedData {
99 | [key: string]: ProcessInfo | NetworkInfo | unknown;
100 | }
101 |
102 | export interface Plugin {
103 | name: string;
104 | icon: string;
105 | description: string;
106 | display: string;
107 | category: string;
108 | results: boolean;
109 | }
110 |
111 | export interface Artefact {
112 | [key: string]: unknown;
113 | __childrens?: Artefact[];
114 | }
115 |
116 | export interface Connection {
117 | __children: Connection[];
118 | Offset: number;
119 | Proto: string;
120 | LocalAddr: string;
121 | LocalPort: number;
122 | ForeignAddr: string;
123 | ForeignPort: number;
124 | State: string;
125 | PID: number;
126 | Owner: string;
127 | Created: string;
128 | id: number;
129 | }
130 |
131 | // Define the structure of a graph node
132 | interface GraphNode {
133 | id: string;
134 | label: string;
135 | x: number;
136 | y: number;
137 | size: number;
138 | }
139 |
140 | // Define the structure of a graph edge
141 | interface GraphEdge {
142 | id: string;
143 | source: string;
144 | target: string;
145 | label: string;
146 | }
147 |
148 | export interface GraphData {
149 | nodes: GraphNode[];
150 | edges: GraphEdge[];
151 | }
152 |
153 | export interface Symbol {
154 | id: number;
155 | name: string;
156 | os: string;
157 | description: number;
158 | }
159 |
--------------------------------------------------------------------------------
/frontend/src/utils/processAnalysis.ts:
--------------------------------------------------------------------------------
1 | import { ProcessInfo } from "../types";
2 |
3 | export const flattenProcesses = (processes: ProcessInfo[]): ProcessInfo[] => {
4 | let result: ProcessInfo[] = [];
5 |
6 | processes.forEach((process) => {
7 | result.push(process);
8 | if (process.__children && process.__children.length > 0) {
9 | result = result.concat(flattenProcesses(process.__children));
10 | }
11 | });
12 |
13 | return result;
14 | };
15 |
16 | export const annotateProcessData = (processTree: ProcessInfo[]): void => {
17 | const processes = flattenProcesses(processTree);
18 |
19 | const processesByPID = new Map();
20 | const processesByName = new Map();
21 |
22 | // Prepare instance counts
23 | const instanceExpectedSingles = [
24 | "smss.exe",
25 | "wininit.exe",
26 | "services.exe",
27 | "lsass.exe",
28 | ];
29 | const suspiciousProcesses = [
30 | "powershell.exe",
31 | "cmd.exe",
32 | "net.exe",
33 | "net1.exe",
34 | "psexec.exe",
35 | "psexesvc.exe",
36 | "schtasks.exe",
37 | "at.exe",
38 | "sc.exe",
39 | "wmic.exe",
40 | "wmiprvse.exe",
41 | "wsmprovhost.exe",
42 | ];
43 |
44 | processes.forEach((process) => {
45 | processesByPID.set(process.PID, process);
46 |
47 | const nameLower = (process.ImageFileName ?? "").toLowerCase();
48 |
49 | if (!processesByName.has(nameLower)) {
50 | processesByName.set(nameLower, []);
51 | }
52 | processesByName.get(nameLower)!.push(process);
53 |
54 | // Initialize anomalies array in process
55 | process.anomalies = [];
56 |
57 | if (process.Wow64) {
58 | process.anomalies.push("Wow64 is enabled for this process.");
59 | }
60 |
61 | // Check number of instances
62 | if (instanceExpectedSingles.includes(nameLower)) {
63 | const instances = processesByName.get(nameLower) || [];
64 | if (instances.length !== 1) {
65 | process.anomalies.push("Unexpected number of instances");
66 | }
67 | }
68 |
69 | // Check parent-child relationships
70 | if (nameLower === "smss.exe" && process.PPID !== 4) {
71 | process.anomalies.push("Unexpected parent PID");
72 | } else if (nameLower === "svchost.exe") {
73 | const servicesProcesses = processesByName.get("services.exe") || [];
74 | if (
75 | servicesProcesses.length !== 1 ||
76 | (servicesProcesses.length === 1 &&
77 | process.PPID !== servicesProcesses[0].PID &&
78 | (
79 | processesByPID.get(process.PPID)?.ImageFileName ?? ""
80 | ).toLowerCase() !== "svchost.exe")
81 | ) {
82 | process.anomalies.push(
83 | servicesProcesses.length !== 1
84 | ? "Cannot verify parent PID (services.exe not found or multiple instances)"
85 | : "Unexpected parent PID",
86 | );
87 | }
88 | }
89 |
90 | // Verify processes are running in expected sessions
91 | if (
92 | instanceExpectedSingles.includes(nameLower) &&
93 | process.SessionId !== 0 &&
94 | process.SessionId
95 | ) {
96 | process.anomalies.push("Unexpected SessionId (should be 0)");
97 | }
98 |
99 | // Flag specific processes and those that have exited unexpectedly
100 | if (suspiciousProcesses.includes(nameLower)) {
101 | process.anomalies.push("The process is usually suspicious");
102 | }
103 |
104 | if (
105 | [
106 | "smss.exe",
107 | "wininit.exe",
108 | "services.exe",
109 | "lsass.exe",
110 | "csrss.exe",
111 | ].includes(nameLower) &&
112 | process.ExitTime
113 | ) {
114 | process.anomalies.push("Exited unexpectedly");
115 | }
116 | });
117 | };
118 |
--------------------------------------------------------------------------------
/frontend/src/components/Investigate/Windows/Buttons/ComputeHandlesButton.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import {
3 | Button,
4 | CircularProgress,
5 | Dialog,
6 | DialogTitle,
7 | DialogContent,
8 | IconButton,
9 | } from "@mui/material";
10 | import { Close, Settings } from "@mui/icons-material";
11 | import axiosInstance from "../../../../utils/axiosInstance";
12 | import { AxiosError } from "axios";
13 | import PluginDataGrid from "../../PluginDataGrid";
14 |
15 | interface ComputeHandlesButtonProps {
16 | evidenceId: string | undefined;
17 | pid: number;
18 | loading: boolean;
19 | setLoading: (loading: boolean) => void;
20 | }
21 |
22 | const ComputeHandlesButton: React.FC = ({
23 | evidenceId,
24 | pid,
25 | loading,
26 | setLoading,
27 | }) => {
28 | const [openDialog, setOpenDialog] = useState(false);
29 |
30 | const handleComputeHandles = async () => {
31 | setLoading(true);
32 | try {
33 | // First, check if the data exists
34 | const response = await axiosInstance.get(
35 | `/api/evidence/${evidenceId}/plugin/volatility3.plugins.windows.handles.Handles.${pid}`,
36 | );
37 | if (
38 | response.data &&
39 | response.data.artefacts &&
40 | response.data.artefacts.length > 0
41 | ) {
42 | setOpenDialog(true);
43 | setLoading(false);
44 | } else {
45 | await axiosInstance.post(`/api/evidence/tasks/handles/`, {
46 | pid,
47 | evidenceId,
48 | });
49 | }
50 | } catch (error) {
51 | if (
52 | error instanceof AxiosError &&
53 | error.response &&
54 | error.response.status === 404
55 | ) {
56 | // Data does not exist, start computation
57 | await axiosInstance.post(`/api/evidence/tasks/handles/`, {
58 | pid,
59 | evidenceId,
60 | });
61 | // Loading remains true
62 | } else {
63 | // Handle other errors
64 | console.error("Error computing handles:", error);
65 | setLoading(false);
66 | alert("An error occurred while checking or computing handles.");
67 | }
68 | }
69 | };
70 |
71 | const handleCloseDialog = () => {
72 | setOpenDialog(false);
73 | };
74 |
75 | return (
76 | <>
77 | : }
85 | >
86 | {loading ? "Computing..." : "Fetch Handles"}
87 |
88 |
89 | {/* Dialog to display the handles data */}
90 |
104 |
105 | Handles
106 |
113 |
114 |
115 |
116 |
117 | {/* Display the PluginDataGrid with the handles data */}
118 |
121 |
122 |
123 | >
124 | );
125 | };
126 |
127 | export default ComputeHandlesButton;
128 |
--------------------------------------------------------------------------------
/frontend/src/assets/react.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/components/Investigate/Linux/Buttons/DumpMapsButton.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import {
3 | Button,
4 | CircularProgress,
5 | Dialog,
6 | DialogTitle,
7 | DialogContent,
8 | IconButton,
9 | } from "@mui/material";
10 | import { Close, Settings } from "@mui/icons-material";
11 | import axiosInstance from "../../../../utils/axiosInstance";
12 | import { AxiosError } from "axios";
13 | import PluginDataGrid from "../../PluginDataGrid";
14 |
15 | interface DumpMapsButtonProps {
16 | evidenceId: string | undefined;
17 | pid: number;
18 | loading: boolean;
19 | setLoading: (loading: boolean) => void;
20 | }
21 |
22 | const DumpMapsButton: React.FC = ({
23 | evidenceId,
24 | pid,
25 | loading,
26 | setLoading,
27 | }) => {
28 | const [openDialog, setOpenDialog] = useState(false);
29 |
30 | const handleComputeHandles = async () => {
31 | setLoading(true);
32 | try {
33 | // First, check if the data exists
34 | const response = await axiosInstance.get(
35 | `/api/evidence/${evidenceId}/plugin/volatility3.plugins.linux.proc.MapsDump.${pid}`,
36 | );
37 | if (
38 | response.data &&
39 | response.data.artefacts &&
40 | response.data.artefacts.length > 0
41 | ) {
42 | // Data exists, open dialog to display results
43 | setOpenDialog(true);
44 | setLoading(false);
45 | } else {
46 | // Data does not exist, start computation
47 | await axiosInstance.post(`/api/evidence/tasks/dump/maps/`, {
48 | pid,
49 | evidenceId,
50 | });
51 | // Loading remains true until task completes and WebSocket updates it
52 | }
53 | } catch (error: unknown) {
54 | if (
55 | error instanceof AxiosError &&
56 | error.response &&
57 | error.response.status === 404
58 | ) {
59 | // Data does not exist, start computation
60 | await axiosInstance.post(`/api/evidence/tasks/dump/process/maps/`, {
61 | pid,
62 | evidenceId,
63 | });
64 | // Loading remains true
65 | } else {
66 | // Handle other errors
67 | console.error("Error dumping process maps:", error);
68 | setLoading(false);
69 | alert("An error occurred while checking or computing handles.");
70 | }
71 | }
72 | };
73 |
74 | const handleCloseDialog = () => {
75 | setOpenDialog(false);
76 | };
77 |
78 | return (
79 | <>
80 | : }
88 | >
89 | {loading ? "Dumping..." : "Show Maps"}
90 |
91 |
92 | {/* Dialog to display the procmaps data */}
93 |
107 |
108 | Handles
109 |
116 |
117 |
118 |
119 |
120 | {/* Display the PluginDataGrid with the handles data */}
121 |
124 |
125 |
126 | >
127 | );
128 | };
129 |
130 | export default DumpMapsButton;
131 |
--------------------------------------------------------------------------------
/frontend/src/components/Dialogs/SymbolCreationDialog.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import {
3 | Dialog,
4 | DialogTitle,
5 | DialogContent,
6 | DialogActions,
7 | Button,
8 | Select,
9 | MenuItem,
10 | FormControl,
11 | InputLabel,
12 | TextField,
13 | } from "@mui/material";
14 | import axiosInstance from "../../utils/axiosInstance";
15 | import { Symbol } from "../../types";
16 |
17 | interface SymbolCreationDialogProps {
18 | open: boolean;
19 | onClose: () => void;
20 | onCreateSuccess: (newSymbol: Symbol) => void;
21 | }
22 |
23 | const SymbolCreationDialog: React.FC = ({
24 | open,
25 | onClose,
26 | onCreateSuccess,
27 | }) => {
28 | const OS_OPTIONS = [
29 | { value: "Windows", label: "Windows" },
30 | { value: "Linux", label: "Linux" },
31 | ];
32 |
33 | const [os, setOs] = useState("");
34 | const [file, setFile] = useState(null);
35 | const [description, setDescription] = useState(""); // Add description state
36 | const [uploading, setUploading] = useState(false);
37 | const [error, setError] = useState(null);
38 |
39 | const handleUpload = async () => {
40 | if (!os || !file || !description) {
41 | setError("Please fill in all fields.");
42 | return;
43 | }
44 |
45 | setUploading(true);
46 | setError(null);
47 |
48 | try {
49 | const formData = new FormData();
50 | formData.append("os", os);
51 | formData.append("description", description);
52 | formData.append("symbols_file", file); // Use 'symbols_file' to match serializer
53 | if (file) {
54 | formData.append("name", file.name); // Add name as the name of the uploaded file
55 | }
56 |
57 | const response = await axiosInstance.post(
58 | "api/upload_symbols/",
59 | formData,
60 | {
61 | headers: {
62 | "Content-Type": "multipart/form-data",
63 | },
64 | },
65 | );
66 | onCreateSuccess(response.data.symbol);
67 | setUploading(false);
68 | onClose();
69 | // Reset form fields after successful upload
70 | setOs("");
71 | setDescription("");
72 | setFile(null);
73 | } catch (error) {
74 | console.error("Upload error:", error); // Log the error
75 | setError("Failed to upload the file. Please try again.");
76 | setUploading(false);
77 | }
78 | };
79 |
80 | return (
81 |
82 | Upload New Symbol
83 |
84 |
85 | Operating System
86 | setOs(e.target.value as string)}
91 | >
92 | {OS_OPTIONS.map((option) => (
93 |
94 | {option.label}
95 |
96 | ))}
97 |
98 |
99 |
100 | {/* Add Description Field */}
101 | setDescription(e.target.value)}
109 | />
110 |
111 |
112 | Select File
113 | setFile(e.target.files ? e.target.files[0] : null)}
117 | />
118 |
119 | {file && Selected File: {file.name}
}
120 | {error && {error}
}
121 |
122 |
123 |
124 | Cancel
125 |
126 |
127 | Upload
128 |
129 |
130 |
131 | );
132 | };
133 |
134 | export default SymbolCreationDialog;
135 |
--------------------------------------------------------------------------------
/frontend/src/pages/dashboard/Dashboard.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { Typography, CircularProgress, Box } from "@mui/material";
3 | import Grid from "@mui/material/Grid";
4 | import axiosInstance from "../../utils/axiosInstance";
5 | import DonutChart from "../../components/Charts/DonutChart";
6 | import LineChart from "../../components/Charts/LineChart";
7 | import StatisticsCard from "../../components/Statistics/StatisticsCard";
8 | import RecentCases from "../../components/RecentItems/RecentCases";
9 | import RecentISF from "../../components/RecentItems/RecentISF";
10 | import { countTasksByDate } from "../../utils/countTasksByDate";
11 | import { Case } from "../../types";
12 | import {
13 | AccountCircle,
14 | BlurLinear,
15 | CasesSharp,
16 | FindReplace,
17 | Memory,
18 | } from "@mui/icons-material";
19 |
20 | interface StatisticsData {
21 | total_evidences: number;
22 | total_evidences_progress: number;
23 | total_cases: number;
24 | total_users: number;
25 | total_symbols: number;
26 | total_evidences_windows: number;
27 | total_evidences_linux: number;
28 | tasks: Array<{ date_created: string }>;
29 | last_5_cases: Case[];
30 | last_5_isf: Array<{ name: string }>;
31 | }
32 |
33 | const Dashboard: React.FC = () => {
34 | const [data, setData] = useState(null);
35 | const [loading, setLoading] = useState(true);
36 | const theme = "dark";
37 |
38 | useEffect(() => {
39 | const fetchStatistics = async () => {
40 | try {
41 | const response =
42 | await axiosInstance.get("/core/statistics/");
43 | setData(response.data);
44 | } catch (error) {
45 | console.error("Error fetching statistics:", error);
46 | } finally {
47 | setLoading(false);
48 | }
49 | };
50 |
51 | fetchStatistics();
52 | }, []);
53 |
54 | if (loading) {
55 | return (
56 |
57 |
58 |
59 | );
60 | }
61 |
62 | if (!data) {
63 | return (
64 |
65 | Failed to load data.
66 |
67 | );
68 | }
69 | const tasksStats = countTasksByDate(data.tasks);
70 |
71 | return (
72 |
73 |
74 | {/* Statistics Cards */}
75 |
76 | }
80 | />
81 |
82 |
83 | }
87 | />
88 |
89 |
90 | }
94 | />
95 |
96 |
97 | }
101 | />
102 |
103 |
104 | }
108 | />
109 |
110 |
111 | {/* Charts */}
112 |
113 |
118 |
119 |
120 |
125 |
126 |
127 | {/* Recent Items */}
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 | );
137 | };
138 |
139 | export default Dashboard;
140 |
--------------------------------------------------------------------------------
/frontend/src/components/Investigate/PluginDataGrid.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { Box, Button } from "@mui/material";
3 | import { DataGrid, GridColDef, useGridApiRef } from "@mui/x-data-grid";
4 | import { useParams } from "react-router-dom";
5 | import axiosInstance from "../../utils/axiosInstance";
6 | import Checkbox from "@mui/material/Checkbox";
7 | import CloseIcon from "@mui/icons-material/Close";
8 | import IconButton from "@mui/material/IconButton";
9 | import { Artefact } from "../../types";
10 |
11 | interface PluginDataGridProps {
12 | pluginName: string;
13 | }
14 |
15 | const PluginDataGrid: React.FC = ({ pluginName }) => {
16 | const { id } = useParams<{ id: string }>();
17 | const [data, setData] = useState([]);
18 | const [loading, setLoading] = useState(true);
19 | const apiRef = useGridApiRef();
20 |
21 | const columns: GridColDef[] = React.useMemo(() => {
22 | if (!data[0]) {
23 | return [];
24 | }
25 | return Object.keys(data[0])
26 | .filter((key) => key !== "__children" && key !== "id")
27 | .map((key) => ({
28 | field: key,
29 | headerName: key,
30 | renderCell: (params) => {
31 | if (key === "File output" && params.value !== "Disabled") {
32 | return (
33 | window.open(`/media/${id}/${params.value}`)}
37 | >
38 | Download
39 |
40 | );
41 | }
42 |
43 | if (key === "Disasm" || key === "Hexdump") {
44 | return {params.value} ;
45 | }
46 |
47 | return typeof params.value === "boolean" ? (
48 | params.value ? (
49 |
50 | ) : (
51 |
52 |
53 |
54 | )
55 | ) : params.value !== null ? (
56 | params.value
57 | ) : (
58 | ""
59 | );
60 | },
61 | }));
62 | }, [data, id]);
63 |
64 | const autosizeOptions = React.useMemo(
65 | () => ({
66 | columns: [...columns].map((col) => col.headerName ?? ""),
67 | includeOutliers: true,
68 | includeHeaders: true,
69 | }),
70 | [columns],
71 | );
72 |
73 | useEffect(() => {
74 | const fetchPlugins = async () => {
75 | try {
76 | const response = await axiosInstance.get(
77 | `/api/evidence/${id}/plugin/${pluginName}`,
78 | );
79 | // Assign consistent unique IDs to each row and flatten children
80 | const artefactsWithId: Artefact[] = [];
81 | response.data.artefacts.forEach((artefact: Artefact, index: number) => {
82 | artefactsWithId.push({ ...artefact, id: index });
83 | if (
84 | Array.isArray(artefact.__children) &&
85 | artefact.__children.length
86 | ) {
87 | artefact.__children.forEach((child: Artefact, idx: number) => {
88 | artefactsWithId.push({ ...child, id: `${index}-${idx}` });
89 | });
90 | }
91 | });
92 | console.log(artefactsWithId);
93 | setData(artefactsWithId);
94 | } catch (error) {
95 | console.error("Error fetching case details", error);
96 | } finally {
97 | setLoading(false);
98 | }
99 | };
100 |
101 | fetchPlugins();
102 | }, [id, pluginName]);
103 |
104 | // Add this useEffect to call autosizeColumns after the data is loaded
105 | useEffect(() => {
106 | if (!loading && data.length > 0) {
107 | const timeoutId = setTimeout(() => {
108 | if (apiRef.current) {
109 | apiRef.current.autosizeColumns(autosizeOptions);
110 | }
111 | }, 200); // Delay to ensure DataGrid has rendered
112 | return () => clearTimeout(timeoutId);
113 | }
114 | }, [loading, data, apiRef, autosizeOptions]);
115 |
116 | return (
117 |
118 | row.id}
126 | pagination
127 | loading={loading}
128 | autosizeOnMount
129 | autosizeOptions={autosizeOptions}
130 | apiRef={apiRef}
131 | getRowHeight={() => "auto"}
132 | />
133 |
134 | );
135 | };
136 |
137 | export default PluginDataGrid;
138 |
--------------------------------------------------------------------------------
/frontend/src/components/Dialogs/CaseCreationDialog.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import axiosInstance from "../../utils/axiosInstance";
3 | import {
4 | Button,
5 | Dialog,
6 | DialogTitle,
7 | DialogContent,
8 | DialogActions,
9 | Typography,
10 | IconButton,
11 | FormControl,
12 | Divider,
13 | TextField,
14 | } from "@mui/material";
15 | import { styled } from "@mui/material/styles";
16 | import CloseIcon from "@mui/icons-material/Close";
17 | import { Case, User } from "../../types";
18 | import InvestigatorSelect from "../InvestigatorSelect";
19 | import { useSnackbar } from "../SnackbarProvider";
20 | import { AxiosError } from "axios";
21 | const BootstrapDialog = styled(Dialog)(({ theme }) => ({
22 | "& .MuiDialogContent-root": {
23 | padding: theme.spacing(2),
24 | },
25 | "& .MuiDialogActions-root": {
26 | padding: theme.spacing(1),
27 | },
28 | }));
29 |
30 | interface AddCaseDialogProps {
31 | open: boolean;
32 | onClose: () => void;
33 | onCreateSuccess: (newCase: Case) => void;
34 | }
35 |
36 | const AddCaseDialog: React.FC = ({
37 | open,
38 | onClose,
39 | onCreateSuccess,
40 | }) => {
41 | const { display_message } = useSnackbar();
42 | const [name, setName] = useState("");
43 | const [description, setDescription] = useState("");
44 | const [selectedUsers, setSelectedUsers] = useState([]);
45 |
46 | const handleCreate = async () => {
47 | const requestData = {
48 | name,
49 | description,
50 | linked_users: selectedUsers.map((user) => user.id),
51 | };
52 | try {
53 | const response = await axiosInstance.post("/api/cases/", requestData);
54 | if (response.status === 201) {
55 | onCreateSuccess(response.data);
56 | display_message("success", "Case created.");
57 | onClose();
58 | setName("");
59 | setDescription("");
60 | setSelectedUsers([]);
61 | }
62 | } catch (error) {
63 | display_message(
64 | "error",
65 | `Case could not be created: ${
66 | error instanceof AxiosError && error.response?.status === 409
67 | ? "A case with this name already exists"
68 | : error
69 | }`,
70 | );
71 | }
72 | };
73 |
74 | return (
75 |
82 |
83 | Create a case
84 |
85 |
86 | The case will contain all of the information about your
87 | investigation.
88 |
89 |
90 |
91 |
92 |
93 | theme.palette.grey[500],
101 | }}
102 | >
103 |
104 |
105 |
106 |
107 | setName(e.target.value)}
120 | />
121 |
122 |
123 | setDescription(e.target.value)}
134 | />
135 |
136 |
140 |
141 |
142 |
143 | Create
144 |
145 |
146 |
147 | );
148 | };
149 |
150 | interface AddCaseDialogProps {
151 | open: boolean;
152 | onClose: () => void;
153 | onCreateSuccess: (newCase: Case) => void;
154 | }
155 |
156 | export default AddCaseDialog;
157 |
--------------------------------------------------------------------------------
/frontend/src/components/Explore/Linux/ProcessGraph.tsx:
--------------------------------------------------------------------------------
1 | import { FC, useMemo, useState } from "react";
2 | import {
3 | SigmaContainer,
4 | FullScreenControl,
5 | ZoomControl,
6 | } from "@react-sigma/core";
7 | import "@react-sigma/core/lib/style.css";
8 | import { Settings } from "sigma/settings";
9 | import { NodeDisplayData, PartialButFor } from "sigma/types";
10 | import GraphDataController from "./GraphDataController";
11 | import GraphEventsController from "./GraphEventsController";
12 | import { EnrichedProcessData, LinuxProcessInfo } from "../../../types";
13 | import {
14 | CenterFocusWeak,
15 | ZoomIn,
16 | ZoomInMap,
17 | ZoomOut,
18 | ZoomOutMap,
19 | } from "@mui/icons-material";
20 | import Grid from "@mui/material/Grid";
21 | import { Box } from "@mui/material";
22 | import ProcessDetails from "./ProcessDetails";
23 | import FilteredPlugins from "./FilteredPlugins";
24 |
25 | function drawLabel(
26 | context: CanvasRenderingContext2D,
27 | data: PartialButFor,
28 | settings: Settings,
29 | ): void {
30 | if (!data.label) return;
31 |
32 | const size = settings.labelSize || 12;
33 | const font = settings.labelFont || "Roboto";
34 | const weight = settings.labelWeight || "normal";
35 |
36 | context.font = `${weight} ${size}px ${font}`;
37 | const width = context.measureText(data.label).width + 8;
38 |
39 | context.fillStyle = "#ffffffcc";
40 | context.fillRect(data.x + data.size, data.y + size / 3 - 15, width, 20);
41 |
42 | context.fillStyle = "#000";
43 | context.fillText(data.label, data.x + data.size + 3, data.y + size / 3);
44 | }
45 |
46 | interface ProcessGraphProps {
47 | data: LinuxProcessInfo[];
48 | }
49 |
50 | const ProcessGraph: FC = ({ data }) => {
51 | const [selectedProcess, setSelectedProcess] =
52 | useState(null);
53 |
54 | const [show, setShow] = useState(false);
55 |
56 | const [enrichedData, setEnrichedData] = useState(
57 | null,
58 | );
59 |
60 | const sigmaSettings: Partial = useMemo(
61 | () => ({
62 | defaultDrawNodeLabel: drawLabel,
63 | defaultDrawNodeHover: drawLabel,
64 | defaultEdgeType: "arrow",
65 | renderEdgeLabels: true,
66 | labelDensity: 0.07,
67 | labelGridCellSize: 60,
68 | labelRenderedSizeThreshold: 1,
69 | labelFont: "Lato, sans-serif",
70 | zIndex: true,
71 | }),
72 | [],
73 | );
74 |
75 | const sigmaStyle = {
76 | height: "80vh",
77 | width: "100%",
78 | backgroundColor: "#121212",
79 | };
80 |
81 | return (
82 |
83 |
84 |
85 |
90 |
91 |
95 | {data && (
96 | <>
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 | {selectedProcess && (
111 |
122 | )}
123 | {selectedProcess && show && (
124 |
133 | )}
134 | >
135 | )}
136 |
137 |
138 |
139 |
140 | );
141 | };
142 |
143 | export default ProcessGraph;
144 |
--------------------------------------------------------------------------------
/frontend/src/components/Explore/Windows/ProcessGraph.tsx:
--------------------------------------------------------------------------------
1 | import { FC, useMemo, useState } from "react";
2 | import {
3 | SigmaContainer,
4 | FullScreenControl,
5 | ZoomControl,
6 | } from "@react-sigma/core";
7 | import "@react-sigma/core/lib/style.css";
8 | import { Settings } from "sigma/settings";
9 | import { NodeDisplayData, PartialButFor } from "sigma/types";
10 | import GraphDataController from "./GraphDataController";
11 | import GraphEventsController from "./GraphEventsController";
12 | import { ProcessInfo, EnrichedProcessData } from "../../../types";
13 | import {
14 | CenterFocusWeak,
15 | ZoomIn,
16 | ZoomInMap,
17 | ZoomOut,
18 | ZoomOutMap,
19 | } from "@mui/icons-material";
20 | import Grid from "@mui/material/Grid";
21 | import { Box } from "@mui/material";
22 | import ProcessDetails from "./ProcessDetails";
23 | import FilteredPlugins from "./FilteredPlugins";
24 |
25 | function drawLabel(
26 | context: CanvasRenderingContext2D,
27 | data: PartialButFor,
28 | settings: Settings,
29 | ): void {
30 | if (!data.label) return;
31 |
32 | const size = settings.labelSize || 12;
33 | const font = settings.labelFont || "Roboto";
34 | const weight = settings.labelWeight || "normal";
35 |
36 | context.font = `${weight} ${size}px ${font}`;
37 | const width = context.measureText(data.label).width + 8;
38 |
39 | context.fillStyle = "#ffffffcc";
40 | context.fillRect(data.x + data.size, data.y + size / 3 - 15, width, 20);
41 |
42 | context.fillStyle = "#000";
43 | context.fillText(data.label, data.x + data.size + 3, data.y + size / 3);
44 | }
45 |
46 | interface ProcessGraphProps {
47 | data: ProcessInfo[];
48 | }
49 |
50 | const ProcessGraph: FC = ({ data }) => {
51 | const [selectedProcess, setSelectedProcess] = useState(
52 | null,
53 | );
54 |
55 | const [show, setShow] = useState(false);
56 |
57 | const [enrichedData, setEnrichedData] = useState(
58 | null,
59 | );
60 |
61 | const sigmaSettings: Partial = useMemo(
62 | () => ({
63 | defaultDrawNodeLabel: drawLabel,
64 | defaultDrawNodeHover: drawLabel,
65 | defaultEdgeType: "arrow",
66 | renderEdgeLabels: true,
67 | labelDensity: 0.07,
68 | labelGridCellSize: 60,
69 | labelRenderedSizeThreshold: 1,
70 | labelFont: "Lato, sans-serif",
71 | zIndex: true,
72 | }),
73 | [],
74 | );
75 |
76 | const sigmaStyle = {
77 | height: "80vh",
78 | width: "100%",
79 | backgroundColor: "#121212",
80 | };
81 |
82 | return (
83 |
84 |
85 |
86 |
91 |
92 |
96 | {data && (
97 | <>
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 | {selectedProcess && (
112 |
123 | )}
124 | {selectedProcess && show && (
125 |
134 | )}
135 | >
136 | )}
137 |
138 |
139 |
140 |
141 | );
142 | };
143 |
144 | export default ProcessGraph;
145 |
--------------------------------------------------------------------------------
/backend/volatility_engine/tasks.py:
--------------------------------------------------------------------------------
1 | from celery import shared_task
2 | from evidences.models import Evidence
3 | from volatility_engine.engine import VolatilityEngine
4 | from channels.layers import get_channel_layer
5 | from asgiref.sync import async_to_sync
6 |
7 |
8 | @shared_task(name="VolWeb.Engine")
9 | def start_extraction(evidence_id):
10 | """
11 | This task will extract all the artefacts using different plugins
12 | """
13 | instance = Evidence.objects.get(id=evidence_id)
14 | engine = VolatilityEngine(instance)
15 | instance.status = 0
16 | instance.save()
17 | engine.start_extraction()
18 | if instance.status != -1:
19 | instance.status = 100
20 | instance.save()
21 |
22 |
23 | @shared_task
24 | def start_timeliner(evidence_id):
25 | """
26 | This task is dedicated to generate the timeline.
27 | We seperate this because this could take a very long time depending on the memory dump.
28 | """
29 | instance = Evidence.objects.get(id=evidence_id)
30 | channel_layer = get_channel_layer()
31 | engine = VolatilityEngine(instance)
32 | result = engine.start_timeliner()
33 | if result:
34 | async_to_sync(channel_layer.group_send)(
35 | f"volatility_tasks_{evidence_id}",
36 | {
37 | "type": "send_notification",
38 | "message": {
39 | "name": "timeliner",
40 | "status": "finished",
41 | "result": "true",
42 | },
43 | },
44 | )
45 | else:
46 | async_to_sync(channel_layer.group_send)(
47 | f"volatility_tasks_{evidence_id}",
48 | {
49 | "type": "send_notification",
50 | "message": {
51 | "name": "timeliner",
52 | "status": "finished",
53 | "result": "false",
54 | },
55 | },
56 | )
57 |
58 |
59 | @shared_task
60 | def dump_process(evidence_id, pid):
61 | """
62 | This task is dedicated to performing a pslist dump.
63 | """
64 | channel_layer = get_channel_layer()
65 | instance = Evidence.objects.get(id=evidence_id)
66 | engine = VolatilityEngine(instance)
67 | result = engine.dump_process(pid)
68 | async_to_sync(channel_layer.group_send)(
69 | f"volatility_tasks_{evidence_id}",
70 | {
71 | "type": "send_notification",
72 | "message": {
73 | "name": "dump",
74 | "pid": pid,
75 | "status": "finished",
76 | "result": result,
77 | },
78 | },
79 | )
80 |
81 |
82 | @shared_task
83 | def dump_windows_handles(evidence_id, pid):
84 | """
85 | This task is dedicated to compute the handles for a specific process.
86 | """
87 | instance = Evidence.objects.get(id=evidence_id)
88 | channel_layer = get_channel_layer()
89 | engine = VolatilityEngine(instance)
90 | engine.compute_handles(pid)
91 | async_to_sync(channel_layer.group_send)(
92 | f"volatility_tasks_{evidence_id}",
93 | {
94 | "type": "send_notification",
95 | "message": {
96 | "name": "handles",
97 | "pid": pid,
98 | "status": "finished",
99 | "msg": "Message",
100 | },
101 | },
102 | )
103 |
104 |
105 | @shared_task
106 | def dump_file(evidence_id, offset):
107 | """
108 | This task is dedicated for trying to dump a file at a specific memory offset.
109 | """
110 | instance = Evidence.objects.get(id=evidence_id)
111 | channel_layer = get_channel_layer()
112 | engine = VolatilityEngine(instance)
113 | if instance.os == "windows":
114 | result = engine.dump_file_windows(offset)
115 | else:
116 | result = engine.dump_file_linux(offset)
117 | async_to_sync(channel_layer.group_send)(
118 | f"volatility_tasks_{evidence_id}",
119 | {
120 | "type": "send_notification",
121 | "message": {
122 | "name": "file_dump",
123 | "status": "finished",
124 | "result": result,
125 | },
126 | },
127 | )
128 |
129 |
130 | @shared_task
131 | def dump_maps(evidence_id, pid):
132 | """
133 | This task is dedicated to compute the maps for a specific process.
134 | """
135 | instance = Evidence.objects.get(id=evidence_id)
136 | channel_layer = get_channel_layer()
137 | engine = VolatilityEngine(instance)
138 | result = engine.dump_process_maps(pid)
139 | async_to_sync(channel_layer.group_send)(
140 | f"volatility_tasks_{evidence_id}",
141 | {
142 | "type": "send_notification",
143 | "message": {
144 | "name": "maps",
145 | "pid": pid,
146 | "status": "finished",
147 | "result": result,
148 | },
149 | },
150 | )
151 |
--------------------------------------------------------------------------------
/frontend/src/components/Explore/Windows/GraphEventsController.tsx:
--------------------------------------------------------------------------------
1 | import { FC, useEffect, useRef, useCallback } from "react";
2 | import { useSigma, useRegisterEvents } from "@react-sigma/core";
3 | import Graph from "graphology";
4 | import { ProcessInfo } from "../../../types";
5 | import FA2Layout from "graphology-layout-forceatlas2/worker";
6 |
7 | interface GraphEventsControllerProps {
8 | data: ProcessInfo[];
9 | onProcessSelect: (process: ProcessInfo | null) => void;
10 | }
11 |
12 | const GraphEventsController: FC = ({
13 | data,
14 | onProcessSelect,
15 | }) => {
16 | const sigma = useSigma();
17 | const graph = sigma.getGraph();
18 | const registerEvents = useRegisterEvents();
19 |
20 | const isDragging = useRef(false);
21 | const draggedNode = useRef(null);
22 |
23 | const findProcessByPID = useCallback(
24 | (data: ProcessInfo[], pid: number): ProcessInfo | null => {
25 | for (const process of data) {
26 | if (process.PID === pid) {
27 | return process;
28 | } else if (process.__children) {
29 | const result = findProcessByPID(process.__children, pid);
30 | if (result) return result;
31 | }
32 | }
33 | return null;
34 | },
35 | [],
36 | );
37 |
38 | const expandNode = useCallback(
39 | (process: ProcessInfo, graph: Graph) => {
40 | const parentId = process.PID.toString();
41 |
42 | const parentAttributes = graph.getNodeAttributes(parentId);
43 | const { x: px, y: py } = parentAttributes;
44 |
45 | const children = process.__children || [];
46 | const N = children.length;
47 |
48 | children.forEach((child, i) => {
49 | const childId = child.PID.toString();
50 |
51 | const angle = (i * 2 * Math.PI) / N;
52 | const distance = 50;
53 | const x = px + distance * Math.cos(angle);
54 | const y = py + distance * Math.sin(angle);
55 |
56 | if (!graph.hasNode(childId)) {
57 | graph.addNode(childId, {
58 | label: `${child.ImageFileName || "Unknown"} - ${child.PID} (${child.__children.length})`,
59 | size: 5,
60 | color:
61 | child.__children.length > 0
62 | ? "#ce93d8"
63 | : child.anomalies && child.anomalies.length > 0
64 | ? "#ffa726"
65 | : "#FFFFFF",
66 | x: x,
67 | y: y,
68 | });
69 | }
70 |
71 | if (!graph.hasEdge(parentId, childId)) {
72 | graph.addEdge(parentId, childId, {
73 | label: "Created",
74 | size: 1,
75 | color: "#fff",
76 | type: "arrow",
77 | });
78 | }
79 | });
80 |
81 | sigma.refresh();
82 | },
83 | [sigma],
84 | );
85 |
86 | useEffect(() => {
87 | const handleClickNode = ({ node }: { node: string }) => {
88 | const nodeAttributes = graph.getNodeAttributes(node);
89 |
90 | // Retrieve the process data for this node
91 | const pid = parseInt(node);
92 | const process = findProcessByPID(data, pid);
93 | if (process) {
94 | onProcessSelect(process); // Notify parent component
95 | }
96 |
97 | // Check if node has been expanded already
98 | if (!nodeAttributes.expanded) {
99 | if (process) {
100 | expandNode(process, graph);
101 | graph.setNodeAttribute(node, "expanded", true);
102 | sigma.refresh();
103 |
104 | // Run ForceSupervisor layout after expanding node
105 | const layout = new FA2Layout(graph, {
106 | settings: { gravity: 0.5 },
107 | });
108 | layout.start();
109 | setTimeout(() => {
110 | layout.stop();
111 | layout.kill();
112 | }, 100);
113 | }
114 | }
115 | };
116 |
117 | const handleDownNode = ({ node }: { node: string }) => {
118 | isDragging.current = true;
119 | draggedNode.current = node;
120 | graph.setNodeAttribute(node, "highlighted", true);
121 | };
122 |
123 | const handleMouseMove = (event: { x: number; y: number }) => {
124 | if (isDragging.current && draggedNode.current) {
125 | const coords = sigma.viewportToGraph({ x: event.x, y: event.y });
126 | graph.setNodeAttribute(draggedNode.current, "x", coords.x);
127 | graph.setNodeAttribute(draggedNode.current, "y", coords.y);
128 | sigma.refresh();
129 | }
130 | };
131 |
132 | const handleMouseUp = () => {
133 | if (isDragging.current && draggedNode.current) {
134 | isDragging.current = false;
135 | graph.removeNodeAttribute(draggedNode.current, "highlighted");
136 | draggedNode.current = null;
137 | }
138 | };
139 |
140 | registerEvents({
141 | clickNode: handleClickNode,
142 | downNode: handleDownNode,
143 | mousemove: handleMouseMove,
144 | mouseup: handleMouseUp,
145 | });
146 | }, [
147 | data,
148 | graph,
149 | sigma,
150 | registerEvents,
151 | onProcessSelect,
152 | expandNode,
153 | findProcessByPID,
154 | ]);
155 |
156 | return null;
157 | };
158 |
159 | export default GraphEventsController;
160 |
--------------------------------------------------------------------------------
/frontend/src/components/Explore/Linux/GraphEventsController.tsx:
--------------------------------------------------------------------------------
1 | import { FC, useEffect, useRef, useCallback } from "react";
2 | import { useSigma, useRegisterEvents } from "@react-sigma/core";
3 | import Graph from "graphology";
4 | import { LinuxProcessInfo } from "../../../types";
5 | import FA2Layout from "graphology-layout-forceatlas2/worker";
6 |
7 | interface GraphEventsControllerProps {
8 | data: LinuxProcessInfo[];
9 | onProcessSelect: (process: LinuxProcessInfo | null) => void;
10 | }
11 |
12 | const GraphEventsController: FC = ({
13 | data,
14 | onProcessSelect,
15 | }) => {
16 | const sigma = useSigma();
17 | const graph = sigma.getGraph();
18 | const registerEvents = useRegisterEvents();
19 |
20 | const isDragging = useRef(false);
21 | const draggedNode = useRef(null);
22 |
23 | const findProcessByPID = useCallback(
24 | (data: LinuxProcessInfo[], pid: number): LinuxProcessInfo | null => {
25 | for (const process of data) {
26 | if (process.PID === pid) {
27 | return process;
28 | } else if (process.__children) {
29 | const result = findProcessByPID(process.__children, pid);
30 | if (result) return result;
31 | }
32 | }
33 | return null;
34 | },
35 | [],
36 | );
37 |
38 | const expandNode = useCallback(
39 | (process: LinuxProcessInfo, graph: Graph) => {
40 | const parentId = process.PID.toString();
41 |
42 | const parentAttributes = graph.getNodeAttributes(parentId);
43 | const { x: px, y: py } = parentAttributes;
44 |
45 | const children = process.__children || [];
46 | const N = children.length;
47 |
48 | children.forEach((child, i) => {
49 | const childId = child.PID.toString();
50 |
51 | const angle = (i * 2 * Math.PI) / N;
52 | const distance = 50;
53 | const x = px + distance * Math.cos(angle);
54 | const y = py + distance * Math.sin(angle);
55 |
56 | if (!graph.hasNode(childId)) {
57 | graph.addNode(childId, {
58 | label: `${child.COMM || "Unknown"} - ${child.PID} (${child.__children.length})`,
59 | size: 5,
60 | color:
61 | child.__children.length > 0
62 | ? "#ce93d8"
63 | : child.anomalies && child.anomalies.length > 0
64 | ? "#ffa726"
65 | : "#FFFFFF",
66 | x: x,
67 | y: y,
68 | });
69 | }
70 |
71 | if (!graph.hasEdge(parentId, childId)) {
72 | graph.addEdge(parentId, childId, {
73 | label: "Created",
74 | size: 1,
75 | color: "#fff",
76 | type: "arrow",
77 | });
78 | }
79 | });
80 |
81 | sigma.refresh();
82 | },
83 | [sigma],
84 | );
85 |
86 | useEffect(() => {
87 | const handleClickNode = ({ node }: { node: string }) => {
88 | const nodeAttributes = graph.getNodeAttributes(node);
89 |
90 | // Retrieve the process data for this node
91 | const pid = parseInt(node);
92 | const process = findProcessByPID(data, pid);
93 | if (process) {
94 | onProcessSelect(process); // Notify parent component
95 | }
96 |
97 | // Check if node has been expanded already
98 | if (!nodeAttributes.expanded) {
99 | if (process) {
100 | expandNode(process, graph);
101 | graph.setNodeAttribute(node, "expanded", true);
102 | sigma.refresh();
103 |
104 | // Run ForceSupervisor layout after expanding node
105 | const layout = new FA2Layout(graph, {
106 | settings: { gravity: 0.5 },
107 | });
108 | layout.start();
109 | setTimeout(() => {
110 | layout.stop();
111 | layout.kill();
112 | }, 100);
113 | }
114 | }
115 | };
116 |
117 | const handleDownNode = ({ node }: { node: string }) => {
118 | isDragging.current = true;
119 | draggedNode.current = node;
120 | graph.setNodeAttribute(node, "highlighted", true);
121 | };
122 |
123 | const handleMouseMove = (event: { x: number; y: number }) => {
124 | if (isDragging.current && draggedNode.current) {
125 | const coords = sigma.viewportToGraph({ x: event.x, y: event.y });
126 | graph.setNodeAttribute(draggedNode.current, "x", coords.x);
127 | graph.setNodeAttribute(draggedNode.current, "y", coords.y);
128 | sigma.refresh();
129 | }
130 | };
131 |
132 | const handleMouseUp = () => {
133 | if (isDragging.current && draggedNode.current) {
134 | isDragging.current = false;
135 | graph.removeNodeAttribute(draggedNode.current, "highlighted");
136 | draggedNode.current = null;
137 | }
138 | };
139 |
140 | registerEvents({
141 | clickNode: handleClickNode,
142 | downNode: handleDownNode,
143 | mousemove: handleMouseMove,
144 | mouseup: handleMouseUp,
145 | });
146 | }, [
147 | data,
148 | graph,
149 | sigma,
150 | registerEvents,
151 | onProcessSelect,
152 | expandNode,
153 | findProcessByPID,
154 | ]);
155 |
156 | return null;
157 | };
158 |
159 | export default GraphEventsController;
160 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | # Introduction
7 |
8 | VolWeb is a digital forensic memory analysis platform that leverages the power of the Volatility 3 framework.
9 | It is dedicated to aiding in investigations and incident responses.
10 |
11 | ## 🧬 Objectives
12 |
13 | The goal of VolWeb is to enhance the efficiency of memory collection and forensic analysis by providing a centralized, visual, and enhanced web application for incident responders and digital forensics investigators.
14 | Once an investigator obtains a memory image from a Linux or Windows system (Mac coming soon), the evidence can be uploaded to VolWeb, which triggers automatic processing and extraction of artifacts using the power of the Volatility 3 framework.
15 |
16 | By utilizing hybrid storage technologies, VolWeb also enables incident responders to directly upload memory images into the VolWeb platform from various locations using dedicated scripts interfaced with the platform and maintained by the community.
17 | Another goal is to allow users to compile technical information, such as Indicators, which can later be imported into modern CTI platforms like OpenCTI, thereby connecting your incident response and CTI teams after your investigation.
18 |
19 | # 📘 Project Documentation and Getting Started Guide
20 |
21 | The project documentation is available on the Wiki .
22 | There, you will be able to deploy the tool in your investigation environment or lab.
23 |
24 | >[!IMPORTANT]
25 | > Take time to read the documentation in order to avoid common miss-configuration issues.
26 |
27 | # Analysis features
28 | A quick disclaimer: VolWeb is meant to be use in conjunction with the volatility3 framework CLI,
29 | it offers a different way to review & investigate some of the results and will not do all of the deep dive analysis job for you.
30 |
31 | ## 💿 Hybrid storage solution
32 |
33 | Your evidences are uploaded to the VolWeb plateform and is using filesystem analyse by default for having the best performances. But you can also bind evidences from a cloud storage solution (AWS/MINIO) and bind them to your cases in order to perform the analysis directly on the cloud.
34 |
35 | ## 🔬 Investigate
36 |
37 | The investigate feature is one of the core feature of VolWeb.
38 | It provides an overview of the available artefacts that were retrived by the custom volatiltiy3 engine in the backend.
39 | If available, you can visualize the process tree and get basic information about each process, dump them etc...
40 | You also get a enhanced view of all of the plugins results by categories.
41 |
42 |
43 |
44 |
45 | ## ፨ Explore
46 | « _Defenders think in lists. Attackers think in graphs. As long as this is true, attackers win._ »
47 |
48 | The explore feature comes with VolWeb 3.0 for Windows investigations (coming soon for Linux).
49 | It enable the memory forensics expert to investigate potential suspicious processes in a graph view allowing another way to look at the data, but also correlate the volatility3 plugins to get more context.
50 |
51 |
52 |
53 | ## 🚨 Capitalize and share STIX V2 Indicators
54 |
55 | When the expert found malicious activies, VolWeb give you the possibility to create STIX V2 Indicators directly from the interface and centralize them in your case.
56 | Once your case is closed, you can generate you STIX bundle and share your Indicators with your community using CTI Platforms like MISP or OpenCTI.
57 |
58 |
59 |
60 |
61 | ## 🪡 Interacting with the REST API
62 |
63 | VolWeb exposes a REST API to allow analysts to interact with the platform. A swagger is available on the platform in oder to get the full documentation.
64 | There is a dedicated repository proposing some scripts maintained by the community: https://github.com/forensicxlab/VolWeb-Scripts .
65 |
66 |
67 |
68 | ## Administration
69 |
70 | VolWeb is using django in the backend. Manage your user and database directly from the admin panel.
71 |
72 |
73 |
74 | # 👔 Issues & Feature request
75 |
76 | If you have encountered a bug, or wish to propose a feature, please feel free to create a [discussion](https://github.com/k1nd0ne/VolWeb/discussions) to enable us to quickly address them. Please provide logs to any issues you are facing.
77 |
78 |
79 | # 🤘 Contributing
80 |
81 | VolWeb is open to contributions. Follow the contributing guideline in the documentation to propose features.
82 |
83 | # Contact
84 |
85 | Contact me at k1nd0ne@mail.com for any questions regarding this tool.
86 |
87 | # Next Release Goals
88 |
89 | Check out the [roadmap](https://github.com/users/k1nd0ne/projects/2)
90 |
91 | Check out the [discussions](https://github.com/k1nd0ne/VolWeb/discussions)
92 |
--------------------------------------------------------------------------------
/frontend/src/components/EvidenceMetadata.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import axiosInstance from "../utils/axiosInstance";
3 | import ReactApexChart from "react-apexcharts";
4 | import { ApexOptions } from "apexcharts";
5 | import { Box, CircularProgress } from "@mui/material";
6 | import Grid from "@mui/material/Grid";
7 | import PluginList from "./Lists/PluginList";
8 |
9 | interface EvidenceMetadataProps {
10 | evidenceId?: string;
11 | theme: "light" | "dark";
12 | }
13 |
14 | const EvidenceMetadata: React.FC = ({
15 | evidenceId,
16 | theme,
17 | }) => {
18 | const [data, setData] = useState<{
19 | categories: Record;
20 | total_ran: number;
21 | total_results: number;
22 | } | null>(null);
23 |
24 | useEffect(() => {
25 | const fetchData = async () => {
26 | if (!evidenceId) return;
27 | try {
28 | const response = await axiosInstance.get(
29 | `/api/evidence-statistics/${evidenceId}/`,
30 | );
31 | setData(response.data);
32 | } catch (error) {
33 | console.error("Error fetching data:", error);
34 | }
35 | };
36 |
37 | fetchData();
38 | }, [evidenceId]);
39 |
40 | if (!data) {
41 | return ;
42 | }
43 |
44 | const categories = Object.keys(data.categories);
45 | const counts = Object.values(data.categories);
46 |
47 | const colors = ["#790909", "#670979", "#097979", "#097907"];
48 | const gradientToColors = ["#790909", "#670979", "#097979", "#097907"];
49 |
50 | const radarOptions: ApexOptions = {
51 | theme: {
52 | mode: "dark" as const,
53 | palette: "palette1",
54 | },
55 | chart: {
56 | type: "radar",
57 | background: "transparent",
58 | foreColor: theme !== "dark" ? "#121212" : "#fff",
59 | },
60 | xaxis: {
61 | categories: categories,
62 | },
63 | title: {
64 | text: "Results by Category",
65 | style: {
66 | color: theme !== "dark" ? "#101418" : "#fff",
67 | },
68 | },
69 | fill: {
70 | opacity: 0.1,
71 | colors: colors,
72 | },
73 | stroke: {
74 | colors: colors,
75 | },
76 | markers: {
77 | size: 6,
78 | colors: colors,
79 | },
80 | legend: {
81 | labels: {
82 | colors: theme !== "dark" ? "#101418" : "#fff",
83 | },
84 | markers: {
85 | fillColors: colors,
86 | },
87 | },
88 | };
89 |
90 | const radarSeries = [
91 | {
92 | name: "Results",
93 | data: counts,
94 | },
95 | ];
96 |
97 | const totalNoResults = data.total_ran - data.total_results;
98 |
99 | const donutOptions: ApexOptions = {
100 | labels: ["Plugins with Results", "Plugins without Results"],
101 | chart: {
102 | type: "donut",
103 | background: "transparent",
104 | foreColor: theme !== "dark" ? "#121212" : "#fff",
105 | },
106 | plotOptions: {
107 | pie: {
108 | startAngle: -90,
109 | endAngle: 270,
110 | },
111 | },
112 | dataLabels: {
113 | enabled: true,
114 | },
115 | fill: {
116 | type: "gradient",
117 | gradient: {
118 | gradientToColors: gradientToColors.slice(0, 2),
119 | },
120 | colors: colors.slice(0, 2),
121 | },
122 | legend: {
123 | formatter: function (
124 | val: string,
125 | opts: {
126 | seriesIndex: number;
127 | w: {
128 | globals: {
129 | series: number[];
130 | };
131 | };
132 | },
133 | ): string {
134 | return `${val} - ${opts.w.globals.series[opts.seriesIndex]}`;
135 | },
136 | labels: {
137 | colors: theme !== "dark" ? "#101418" : "#fff",
138 | },
139 | markers: {
140 | fillColors: colors.slice(0, 2),
141 | },
142 | },
143 | title: {
144 | text: "Plugins Ran vs Results",
145 | style: {
146 | color: theme !== "dark" ? "#101418" : "#fff",
147 | },
148 | },
149 | responsive: [
150 | {
151 | breakpoint: 480,
152 | options: {
153 | chart: {
154 | width: 100,
155 | },
156 | legend: {
157 | position: "bottom",
158 | },
159 | },
160 | },
161 | ],
162 | };
163 |
164 | const donutSeries: number[] = [data.total_results, totalNoResults];
165 |
166 | return (
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
184 |
185 |
186 |
191 |
192 |
193 |
194 |
195 |
196 | );
197 | };
198 |
199 | export default EvidenceMetadata;
200 |
--------------------------------------------------------------------------------