├── 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 | 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 | 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 | 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 | 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 | 79 | (window.location.href = "/swagger/")}> 80 | Swagger 81 | 82 | (window.location.href = "/admin/")}> 83 | Administration 84 | 85 | Logout 86 | 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 | 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 | 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 | 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 | 98 | 99 | 100 | {/* Add Description Field */} 101 | setDescription(e.target.value)} 109 | /> 110 | 111 | 119 | {file &&
Selected File: {file.name}
} 120 | {error &&
{error}
} 121 |
122 | 123 | 126 | 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 | 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 | 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 |
112 |
113 | 120 |
121 |
122 | )} 123 | {selectedProcess && show && ( 124 |
125 |
126 | 131 |
132 |
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 |
113 |
114 | 121 |
122 |
123 | )} 124 | {selectedProcess && show && ( 125 |
126 |
127 | 132 |
133 |
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 | VolWeb 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 | image 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 | image 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 | image 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 | image 67 | 68 | ## Administration 69 | 70 | VolWeb is using django in the backend. Manage your user and database directly from the admin panel. 71 | 72 | image 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 | --------------------------------------------------------------------------------