55 Commits
v1.1 ... master

Author SHA1 Message Date
dae4f32293 Refactor audio playback logic in FakeAdminResponder to ensure that audio plays on start
All checks were successful
Build and push image / docker-build (push) Successful in 4m55s
2025-12-10 23:17:34 +03:00
f2a297f77b Add audio playback functionality to FakeAdminResponder
All checks were successful
Build and push image / docker-build (push) Successful in 3m53s
2025-12-10 23:03:13 +03:00
dan
48f4aa761b Обновить docker-compose.yml
All checks were successful
Build and push image / docker-build (push) Successful in 10s
2025-12-10 20:28:11 +03:00
dan
50ef41d612 Обновить .gitea/workflows/build.yml
All checks were successful
Build and push image / docker-build (push) Successful in 4m22s
2025-12-10 20:21:46 +03:00
Dan
104acc6486 new image name
Some checks failed
Build and push image / docker-build (push) Failing after 4s
2025-12-10 19:42:23 +03:00
Dan
c590c02409 New images
Some checks failed
Build and push image / docker-build (push) Failing after 13s
2025-12-10 19:37:55 +03:00
cbff26abc8 add .env.example for easier deployment
Some checks failed
Build and push image / docker-build (push) Failing after 2s
2025-12-10 16:16:19 +00:00
49244b334a Remove GitLab CI configuration and update Gitea workflow to use 'master' branch and specify Dockerfile path.
Some checks failed
Build and push image / docker-build (push) Failing after 12s
2025-12-10 15:32:56 +00:00
Dan
2c759980c8 migrate to selfhost 2025-12-10 02:49:57 +03:00
dan
5b84397660 Allow raw capture in compose 2025-12-06 21:47:07 +03:00
dan
c8729c8278 Ignore redesign refs in docker build 2025-12-06 21:30:32 +03:00
dan
3734e21d0a Align pixel checkboxes inside boxes 2025-12-06 21:05:10 +03:00
dan
eead1ebfb3 Use clean pixel checkmarks 2025-12-06 20:09:39 +03:00
dan
edcf2a0987 Pixel checkboxes, button spacing, and use fork image 2025-12-06 20:07:10 +03:00
dan
f52d68300c Restructure fake packets layout 2025-12-06 19:43:42 +03:00
dan
096628b25f Restyle fake UI and inputs, hide scrollbars 2025-12-06 19:34:00 +03:00
dan
2ea0140cf6 Lock Patterns dropdown width 2025-12-06 18:56:41 +03:00
dan
99267f781f Refine decoy UI text and lock dark theme 2025-12-06 18:46:47 +03:00
dan
5703a7230d Deepen neon palette and fix fun decoy rendering 2025-12-06 18:33:24 +03:00
dan
9dfc70f61e Tighten branding and decoy fixes 2025-12-06 18:14:25 +03:00
dan
c4af8465aa Inline frontend instead of submodule 2025-12-06 17:34:40 +03:00
dan
2d265bb71d Refresh setup docs for the new branding 2025-12-06 17:28:43 +03:00
dan
1b9dd795de Add fake admin decoy and neon redesign 2025-12-06 17:28:23 +03:00
Sergey
938031f1de Use RawHTTP library to process HTTP streams (packmate/Packmate!23) 2023-07-31 15:42:17 +00:00
Sergey
7986658bd1 Update configuration 2023-07-26 18:21:49 +00:00
Sergey
4fed53244d Merge branch 'update-frontend' into 'master'
Update frontend to show packet offsets

See merge request packmate/Packmate!21
2023-07-24 22:30:47 +00:00
Sergey Shkurov
37fd548364 Update frontend to show packet offsets 2023-07-25 02:28:56 +04:00
Sergey Shkurov
fcd7918125 Update frontend to use dark theme 2023-05-01 23:23:11 +02:00
Sergey Shkurov
c88ca8abbd Update frontend 2023-05-01 21:18:26 +02:00
Sergey
15206188a2 Merge branch 'display-stream-size' into 'master'
Display stream size

See merge request packmate/Packmate!20
2023-04-30 22:22:01 +00:00
Sergey
4346445af9 Display stream size 2023-04-30 22:22:01 +00:00
Sergey
f1d67f696d Merge branch 'pattern-updates' into 'master'
Pattern updates

Closes #32

See merge request packmate/Packmate!19
2023-04-30 00:08:15 +00:00
Sergey Shkurov
4b45f7dee7 Update frontend 2023-04-30 01:50:05 +02:00
Sergey Shkurov
a8ee7363d4 Revert adding field 2023-04-29 04:51:57 +02:00
Sergey Shkurov
25d0921aed Update frontend 2023-04-29 04:40:46 +02:00
Sergey Shkurov
73fa5b1373 Add support for pattern updating 2023-04-28 04:08:16 +02:00
Sergey Shkurov
40136ad9d9 Update ServiceController endpoints 2023-04-28 03:59:01 +02:00
Sergey Shkurov
0b50f202fc Move dto transformation into services 2023-04-28 03:27:28 +02:00
Sergey Shkurov
288d24fffc Send pattern ids instead of patterns in streams 2023-04-28 02:02:28 +02:00
Sergey
40b42934b6 Merge branch 'pattern-removal' into 'master'
Implement pattern removal

Closes #29

See merge request packmate/Packmate!18
2023-04-27 23:19:16 +00:00
Sergey
4cd5e72fee Implement pattern removal 2023-04-27 23:19:16 +00:00
Sergey
145f3e63c8 Merge branch 'update-versions' into 'master'
Update versions

See merge request packmate/Packmate!17
2023-04-27 21:22:40 +00:00
Sergey Shkurov
6ea53719fd Remove DISTINCT 2023-04-27 23:19:19 +02:00
Sergey Shkurov
8bbd135e96 Refactor code 2023-04-27 22:35:03 +02:00
Sergey Shkurov
79315c3c18 Update jna dependency for MacOS 2023-04-27 22:35:02 +02:00
Sergey Shkurov
67c5462018 Fix a possible bug 2023-04-27 22:35:02 +02:00
Sergey Shkurov
4e2473a3cc Update libraries 2023-04-27 22:35:02 +02:00
Sergey Shkurov
ea45f1b9e5 Use gradle.kts 2023-04-27 22:35:02 +02:00
Sergey Shkurov
93ec39b561 Prepare to move to gradle.kts 2023-04-27 22:35:02 +02:00
Sergey Shkurov
7878ecebfc Fix hashtag symbols becoming links 2023-04-27 22:35:02 +02:00
Sergey Shkurov
7afb9dc5fb Update Spring Boot 2 2023-04-27 22:35:02 +02:00
Sergey Shkurov
8d33c6a6e1 Update gradle version 2023-04-27 22:35:02 +02:00
Sergey
1b6e619475 Merge branch 'failure-analyzer' into 'master'
Add failure analyzers

Closes #30

See merge request packmate/Packmate!16
2023-04-25 16:19:49 +00:00
Sergey Shkurov
0d756ec39c Add failure analyzer for incorrect interface name 2023-04-25 11:28:28 +02:00
Sergey Shkurov
eef33308a5 Add failure analyzer for incorrect pcap file 2023-04-24 02:20:21 +03:00
124 changed files with 36722 additions and 799 deletions

View File

@@ -6,3 +6,4 @@ docker-compose.yml
Dockerfile_*
data
README*
references_for_redisign

33
.env.example Normal file
View File

@@ -0,0 +1,33 @@
# Default mode - intercept live traffic
PACKMATE_MODE=LIVE
# Alternative modes:
# Parse pcap file:
# PACKMATE_MODE=FILE
# Just show already captured streams:
# PACKMATE_MODE=VIEW
# Local IP on network interface or in pcap file to tell incoming packets from outgoing
PACKMATE_LOCAL_IP=192.168.137.147
# Username for the web interface
PACKMATE_WEB_LOGIN=SomeUser
# Password for the web interface
PACKMATE_WEB_PASSWORD=SomeSecurePassword
# Settings for PACKMATE_MODE=LIVE
# Interface to capture on:
PACKMATE_INTERFACE=eth0
# Settings for PACKMATE_MODE=FILE
# Name of the file in pcaps directory to parse:
# PACKMATE_PCAP_FILE=dump.pcap
# Delete old streams (except for those marked favorite). Recommended to keep enabled
PACKMATE_OLD_STREAMS_CLEANUP_ENABLED=true
# How often to do the cleanup (best to keep the interval low)
PACKMATE_OLD_STREAMS_CLEANUP_INTERVAL=5
# Stream older than this value (in minutes) will be deleted
PACKMATE_OLD_STREAMS_CLEANUP_THRESHOLD=240
# Optional settings:
# PACKMATE_DB_PASSWORD=K604YnL3G1hp2RDkCZNjGpxbyNpNHTRb
# BUILD_TAG=latest

View File

@@ -0,0 +1,41 @@
name: Build and push image
on:
push:
branches:
- master
jobs:
docker-build:
runs-on: [self-hosted, linux, x64, docker]
env:
IMAGE_NAME: cr.danosito.com/0xb00b5/0xb00b5-packmate
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: recursive
- name: Login to registry
run: |
echo "${{ vars.REGISTRY_PASSWORD }}" | docker login cr.danosito.com \
-u "${{ vars.REGISTRY_USER }}" --password-stdin
- name: Build image
env:
IMAGE_TAG: ${{ gitea.sha }}
run: |
docker build \
-f docker/Dockerfile_app \
-t "${IMAGE_NAME}:${IMAGE_TAG}" \
-t "${IMAGE_NAME}:latest" \
.
- name: Push image
env:
IMAGE_TAG: ${{ gitea.sha }}
run: |
docker push "${IMAGE_NAME}:${IMAGE_TAG}"
docker push "${IMAGE_NAME}:latest"

2
.gitignore vendored
View File

@@ -1,4 +1,4 @@
src/main/resources/static/*
references_for_redisign
*.pcap
data

View File

@@ -1,24 +0,0 @@
docker-build:
image: docker:latest
stage: build
variables:
GIT_SUBMODULE_STRATEGY: normal
services:
- docker:dind
before_script:
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
script:
- touch .env
- |
if [[ "$CI_COMMIT_BRANCH" == "$CI_DEFAULT_BRANCH" ]]; then # master
export BUILD_TAG=latest
echo "Running on default branch '$CI_DEFAULT_BRANCH'"
else # tag
export BUILD_TAG="$CI_COMMIT_TAG"
echo "Running on tag = $BUILD_TAG"
fi
- docker compose build
- docker compose push
only:
- master
- tags

3
.gitmodules vendored
View File

@@ -1,3 +0,0 @@
[submodule "frontend"]
path = frontend
url = https://gitlab.com/packmate/packmate-frontend.git

View File

@@ -1,10 +1,10 @@
<div align="center">
# Packmate
# 0xb00b5 team Packmate
</div>
### [[EN](README_EN.md) | RU]
Утилита перехвата и анализа трафика для CTF.
Утилита перехвата и анализа трафика для CTF, переосмысленная в пиксельном неоне.
#### Фичи:
* Поддерживает перехват живого трафика и обработку pcap файлов
@@ -23,27 +23,31 @@
* Разархивирует GZIP в HTTP на лету
* Разархивирует сжатые WebSockets
* Расшифровывает TLS на RSA при наличии приватного ключа
* Обманка для входа `admin:admin` с режимами `fun` и `fake_packets`, чтобы любопытные так и не добрались до настоящих пакетов
![Скриншот главного окна](screenshots/Screenshot.png)
### Обманка admin:admin
Для входа с кредами `admin:admin` добавлена обманка (включена по умолчанию). Управляется через переменные `PACKMATE_FAKE_ADMIN_ENABLED` и `PACKMATE_FAKE_ADMIN_MODE` (`fun` или `fake_packets`) и не дает добраться до настоящего интерфейса.
## Быстрый запуск
Для быстрого запуска Packmate следует использовать [этот стартер](https://gitlab.com/packmate/starter/-/blob/master/README.md).
Для быстрого запуска 0xb00b5 team Packmate следует использовать [этот стартер](https://gitlab.com/packmate/starter/-/blob/master/README.md).
## Полный запуск
Ниже следует инструкция для тех, кто хочет собрать Packmate самостоятельно.
Ниже следует инструкция для тех, кто хочет собрать 0xb00b5 team Packmate самостоятельно.
### Клонирование
Поскольку этот репозиторий содержит фронтенд как git submodule, его необходимо клонировать так:
```bash
git clone --recurse-submodules https://gitlab.com/packmate/Packmate.git
git clone --recurse-submodules https://git.danosito.com/0xb00b5/0xb00b5-packmate.git
# Или, на старых версиях git
git clone --recursive https://gitlab.com/packmate/Packmate.git
git clone --recursive https://git.danosito.com/0xb00b5/0xb00b5-packmate.git
```
Если репозиторий уже был склонирован без подмодулей, необходимо выполнить:
```bash
git pull # Забираем свежую версию мастер-репы из gitlab
git pull # Забираем свежую версию мастер-репы
git submodule update --init --recursive
```
@@ -56,7 +60,7 @@ git submodule update --init --recursive
sudo docker compose up --build -d
```
При успешном запуске Packmate будет видно с любого хоста на порту `65000`.
При успешном запуске 0xb00b5 team Packmate будет видно с любого хоста на порту `65000`.
БД будет слушать на порту 65001, но будет разрешать подключения только с localhost.
## Использование
@@ -64,5 +68,5 @@ sudo docker compose up --build -d
<div align="right">
*desu~*
*@danosito*
</div>

View File

@@ -1,10 +1,10 @@
<div align="center">
# Packmate
# 0xb00b5 team Packmate
</div>
### [EN | [RU](README.md)]
Advanced network traffic flow analyzer for A/D CTFs.
Advanced network traffic flow analyzer for A/D CTFs with a pixel-neon twist.
#### Features:
* Can monitor live traffic or analyze pcap files
@@ -23,22 +23,26 @@ Advanced network traffic flow analyzer for A/D CTFs.
* Can automatically decompress GZIPed HTTP
* Can automatically deflate WebSockets with permessages-deflate extension
* Can automatically decrypt TLS with RSA using given private key (like Wireshark)
* Decoy login for `admin:admin` with `fun` and `fake_packets` modes so snoopers never see the real data
![Main window](screenshots/Screenshot.png)
### Admin:admin decoy
The admin:admin credentials now trigger a decoy (enabled by default). Configure it via `PACKMATE_FAKE_ADMIN_ENABLED` and `PACKMATE_FAKE_ADMIN_MODE` (`fun` or `fake_packets`) to keep everyone away from the real interface.
## Quick Start
To quickly start using Packmate, use [this starter](https://gitlab.com/packmate/starter/-/blob/master/README_EN.md).
To quickly start using 0xb00b5 team Packmate, use [this starter](https://gitlab.com/packmate/starter/-/blob/master/README_EN.md).
## Full Build
Below are the instructions for those who want to build Packmate on their own.
Below are the instructions for those who want to build 0xb00b5 team Packmate on their own.
### Cloning
As this repository contains frontend part as a git submodule, it has to be cloned like this:
```bash
git clone --recurse-submodules https://gitlab.com/packmate/Packmate.git
git clone --recurse-submodules https://git.danosito.com/0xb00b5/0xb00b5-packmate.git
# Or if you have older git
git clone --recursive https://gitlab.com/packmate/Packmate.git
git clone --recursive https://git.danosito.com/0xb00b5/0xb00b5-packmate.git
```
If the repository was already cloned without submodule, just run:
@@ -56,7 +60,7 @@ After filling in env file you can launch the app:
sudo docker-compose up --build -d
```
If everything went fine, Packmate will be available on port `65000` from any host.
If everything went fine, 0xb00b5 team Packmate will be available on port `65000` from any host.
Database with listen on port 65001, but will only accept connections from localhost.
## Usage

View File

@@ -1,50 +0,0 @@
plugins {
id 'org.springframework.boot' version '2.6.3'
id 'java'
}
apply plugin: 'io.spring.dependency-management'
group = 'ru.serega6531'
version = '1.0-SNAPSHOT'
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation "org.springframework.boot:spring-boot-starter-security"
implementation "org.springframework.boot:spring-boot-starter-websocket"
implementation 'org.springframework.session:spring-session-core'
implementation 'com.github.jmnarloch:modelmapper-spring-boot-starter:1.1.0'
implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.12.0'
implementation group: 'commons-io', name: 'commons-io', version: '2.11.0'
implementation 'org.pcap4j:pcap4j-core:1.8.2'
implementation 'org.pcap4j:pcap4j-packetfactory-static:1.8.2'
implementation group: 'com.google.guava', name: 'guava', version: '31.0.1-jre'
implementation group: 'org.java-websocket', name: 'Java-WebSocket', version: '1.5.1'
implementation group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.69'
implementation group: 'org.bouncycastle', name: 'bctls-jdk15on', version: '1.70'
implementation group: 'org.modelmapper', name: 'modelmapper', version: '2.4.5'
compileOnly 'org.jetbrains:annotations:22.0.0'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'org.postgresql:postgresql'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.junit.jupiter:junit-jupiter:5.8.2'
}
test {
useJUnitPlatform()
}

61
build.gradle.kts Normal file
View File

@@ -0,0 +1,61 @@
plugins {
id("org.springframework.boot") version "3.0.6"
id("java")
id("io.spring.dependency-management") version "1.1.0"
}
group = "ru.serega6531"
version = "1.0-SNAPSHOT"
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
configurations {
get("compileOnly").apply {
extendsFrom(configurations.annotationProcessor.get())
}
}
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-websocket")
implementation("org.springframework.session:spring-session-core")
implementation(group = "org.apache.commons", name = "commons-lang3", version = "3.12.0")
implementation(group = "commons-io", name = "commons-io", version = "2.11.0")
implementation("org.pcap4j:pcap4j-core:1.8.2")
implementation("org.pcap4j:pcap4j-packetfactory-static:1.8.2")
constraints {
implementation("net.java.dev.jna:jna:5.13.0") {
because("upgraded version required to run on MacOS")
// https://stackoverflow.com/questions/70368863/unsatisfiedlinkerror-for-m1-macs-while-running-play-server-locally
}
}
implementation(group = "com.google.guava", name = "guava", version = "31.1-jre")
implementation(group = "org.java-websocket", name = "Java-WebSocket", version = "1.5.3")
implementation(group = "org.bouncycastle", name = "bcprov-jdk15on", version = "1.70")
implementation(group = "org.bouncycastle", name = "bctls-jdk15on", version = "1.70")
implementation(group = "org.modelmapper", name = "modelmapper", version = "3.1.1")
implementation("com.athaydes.rawhttp:rawhttp-core:2.5.2")
compileOnly("org.jetbrains:annotations:24.0.1")
compileOnly("org.projectlombok:lombok")
runtimeOnly("org.springframework.boot:spring-boot-devtools")
runtimeOnly("org.postgresql:postgresql")
annotationProcessor("org.projectlombok:lombok")
testImplementation("org.junit.jupiter:junit-jupiter:5.9.2")
}
tasks.getByName<Test>("test") {
useJUnitPlatform()
}

View File

@@ -6,19 +6,25 @@ services:
LOCAL_IP: ${PACKMATE_LOCAL_IP}
MODE: ${PACKMATE_MODE:-LIVE}
PCAP_FILE: ${PACKMATE_PCAP_FILE:-}
WEB_LOGIN: ${PACKMATE_WEB_LOGIN:-BinaryBears}
WEB_PASSWORD: ${PACKMATE_WEB_PASSWORD:-123456}
WEB_LOGIN: ${PACKMATE_WEB_LOGIN:-0xb00b5}
WEB_PASSWORD: ${PACKMATE_WEB_PASSWORD:-87654321}
FAKE_ADMIN_AUTH_ENABLED: ${PACKMATE_FAKE_ADMIN_ENABLED:-true}
FAKE_ADMIN_MODE: ${PACKMATE_FAKE_ADMIN_MODE:-fun}
OLD_STREAMS_CLEANUP_ENABLED: ${PACKMATE_OLD_STREAMS_CLEANUP_ENABLED:-false}
OLD_STREAMS_CLEANUP_INTERVAL: ${PACKMATE_OLD_STREAMS_CLEANUP_INTERVAL:-5}
OLD_STREAMS_CLEANUP_THRESHOLD: ${PACKMATE_OLD_STREAMS_CLEANUP_THRESHOLD:-240}
env_file:
- .env
cap_add:
- NET_ADMIN
- NET_RAW
privileged: true
container_name: packmate-app
build:
context: .
dockerfile: docker/Dockerfile_app
network_mode: "host"
image: registry.gitlab.com/packmate/packmate:${BUILD_TAG:-latest}
image: cr.danosito.com/0xb00b5/0xb00b5-packmate:${BUILD_TAG:-latest}
volumes:
- "./pcaps/:/app/pcaps/:ro"
- "./rsa_keys/:/app/rsa_keys/:ro"

View File

@@ -1,5 +1,6 @@
FROM node:19-alpine
WORKDIR /tmp/build/
RUN apk add --no-cache python3 make g++
COPY ./frontend/ .
RUN export NODE_OPTIONS=--openssl-legacy-provider && npm install && npm run build
@@ -17,11 +18,15 @@ COPY --from=1 /tmp/compile/build/libs/packmate-*-SNAPSHOT.jar app.jar
CMD [ "java", "-Djava.net.preferIPv4Stack=true", "-Djava.net.preferIPv4Addresses=true", \
"-jar", "/app/app.jar", "--spring.datasource.url=jdbc:postgresql://127.0.0.1:65001/packmate", \
"--spring.datasource.password=${DB_PASSWORD}", \
"--capture-mode=${MODE}", "--pcap-file=${PCAP_FILE}", \
"--interface-name=${INTERFACE}", "--local-ip=${LOCAL_IP}", "--account-login=${WEB_LOGIN}", \
"--old-streams-cleanup-enabled=${OLD_STREAMS_CLEANUP_ENABLED}", "--cleanup-interval=${OLD_STREAMS_CLEANUP_INTERVAL}", \
"--old-streams-threshold=${OLD_STREAMS_CLEANUP_THRESHOLD}", \
"--account-password=${WEB_PASSWORD}", "--server.port=65000", "--server.address=0.0.0.0" \
"--packmate.capture-mode=${MODE}", "--packmate.pcap-file=${PCAP_FILE}", \
"--packmate.interface-name=${INTERFACE}", "--packmate.local-ip=${LOCAL_IP}", \
"--packmate.web.account-login=${WEB_LOGIN}", "--packmate.web.account-password=${WEB_PASSWORD}", \
"--packmate.web.fake-admin.enabled=${FAKE_ADMIN_AUTH_ENABLED}", \
"--packmate.web.fake-admin.mode=${FAKE_ADMIN_MODE}", \
"--packmate.cleanup.enabled=${OLD_STREAMS_CLEANUP_ENABLED}", \
"--packmate.cleanup.interval=${OLD_STREAMS_CLEANUP_INTERVAL}", \
"--packmate.cleanup.threshold=${OLD_STREAMS_CLEANUP_THRESHOLD}", \
"--server.port=65000", "--server.address=0.0.0.0" \
]
EXPOSE 65000

View File

@@ -1,6 +1,6 @@
## Настройка
Packmate использует настройки из файла `.env` (в той же папке, что и `docker-compose.yml`)
0xb00b5 team Packmate использует настройки из файла `.env` (в той же папке, что и `docker-compose.yml`)
### Основные настройки
```dotenv
@@ -10,13 +10,17 @@ PACKMATE_LOCAL_IP=10.20.1.1
PACKMATE_WEB_LOGIN=SomeUser
# Пароль для web-авторизации
PACKMATE_WEB_PASSWORD=SomeSecurePassword
# Включает обманку при вводе admin:admin
PACKMATE_FAKE_ADMIN_ENABLED=true
# fun или fake_packets - варианты обманки
PACKMATE_FAKE_ADMIN_MODE=fun
```
### Режим работы
Packmate поддерживает три основных режима работы: `LIVE`, `FILE` и `VIEW`.
1. `LIVE` - это основной режим работы во время CTF. Packmate обрабатывает живой трафик и сразу выводит результаты.
2. `FILE` - обрабатывает трафик из pcap файлов. Полезен для анализа трафика с прошедших CTF, где не был запущен Packmate, или тех, где невозможно запустить его на вулнбоксе.
3. `VIEW` - Packmate не обрабатывает трафик, а только показывает уже обработанные стримы. Полезен для разборов после завершения CTF.
0xb00b5 team Packmate поддерживает три основных режима работы: `LIVE`, `FILE` и `VIEW`.
1. `LIVE` - это основной режим работы во время CTF. 0xb00b5 team Packmate обрабатывает живой трафик и сразу выводит результаты.
2. `FILE` - обрабатывает трафик из pcap файлов. Полезен для анализа трафика с прошедших CTF, где не был запущен 0xb00b5 team Packmate, или тех, где невозможно запустить его на вулнбоксе.
3. `VIEW` - 0xb00b5 team Packmate не обрабатывает трафик, а только показывает уже обработанные стримы. Полезен для разборов после завершения CTF.
<details>
<summary>Настройка LIVE</summary>
@@ -52,7 +56,7 @@ PACKMATE_PCAP_FILE=dump.pcap
<details>
<summary>Настройка VIEW</summary>
В этом режиме Packmate просто показывает уже имеющиеся данные.
В этом режиме 0xb00b5 team Packmate просто показывает уже имеющиеся данные.
```dotenv
# Режим работы - просмотр
@@ -62,7 +66,7 @@ PACKMATE_MODE=VIEW
</details>
### Очистка БД
На крупных CTF через какое-то время накапливается большое количество трафика. Это замедляет работу Packmate и занимает много места на диске.
На крупных CTF через какое-то время накапливается большое количество трафика. Это замедляет работу 0xb00b5 team Packmate и занимает много места на диске.
Для оптимизации работы, рекомендуется включить регулярную очистку БД от старых стримов. Это будет работать только в режиме `LIVE`.
```dotenv
@@ -79,7 +83,7 @@ PACKMATE_OLD_STREAMS_CLEANUP_THRESHOLD=240
```dotenv
# Пароль от БД. Из-за того, что БД принимает подключения только с localhost, менять его необязательно, но можно, для дополнительной безопасности.
PACKMATE_DB_PASSWORD=K604YnL3G1hp2RDkCZNjGpxbyNpNHTRb
# Версия Packmate. Можно изменить, если нужно использовать другой образ из docker registry.
# Версия 0xb00b5 team Packmate. Можно изменить, если нужен другой тег образа из cr.danosito.com/danosito/0xb00b5-packmate.
BUILD_TAG=latest
```

View File

@@ -1,6 +1,6 @@
## Setup
Packmate uses properties from the `.env` file (in the same directory as `docker-compose.yml`)
0xb00b5 team Packmate uses properties from the `.env` file (in the same directory as `docker-compose.yml`)
### Primary settings
```dotenv
@@ -10,13 +10,17 @@ PACKMATE_LOCAL_IP=10.20.1.1
PACKMATE_WEB_LOGIN=SomeUser
# Password for the web interface
PACKMATE_WEB_PASSWORD=SomeSecurePassword
# Enable decoy flow for admin:admin login
PACKMATE_FAKE_ADMIN_ENABLED=true
# fun or fake_packets - pick the decoy flavor
PACKMATE_FAKE_ADMIN_MODE=fun
```
### Modes of operation
Packmate supports 3 modes of operation: `LIVE`, `FILE` и `VIEW`.
1. `LIVE` - the usual mode during a CTF. Packmate processes live traffic and instantly displays the results.
2. `FILE` - processes traffic from pcap files. Useful to analyze traffic from past CTFs where Packmate wasn't launched, or CTFs where it's impossible to use it on the vulnbox.
3. `VIEW` - Packmate does not process any traffic, but simply shows already processed streams. Useful for post-game analyses.
0xb00b5 team Packmate supports 3 modes of operation: `LIVE`, `FILE` и `VIEW`.
1. `LIVE` - the usual mode during a CTF. 0xb00b5 team Packmate processes live traffic and instantly displays the results.
2. `FILE` - processes traffic from pcap files. Useful to analyze traffic from past CTFs where 0xb00b5 team Packmate wasn't launched, or CTFs where it's impossible to use it on the vulnbox.
3. `VIEW` - 0xb00b5 team Packmate does not process any traffic, but simply shows already processed streams. Useful for post-game analyses.
<details>
<summary>LIVE setup</summary>
@@ -52,7 +56,7 @@ PACKMATE_PCAP_FILE=dump.pcap
<details>
<summary>VIEW setup</summary>
In that mode, Packmate simply shows already existing data.
In that mode, 0xb00b5 team Packmate simply shows already existing data.
```dotenv
# Mode: viewing the data
@@ -62,7 +66,7 @@ PACKMATE_MODE=VIEW
</details>
### Database cleanup
On large CTFsб after some time a lot of traffic will pile up. This can slow Packmate down and take a lot of drive space.
On large CTFsб after some time a lot of traffic will pile up. This can slow 0xb00b5 team Packmate down and take a lot of drive space.
To optimize the workflow, it is recommended to enable periodical database cleanup of old streams. It will only work in the `LIVE` mode.
```dotenv
@@ -79,7 +83,7 @@ PACKMATE_OLD_STREAMS_CLEANUP_THRESHOLD=240
```dotenv
# Database password. Considering it only listens on localhost, it's not mandatory to change it, but you can do it for additional security.
PACKMATE_DB_PASSWORD=K604YnL3G1hp2RDkCZNjGpxbyNpNHTRb
# Packmate version. Change it if you want to use a different version from the docker registry.
# 0xb00b5 team Packmate version. Change it if you want to use a different tag from cr.danosito.com/danosito/0xb00b5-packmate.
BUILD_TAG=latest
```

View File

@@ -75,9 +75,9 @@
Совет: иногда на CTF забывают перезаписать TTL пакетов внутри сети. В таком случае по TTL можно отличить запросы от чекеров и от других команд.
Совет #2: по User-Agent можно отличать запросы из разных источников. К примеру, можно предположить, что на скриншоте выше запросы 4 и 5 пришли из разных источников.
Совет #&#8203;2: по User-Agent можно отличать запросы из разных источников. К примеру, можно предположить, что на скриншоте выше запросы 4 и 5 пришли из разных источников.
Совет #3: нажимайте на звездочку, чтобы добавить интересный стрим в избранное. Этот стрим будет выделен в списке, и появится в списке избранных стримов.
Совет #&#8203;3: нажимайте на звездочку, чтобы добавить интересный стрим в избранное. Этот стрим будет выделен в списке, и появится в списке избранных стримов.
#### Управление просмотром
@@ -101,7 +101,7 @@
Совет: создавайте отдельные паттерны для входящих и исходящих флагов. Так легче отличать чекер, кладущий флаги, от эксплоитов.
Совет #2: используйте Lookback для исследования найденных эксплоитов.
Совет #&#8203;2: используйте Lookback для исследования найденных эксплоитов.
Пример: вы обнаружили, что сервис только что отдал флаг пользователю `abc123` без видимых причин.
Можно предположить, что атакующая команда создала этого пользователя и подготовила эксплоит в другом стриме.

View File

@@ -68,9 +68,9 @@ you can switch between binary and text representation using the button in the si
Tip: Sometimes during CTFs, admins forget to overwrite the TTL of packets inside the network. In such cases, you can differentiate requests from checkers and other teams based on TTL.
Tip #2: User-Agent can be used to differentiate requests from different sources. For example, in the screenshot above, requests 4 and 5 may have come from different sources.
Tip #&#8203;2: User-Agent can be used to differentiate requests from different sources. For example, in the screenshot above, requests 4 and 5 may have come from different sources.
Tip #3: Click on the star icon to add an interesting stream to your favorites. This stream will be highlighted in the list and will appear in the list of favorite streams.
Tip #&#8203;3: Click on the star icon to add an interesting stream to your favorites. This stream will be highlighted in the list and will appear in the list of favorite streams.
#### Control Panel
@@ -94,7 +94,7 @@ Tip #3: Click on the star icon to add an interesting stream to your favorites. T
Tip: Create separate patterns for incoming and outgoing flags to easily distinguish between flag checkers and exploits.
Tip #2: Use Lookback to investigate discovered exploits.
Tip #&#8203;2: Use Lookback to investigate discovered exploits.
Example: You found that the service just handed out a flag to user `abc123` without an apparent reason.
You can assume that the attacking team created this user and prepared an exploit in another stream.

Submodule frontend deleted from cfdfc9e578

86
frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,86 @@
.idea
cmake-build-debug/
cmake-build-release/
*.iws
out/
.idea_modules/
atlassian-ide-plugin.xml
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
*.bak
/.tgitconfig
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pids
*.pid
*.seed
*.pid.lock
lib-cov
coverage
.nyc_output
.grunt
bower_components
.lock-wscript
build/Release
node_modules/
jspm_packages/
typings/
.npm
.eslintcache
.node_repl_history
*.tgz
.yarn-integrity
.env
.next
Thumbs.db
ehthumbs.db
ehthumbs_vista.db
*.stackdump
[Dd]esktop.ini
$RECYCLE.BIN/
*.cab
*.msi
*.msix
*.msm
*.msp
*.lnk
[._]*.s[a-v][a-z]
[._]*.sw[a-p]
[._]s[a-v][a-z]
[._]sw[a-p]
Session.vim
.netrwhist
*~
tags
[._]*.un~
\#*\#
/.emacs.desktop
/.emacs.desktop.lock
*.elc
auto-save-list
tramp
.\#*
.org-id-locations
*_archive
*_flymake.*
/eshell/history
/eshell/lastdir
/elpa/
*.rel
/auto/
.cask/
dist/
flycheck_*.el
/server/
.projectile
.dir-locals.el
.fuse_hidden*
.directory
.Trash-*
.nfs*
$*$

29
frontend/README.md Normal file
View File

@@ -0,0 +1,29 @@
# packmate-frontend
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Run your tests
```
npm run test
```
### Lints and fixes files
```
npm run lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

8
frontend/babel.config.js Normal file
View File

@@ -0,0 +1,8 @@
module.exports = {
presets: [
'@vue/app',
],
plugins: [
'@babel/plugin-proposal-optional-chaining',
],
};

31852
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

78
frontend/package.json Normal file
View File

@@ -0,0 +1,78 @@
{
"name": "packmate-frontend",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"axios": "^1.4.0",
"bootstrap": "^4.6.2",
"bootstrap-darkmode": "^0.9.1",
"bootstrap-vue": "^2.23.1",
"core-js": "^3.26.1",
"idb": "^5.0.8",
"sockjs-client": "^1.6.1",
"vue": "^2.6.14",
"vue-infinite-loading": "^2.4.5",
"vue-router": "^3.6.5",
"vuex": "^3.6.2",
"vuex-persistedstate": "^3.2.1",
"vuex-shared-mutations": "^1.0.2"
},
"devDependencies": {
"@babel/plugin-proposal-optional-chaining": "^7.18.9",
"@fortawesome/fontawesome-free": "^5.15.4",
"@vue/cli-plugin-babel": "^4.5.19",
"@vue/cli-plugin-eslint": "^4.5.19",
"@vue/cli-service": "^4.3.0",
"babel-eslint": "^10.1.0",
"eslint": "^6.8.0",
"eslint-plugin-vue": "^6.2.2",
"node-sass": "^8.0.0",
"sass-loader": "^10.4.1",
"vue-template-compiler": "^2.7.14"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"rules": {
"indent": [
"off"
],
"no-mixed-spaces-and-tabs": "off",
"comma-dangle": [
"warn",
{
"arrays": "always",
"objects": "always",
"imports": "always",
"exports": "always",
"functions": "never"
}
],
"no-console": "off"
},
"parserOptions": {
"parser": "babel-eslint"
}
},
"postcss": {
"plugins": {
"autoprefixer": {}
}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 8"
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/mstile-150x150.png"/>
<TileColor>#afd9f0</TileColor>
</tile>
</msapplication>
</browserconfig>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
frontend/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta content="IE=edge" http-equiv="X-UA-Compatible">
<title>0xb00b5 team Packmate</title>
<meta name="author" content="@danosito">
<meta content="width=device-width,initial-scale=1.0" name="viewport">
<!--suppress HtmlUnknownTarget -->
<link href="<%= BASE_URL %>favicon.ico" rel="icon">
</head>
<body>
<noscript>
<strong>Please enable JavaScript to continue!!</strong>
</noscript>
<div id="app"></div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,135 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="1453.000000pt" height="1453.000000pt" viewBox="0 0 1453.000000 1453.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.11, written by Peter Selinger 2001-2013
</metadata>
<g transform="translate(0.000000,1453.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M7065 14523 c-1372 -40 -2685 -458 -3825 -1216 -157 -105 -420 -294
-420 -303 0 -2 17 3 38 11 20 8 84 26 142 40 58 14 148 39 200 54 127 39 219
58 314 66 44 4 100 13 124 20 25 8 81 17 126 21 44 3 89 11 99 16 95 49 332
73 722 73 306 -1 463 -10 636 -40 167 -28 298 -56 404 -85 44 -12 103 -27 130
-34 60 -16 250 -77 320 -104 28 -11 86 -33 130 -50 175 -66 550 -246 619 -296
17 -13 67 -44 111 -70 44 -26 102 -61 129 -78 l49 -32 47 17 c88 31 191 13
245 -43 l28 -29 51 46 c69 62 286 203 366 238 36 16 70 32 75 36 10 7 190 82
290 121 108 42 410 138 433 138 8 0 28 6 45 14 74 32 376 88 657 120 368 43
516 44 1020 6 413 -30 709 -73 915 -133 100 -28 208 -97 265 -170 45 -55 45
-64 11 -142 -10 -22 -30 -69 -44 -105 -67 -171 -218 -472 -339 -680 -99 -168
-127 -225 -128 -254 0 -10 30 -52 68 -94 117 -130 132 -147 132 -154 0 -5 -30
-56 -66 -115 -37 -60 -80 -133 -97 -163 l-29 -54 13 -106 c17 -136 15 -322 -4
-430 -18 -94 -51 -233 -61 -248 -3 -5 -12 -57 -21 -115 -11 -82 -22 -122 -50
-176 -40 -81 -39 -76 -22 -93 12 -12 35 -47 179 -269 59 -90 124 -186 228
-337 24 -34 64 -86 89 -115 25 -29 69 -80 96 -112 67 -79 238 -252 298 -299
104 -84 198 -144 317 -205 126 -63 185 -100 177 -108 -5 -4 -168 10 -302 27
-174 21 -383 102 -532 206 -37 26 -81 57 -98 68 -51 36 -118 98 -163 150 -59
71 -58 69 -75 102 -14 25 -16 26 -17 8 0 -12 5 -46 11 -75 6 -30 17 -88 24
-129 7 -41 23 -127 35 -190 25 -135 46 -281 60 -435 6 -60 17 -171 25 -245 8
-74 17 -227 20 -340 3 -113 10 -234 15 -270 5 -36 12 -361 16 -723 6 -514 4
-679 -6 -755 -15 -117 -5 -231 36 -387 14 -55 30 -122 35 -150 5 -27 14 -59
19 -70 5 -11 21 -55 35 -97 57 -171 195 -440 297 -582 15 -21 30 -55 34 -75 3
-20 19 -49 34 -66 37 -39 59 -94 49 -120 -7 -18 -8 -18 -8 -2 -2 36 -77 134
-206 268 -121 125 -211 233 -277 334 -114 176 -160 253 -217 370 -76 158 -129
292 -157 400 -6 22 -10 -1 -15 -85 -11 -185 -39 -500 -49 -560 -6 -30 -14 -84
-19 -120 -20 -141 -42 -266 -51 -290 -5 -14 -12 -45 -16 -70 -3 -24 -10 -51
-14 -60 -4 -8 -15 -40 -24 -70 -8 -30 -24 -72 -35 -93 l-19 -38 59 -112 c32
-62 66 -126 74 -144 34 -73 147 -194 252 -270 29 -21 54 -43 56 -48 3 -7 -48
-9 -147 -7 -125 3 -164 7 -227 27 -75 23 -179 73 -210 101 -8 8 -20 14 -26 14
-24 0 -203 227 -203 258 0 6 -4 12 -8 12 -4 0 -14 16 -21 35 -15 44 -27 45
-34 3 -3 -18 -13 -67 -21 -108 -8 -41 -19 -97 -25 -125 -9 -47 -40 -144 -61
-190 -49 -111 -60 -139 -60 -151 0 -8 -4 -14 -10 -14 -5 0 -10 -5 -10 -11 0
-14 -72 -124 -134 -204 -79 -103 -216 -211 -346 -275 -36 -17 -73 -35 -82 -41
-10 -5 -24 -9 -31 -9 -7 0 -29 -11 -47 -25 -20 -15 -36 -21 -38 -15 -4 11 56
118 111 199 29 43 161 298 184 356 22 57 113 322 113 331 0 3 -28 -15 -62 -39
-93 -66 -223 -138 -308 -171 -41 -15 -84 -32 -95 -37 -18 -7 -178 -49 -189
-49 -7 0 -98 -158 -119 -205 -23 -53 -38 -164 -25 -188 5 -10 8 -20 6 -22 -14
-14 -74 70 -99 137 -41 111 -64 391 -39 477 5 19 7 37 5 40 -3 2 -59 -51 -126
-119 -117 -120 -180 -209 -249 -350 -12 -25 -26 -53 -31 -63 -5 -9 -12 -37
-15 -61 l-6 -43 88 -5 c107 -6 186 -41 249 -109 22 -24 80 -88 129 -142 139
-152 297 -276 471 -367 44 -23 99 -55 123 -71 24 -16 47 -29 53 -29 5 0 14
-11 20 -24 9 -18 26 -28 78 -41 36 -9 104 -28 151 -42 162 -48 179 -52 287
-56 153 -6 377 -46 513 -91 54 -17 75 -20 125 -13 108 15 388 22 510 14 66 -4
206 -11 310 -15 189 -8 190 -8 260 -42 125 -60 200 -115 256 -187 29 -38 56
-72 61 -78 10 -11 305 279 472 465 1000 1111 1631 2489 1815 3965 111 884 56
1797 -159 2654 -293 1167 -851 2216 -1656 3111 -124 138 -431 444 -564 561
-775 687 -1713 1214 -2692 1514 -605 185 -1213 288 -1858 315 -135 5 -276 9
-315 8 -38 -1 -110 -3 -160 -5z"/>
<path d="M2499 12743 c-148 -127 -480 -454 -623 -613 -1067 -1189 -1702 -2650
-1846 -4242 -43 -480 -29 -1086 35 -1568 198 -1471 822 -2821 1811 -3920 137
-152 430 -443 579 -574 106 -94 110 -96 119 -74 21 50 111 196 156 253 42 52
61 67 138 104 50 23 114 50 144 59 68 20 327 28 418 11 36 -6 94 -15 130 -20
36 -4 124 -18 195 -29 72 -12 158 -25 193 -30 54 -8 152 -34 204 -55 13 -5 21
0 29 18 6 14 21 28 33 32 18 5 569 26 764 29 46 1 72 5 72 12 0 7 15 23 33 36
54 41 222 138 239 138 8 0 22 7 29 16 7 9 40 37 74 62 58 45 485 466 555 548
19 23 67 78 105 124 39 45 80 86 93 91 31 12 84 11 116 -1 23 -8 26 -7 26 14
0 19 -27 65 -50 86 -9 8 -72 90 -98 128 -14 21 -42 70 -63 111 l-37 73 -7 -33
c-9 -46 -46 -158 -62 -189 -7 -14 -13 -29 -13 -35 0 -18 -44 -98 -93 -172 -81
-121 -197 -214 -197 -157 0 13 -67 149 -85 172 -92 115 -132 161 -139 161 -5
1 -37 27 -71 58 -35 32 -69 63 -76 68 -24 18 -86 55 -93 55 -7 0 46 -114 57
-118 4 -2 7 -8 7 -14 0 -5 17 -40 39 -76 105 -182 127 -232 101 -232 -5 0 -10
4 -10 8 0 5 -10 14 -22 19 -62 29 -319 251 -360 310 -16 24 -37 51 -47 61 -21
21 -66 79 -113 147 -28 42 -103 193 -145 295 -8 21 -11 14 -16 -48 -8 -79 -45
-261 -57 -277 -4 -5 -11 -26 -15 -45 -4 -19 -11 -39 -15 -45 -4 -5 -13 -28
-20 -50 -35 -108 -220 -355 -266 -355 -13 0 -26 -4 -29 -10 -3 -5 -34 -26 -68
-46 -70 -41 -77 -48 -77 -87 0 -16 -4 -26 -10 -22 -15 9 -12 73 3 82 40 24
110 216 126 343 6 41 15 106 21 143 10 58 9 71 -3 85 -18 20 -127 181 -127
187 0 2 -13 23 -29 47 -16 24 -42 66 -57 94 -16 28 -60 104 -99 168 -111 186
-308 565 -330 636 -4 14 -10 27 -14 30 -3 3 -14 21 -23 40 l-17 35 0 -60 c0
-107 60 -452 88 -505 5 -9 12 -31 16 -50 4 -19 11 -39 15 -45 4 -5 11 -26 15
-45 19 -90 158 -362 271 -530 48 -71 85 -132 83 -135 -6 -5 -145 86 -241 158
-128 97 -297 286 -298 335 0 6 -3 12 -8 12 -7 0 -38 56 -65 120 -9 19 -25 57
-36 83 -11 26 -23 63 -27 83 -3 19 -10 36 -14 39 -5 3 -11 24 -15 48 -4 23
-11 47 -16 53 -4 5 -13 44 -19 85 -6 40 -17 83 -23 94 -16 28 -22 822 -7 965
6 58 16 164 21 235 6 72 17 153 24 180 20 73 28 132 17 120 -5 -5 -23 -35 -39
-65 -50 -92 -58 -105 -70 -113 -7 -4 -13 -12 -13 -18 0 -13 -41 -72 -58 -82
-6 -4 -12 -14 -12 -20 0 -17 -188 -217 -204 -217 -7 0 -24 -10 -37 -21 -45
-41 -214 -145 -259 -161 -25 -9 -76 -19 -114 -22 -38 -4 -82 -14 -98 -22 -16
-8 -33 -12 -38 -9 -15 9 16 36 74 66 70 35 96 51 118 72 10 9 23 17 28 17 6 0
10 5 10 11 0 7 11 22 24 34 42 39 114 152 167 263 65 137 63 131 95 237 15 50
30 95 35 100 4 6 10 28 14 50 4 22 13 46 19 54 14 18 13 241 -2 250 -5 3 -13
38 -17 76 -3 39 -10 79 -15 90 -5 11 -14 67 -20 125 -6 58 -15 140 -20 183
-45 362 -43 1174 4 1477 8 52 18 124 22 160 3 35 10 71 15 80 4 8 12 44 18 80
12 83 66 302 80 330 5 9 12 31 16 50 4 19 13 46 21 60 7 14 31 70 54 125 22
55 45 108 50 117 19 34 9 36 -47 8 -117 -57 -191 -86 -263 -101 -25 -6 -54
-14 -65 -20 -58 -27 -335 -42 -460 -24 -80 11 -194 72 -302 163 -80 66 -71 73
47 42 56 -15 316 -8 333 9 6 6 24 11 40 11 16 0 43 6 60 14 18 8 61 27 97 42
36 16 83 39 105 51 22 13 51 28 65 35 14 7 27 15 30 18 3 3 16 11 30 18 48 24
70 39 126 83 31 24 90 71 132 104 43 33 82 64 89 70 6 5 28 21 47 35 42 29 42
45 3 113 -42 75 -57 115 -76 212 -34 173 -2 389 109 725 27 82 60 213 60 237
0 6 -20 35 -44 63 -48 56 -58 90 -44 156 11 48 32 11 -207 359 -126 183 -363
575 -408 675 -13 30 -36 80 -50 110 -60 129 -66 151 -52 198 7 23 12 42 10 42
-1 0 -54 -44 -116 -97z"/>
<path d="M6445 1929 c-5 -7 -25 -8 -56 -4 -29 4 -49 3 -49 -3 0 -5 -35 -12
-77 -16 -80 -6 -212 -31 -248 -46 -11 -4 -36 -14 -55 -21 -19 -7 -53 -24 -75
-39 -46 -30 -58 -37 -142 -86 -35 -19 -65 -40 -69 -45 -3 -5 13 -48 36 -97 62
-131 68 -143 90 -200 11 -29 20 -62 20 -73 0 -11 16 -47 35 -80 19 -33 38 -72
41 -87 4 -15 10 -31 14 -37 14 -18 32 -174 28 -240 -3 -54 -1 -65 12 -65 15 0
145 40 190 58 14 6 59 20 100 32 41 12 84 25 95 30 51 20 248 80 266 80 11 0
52 14 91 30 107 46 113 40 38 -35 -27 -28 -50 -54 -50 -58 0 -4 -46 -54 -102
-110 -104 -103 -238 -259 -238 -276 0 -5 -3 -11 -8 -13 -7 -3 -27 -30 -133
-177 -18 -24 -49 -76 -69 -115 -20 -39 -44 -84 -54 -99 -10 -15 -17 -29 -15
-32 13 -13 474 -67 765 -89 l70 -6 11 68 c15 90 43 205 54 216 5 6 9 17 9 26
0 23 103 221 159 305 25 38 61 99 79 135 18 35 40 74 50 85 29 35 132 252 132
279 0 9 4 16 9 16 5 0 14 21 20 46 10 40 74 126 84 113 10 -12 27 -70 32 -109
7 -46 38 -152 60 -205 7 -16 16 -41 20 -55 3 -14 12 -38 20 -55 7 -16 18 -41
23 -55 24 -55 75 -158 105 -211 35 -62 137 -221 147 -229 3 -3 20 -34 37 -70
18 -36 48 -88 67 -117 19 -29 37 -65 41 -81 3 -17 14 -37 24 -46 17 -15 29
-15 167 4 182 25 536 89 547 100 7 7 -67 111 -85 118 -4 2 -8 8 -8 14 0 11
-88 121 -101 126 -5 2 -9 8 -9 13 0 5 -19 33 -42 62 -24 29 -54 68 -68 86 -14
18 -34 45 -46 59 -32 41 -124 171 -124 176 0 3 -19 33 -43 68 -24 35 -53 82
-65 106 l-22 42 28 0 c15 -1 54 -7 87 -15 33 -8 83 -19 110 -25 28 -6 57 -15
66 -20 9 -5 33 -12 53 -15 20 -4 53 -16 73 -26 21 -11 48 -19 60 -19 13 0 23
-4 23 -9 0 -5 13 -11 29 -14 16 -4 56 -19 89 -35 l60 -28 6 60 c3 32 6 85 6
117 0 33 4 59 9 59 4 0 11 25 14 56 3 31 11 62 16 67 6 6 11 19 11 29 0 10 4
18 8 18 4 0 15 28 25 63 10 34 49 124 87 199 72 141 83 184 53 195 -10 4 -37
19 -60 35 -23 15 -45 28 -49 28 -8 0 -22 6 -169 75 -44 21 -100 40 -125 43
-25 2 -50 8 -55 13 -6 4 -51 13 -100 19 -50 7 -100 17 -113 23 -26 12 -340 -5
-407 -23 -100 -26 -153 -44 -194 -62 -38 -18 -55 -20 -146 -14 -56 3 -127 13
-157 21 -30 7 -74 12 -99 10 -99 -8 -216 -28 -246 -42 -29 -13 -37 -13 -75 0
-64 22 -148 45 -193 53 -22 3 -51 13 -64 21 -14 9 -59 18 -108 21 -46 3 -86 9
-89 13 -8 13 -192 11 -199 -2z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.3 KiB

View File

@@ -0,0 +1,21 @@
{
"name": "Packmate",
"short_name": "Packmate",
"start_url": ".",
"description": "Packet monitoring tool for CTFs.",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#afd9f0",
"background_color": "#ffffff",
"display": "standalone"
}

310
frontend/src/App.vue Normal file
View File

@@ -0,0 +1,310 @@
<template>
<div id="app">
<div class="bg-lines"></div>
<Navbar/>
<div class="container-fluid app-frame">
<div class="row">
<transition name="fade" mode="out-in">
<router-view name="sidebar" ref="sidebar"/>
</transition>
<main role="main" class="col-sm-9 ml-sm-auto px-4 app-main">
<transition name="fade" mode="out-in">
<router-view name="content"/>
</transition>
</main>
</div>
</div>
<Settings/>
</div>
</template>
<!--suppress JSUnresolvedVariable -->
<script>
export const DB_OBJSTORE_COUNTERS_HISTORY = 'countersHistory';
import Navbar from './components/Navbar';
import Settings from './views/Settings';
import SockJS from 'sockjs-client';
import {openDB,} from 'idb';
export default {
data() {
return {
websocket: null,
db: null,
};
},
mounted() {
this.connectWs();
openDB('packmate', 1, {
upgrade(db, oldVersion, newVersion) {
console.info('[IDB] Creating new database! Old rev %d, new rev %d',
oldVersion, newVersion);
db.createObjectStore(DB_OBJSTORE_COUNTERS_HISTORY, {
autoIncrement: false,
keyPath: null,
});
},
})
.then(db => {
this.db = db;
})
.catch(e => {
console.error('[IDB] Failed to open DB!', e);
});
},
beforeDestroy() {
this.websocket?.close();
},
methods: {
connectWs() {
if (this.websocket !== null) return;
this.websocket = new SockJS(this.$http.defaults.baseURL + '/ws');
this.websocket.onopen = () => {
console.info('[WS] Connected');
};
this.websocket.onclose = (ev) => {
console.info('[WS] Disconnected', ev.code, ev.reason);
this.websocket = null;
if (ev.code === 1008) {
console.info('[WS] Security timeout, reconnecting...');
this.connectWs();
}
if (ev.code !== 1000) { // Normal closure
setTimeout(this.connectWs, 3000);
console.info('[WS] Reconnecting...');
}
};
this.websocket.onmessage = (ev) => {
const parsed = JSON.parse(ev.data);
switch (parsed.type) {
case 'NEW_STREAM': {
this.$refs.sidebar.addStreamFromWs(parsed.value);
break;
}
case 'SAVE_SERVICE': {
this.addServiceFromWs(parsed.value);
break;
}
case 'DELETE_SERVICE': {
this.deleteServiceFromWs(parsed.value);
break;
}
case 'SAVE_PATTERN': {
this.addPatternFromWs(parsed.value);
break;
}
case 'COUNTERS_UPDATE': {
const data = parsed.value;
this.$store.commit('setCurrentPacketsCount', data.totalPackets);
this.$store.commit('setCurrentStreamsCount', data.totalStreams);
this.$store.commit('setCurrentServicesPacketsCount', data.servicesPackets);
this.$store.commit('setCurrentServicesStreamsCount', data.servicesStreams);
console.debug('Adding new counters to DB', parsed.value);
const tx = this.db.transaction(DB_OBJSTORE_COUNTERS_HISTORY, 'readwrite');
tx.store.add({
newPacketsCount: data.totalPackets,
newStreamsCount: data.totalStreams,
servicesPackets: data.servicesPackets,
servicesStreams: data.servicesStreams,
}, Date.now()).then(() => {
console.debug('[IDB] Added entry');
}).catch(e => {
console.error('[IDB] Failed to add entry!', e);
});
break;
}
case 'ENABLE_PATTERN': {
this.togglePatternFromWs(parsed.value, true);
break;
}
case 'DISABLE_PATTERN': {
this.togglePatternFromWs(parsed.value, false);
break;
}
case 'PCAP_STARTED': {
this.$store.commit('startPcap');
this.$bvToast.toast(`Pcap file processing started`, {
title: 'Notification',
variant: 'info',
autoHideDelay: 5000,
});
break;
}
case 'PCAP_STOPPED': {
this.$bvToast.toast(`All streams processed`, {
title: 'Notification',
variant: 'success',
autoHideDelay: 5000,
});
break;
}
case 'FINISH_LOOKBACK': {
console.debug('Lookback completed');
this.$bvToast.toast(`Lookback completed`, {
title: 'Notification',
variant: 'success',
autoHideDelay: 5000,
});
this.$refs.sidebar.streams = [];
this.$refs.sidebar.$refs.infiniteLoader.stateChanger.reset();
break;
}
default: {
console.error('[WS] Event is not implemented!', parsed);
break;
}
}
};
this.websocket.onerror = (ev) => {
console.warn('[WS] Error', ev);
};
},
addPatternFromWs(pattern) {
const foundIndex = this.$store.state.patterns.findIndex(el => el.id === pattern.id);
if (foundIndex === -1) {
this.$store.commit('addPattern', pattern);
return;
}
let newPatterns = this.$store.state.patterns.slice();
newPatterns.splice(foundIndex, 1, pattern);
this.$store.commit('setPatterns', newPatterns);
},
togglePatternFromWs(id, enabled) {
this.$store.state.patterns.forEach(pattern => {
if (pattern.id === id) {
pattern.enabled = enabled;
}
});
},
addServiceFromWs(service) {
const foundIndex = this.$store.state.services.findIndex(el => el.port === service.port);
if (foundIndex === -1) {
this.$store.commit('addService', service);
return;
}
let newServices = this.$store.state.services.slice();
newServices.splice(foundIndex, 1, service);
this.$store.commit('setServices', newServices);
},
deleteServiceFromWs(port) {
this.$store.commit('setServices', this.$store.state.services.filter(o => o.port !== port));
},
},
components: {
Settings,
Navbar,
},
};
</script>
<style>
/* cyrillic-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-display: swap;
src: local('Open Sans Regular'), local('OpenSans-Regular'), url(https://fonts.gstatic.com/s/opensans/v16/mem8YaGs126MiZpBA-UFWJ0bbck.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-display: swap;
src: local('Open Sans Regular'), local('OpenSans-Regular'), url(https://fonts.gstatic.com/s/opensans/v16/mem8YaGs126MiZpBA-UFUZ0bbck.woff2) format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-display: swap;
src: local('Open Sans Regular'), local('OpenSans-Regular'), url(https://fonts.gstatic.com/s/opensans/v16/mem8YaGs126MiZpBA-UFWZ0bbck.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-display: swap;
src: local('Open Sans Regular'), local('OpenSans-Regular'), url(https://fonts.gstatic.com/s/opensans/v16/mem8YaGs126MiZpBA-UFVp0bbck.woff2) format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-display: swap;
src: local('Open Sans Regular'), local('OpenSans-Regular'), url(https://fonts.gstatic.com/s/opensans/v16/mem8YaGs126MiZpBA-UFWp0bbck.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-display: swap;
src: local('Open Sans Regular'), local('OpenSans-Regular'), url(https://fonts.gstatic.com/s/opensans/v16/mem8YaGs126MiZpBA-UFW50bbck.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-display: swap;
src: local('Open Sans Regular'), local('OpenSans-Regular'), url(https://fonts.gstatic.com/s/opensans/v16/mem8YaGs126MiZpBA-UFVZ0b.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
html * {
font-family: 'Open Sans', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-size: .875rem;
}
body {
overflow-y: scroll;
}
/*noinspection CssUnusedSymbol*/
.fade-enter-active, .fade-leave-active {
transition: opacity .3s;
}
/*noinspection CssUnusedSymbol*/
.fade-enter, .fade-leave-to {
opacity: 0;
}
/* Patterns dropdown fix */
/*noinspection CssUnusedSymbol*/
.dropdown-menu.show {
position: fixed !important;
/*transform: translate3d(15px, 38px, 0) !important;*/
transform: none !important;
left: 15px !important;
top: 38px !important;
}
[role="main"] {
padding-top: 55px; /* Space for fixed navbar */
}
</style>

View File

@@ -0,0 +1,309 @@
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&family=JetBrains+Mono:wght@400;600&display=swap');
:root {
--bg: #05030a;
--bg-2: #0a0a18;
--panel: rgba(9, 7, 18, 0.9);
--panel-strong: rgba(14, 10, 24, 0.95);
--accent: #c66bff;
--accent-2: #7a1dff;
--text: #e6e0ff;
--muted: #9b90c8;
--mono-font: 'JetBrains Mono', 'Ubuntu Mono', monospace;
--pixel-font: 'Press Start 2P', 'JetBrains Mono', monospace;
}
[data-theme="light"] {
--bg: #f0eaff;
--bg-2: #e6ddff;
--panel: rgba(245, 240, 255, 0.9);
--panel-strong: rgba(235, 227, 255, 0.95);
--text: #1a1233;
--muted: #5d4d80;
}
* {
box-sizing: border-box;
scrollbar-width: none;
}
*::-webkit-scrollbar {
width: 0;
height: 0;
}
body {
background:
radial-gradient(circle at 18% 24%, rgba(198, 107, 255, 0.18), transparent 25%),
radial-gradient(circle at 82% 12%, rgba(122, 29, 255, 0.12), transparent 25%),
linear-gradient(135deg, #020107 0%, #0a0820 50%, #03010b 100%);
color: var(--text);
font-family: var(--pixel-font);
letter-spacing: 0.6px;
min-height: 100vh;
}
body[data-theme="light"] {
background: linear-gradient(135deg, #f0eaff 0%, #e6ddff 40%, #f6f1ff 100%);
}
#app {
position: relative;
}
.bg-lines {
position: fixed;
inset: 0;
background-image: linear-gradient(rgba(122, 29, 255, 0.1) 1px, transparent 1px), linear-gradient(90deg, rgba(198, 107, 255, 0.12) 1px, transparent 1px);
background-size: 120px 120px;
pointer-events: none;
z-index: 0;
}
.container-fluid.app-frame {
padding-top: 84px;
position: relative;
z-index: 1;
}
.neon-navbar {
background: var(--panel-strong) !important;
border-bottom: 1px solid rgba(122, 29, 255, 0.45);
box-shadow: 0 10px 35px rgba(0, 0, 0, 0.55), 0 0 28px rgba(122, 29, 255, 0.35);
padding: 12px 18px;
}
.navbar-brand {
font-family: var(--pixel-font);
letter-spacing: 1px;
color: var(--accent) !important;
display: flex;
align-items: center;
gap: 10px;
}
.navbar-brand .brand-dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: linear-gradient(135deg, var(--accent), var(--accent-2));
box-shadow: 0 0 10px rgba(122, 29, 255, 0.65);
}
.navbar-sub {
font-size: 10px;
color: var(--muted);
margin-left: 6px;
}
.navbar-metrics {
display: flex;
gap: 10px;
align-items: center;
margin-left: 12px;
flex-wrap: wrap;
}
.metric-chip {
background: rgba(122, 29, 255, 0.12);
border: 1px solid rgba(122, 29, 255, 0.28);
color: var(--text);
padding: 8px 10px;
border-radius: 10px;
display: grid;
grid-template-columns: auto auto;
column-gap: 8px;
align-items: center;
font-size: 11px;
box-shadow: 0 0 14px rgba(122, 29, 255, 0.24);
}
.metric-chip .label {
color: var(--muted);
}
.metric-chip .value {
color: var(--accent);
}
.navbar-nav .nav-link,
.neon-tab {
color: var(--text) !important;
border-radius: 12px;
padding: 8px 10px;
margin: 4px 4px;
transition: all .2s ease;
background: transparent;
}
.navbar-nav .nav-link:hover,
.navbar-nav .nav-item.active > .nav-link,
.neon-tab:hover {
background: rgba(122, 29, 255, 0.24);
box-shadow: 0 0 16px rgba(122, 29, 255, 0.28);
color: #fff !important;
}
.navbar-cogs > i {
color: var(--muted);
}
.navbar-cogs > i:hover {
color: var(--accent);
}
.app-main {
background: var(--panel);
border: 1px solid rgba(122, 29, 255, 0.35);
box-shadow: inset 0 0 30px rgba(0, 0, 0, 0.45), 0 0 38px rgba(122, 29, 255, 0.18);
border-radius: 18px;
padding: 28px;
margin-bottom: 24px;
min-height: calc(100vh - 120px);
}
.sidebar {
background: var(--panel);
border-right: 1px solid rgba(122, 29, 255, 0.25);
box-shadow: inset -10px 0 24px rgba(0, 0, 0, 0.35);
min-height: calc(100vh - 84px);
padding-top: 14px;
}
.sidebar .btn {
font-family: var(--pixel-font);
letter-spacing: 0.6px;
border-radius: 10px;
border-color: rgba(122, 29, 255, 0.35);
color: var(--text);
}
.sidebar .btn-outline-primary,
.sidebar .btn-outline-info,
.sidebar .btn-outline-success,
.sidebar .btn-outline-warning {
background: rgba(255, 255, 255, 0.02);
}
.sidebar .btn:hover {
box-shadow: 0 0 12px rgba(122, 29, 255, 0.3);
}
.sidebar-sticky {
padding: 8px 6px 14px;
max-height: calc(100vh - 180px);
overflow-y: auto;
}
.stream-item {
margin: 8px 0;
border-radius: 14px;
border: 1px solid rgba(122, 29, 255, 0.25);
background: linear-gradient(125deg, rgba(122, 29, 255, 0.12), rgba(7, 4, 15, 0.9));
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.3);
}
.stream-item .nav-link {
color: var(--text) !important;
padding: 12px;
}
.stream-item .nav-link:hover {
background: rgba(122, 29, 255, 0.12);
}
.highlight {
border-color: rgba(122, 29, 255, 0.55);
box-shadow: 0 0 12px rgba(122, 29, 255, 0.35);
}
.legend-bar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 14px;
margin-bottom: 18px;
font-size: 12px;
}
.legend-pill {
padding: 8px 10px;
border-radius: 10px;
border: 1px solid rgba(122, 29, 255, 0.35);
background: rgba(122, 29, 255, 0.12);
box-shadow: 0 0 12px rgba(122, 29, 255, 0.2);
color: var(--text);
}
.packet-outgoing, .packet-incoming {
border-radius: 16px;
padding: 14px;
margin-bottom: 14px;
border: 1px solid rgba(122, 29, 255, 0.28);
}
.packet-incoming {
background: radial-gradient(circle at 18% 20%, rgba(122, 29, 255, 0.16), rgba(9, 7, 18, 0.92));
}
.packet-outgoing {
background: radial-gradient(circle at 18% 20%, rgba(255, 106, 213, 0.12), rgba(10, 17, 40, 0.9));
}
.packet-incoming div,
.packet-outgoing div {
font-size: 11px;
color: var(--muted);
font-family: var(--pixel-font);
}
.packet-incoming button,
.packet-outgoing button {
color: var(--accent);
}
.packet-incoming p,
.packet-outgoing p {
font-family: var(--mono-font);
font-size: 13px;
background: rgba(0, 0, 0, 0.22);
padding: 12px;
border-radius: 10px;
color: var(--text);
box-shadow: inset 0 0 12px rgba(0,0,0,0.35);
}
.btn, .form-control, .custom-select, .page-link {
font-family: var(--pixel-font);
letter-spacing: 0.5px;
}
.modal-content {
background: var(--panel);
color: var(--text);
border: 1px solid rgba(122, 29, 255, 0.35);
box-shadow: 0 0 24px rgba(0, 0, 0, 0.45);
}
.dropdown-menu {
background: var(--panel);
color: var(--text);
}
.dropdown-item {
color: var(--text);
}
.dropdown-item:hover {
background: rgba(122, 29, 255, 0.18);
}
.toast {
background: var(--panel-strong);
color: var(--text);
border: 1px solid rgba(255, 106, 213, 0.22);
}
.theme-toggle {
margin-left: auto;
}

View File

@@ -0,0 +1,183 @@
<template>
<nav class="navbar navbar-dark navbar-expand fixed-top bg-dark flex-md-nowrap p-0 shadow neon-navbar">
<div class="d-flex align-items-center pl-3">
<span class="navbar-brand mb-0 ml-2">
<span class="brand-dot"></span>
0xb00b5 PM
</span>
</div>
<div class="navbar-metrics">
<span class="metric-chip">
<span class="label">SPM</span>
<span class="value">{{ this.$store.state.currentStreamsCount }}</span>
</span>
<span class="metric-chip">
<span class="label">PPS</span>
<span class="value">{{ packetsPerStream }}</span>
</span>
</div>
<PatternsDropdown ref="patternsDropdown"/>
<span v-if="this.$route.query.pattern" class="navbar-text navbar-nowrap">
{{ selectedPatternText }}
</span>
<div class="navbar-collapse collapse">
<ul class="navbar-nav px-1 mr-auto">
<li class="nav-item text-nowrap">
<router-link class="nav-link" :to="{name:'stream', params: {}, query: $route.query}" exact>All</router-link>
</li>
<template v-for="service in this.$store.state.services">
<router-link :key="service.port" tag="li" class="nav-item text-nowrap edit-button"
:to="{name:'stream', params: {servicePort: service.port}, query: $route.query}">
<a class="nav-link">
{{service.name}} #{{service.port}}
({{ getSpmForService(service.port) }}
<u title="Streams per minute">SPM</u>)
</a>
<a class="nav-link pl-0" style="cursor: pointer" @click.stop.prevent="editService(service)">
<i class="fas fa-pencil-alt"/>
</a>
</router-link>
</template>
<li class="nav-item text-nowrap" style="padding-left: 1em;">
<div class="my-2 mr-3 navbar-cogs" style="cursor: pointer;"
@click.stop.prevent="showAddService">
<i class="fas fa-plus-circle"/>
</div>
</li>
</ul>
<div class="my-2 my-lg-0 mr-3 navbar-cogs" style="cursor: pointer;"
@click.stop.prevent="showSettings">
<i class="fas fa-cogs"/>
</div>
</div>
<ServiceModal :creating="serviceModalIsCreating" :initial-service="serviceModalEditingService"
@service-update-needed="updateServices"/>
</nav>
</template>
<script>
import PatternsDropdown from './PatternsDropdown';
import ServiceModal from '../views/ServiceModal';
export default {
name: 'Navbar',
computed: {
packetsPerStream: function() {
let streams = this.$store.state.currentStreamsCount;
let packets = this.$store.state.currentPacketsCount;
if (streams === 0) {
return 0;
} else {
let pps = packets / streams;
return Math.round((pps + Number.EPSILON) * 10) / 10;
}
},
selectedPatternText: function () {
let selected = this.$route.query.pattern;
if (typeof selected === 'string') {
selected = parseInt(selected);
}
let pattern = this.$store.state.patterns.find(o => o.id === selected);
if (pattern) {
return `[Selected: ${pattern.name}]`;
} else {
return '[Invalid pattern]';
}
},
},
data() {
return {
serviceModalIsCreating: true,
serviceModalEditingService: {},
};
},
mounted() {
this.updateServices();
},
methods: {
getSpmForService(port) {
return this.$store.state.currentServicesStreamsCount[port] ?? 0;
},
editService(service) {
this.serviceModalIsCreating = false;
this.serviceModalEditingService = {};
this.$nextTick(() => {
this.serviceModalEditingService = service;
this.$bvModal.show('serviceModal');
});
},
showSettings() {
this.$bvModal.show('settingsModal');
console.debug('Showing settings...');
},
showAddService() {
this.serviceModalIsCreating = true;
this.serviceModalEditingService = {};
this.$bvModal.show('serviceModal');
console.debug('Showing addService...');
},
updateServices() {
this.$http.get('service/')
.then(r => this.$store.commit('setServices', r.data))
.catch(e => {
this.$bvToast.toast(`Failed to load services: ${e}`, {
title: 'Error',
variant: 'danger',
});
console.error('Failed to load services:', e);
});
},
},
components: {
ServiceModal,
PatternsDropdown,
},
};
</script>
<style scoped>
.nav-link {
transition: all .3s;
color: var(--text) !important;
}
.edit-button {
display: inherit;
}
.navbar-cogs > i {
transition: all .3s;
color: var(--muted);
}
.navbar-cogs > i:hover {
color: var(--accent);
}
.navbar-nowrap {
white-space: nowrap;
}
nav {
overflow-x: auto;
scrollbar-width: none;
}
u {
text-underline-position: under;
text-decoration-style: dotted;
}
nav::-webkit-scrollbar {
width: 0;
height: 0;
}
</style>

View File

@@ -0,0 +1,229 @@
<template>
<div :class="{'packet-incoming': packet.incoming, 'packet-outgoing': !packet.incoming}">
<div>#{{ packet.id }} at {{ dateToText(packet.timestamp) }}
<template v-if="offset !== null"> (+{{ offset }} ms)</template>
{{ printPacketFlags(packet) }}
<button @click.prevent="copyRaw" class="btn btn-link copy-btn">Copy HEX</button>
<button @click.prevent="copyText" class="btn btn-link copy-btn">Copy text</button>
<button @click.prevent="copyPythonBytes" class="btn btn-link copy-btn">Copy as Python bytes</button>
</div>
<p v-if="!this.$store.state.hexdumpMode"
class="pt-2 pb-2 mb-3"
v-html="stringdata"/>
<p v-else
class="pt-2 pb-2 mb-3"
v-html="hexdata"/>
</div>
</template>
<!--suppress JSUnresolvedVariable, JSDeprecatedSymbols -->
<script>
export default {
name: 'Packet',
props: {
packet: {
id: Number(),
matches: Array(),
timestamp: Number(),
incoming: Boolean(),
ungzipped: Boolean(),
webSocketParsed: Boolean(),
tlsDecrypted: Boolean(),
content: String(),
},
offset: Number(),
},
computed: {
hexdata() {
const dataString = this.atou(this.packet.content);
const dump = this.hexdump(dataString, this.$store.state.hexdumpBlockSize, this.$store.state.hexdumpLineNumberBase);
return this.escapeHtml(dump)
.split('\n')
.join('<br>'); // Replace all \n to <br>
},
stringdata() {
const dataString = this.atou(this.packet.content);
const dump = this.highlightPatterns(dataString);
return this.escapeHtml(dump)
.split('\n')
.join('<br>');
},
},
methods: {
atou(b64) {
const text = atob(b64);
const length = text.length;
const bytes = new Uint8Array(length);
for (let i = 0; i < length; i++) {
bytes[i] = text.charCodeAt(i);
}
const decoder = new TextDecoder();
return decoder.decode(bytes);
},
printPacketFlags(packet) {
let flags = [];
if (packet.ungzipped) {
flags.push('GZIP');
}
if (packet.webSocketParsed) {
flags.push('WS');
}
if (packet.tlsDecrypted) {
flags.push('TLS');
}
return flags.join(' ');
},
hexdump(buffer, blockSize, lineNumberBase) {
blockSize = parseInt(blockSize, 10) || 16;
lineNumberBase = parseInt(lineNumberBase, 10) || 10;
let lines = [];
const hex = '0123456789ABCDEF';
for (let b = 0; b < buffer.length; b += blockSize) {
const block = buffer.slice(b, Math.min(b + blockSize, buffer.length));
const addr = ('0000000000' + b.toString(lineNumberBase)).slice(-10);
let codes = block.split('').map(ch => {
const code = ch.charCodeAt(0);
return ' ' + hex[(0xF0 & code) >> 4] + hex[0x0F & code];
}).join('');
codes += ' ..'.repeat(blockSize - block.length);
// eslint-disable-next-line no-control-regex
let chars = block.replace(/[\x00-\x1F]/g, '.');
chars += ' '.repeat(blockSize - block.length);
lines.push(addr + ':' + codes + ' |' + chars + '|');
}
return lines.join('\n');
},
dateToText(unixTimestamp) {
return new Date(unixTimestamp).toLocaleDateString('ru-RU', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});
},
escapeHtml(in_) {
return in_.replace(/(<span style="background-color: #(?:[A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})">|<\/span>)|[&<>"'/]/g, ($0, $1) => {
const entityMap = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
'\'': '&#39;',
'/': '&#x2F;',
};
return $1 ? $1 : entityMap[$0];
});
},
highlightPatterns(raw) {
const patterns = this.$store.state.patterns.reduce((obj, item) => {
obj[item.id] = item;
return obj;
}, {}); // Array to object
let offset = 0;
this.packet.matches
.filter(match => !patterns[match.patternId].deleted)
.sort((a, b) => a.startPosition - b.startPosition)
.forEach(match => {
const pattern = patterns[match.patternId];
if (!pattern) {
console.info(`Pattern #${match.patternId} does not exist`);
return;
}
const firstTag = `<span style="background-color: ${pattern.color}">`;
const secondTag = '</span>';
const positionStart = match.startPosition + offset;
raw = raw.substring(0, positionStart) + firstTag + raw.substring(positionStart);
offset += firstTag.length;
const positionEnd = match.endPosition + offset + 1;
raw = raw.substring(0, positionEnd) + secondTag + raw.substring(positionEnd);
offset += secondTag.length;
});
return raw;
},
copyPythonBytes() {
const data = 'b\'' + atob(this.packet.content)
.split('')
.map((aChar) => {
return '\\x' + ('0' + aChar.charCodeAt(0).toString(16)).slice(-2);
})
.join('') + '\'';
this.copyContent(data);
},
copyRaw() {
this.copyContent(Buffer.from(this.packet.content, 'base64').toString('hex'));
},
copyText() {
this.copyContent(atob(this.packet.content));
},
copyContent(data) {
const tempEl = document.createElement('textarea');
tempEl.value = data; // Chrome
tempEl.textContent = data; // Firefox
document.body.appendChild(tempEl);
tempEl.select();
const result = document.execCommand('copy');
document.body.removeChild(tempEl);
console.debug('Copy result is', result);
},
},
};
</script>
<style scoped>
.copy-btn {
margin-left: 10px;
font-family: var(--pixel-font);
font-size: 10px;
letter-spacing: 0.4px;
padding-left: 6px;
padding-right: 6px;
}
</style>
<style scoped>
.packet-outgoing {
background: linear-gradient(135deg, rgba(255, 106, 213, 0.12), rgba(14, 18, 40, 0.85));
border: 1px solid rgba(255, 106, 213, 0.35);
box-shadow: 0 0 14px rgba(255, 106, 213, 0.2);
}
.packet-incoming {
background: linear-gradient(135deg, rgba(94, 242, 255, 0.16), rgba(14, 18, 40, 0.85));
border: 1px solid rgba(94, 242, 255, 0.35);
box-shadow: 0 0 14px rgba(94, 242, 255, 0.18);
}
div {
font-size: 11px;
color: var(--muted);
font-family: var(--pixel-font);
}
p {
font-family: var(--mono-font);
font-size: 13px;
margin-bottom: 10px;
padding: 10px 12px;
word-break: break-word;
background: rgba(0, 0, 0, 0.25);
border-radius: 10px;
color: var(--text);
}
button {
padding: 0;
top: -0.1em;
position: relative;
margin-left: 5px;
color: var(--accent);
}
</style>

View File

@@ -0,0 +1,229 @@
<template>
<b-dropdown no-flip text="Patterns" variant="dark" class="patterns-dropdown">
<li role="presentation" style="padding-left: 0.5em; padding-right: 0.5em;">
<button role="menuitem" type="button" class="btn btn-sm btn-primary btn-block"
@click.stop.prevent="showAddPattern">
<i class="fas fa-plus"/>
</button>
</li>
<b-dropdown-divider/>
<b-dropdown-item-button @click.stop.prevent="resetPatternSelection">
<strong>All streams</strong>
</b-dropdown-item-button>
<b-dropdown-item-button v-for="pattern in existingPatterns"
:key="pattern.id" @click.stop.prevent="openPattern(pattern)"
:class="{ 'ignore-pattern' : pattern.actionType === 'IGNORE' }">
<strong v-if="pattern.enabled" :style="getPatternColor">{{ pattern.name }}</strong>
<s v-else :style="`color: ${pattern.color};`">{{ pattern.name }}</s>:
<code>{{ getSearchTypeValue(pattern.searchType, pattern.value) }}</code>;
<template v-if="pattern.actionType === 'FIND'">search </template>
<template v-else>ignore </template>
<template v-if="pattern.directionType === 'BOTH'">anywhere </template>
<template v-else-if="pattern.directionType === 'INPUT'">in request </template>
<template v-else>in response </template>
<template v-if="pattern.serviceId === null">of any service</template>
<template v-else>of service {{ getServiceName(pattern.serviceId) }} #{{ pattern.serviceId }}</template>
<div class="float-right" style="margin-left: 2em;">
<button type="button" class="btn btn-outline-info btn-sm mr-1"
@click.stop.prevent="showEditPattern(pattern)"
title="Edit pattern">
<i class="fas fa-edit"></i>
</button>
<button v-if="pattern.actionType === 'FIND'" type="button" class="btn btn-outline-warning btn-sm mr-1"
@click.stop.prevent="showLookBack(pattern)"
title="Apply pattern to older streams">
<i class="fas fa-backward"></i>
</button>
<button v-if="pattern.enabled" type="button" class="btn btn-outline-danger btn-sm mr-1"
@click.stop.prevent="togglePattern(pattern)"
title="Stop matching streams with this pattern">
<i class="fas fa-pause"></i>
</button>
<button v-else type="button" class="btn btn-outline-success btn-sm mr-1"
@click.stop.prevent="togglePattern(pattern)"
title="Start matching streams with this pattern again">
<i class="fas fa-play"></i>
</button>
<button type="button" class="btn btn-outline-danger btn-sm"
@click.stop.prevent="deletePattern(pattern)"
title="Permanently delete this pattern">
<i class="fas fa-trash"></i>
</button>
</div>
</b-dropdown-item-button>
<PatternModal :creating="patternModalIsCreating" :initial-pattern="patternModalEditingPattern" />
<LookBack :pattern-id="patternIdForLookback" />
</b-dropdown>
</template>
<!--suppress JSUnresolvedReference -->
<script>
import PatternModal from '@/views/PatternModal.vue';
import LookBack from '@/views/LookBack.vue';
export default {
name: 'PatternsDropdown',
components: {LookBack, PatternModal, },
data() {
return {
patternModalIsCreating: true,
patternModalEditingPattern: {},
patternIdForLookback: 0,
};
},
mounted() {
this.updatePatterns();
},
computed: {
existingPatterns: function () {
return this.$store.state.patterns.filter(p => !p.deleted)
},
},
methods: {
getSearchTypeValue(searchType, value) {
if (searchType === 'REGEX') return `/${value}/`;
else if (searchType === 'SUBSTRING') return `'${value}'`;
else return `0x${value}`;
},
getPatternColor(pattern) {
if (pattern.actionType === 'FIND') {
return `color: ${pattern.color};`;
} else {
return `color: inherit;`;
}
},
getServiceName(port) {
return this.$store.state.services.find(o => o.port === port)?.name ?? '<Deleted service>'
},
showAddPattern() {
this.patternModalIsCreating = true;
this.patternModalEditingPattern = {};
this.$bvModal.show('patternModal');
console.debug('Showing patternModal (create)');
},
showEditPattern(pattern) {
this.patternModalIsCreating = false;
this.patternModalEditingPattern = {};
this.$nextTick(() => {
this.patternModalEditingPattern = pattern;
this.$bvModal.show('patternModal');
console.debug('Showing patternModal (edit)');
});
},
showLookBack(pattern) {
this.patternIdForLookback = pattern.id;
this.$bvModal.show('lookBackModal');
},
updatePatterns() {
this.$http.get('pattern/')
.then(r => this.$store.commit('setPatterns', r.data))
.catch(e => {
this.$bvToast.toast(`Failed to load patterns: ${e}`, {
title: 'Error',
variant: 'danger',
});
console.error('Failed to load patterns:', e);
});
},
openPattern(pattern) {
if (pattern.actionType === 'IGNORE') {
return;
}
console.debug('Opening pattern w/ id', pattern.id);
this.$router.push({
name: 'stream',
params: this.$route.params,
query: {pattern: pattern.id,},
}, () => {});
},
resetPatternSelection() {
console.debug('Resetting pattern selection');
this.$router.push({
name: 'stream',
params: this.$route.params,
}, () => {});
},
togglePattern(pattern) {
const enabled = !pattern.enabled;
console.debug('Toggling pattern', pattern);
this.$http.post(`pattern/${pattern.id}/enable`, null, {
params: {
enabled,
},
})
.then(response => {
const data = response.data;
console.debug('Done toggling pattern', data);
this.$emit('patternAddComplete');
}).catch(e => {
this.$bvToast.toast(`Failed to toggle pattern: ${e}`, {
title: 'Error',
variant: 'danger',
});
console.error('Failed to toggle pattern', e);
});
},
deletePattern(pattern) {
console.debug('Deleting pattern', pattern);
this.$http.delete(`pattern/${pattern.id}`, null)
.then(() => {
console.debug('Done deleting pattern');
this.$emit('patternAddComplete');
}).catch(e => {
this.$bvToast.toast(`Failed to delete pattern: ${e}`, {
title: 'Error',
variant: 'danger',
});
console.error('Failed to delete pattern', e);
});
},
},
};
</script>
<style scoped>
code {
font-family: "Ubuntu Mono", "Lucida Console", monospace;
font-size: 100%;
}
.patterns-dropdown {
display: inline-flex;
width: 128px !important;
min-width: 128px !important;
max-width: 128px !important;
flex: 0 0 128px !important;
flex-shrink: 0 !important;
box-sizing: border-box;
}
.patterns-dropdown > .btn {
display: inline-flex;
justify-content: center;
align-items: center;
font-size: 9.8px;
letter-spacing: 0.05px;
min-width: 128px !important;
width: 128px !important;
max-width: 128px !important;
flex: 0 0 128px !important;
padding-left: 8px;
padding-right: 8px;
white-space: nowrap;
}
</style>
<style>
.ignore-pattern button {
cursor: default !important;
}
</style>

View File

@@ -0,0 +1,272 @@
<template>
<nav class="col-sm-3 d-none d-sm-block sidebar">
<div class="m-2 d-flex flex-wrap align-items-center" style="gap: 6px;">
<button type="button" class="btn btn-sm"
:title="this.$store.state.pause ? 'Continue' : 'Pause new streams'"
:class="this.$store.state.pause ? 'btn-danger' : 'btn-outline-success'"
@click.stop.prevent="togglePause">
<i :class="this.$store.state.pause ? 'fas fa-play' : 'fas fa-pause'"/>
</button>
<button type="button" class="btn btn-sm ml-1"
:title="this.$store.state.displayFavoritesOnly ? 'Show all streams' : 'Show only favorite streams'"
:class="this.$store.state.displayFavoritesOnly ? 'btn-danger' : 'btn-outline-danger'"
@click.stop.prevent="toggleFavorites">
<i class="fas fa-star"/>
</button>
<button type="button" class="btn btn-outline-primary btn-sm ml-1"
:title="this.$store.state.hexdumpMode ? 'Switch to text view' : 'Switch to hexdump view'"
@click.stop.prevent="toggleHexdump">
<i :class="this.$store.state.hexdumpMode ? 'far fa-file-code' : 'fas fa-align-left'"/>
</button>
<button type="button" class="btn btn-sm btn-outline-warning ml-1" v-if="!this.$store.state.pcapStarted"
title="Start pcap file processing"
@click.stop.prevent="startPcap">
<i class="fas fa-arrow-circle-down"/>
</button>
<button type="button" class="btn btn-sm btn-outline-info" style="float: right;"
title="Scroll to top"
@click.stop.prevent="scrollUp">
<i class="fas fa-angle-double-up"/>
</button>
</div>
<div class="sidebar-sticky">
<ul class="nav flex-column">
<SidebarStream v-for="stream in streams"
:key="stream.id"
:stream="stream"/>
<infinite-loading @infinite="infiniteLoadingHandler" ref="infiniteLoader"/>
</ul>
</div>
</nav>
</template>
<script>
import SidebarStream from './SidebarStream';
export default {
name: 'Sidebar',
props: ['servicePort', 'streamId',],
data() {
return {
streams: [],
navigationKeysCallback: (e) => {
if (!e.ctrlKey) return;
if (e.key === 'ArrowUp') {
e.preventDefault();
const index = this.streams.findIndex(e => e.id === this.$route.params.streamId);
const newStream = this.streams[index - 1];
if (!newStream) return;
const newId = newStream.id;
this.$router.push({
name: 'stream',
params: {servicePort: this.$route.params.servicePort, streamId: newId,},
query: this.$route.query,
});
document.getElementById(`stream-${newId}`).scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'center',
});
} else if (e.key === 'ArrowDown') {
e.preventDefault();
const index = this.streams.findIndex(e => e.id === this.$route.params.streamId);
const newStream = this.streams[index + 1];
if (!newStream) return;
const newId = newStream.id;
this.$router.push({
name: 'stream',
params: {servicePort: this.$route.params.servicePort, streamId: newId,},
query: this.$route.query,
});
document.getElementById(`stream-${newId}`).scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'center',
});
} else if (e.key === 'Home') {
const newStream = this.streams[0];
if (!newStream) return;
const newId = newStream.id;
this.$router.push({
name: 'stream',
params: {servicePort: this.$route.params.servicePort, streamId: newId,},
query: this.$route.query,
});
document.getElementById(`stream-${newId}`).scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'center',
});
} else if (e.key === 'End') {
const newStream = this.streams[this.streams.length - 1];
if (!newStream) return;
const newId = newStream.id;
this.$router.push({
name: 'stream',
params: {servicePort: this.$route.params.servicePort, streamId: newId,},
query: this.$route.query,
});
document.getElementById(`stream-${newId}`).scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'center',
});
}
},
};
},
watch: {
'$route.params.servicePort': function () {
console.debug('Port reselected');
this.streams = [];
this.$refs.infiniteLoader.stateChanger.reset();
},
'$route.query.pattern': function () {
console.debug('Pattern selected');
this.streams = [];
this.$refs.infiniteLoader.stateChanger.reset();
},
},
methods: {
infiniteLoadingHandler($state) {
const ourStreams = this.streams;
const pageSize = this.$store.state.pageSize;
let startsFrom;
if (ourStreams?.length && ourStreams[ourStreams.length - 1]) {
startsFrom = ourStreams[ourStreams.length - 1].id;
} else {
startsFrom = null;
}
this.$http.post(`/stream/${this.$route?.params?.servicePort || 'all'}`, {
startingFrom: startsFrom,
pageSize: pageSize,
pattern: this.$route.query.pattern ? {id: this.$route.query.pattern,} : null,
favorites: this.$store.state.displayFavoritesOnly,
}).then(r => {
const data = r.data;
if (data?.length === 0) {
console.log('Finished loading streams (empty page)');
return $state.complete();
}
if (data[0]?.id === this.streams[0]?.id) {
console.log('Finished loading streams (overlap detected)');
return $state.complete();
}
this.streams.push(...data);
if (data.length < pageSize) {
// this was the last page
console.log('Finished loading streams (last page was not full)');
$state.complete();
} else {
console.log('Loaded another page of streams');
$state.loaded()
}
}).catch(e => {
this.$bvToast.toast(`Failed to load portion of streams: ${e}`, {
title: 'Error',
variant: 'danger',
});
console.error('Failed to load portion of streams:', e);
return $state.error();
});
},
toggleFavorites() {
this.$store.commit('toggleDisplayFavoritesOnly');
this.streams = [];
this.$refs.infiniteLoader.stateChanger.reset();
},
togglePause() {
this.$store.commit('togglePause');
},
toggleHexdump() {
this.$store.commit('toggleHexdumpMode');
},
startPcap() {
this.$http.post('pcap/start')
.then(() => {
this.$store.commit('startPcap');
}).catch(e => {
this.$bvToast.toast(`Failed to start pcap: ${e}`, {
title: 'Error',
variant: 'danger',
});
console.error('Failed to start pcap', e);
});
},
addStreamFromWs(stream) {
const currentPort = parseInt(this.$route?.params?.servicePort, 10);
if (currentPort && currentPort !== stream.service) return;
if (this.$store.state.displayFavoritesOnly || this.$store.state.pause) return;
if (this.$route.query.pattern && !stream.foundPatterns.some(e => e.id === this.$route.query.pattern)) {
return;
}
this.streams.unshift(stream);
},
scrollUp() {
const newStream = this.streams[0];
if (!newStream) return;
const newId = newStream.id;
this.$router.push({
name: 'stream',
params: {servicePort: this.$route.params.servicePort, streamId: newId,},
query: this.$route.query,
});
document.getElementById(`stream-${newId}`).scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'center',
});
},
},
mounted() {
document.addEventListener('keydown', this.navigationKeysCallback);
this.$http.get('pcap/started')
.then(r => this.$store.state.pcapStarted = r.data)
.catch(e => {
console.error('Failed to get pcap status, defaulting to true:', e);
this.$store.state.pcapStarted = true;
});
},
beforeDestroy() {
document.removeEventListener('keydown', this.navigationKeysCallback);
},
components: {
SidebarStream,
},
};
</script>
<style scoped>
.sidebar {
position: fixed;
top: 0;
bottom: 0;
left: 0;
z-index: 100; /* Behind the navbar */
padding: 40px 0 0; /* Height of navbar */
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
}
.sidebar-sticky {
position: relative;
top: 0;
height: calc(100vh - 85px);
/*padding-top: .5rem;*/
overflow-x: hidden;
overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
}
@supports (position: sticky) {
.sidebar-sticky {
position: sticky;
}
}
</style>

View File

@@ -0,0 +1,147 @@
<template>
<li :id="`stream-${stream.id}`" class="nav-item stream-item" :class="{highlight: favorite0}">
<router-link class="nav-link"
:to="{name: 'stream', params: {servicePort: this.stream.service, streamId: this.stream.id}, query: this.$route.query}">
<a @click.stop.prevent="toggleFavorite">
<i class="fa-star" style="color: #DC3545" :class="favorite0 ? 'fas' : 'far'"/>
</a>
{{ stream.id }} {{ stream.protocol }}
<template v-if="stream.ttl">TTL {{ stream.ttl }}</template>
<template v-if="shouldShowServiceName"><br/>{{ getServiceName(stream.service) }} #{{stream.service}}</template>
<br/>
{{ dateToText(stream.startTimestamp) }}
<template v-if="!dateMatches(stream.startTimestamp, stream.endTimestamp)">
- {{ dateToText(stream.endTimestamp, dayMatches(stream.startTimestamp, stream.endTimestamp)) }}
</template>
<template v-if="stream.userAgentHash"><br/>UA: {{ stream.userAgentHash }}</template>
<br/>
{{ stream.sizeBytes }} bytes in {{ stream.packetsCount }} packets
<br/>
<span v-for="pattern in notDeletedFoundPatterns"
:key="pattern.id"
:style="`color: ${pattern.color};`">
{{ pattern.name }}
</span>
</router-link>
</li>
</template>
<script>
export default {
name: 'SidebarStream',
props: {
stream: {
id: Number(),
protocol: String(),
service: Number(),
startTimestamp: Number(),
endTimestamp: Number(),
foundPatternsIds: Array(),
favorite: Boolean(),
ttl: Number(),
userAgentHash: String(),
sizeBytes: Number(),
packetsCount: Number(),
},
},
computed: {
shouldShowServiceName: function () {
return this.$route?.params?.servicePort === undefined;
},
foundPatterns: function () {
return this.$store.state.patterns.filter(p => this.stream.foundPatternsIds.includes(p.id));
},
notDeletedFoundPatterns: function () {
return this.foundPatterns.filter(p => !p.deleted);
},
},
data: function () {
return {
favorite0: this.stream.favorite,
getServiceName: function (port) {
return this.$store.state.services.find(o => o.port === port)?.name ?? '<Deleted service>'
},
};
},
methods: {
dateToText(unixTimestamp, short = false) {
const date = new Date(unixTimestamp);
const options = {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
};
if (!short && !this.isToday(date)) {
options.month = '2-digit';
options.day = '2-digit';
return date.toLocaleDateString('ru-RU', options);
}
return date.toLocaleTimeString('ru-RU', options);
},
isToday(someDate) {
const today = new Date();
return someDate.getDate() === today.getDate() &&
someDate.getMonth() === today.getMonth() &&
someDate.getFullYear() === today.getFullYear();
},
dayMatches(rFirst, rSecond) {
const first = new Date(rFirst);
const second = new Date(rSecond);
return first.getDate() === second.getDate() &&
first.getMonth() === second.getMonth() &&
first.getFullYear() === second.getFullYear();
},
dateMatches(rFirst, rSecond) {
const first = new Date(rFirst);
const second = new Date(rSecond);
return first.getDate() === second.getDate() &&
first.getMonth() === second.getMonth() &&
first.getFullYear() === second.getFullYear() &&
first.getHours() === second.getHours() &&
first.getMinutes() === second.getMinutes() &&
first.getSeconds() === second.getSeconds();
},
toggleFavorite() {
this.$http.post(`stream/${this.stream.id}/${this.favorite0 ? 'unfavorite' : 'favorite'}`)
.then(() => this.favorite0 = !this.favorite0)
.catch(e => {
this.$bvToast.toast(`Failed to fav service: ${e}`, {
title: 'Error',
variant: 'danger',
});
console.error('Failed to fav service', e);
});
},
},
};
</script>
<style scoped>
.nav-link {
font-weight: 600;
color: var(--text);
font-family: var(--pixel-font);
font-size: 12px;
line-height: 1.35;
}
/*noinspection CssUnusedSymbol*/
.nav-link.active {
color: var(--accent);
text-shadow: 0 0 10px rgba(94, 242, 255, 0.35);
}
.highlight {
background: rgba(255, 106, 213, 0.18);
box-shadow: 0 0 12px rgba(255, 106, 213, 0.28);
}
.stream-item {
transition: all .3s;
}
</style>

View File

@@ -0,0 +1,127 @@
<template>
<div>
<input
@change="toggleTheme"
id="checkbox"
type="checkbox"
class="switch-checkbox"
/>
<label for="checkbox" class="switch-label">
<span class="checkbox-emoji">🌙</span>
<span class="checkbox-emoji"></span>
<div
class="switch-toggle"
:class="{ 'switch-toggle-checked': chosenTheme === 'dark' }"
></div>
</label>
</div>
</template>
<script>
export default {
mounted() {
this.initTheme();
},
computed: {
chosenTheme: {
get() {
return this.$store.state.theme || this.detectTheme();
},
set(theme) {
this.$store.commit('setTheme', theme);
this.displayTheme(theme);
},
},
},
methods: {
toggleTheme() {
const activeTheme = this.chosenTheme;
if (activeTheme === "light") {
this.chosenTheme = 'dark';
} else {
this.chosenTheme = 'light';
}
console.debug('Toggling theme from ', activeTheme, ' to ', this.chosenTheme);
},
initTheme() {
this.displayTheme(this.chosenTheme);
},
detectTheme() {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
},
displayTheme(theme) {
document.body.setAttribute('data-theme', theme);
},
},
};
</script>
<style scoped>
.switch-checkbox {
display: none;
}
.switch-label {
align-items: center;
background: var(--button-text-primary-color);
border: calc(var(--button-element-size) * 0.025) solid var(--button-accent-color);
border-radius: var(--button-element-size);
cursor: pointer;
display: flex;
font-size: calc(var(--button-element-size) * 0.3);
height: calc(var(--button-element-size) * 0.35);
position: relative;
padding: calc(var(--button-element-size) * 0.1);
transition: background 0.5s ease;
justify-content: space-between;
width: var(--button-element-size);
z-index: 1;
box-sizing: content-box;
}
.switch-toggle {
position: absolute;
background-color: var(--button-background-color-primary);
border-radius: 50%;
top: calc(var(--button-element-size) * 0.07);
left: calc(var(--button-element-size) * 0.07);
height: calc(var(--button-element-size) * 0.4);
width: calc(var(--button-element-size) * 0.4);
transform: translateX(0);
transition: transform 0.3s ease, background-color 0.5s ease;
}
.switch-toggle-checked {
transform: translateX(calc(var(--button-element-size) * 0.6)) !important;
}
.checkbox-emoji {
color: transparent;
text-shadow: 0 0 0 var(--button-background-color-primary);
}
>>> {
--button-element-size: 3rem;
--button-background-color-primary: var(--panel);
--button-background-color-secondary: var(--bg-2);
--button-accent-color: var(--accent-2);
--button-text-primary-color: var(--text);
}
[data-theme=light] * {
--button-background-color-primary: #f0f4ff;
--button-background-color-secondary: #e4ebff;
--button-accent-color: var(--accent);
--button-text-primary-color: #0d1b2f;
}
</style>

48
frontend/src/main.js Normal file
View File

@@ -0,0 +1,48 @@
import Vue from 'vue';
import BootstrapVue from 'bootstrap-vue';
import Axios from 'axios';
import InfiniteLoading from 'vue-infinite-loading';
import App from './App.vue';
import router from './router';
import store from './store';
import 'bootstrap/dist/css/bootstrap.css';
import 'bootstrap-vue/dist/bootstrap-vue.css';
import '@fortawesome/fontawesome-free/css/all.css';
import "bootstrap-darkmode/scss/darktheme.scss";
import './assets/neon.css';
Vue.config.productionTip = false;
Vue.use(BootstrapVue);
Vue.use(InfiniteLoading, {
slots: {
noResults: 'No results...',
noMore: 'No more data',
error: 'An error has occurred!',
errorBtnText: 'Retry',
},
props: {
spinner: 'waveDots',
},
});
const axiosInstance = Axios.create({
baseURL: '/api', // TO!DO: edit for release!
// baseURL: 'http://192.168.79.131:65000/api',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
timeout: 5000,
withCredentials: true,
maxContentLength: 20000,
});
// noinspection JSUnusedGlobalSymbols
Vue.prototype.$http = axiosInstance;
// noinspection JSUnusedGlobalSymbols
new Vue({
router,
store,
render: h => h(App),
}).$mount('#app');

View File

@@ -0,0 +1,118 @@
// https://github.com/saikojosh/Object-Assign-Deep/blob/master/objectAssignDeep.js
/*
* A unified way of returning a string that describes the type of the given variable.
*/
function getTypeOf(input) {
if (input === null) {
return 'null';
} else if (typeof input === 'undefined') {
return 'undefined';
} else if (typeof input === 'object') {
return (Array.isArray(input) ? 'array' : 'object');
}
return typeof input;
}
/*
* Branching logic which calls the correct function to clone the given value base on its type.
*/
function cloneValue(value) {
// The value is an object so let's clone it.
if (getTypeOf(value) === 'object') {
return quickCloneObject(value);
}
// The value is an array so let's clone it.
else if (getTypeOf(value) === 'array') {
return quickCloneArray(value);
}
// Any other value can just be copied.
return value;
}
/*
* Enumerates the given array and returns a new array, with each of its values cloned (i.e. references broken).
*/
function quickCloneArray(input) {
return input.map(cloneValue);
}
/*
* Enumerates the properties of the given object (ignoring the prototype chain) and returns a new object, with each of
* its values cloned (i.e. references broken).
*/
function quickCloneObject(input) {
const output = {};
for (const key in input) {
if (!Object.prototype.hasOwnProperty.call(input, key)) {
continue;
}
output[key] = cloneValue(input[key]);
}
return output;
}
/*
* Does the actual deep merging.
*/
function executeDeepMerge(target, _objects = [], _options = {}) {
const options = {
arrayBehaviour: _options.arrayBehaviour || 'replace', // Can be "merge" or "replace".
};
// Ensure we have actual objects for each.
const objects = _objects.map(object => object || {});
const output = target || {};
// Enumerate the objects and their keys.
for (let oindex = 0; oindex < objects.length; oindex++) {
const object = objects[oindex];
const keys = Object.keys(object);
for (let kindex = 0; kindex < keys.length; kindex++) {
const key = keys[kindex];
const value = object[key];
const type = getTypeOf(value);
const existingValueType = getTypeOf(output[key]);
if (type === 'object') {
if (existingValueType !== 'undefined') {
const existingValue = (existingValueType === 'object' ? output[key] : {});
output[key] = executeDeepMerge({}, [existingValue, quickCloneObject(value),], options);
} else {
output[key] = quickCloneObject(value);
}
} else if (type === 'array') {
if (existingValueType === 'array') {
const newValue = quickCloneArray(value);
output[key] = (options.arrayBehaviour === 'merge' ? output[key].concat(newValue) : newValue);
} else {
output[key] = quickCloneArray(value);
}
} else {
output[key] = value;
}
}
}
return output;
}
export function objectAssignDeep(target, ...objects) {
return executeDeepMerge(target, objects);
}

21
frontend/src/router.js Normal file
View File

@@ -0,0 +1,21 @@
import Vue from 'vue';
import Router from 'vue-router';
import ContentStream from './views/ContentStream';
import Sidebar from './components/Sidebar';
Vue.use(Router);
export default new Router({
routes: [
{
path: '/:servicePort?/:streamId?',
name: 'stream',
props: true,
components: {
sidebar: Sidebar,
content: ContentStream,
},
},
],
linkActiveClass: 'active',
});

89
frontend/src/store.js Normal file
View File

@@ -0,0 +1,89 @@
import Vue from 'vue';
import Vuex from 'vuex';
import createMutationsSharer from 'vuex-shared-mutations';
import createPersistedState from 'vuex-persistedstate';
Vue.use(Vuex);
// noinspection JSUnusedLocalSymbols,JSUnusedGlobalSymbols
export default new Vuex.Store({
state: {
theme: null,
hexdumpBlockSize: 16,
hexdumpLineNumberBase: 10,
pageSize: 25,
displayFavoritesOnly: false,
pause: false,
pcapStarted: true,
hexdumpMode: false,
serviceModalName: '',
serviceModalId: 0,
patterns: [],
services: [],
currentPacketsCount: 0,
currentStreamsCount: 0,
currentServicesPacketsCount: {},
currentServicesStreamsCount: {},
},
mutations: {
setTheme: (s, p) => s.theme = p,
setHexdumpBlockSize: (s, p) => s.hexdumpBlockSize = p,
setHexdumpLineNumberBase: (s, p) => s.hexdumpLineNumberBase = p,
setPageSize: (s, p) => s.pageSize = p,
// eslint-disable-next-line no-unused-vars
toggleDisplayFavoritesOnly: (s, p) => s.displayFavoritesOnly = !s.displayFavoritesOnly,
// eslint-disable-next-line no-unused-vars
togglePause: (s, p) => s.pause = !s.pause,
// eslint-disable-next-line no-unused-vars
startPcap: (s, p) => s.pcapStarted = true,
// eslint-disable-next-line no-unused-vars
toggleHexdumpMode: (s, p) => s.hexdumpMode = !s.hexdumpMode,
setServiceModalName: (s, p) => s.serviceModalName = p,
setServiceModalId: (s, p) => s.serviceModalId = p,
setPatterns: (s, p) => s.patterns = p,
addPattern: (s, p) => s.patterns.push(p),
setServices: (s, p) => s.services = p,
addService: (s, p) => s.services.push(p),
setCurrentPacketsCount: (s, p) => s.currentPacketsCount = p,
setCurrentStreamsCount: (s, p) => s.currentStreamsCount = p,
setCurrentServicesPacketsCount: (s, p) => s.currentServicesPacketsCount = p,
setCurrentServicesStreamsCount: (s, p) => s.currentServicesStreamsCount = p,
},
actions: {},
plugins: [
createPersistedState(),
createMutationsSharer({
// eslint-disable-next-line no-unused-vars
predicate: (mutation, state) => {
console.debug('Got mutation:', mutation);
const mName = mutation?.type;
const process = mName !== 'toggleDisplayFavoritesOnly'
&& mName !== 'setTheme'
&& mName !== 'setServiceModalName'
&& mName !== 'setServiceModalId'
&& mName !== 'togglePause'
&& mName !== 'startPcap'
&& mName !== 'toggleHexdumpMode'
&& mName !== 'setPatterns'
&& mName !== 'addPattern'
&& mName !== 'setServices'
&& mName !== 'addService'
&& mName !== 'setCurrentPacketsCount'
&& mName !== 'setCurrentStreamsCount'
&& mName !== 'setCurrentServicesPacketsCount'
&& mName !== 'setCurrentServicesStreamsCount';
console.debug('Processing?', process);
return process;
},
}),
],
});

View File

@@ -0,0 +1,126 @@
<template>
<div>
<div class="legend-bar">
<div class="d-flex align-items-center" style="gap: 10px;">
<span class="legend-pill request-pill">Request</span>
<span class="legend-pill response-pill">Response</span>
</div>
</div>
<Packet v-for="packetWithOffset in packetsWithOffsets"
:key="packetWithOffset.packet.id"
:packet="packetWithOffset.packet"
:offset="packetWithOffset.offset" />
<infinite-loading @infinite="infiniteLoadingHandler" ref="infiniteLoader">
<span slot="no-results"></span>
</infinite-loading>
</div>
</template>
<script>
import Packet from '../components/Packet';
export default {
name: 'ContentStream',
props: ['servicePort', 'streamId',],
data() {
return {
packets: [],
};
},
computed: {
packetsWithOffsets: function() {
return this.packets.map((el, i) => {
let offset = null;
if (i !== 0) {
offset = el.timestamp - this.packets[i - 1].timestamp;
}
return {packet: el, offset: offset,}
})
},
},
watch: {
'$route.params.streamId': function () {
this.packets = [];
this.$refs.infiniteLoader.stateChanger.reset();
},
},
methods: {
infiniteLoadingHandler($state) {
if (!this.$route.params.streamId) return $state.complete();
const packets = this.packets;
const pageSize = this.$store.state.pageSize;
let startsFrom;
if (packets && packets.length && packets[packets.length - 1]) {
startsFrom = packets[packets.length - 1].id;
} else {
startsFrom = null;
}
this.$http.post(`packet/${this.$route.params.streamId}`, {
startingFrom: startsFrom,
pageSize: pageSize,
}).then(response => {
const data = response.data;
if (data.length === 0) {
console.log('Finished loading packets (empty page)');
return $state.complete();
}
if (data[0] && this.packets[0] && data[0].id === this.packets[0].id) {
console.log('Finished loading packets (overlap detected)');
return $state.complete();
}
this.packets.push(...data);
if (data.length < pageSize) {
// this was the last page
console.log('Finished loading packets (last page was not full)');
$state.complete();
} else {
console.log('Loaded another page of packets');
$state.loaded();
}
}).catch(e => {
this.$bvToast.toast(`Failed to load portion of packets: ${e}`, {
title: 'Error',
variant: 'danger',
});
console.error('Failed to load portion of packets:', e);
return $state.error();
});
},
},
components: {
Packet,
},
};
</script>
<style>
:root {
--response-packet-color: rgba(255, 106, 213, 0.16);
--request-packet-color: rgba(94, 242, 255, 0.2);
}
:root [data-theme=dark] {
color-scheme: dark;
--response-packet-color: rgba(255, 106, 213, 0.16);
--request-packet-color: rgba(94, 242, 255, 0.2);
}
.request-pill {
border-color: rgba(94, 242, 255, 0.45);
color: #c9f8ff;
background: rgba(94, 242, 255, 0.1);
}
.response-pill {
border-color: rgba(255, 106, 213, 0.45);
color: #ffd6f6;
background: rgba(255, 106, 213, 0.1);
}
</style>

View File

@@ -0,0 +1,57 @@
<template>
<b-modal @ok="lookBack" id="lookBackModal"
title="Search into the past"
cancel-title="Cancel"
centered scrollable>
<form ref="lookBackForm">
<b-form-group
label-cols-sm="4"
label-cols-lg="3"
label="Minutes"
description="How far into the past do we want to look"
label-for="lookback-minutes">
<b-form-input @keydown.native.enter="lookBack" id="lookback-minutes" required v-model="minutes"/>
</b-form-group>
</form>
</b-modal>
</template>
<script>
export default {
name: 'LookBack',
props: {
patternId: Number,
},
data() {
return {
minutes: 5,
};
},
methods: {
checkValidity() {
return this.$refs.lookBackForm.reportValidity();
},
lookBack(ev) {
ev.preventDefault();
if (!this.checkValidity()) {
console.debug('Form is invalid');
return;
}
console.debug(`Looking back with pattern ${this.patternId}`);
this.$http.post(`pattern/${this.patternId}/lookback`, this.minutes)
.then(() => {
console.debug('Lookback started');
this.$bvModal.hide('lookBackModal');
}).catch(e => {
this.$bvToast.toast(`Failed to start lookback: ${e}`, {
title: 'Error',
variant: 'danger',
});
console.error('Failed to start lookback', e);
});
},
},
};
</script>

View File

@@ -0,0 +1,205 @@
<template>
<b-modal @ok="addPattern" @cancel="reset" id="patternModal"
:title="creating ? 'Creating pattern' : 'Editing pattern'"
cancel-title="Cancel"
centered scrollable>
<form ref="addPatternForm">
<b-form-group
label-cols-sm="4"
label-cols-lg="3"
label="Name"
description="It is displayed in the list and highlighted in the packet contents"
label-for="pattern-name">
<b-form-input @keydown.native.enter="addPattern" id="pattern-name" required v-model="newPattern.name"/>
</b-form-group>
<b-form-group
v-if="creating"
label-cols-sm="4"
label-cols-lg="3"
label="Pattern"
description="Substring, RegEx or bytes"
label-for="pattern-value">
<b-form-input @keydown.native.enter="addPattern" @keydown="validateKey"
id="pattern-value" required v-model="newPattern.value"
:placeholder="getPlaceholder()"/>
</b-form-group>
<b-form-group
v-if="creating"
label-cols-sm="4"
label-cols-lg="3"
label="Search action"
description="What to do with matching streams"
label-for="pattern-actionType">
<b-form-select id="pattern-actionType" required v-model="newPattern.actionType">
<option value="FIND" selected>Highlight found pattern</option>
<option value="IGNORE">Ignore matching streams</option>
</b-form-select>
</b-form-group>
<b-form-group v-if="newPattern.actionType === 'FIND'"
label-cols-sm="4"
label-cols-lg="3"
label="Color"
description="The highlight color of the pattern in the packets and streams"
label-for="pattern-color">
<b-form-input id="pattern-color" required type="color" v-model="newPattern.color"/>
</b-form-group>
<b-form-group
v-if="creating"
label-cols-sm="4"
label-cols-lg="3"
label="Search method"
description="The way to search for patterns in packets"
label-for="pattern-searchType">
<b-form-select id="pattern-searchType" required v-model="newPattern.searchType">
<option value="REGEX" selected>Regular expression</option>
<option value="SUBSTRING">Substring</option>
<option value="SUBBYTES">Bytes</option>
</b-form-select>
</b-form-group>
<b-form-group
v-if="creating"
label-cols-sm="4"
label-cols-lg="3"
label="Search type"
description="In which packets to search for a pattern"
label-for="pattern-type">
<b-form-select id="pattern-type" required v-model="newPattern.directionType">
<option value="INPUT">Request</option>
<option value="OUTPUT">Response</option>
<option value="BOTH" selected>Anywhere</option>
</b-form-select>
</b-form-group>
<b-form-group
v-if="creating"
label-cols-sm="4"
label-cols-lg="3"
label="Service"
description="Apply this pattern only to the specific service"
label-for="pattern-service">
<b-form-select id="pattern-service" :options="serviceOptions"
v-model="newPattern.serviceId"></b-form-select>
</b-form-group>
</form>
</b-modal>
</template>
<script>
const hexRegex = /[0-9A-Fa-f ]/;
const defaultPattern = {
name: '',
value: '',
color: '#FF7474',
searchType: 'SUBSTRING',
directionType: 'BOTH',
actionType: 'FIND',
serviceId: null,
};
export default {
name: 'PatternModal',
props: {
creating: Boolean,
initialPattern: {
name: String,
value: String,
color: String,
searchType: String,
directionType: String,
actionType: String,
serviceId: Number,
},
},
data() {
return {
newPattern: {},
};
},
watch: {
initialPattern() {
console.debug('initialService changed, reassigning...', this.initialPattern);
this.newPattern = {...defaultPattern, ...this.initialPattern,};
},
},
computed: {
serviceOptions: function () {
let services = this.$store.state.services;
let options = services.map(service => {
return {
value: service.port,
text: `${service.name} #${service.port}`,
}
});
return [
{ value: null, text: 'Any service', },
...options,
];
},
},
methods: {
getPlaceholder() {
if (this.newPattern.searchType === 'REGEX') return '[A-Z0-9]{31}=';
else if (this.newPattern.searchType === 'SUBSTRING') return 'HTTP/2';
else return 'DEAD BEEF 1337';
},
validateKey(e) {
console.log('', e);
if (this.newPattern.searchType !== 'SUBBYTES') return;
if (!hexRegex.test(e?.key)) {
e.preventDefault();
}
},
checkValidity() {
return this.$refs.addPatternForm.reportValidity();
},
reset() {
this.newPattern = {...defaultPattern,};
},
addPattern(ev) {
ev.preventDefault();
if (!this.checkValidity()) {
console.debug('Form is invalid');
return;
}
console.debug('Adding/editing pattern...', this.newPattern);
if (this.newPattern.searchType === 'SUBBYTES') {
this.newPattern.value = this.newPattern.value.replace(/\s+/g, '').toLowerCase();
}
let url;
if (this.creating) {
url = 'pattern/'
} else {
url = 'pattern/' + this.newPattern.id
}
this.$http.post(url, this.newPattern)
.then(response => {
const data = response.data;
console.debug('Done adding/editing pattern', data);
this.$emit('patternAddComplete');
this.reset();
this.$bvModal.hide('patternModal');
}).catch(e => {
this.$bvToast.toast(`Failed to add pattern: ${e}`, {
title: 'Error',
variant: 'danger',
});
console.error('Failed to add pattern', e);
});
},
},
};
</script>

View File

@@ -0,0 +1,186 @@
<template>
<b-modal id="serviceModal" size="lg"
:title="creating ? 'Creating service' : 'Editing service'"
centered scrollable @ok.prevent="submit">
<b-form ref="serviceForm">
<b-form-group label-cols-sm="4"
label="Name"
label-for="service-name">
<b-form-input id="service-name" required v-model="service.name"
@keyup.enter.stop.prevent="submit"/>
</b-form-group>
<b-form-group v-if="creating"
label-cols-sm="4"
label="Port"
label-for="service-port">
<b-form-input id="service-port" required
type="number" min="1" max="65535" v-model.number="service.port"
@keyup.enter.stop.prevent="submit"/>
</b-form-group>
<b-form-group label-cols-sm="4"
label="Is an HTTP service"
label-for="service-is-http">
<b-form-checkbox id="service-is-http" required
v-model="service.http" />
</b-form-group>
<b-form-group label-cols-sm="4"
label="Apply urldecode"
label-for="service-urldecode">
<b-form-checkbox id="service-urldecode" required
v-model="service.urldecodeHttpRequests"/>
</b-form-group>
<b-form-group label-cols-sm="4"
label="Merge adjacent packets"
label-for="service-mergeAdjacent">
<b-form-checkbox id="service-mergeAdjacent" required
v-model="service.mergeAdjacentPackets"/>
</b-form-group>
<b-form-group label-cols-sm="4"
label="Inflate WebSockets"
label-for="service-inflateWS">
<b-form-checkbox id="service-inflateWS" required
v-model="service.parseWebSockets"/>
</b-form-group>
<b-form-group label-cols-sm="4"
label="Decrypt TLS (TLS_RSA_WITH_AES only)"
label-for="service-decryptTls">
<b-form-checkbox id="service-decryptTls" required
v-model="service.decryptTls"/>
</b-form-group>
<b-button v-if="!creating" variant="danger" @click="deleteService">Delete</b-button>
</b-form>
</b-modal>
</template>
<script>
import {objectAssignDeep,} from '@/objectAssignDeep';
export default {
name: 'ServiceModal',
props: {
creating: Boolean,
initialService: {
name: String,
port: Number,
http: Boolean,
urldecodeHttpRequests: Boolean,
mergeAdjacentPackets: Boolean,
parseWebSockets: Boolean,
decryptTls: Boolean,
},
},
data() {
return {
service: {},
};
},
watch: {
initialService() {
console.debug('initialService changed, reassigning...', this.initialService);
this.service = objectAssignDeep({}, this.initialService);
},
},
methods: {
submit() {
if (!this.$refs.serviceForm.reportValidity()) {
return;
}
console.debug('Submitting service...', this.service, this.creating);
let url;
if (this.creating) {
url = 'service/';
} else {
url = 'service/' + this.initialService.port
}
this.$http.post(url, this.service)
.then(response => {
console.info('Done editing/creating service', response.data);
this.$emit('service-update-needed');
this.$bvModal.hide('serviceModal');
})
.catch(e => {
this.$bvToast.toast(`Failed to ${this.creating ? 'create' : 'edit'} service: ${e}`, {
title: 'Error',
variant: 'danger',
});
console.error('Failed to edit/create service', e);
});
},
deleteService() {
this.$http.delete(`service/${this.service.port}`)
.then(() => {
console.info('Done deleting service', this.service);
this.$emit('service-update-needed');
this.$bvModal.hide('serviceModal');
}).catch(e => {
this.$bvToast.toast(`Failed to delete service: ${e}`, {
title: 'Error',
variant: 'danger',
});
console.error('Failed to delete service', e);
});
},
},
};
</script>
<style scoped>
.form-control {
background: rgba(122, 29, 255, 0.06);
border: 1px solid rgba(122, 29, 255, 0.5);
color: var(--accent);
font-family: var(--pixel-font);
font-size: 11px;
letter-spacing: 0.5px;
box-shadow: none;
}
.form-control:focus {
background: rgba(122, 29, 255, 0.12);
border-color: rgba(198, 107, 255, 0.9);
color: var(--text);
box-shadow: 0 0 12px rgba(198, 107, 255, 0.25);
}
::v-deep .custom-control-input:focus ~ .custom-control-label::before {
box-shadow: 0 0 8px rgba(198, 107, 255, 0.5);
}
::v-deep .custom-checkbox .custom-control-label::before {
border: 1px solid rgba(122, 29, 255, 0.7);
background: rgba(122, 29, 255, 0.08);
box-shadow: inset 0 0 0 1px rgba(198, 107, 255, 0.25);
}
::v-deep .custom-control-label::after {
background-size: 100% 100%;
}
::v-deep .custom-checkbox .custom-control-input:checked ~ .custom-control-label::before {
background: rgba(122, 29, 255, 0.25);
border-color: rgba(198, 107, 255, 0.9);
box-shadow: 0 0 10px rgba(198, 107, 255, 0.35);
}
::v-deep .custom-checkbox .custom-control-input:checked ~ .custom-control-label::after {
background: transparent;
content: "";
left: -1.3rem;
top: 0.2rem;
width: 1rem;
height: 1rem;
opacity: 1;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' shape-rendering='crispEdges'%3E%3Crect x='2' y='8' width='2' height='2' fill='%23c66bff'/%3E%3Crect x='4' y='10' width='2' height='2' fill='%23c66bff'/%3E%3Crect x='6' y='8' width='2' height='2' fill='%23c66bff'/%3E%3Crect x='8' y='6' width='2' height='2' fill='%23c66bff'/%3E%3Crect x='10' y='4' width='2' height='2' fill='%23c66bff'/%3E%3C/svg%3E");
}
</style>

View File

@@ -0,0 +1,63 @@
<template>
<b-modal id="settingsModal" title="Settings" centered scrollable ok-only>
<b-form-group
label-cols-sm="4"
label-cols-lg="3"
description="The number of bytes to display in the binary representation of the packet"
label="HEX block width"
label-for="settings-hexdumpBlockSize">
<b-form-input type="number" id="settings-hexdumpBlockSize" v-model.number="hexdumpBlockSize"/>
</b-form-group>
<b-form-group
label-cols-sm="4"
label-cols-lg="3"
description="The number system used in line numbers of a binary representation of a packet"
label="Line numbering"
label-for="settings-hexdumpLineNumberBase">
<b-form-select id="settings-hexdumpLineNumberBase" v-model="hexdumpLineNumberBase">
<option :value="10" selected>Decimal</option>
<option :value="16">Hexadecimal</option>
</b-form-select>
</b-form-group>
<b-form-group
label-cols-sm="4"
label-cols-lg="3"
description="The number of streams to download at a time"
label="Page size"
label-for="settings-pageSize">
<b-form-input type="number" id="settings-pageSize" v-model.number="pageSize"/>
</b-form-group>
</b-modal>
</template>
<script>
export default {
name: 'Settings',
computed: {
hexdumpBlockSize: {
get() {
return this.$store.state.hexdumpBlockSize;
},
set(v) {
this.$store.commit('setHexdumpBlockSize', v);
},
},
hexdumpLineNumberBase: {
get() {
return this.$store.state.hexdumpLineNumberBase;
},
set(v) {
this.$store.commit('setHexdumpLineNumberBase', v);
},
},
pageSize: {
get() {
return this.$store.state.pageSize;
},
set(v) {
this.$store.commit('setPageSize', v);
},
},
},
};
</script>

3
frontend/vue.config.js Normal file
View File

@@ -0,0 +1,3 @@
module.exports = {
lintOnSave: false,
};

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

After

Width:  |  Height:  |  Size: 353 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -3,4 +3,4 @@ pluginManagement {
gradlePluginPortal()
}
}
rootProject.name = 'packmate'
rootProject.name = "packmate"

View File

@@ -1,28 +1,35 @@
package ru.serega6531.packmate.configuration;
import org.modelmapper.Converter;
import org.modelmapper.ModelMapper;
import org.modelmapper.TypeMap;
import org.pcap4j.core.PcapNativeException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import ru.serega6531.packmate.model.enums.CaptureMode;
import ru.serega6531.packmate.model.Pattern;
import ru.serega6531.packmate.model.Stream;
import ru.serega6531.packmate.model.pojo.StreamDto;
import ru.serega6531.packmate.pcap.FilePcapWorker;
import ru.serega6531.packmate.pcap.LivePcapWorker;
import ru.serega6531.packmate.pcap.NoOpPcapWorker;
import ru.serega6531.packmate.pcap.PcapWorker;
import ru.serega6531.packmate.properties.PackmateProperties;
import ru.serega6531.packmate.service.ServicesService;
import ru.serega6531.packmate.service.StreamService;
import ru.serega6531.packmate.service.SubscriptionService;
import java.net.UnknownHostException;
import java.util.Set;
import java.util.stream.Collectors;
@Configuration
@EnableScheduling
@EnableAsync
@ConfigurationPropertiesScan("ru.serega6531.packmate.properties")
public class ApplicationConfiguration {
@Bean(destroyMethod = "stop")
@@ -30,20 +37,37 @@ public class ApplicationConfiguration {
public PcapWorker pcapWorker(ServicesService servicesService,
StreamService streamService,
SubscriptionService subscriptionService,
@Value("${local-ip}") String localIpString,
@Value("${interface-name}") String interfaceName,
@Value("${pcap-file}") String filename,
@Value("${capture-mode}") CaptureMode captureMode) throws PcapNativeException, UnknownHostException {
return switch (captureMode) {
case LIVE -> new LivePcapWorker(servicesService, streamService, localIpString, interfaceName);
case FILE -> new FilePcapWorker(servicesService, streamService, subscriptionService, localIpString, filename);
PackmateProperties properties
) throws PcapNativeException, UnknownHostException {
return switch (properties.captureMode()) {
case LIVE -> new LivePcapWorker(servicesService, streamService, properties.localIp(), properties.interfaceName());
case FILE ->
new FilePcapWorker(servicesService, streamService, subscriptionService, properties.localIp(), properties.pcapFile());
case VIEW -> new NoOpPcapWorker();
};
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
public ModelMapper modelMapper() {
ModelMapper modelMapper = new ModelMapper();
addStreamMapper(modelMapper);
return modelMapper;
}
private void addStreamMapper(ModelMapper modelMapper) {
TypeMap<Stream, StreamDto> streamMapper = modelMapper.createTypeMap(Stream.class, StreamDto.class);
Converter<Set<Pattern>, Set<Integer>> patternSetToIdSet = ctx -> ctx.getSource()
.stream()
.map(Pattern::getId)
.collect(Collectors.toSet());
streamMapper.addMappings(mapping ->
mapping.using(patternSetToIdSet)
.map(Stream::getFoundPatterns, StreamDto::setFoundPatternsIds)
);
}
}

View File

@@ -1,57 +1,80 @@
package ru.serega6531.packmate.configuration;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.EventListener;
import org.springframework.security.authentication.event.AuthenticationFailureBadCredentialsEvent;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import ru.serega6531.packmate.properties.PackmateProperties;
import ru.serega6531.packmate.security.FakeAdminAuthFilter;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@Configuration
@EnableWebSecurity
@Slf4j
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
public class SecurityConfiguration {
@Value("${account-login}")
private String login;
@Bean
public InMemoryUserDetailsManager userDetailsService(PackmateProperties properties, PasswordEncoder passwordEncoder) {
List<UserDetails> users = new ArrayList<>();
@Value("${account-password}")
private String password;
users.add(User.builder()
.username(properties.web().accountLogin())
.password(passwordEncoder.encode(properties.web().accountPassword()))
.roles("USER")
.build());
private final PasswordEncoder passwordEncoder;
Optional.ofNullable(properties.web().fakeAdmin())
.filter(PackmateProperties.FakeAdminProperties::enabled)
.ifPresent(fakeAdmin -> users.add(User.builder()
.username("admin")
.password(passwordEncoder.encode("admin"))
.roles("FAKE")
.build()));
@Autowired
public SecurityConfiguration(PasswordEncoder passwordEncoder) {
this.passwordEncoder = passwordEncoder;
return new InMemoryUserDetailsManager(users);
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser(login)
.password(passwordEncoder.encode(password))
.authorities("ROLE_USER");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf()
@Bean
public SecurityFilterChain filterChain(HttpSecurity http, FakeAdminAuthFilter fakeAdminAuthFilter) throws Exception {
return http.csrf()
.disable()
.authorizeRequests()
.antMatchers("/site.webmanifest")
.permitAll()
.anyRequest().authenticated()
.authorizeHttpRequests((auth) ->
auth.requestMatchers("/site.webmanifest", "/fake-admin/**", "/fake/**", "/api/fake/**")
.permitAll()
.requestMatchers("/api/**", "/ws/**")
.hasRole("USER")
.anyRequest()
.authenticated()
)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.httpBasic()
.and()
.headers()
.frameOptions()
.sameOrigin();
.sameOrigin()
.and()
.addFilterAfter(fakeAdminAuthFilter, BasicAuthenticationFilter.class)
.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@EventListener

View File

@@ -0,0 +1,27 @@
package ru.serega6531.packmate.controller;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import ru.serega6531.packmate.security.FakeAdminResponder;
@RestController
@RequestMapping("/fake-admin")
@RequiredArgsConstructor
public class FakeAdminController {
private final FakeAdminResponder responder;
@GetMapping(value = "/fun", produces = MediaType.TEXT_HTML_VALUE)
public ResponseEntity<String> fun() {
return ResponseEntity.ok(responder.funPageHtml());
}
@GetMapping(value = "/packets", produces = MediaType.TEXT_HTML_VALUE)
public ResponseEntity<String> fakePackets() {
return ResponseEntity.ok(responder.fakePacketsHtml());
}
}

View File

@@ -0,0 +1,23 @@
package ru.serega6531.packmate.controller;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import ru.serega6531.packmate.model.pojo.FakeServiceDto;
import ru.serega6531.packmate.service.ServicesService;
import java.util.List;
@RestController
@RequestMapping("/api/fake/")
@RequiredArgsConstructor
public class FakeFacadeController {
private final ServicesService servicesService;
@GetMapping("services")
public List<FakeServiceDto> getServices() {
return servicesService.findAllForFakeFacade();
}
}

View File

@@ -1,14 +1,16 @@
package ru.serega6531.packmate.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import ru.serega6531.packmate.model.Packet;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import ru.serega6531.packmate.model.pojo.PacketDto;
import ru.serega6531.packmate.model.pojo.PacketPagination;
import ru.serega6531.packmate.service.StreamService;
import java.util.List;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/api/packet/")
@@ -23,10 +25,7 @@ public class PacketController {
@PostMapping("/{streamId}")
public List<PacketDto> getPacketsForStream(@PathVariable long streamId, @RequestBody PacketPagination pagination) {
List<Packet> packets = streamService.getPackets(streamId, pagination.getStartingFrom(), pagination.getPageSize());
return packets.stream()
.map(streamService::packetToDto)
.collect(Collectors.toList());
return streamService.getPackets(streamId, pagination.getStartingFrom(), pagination.getPageSize());
}
}

View File

@@ -1,13 +1,20 @@
package ru.serega6531.packmate.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import ru.serega6531.packmate.model.Pattern;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import ru.serega6531.packmate.model.pojo.PatternCreateDto;
import ru.serega6531.packmate.model.pojo.PatternDto;
import ru.serega6531.packmate.model.pojo.PatternUpdateDto;
import ru.serega6531.packmate.service.PatternService;
import java.util.List;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/api/pattern/")
@@ -24,14 +31,19 @@ public class PatternController {
public List<PatternDto> getPatterns() {
return service.findAll()
.stream().map(service::toDto)
.collect(Collectors.toList());
.toList();
}
@PostMapping("/{id}")
@PostMapping("/{id}/enable")
public void enable(@PathVariable int id, @RequestParam boolean enabled) {
service.enable(id, enabled);
}
@DeleteMapping("/{id}")
public void delete(@PathVariable int id) {
service.delete(id);
}
@PostMapping("/{id}/lookback")
public void lookBack(@PathVariable int id, @RequestBody int minutes) {
if (minutes < 1) {
@@ -42,11 +54,13 @@ public class PatternController {
}
@PostMapping
public PatternDto addPattern(@RequestBody PatternDto dto) {
dto.setEnabled(true);
Pattern pattern = service.fromDto(dto);
Pattern saved = service.save(pattern);
return service.toDto(saved);
public PatternDto addPattern(@RequestBody PatternCreateDto dto) {
return service.create(dto);
}
@PostMapping("/{id}")
public PatternDto updatePattern(@PathVariable int id, @RequestBody PatternUpdateDto dto) {
return service.update(id, dto);
}
}

View File

@@ -1,13 +1,19 @@
package ru.serega6531.packmate.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import ru.serega6531.packmate.model.CtfService;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import ru.serega6531.packmate.model.pojo.ServiceCreateDto;
import ru.serega6531.packmate.model.pojo.ServiceDto;
import ru.serega6531.packmate.model.pojo.ServiceUpdateDto;
import ru.serega6531.packmate.service.ServicesService;
import java.util.List;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/api/service/")
@@ -22,9 +28,7 @@ public class ServiceController {
@GetMapping
public List<ServiceDto> getServices() {
return service.findAll().stream()
.map(service::toDto)
.collect(Collectors.toList());
return service.findAll();
}
@DeleteMapping("/{port}")
@@ -33,9 +37,13 @@ public class ServiceController {
}
@PostMapping
public CtfService addService(@RequestBody ServiceDto dto) {
CtfService newService = this.service.fromDto(dto);
return this.service.save(newService);
public ServiceDto addService(@RequestBody ServiceCreateDto dto) {
return this.service.create(dto);
}
@PostMapping("/{port}")
public ServiceDto updateService(@PathVariable int port, @RequestBody ServiceUpdateDto dto) {
return this.service.update(port, dto);
}
}

View File

@@ -1,14 +1,17 @@
package ru.serega6531.packmate.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import ru.serega6531.packmate.model.pojo.StreamPagination;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import ru.serega6531.packmate.model.pojo.StreamDto;
import ru.serega6531.packmate.model.pojo.StreamPagination;
import ru.serega6531.packmate.service.StreamService;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/api/stream/")
@@ -23,16 +26,12 @@ public class StreamController {
@PostMapping("/all")
public List<StreamDto> getStreams(@RequestBody StreamPagination pagination) {
return service.findAll(pagination, Optional.empty(), pagination.isFavorites()).stream()
.map(service::streamToDto)
.collect(Collectors.toList());
return service.findAll(pagination, Optional.empty(), pagination.isFavorites());
}
@PostMapping("/{port}")
public List<StreamDto> getStreams(@PathVariable int port, @RequestBody StreamPagination pagination) {
return service.findAll(pagination, Optional.of(port), pagination.isFavorites()).stream()
.map(service::streamToDto)
.collect(Collectors.toList());
return service.findAll(pagination, Optional.of(port), pagination.isFavorites());
}
@PostMapping("/{id}/favorite")

View File

@@ -0,0 +1,15 @@
package ru.serega6531.packmate.exception;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.File;
@EqualsAndHashCode(callSuper = true)
@Data
public class PcapFileNotFoundException extends RuntimeException {
private final File file;
private final File directory;
}

View File

@@ -0,0 +1,15 @@
package ru.serega6531.packmate.exception;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
@EqualsAndHashCode(callSuper = true)
@Data
public class PcapInterfaceNotFoundException extends RuntimeException {
private final String requestedInterface;
private final List<String> existingInterfaces;
}

View File

@@ -0,0 +1,42 @@
package ru.serega6531.packmate.exception.analyzer;
import org.springframework.boot.diagnostics.AbstractFailureAnalyzer;
import org.springframework.boot.diagnostics.FailureAnalysis;
import ru.serega6531.packmate.exception.PcapFileNotFoundException;
import java.io.File;
import java.util.Arrays;
import java.util.List;
public class PcapFileNotFoundFailureAnalyzer extends AbstractFailureAnalyzer<PcapFileNotFoundException> {
@Override
protected FailureAnalysis analyze(Throwable rootFailure, PcapFileNotFoundException cause) {
String description = "The file " + cause.getFile().getAbsolutePath() + " was not found";
String existingFilesMessage;
File[] existingFiles = cause.getDirectory().listFiles();
if (existingFiles == null) {
return new FailureAnalysis(
description,
"Make sure you've put the pcap file to the ./pcaps directory, not the root directory. " +
"The directory currently does not exist",
cause
);
}
if (existingFiles.length == 0) {
existingFilesMessage = "The pcaps directory is currently empty";
} else {
List<String> existingFilesNames = Arrays.stream(existingFiles).map(File::getName).toList();
existingFilesMessage = "The files present in " + cause.getDirectory().getAbsolutePath() + " are: " + existingFilesNames;
}
return new FailureAnalysis(
description,
"Please verify the file name. Make sure you've put the pcap file to the ./pcaps directory, not the root directory.\n" +
existingFilesMessage,
cause
);
}
}

View File

@@ -0,0 +1,16 @@
package ru.serega6531.packmate.exception.analyzer;
import org.springframework.boot.diagnostics.AbstractFailureAnalyzer;
import org.springframework.boot.diagnostics.FailureAnalysis;
import ru.serega6531.packmate.exception.PcapInterfaceNotFoundException;
public class PcapInterfaceNotFoundFailureAnalyzer extends AbstractFailureAnalyzer<PcapInterfaceNotFoundException> {
@Override
protected FailureAnalysis analyze(Throwable rootFailure, PcapInterfaceNotFoundException cause) {
return new FailureAnalysis(
"The interface \"" + cause.getRequestedInterface() + "\" was not found",
"Check the interface name in the config. Existing interfaces are: " + cause.getExistingInterfaces(),
cause
);
}
}

View File

@@ -3,10 +3,10 @@ package ru.serega6531.packmate.model;
import lombok.*;
import org.hibernate.Hibernate;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.util.Objects;
@Getter
@@ -25,9 +25,7 @@ public class CtfService {
private boolean decryptTls;
private boolean processChunkedEncoding;
private boolean ungzipHttp;
private boolean http;
private boolean urldecodeHttpRequests;

View File

@@ -5,7 +5,7 @@ import org.hibernate.Hibernate;
import org.hibernate.annotations.GenericGenerator;
import org.hibernate.annotations.Parameter;
import javax.persistence.*;
import jakarta.persistence.*;
import java.util.Objects;
@Entity

View File

@@ -5,7 +5,7 @@ import org.hibernate.Hibernate;
import org.hibernate.annotations.GenericGenerator;
import org.hibernate.annotations.Parameter;
import javax.persistence.*;
import jakarta.persistence.*;
import java.util.Objects;
import java.util.Set;
@@ -24,7 +24,7 @@ import java.util.Set;
}
)
@AllArgsConstructor
@Builder
@Builder(toBuilder = true)
@Table(indexes = { @Index(name = "stream_id_index", columnList = "stream_id") })
public class Packet {
@@ -49,11 +49,13 @@ public class Packet {
private boolean incoming; // true если от клиента к серверу, иначе false
private boolean ungzipped;
private boolean httpProcessed = false;
private boolean webSocketParsed;
private boolean webSocketParsed = false;
private boolean tlsDecrypted;
private boolean tlsDecrypted = false;
private boolean hasHttpBody = false;
@Column(nullable = false)
private byte[] content;

View File

@@ -1,5 +1,10 @@
package ru.serega6531.packmate.model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
@@ -11,14 +16,13 @@ import ru.serega6531.packmate.model.enums.PatternActionType;
import ru.serega6531.packmate.model.enums.PatternDirectionType;
import ru.serega6531.packmate.model.enums.PatternSearchType;
import javax.persistence.*;
import java.util.Objects;
@Getter
@Setter
@RequiredArgsConstructor
@ToString
@Entity
@Entity(name = "pattern")
@GenericGenerator(
name = "pattern_generator",
strategy = "org.hibernate.id.enhanced.SequenceStyleGenerator",
@@ -34,8 +38,12 @@ public class Pattern {
@GeneratedValue(generator = "pattern_generator")
private Integer id;
@Column(nullable = false)
private boolean enabled;
@Column(nullable = false)
private boolean deleted = false;
@Column(nullable = false)
private String name;

View File

@@ -9,7 +9,7 @@ import org.hibernate.annotations.GenericGenerator;
import org.hibernate.annotations.Parameter;
import ru.serega6531.packmate.model.enums.Protocol;
import javax.persistence.*;
import jakarta.persistence.*;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
@@ -53,7 +53,7 @@ public class Stream {
private long endTimestamp;
@ManyToMany(fetch = FetchType.EAGER)
@ManyToMany
@JoinTable(
name = "stream_found_patterns",
joinColumns = @JoinColumn(name = "stream_id"),
@@ -70,6 +70,12 @@ public class Stream {
@Column(columnDefinition = "char(3)")
private String userAgentHash;
@Column(name = "size_bytes", nullable = false)
private Integer sizeBytes;
@Column(name = "packets_count", nullable = false)
private Integer packetsCount;
@Override
public boolean equals(Object o) {
if (this == o) return true;

View File

@@ -0,0 +1,6 @@
package ru.serega6531.packmate.model.enums;
public enum FakeAdminMode {
FUN,
FAKE_PACKETS
}

View File

@@ -2,7 +2,7 @@ package ru.serega6531.packmate.model.enums;
public enum SubscriptionMessageType {
SAVE_SERVICE, SAVE_PATTERN,
DELETE_SERVICE, DELETE_PATTERN,
DELETE_SERVICE,
NEW_STREAM,
FINISH_LOOKBACK,
COUNTERS_UPDATE,

View File

@@ -1,23 +1,8 @@
package ru.serega6531.packmate.model.pojo;
import lombok.Getter;
import java.util.Map;
@Getter
public class CountersHolder {
public record CountersHolder(Map<Integer, Integer> servicesPackets, Map<Integer, Integer> servicesStreams,
int totalPackets, int totalStreams) {
private final Map<Integer, Integer> servicesPackets;
private final Map<Integer, Integer> servicesStreams;
private final int totalPackets;
private final int totalStreams;
public CountersHolder(Map<Integer, Integer> servicesPackets, Map<Integer, Integer> servicesStreams,
int totalPackets, int totalStreams) {
this.servicesPackets = servicesPackets;
this.servicesStreams = servicesStreams;
this.totalPackets = totalPackets;
this.totalStreams = totalStreams;
}
}

View File

@@ -0,0 +1,12 @@
package ru.serega6531.packmate.model.pojo;
import lombok.Builder;
import lombok.Value;
@Value
@Builder
public class FakeServiceDto {
int port;
String name;
String packetKind;
}

View File

@@ -14,6 +14,7 @@ public class PacketDto {
private boolean ungzipped;
private boolean webSocketParsed;
private boolean tlsDecrypted;
private boolean hasHttpBody;
private byte[] content;
}

View File

@@ -0,0 +1,19 @@
package ru.serega6531.packmate.model.pojo;
import lombok.Data;
import ru.serega6531.packmate.model.enums.PatternActionType;
import ru.serega6531.packmate.model.enums.PatternDirectionType;
import ru.serega6531.packmate.model.enums.PatternSearchType;
@Data
public class PatternCreateDto {
private String name;
private String value;
private String color;
private PatternSearchType searchType;
private PatternDirectionType directionType;
private PatternActionType actionType;
private Integer serviceId;
}

View File

@@ -10,6 +10,7 @@ public class PatternDto {
private int id;
private boolean enabled;
private boolean deleted;
private String name;
private String value;
private String color;

View File

@@ -0,0 +1,11 @@
package ru.serega6531.packmate.model.pojo;
import lombok.Data;
@Data
public class PatternUpdateDto {
private String name;
private String color;
}

View File

@@ -0,0 +1,16 @@
package ru.serega6531.packmate.model.pojo;
import lombok.Data;
@Data
public class ServiceCreateDto {
private int port;
private String name;
private boolean decryptTls;
private boolean http;
private boolean urldecodeHttpRequests;
private boolean mergeAdjacentPackets;
private boolean parseWebSockets;
}

View File

@@ -8,8 +8,7 @@ public class ServiceDto {
private int port;
private String name;
private boolean decryptTls;
private boolean processChunkedEncoding;
private boolean ungzipHttp;
private boolean http;
private boolean urldecodeHttpRequests;
private boolean mergeAdjacentPackets;
private boolean parseWebSockets;

View File

@@ -0,0 +1,16 @@
package ru.serega6531.packmate.model.pojo;
import lombok.Data;
@Data
public class ServiceUpdateDto {
private int port;
private String name;
private boolean decryptTls;
private boolean http;
private boolean urldecodeHttpRequests;
private boolean mergeAdjacentPackets;
private boolean parseWebSockets;
}

View File

@@ -13,9 +13,11 @@ public class StreamDto {
private Protocol protocol;
private long startTimestamp;
private long endTimestamp;
private Set<PatternDto> foundPatterns;
private Set<Integer> foundPatternsIds;
private boolean favorite;
private int ttl;
private String userAgentHash;
private int sizeBytes;
private int packetsCount;
}

View File

@@ -1,29 +1,18 @@
package ru.serega6531.packmate.model.pojo;
import lombok.AllArgsConstructor;
import lombok.Getter;
import ru.serega6531.packmate.model.enums.Protocol;
import java.net.InetAddress;
@AllArgsConstructor
@Getter
public class UnfinishedStream {
private final InetAddress firstIp;
private final InetAddress secondIp;
private final int firstPort;
private final int secondPort;
private final Protocol protocol;
public record UnfinishedStream(InetAddress firstIp, InetAddress secondIp, int firstPort, int secondPort,
Protocol protocol) {
@Override
public boolean equals(Object obj) {
if (!(obj instanceof UnfinishedStream)) {
if (!(obj instanceof UnfinishedStream o)) {
return false;
}
UnfinishedStream o = (UnfinishedStream) obj;
boolean ipEq1 = firstIp.equals(o.firstIp) && secondIp.equals(o.secondIp);
boolean ipEq2 = firstIp.equals(o.secondIp) && secondIp.equals(o.firstIp);
boolean portEq1 = firstPort == o.firstPort && secondPort == o.secondPort;

View File

@@ -52,11 +52,11 @@ public abstract class AbstractPcapWorker implements PcapWorker, PacketListener {
protected AbstractPcapWorker(ServicesService servicesService,
StreamService streamService,
String localIpString) throws UnknownHostException {
InetAddress localIp) throws UnknownHostException {
this.servicesService = servicesService;
this.streamService = streamService;
this.localIp = InetAddress.getByName(localIpString);
this.localIp = localIp;
BasicThreadFactory factory = new BasicThreadFactory.Builder()
.namingPattern("pcap-loop").build();

View File

@@ -6,6 +6,7 @@ import org.apache.tomcat.util.threads.InlineExecutorService;
import org.pcap4j.core.PcapNativeException;
import org.pcap4j.core.Pcaps;
import org.pcap4j.packet.Packet;
import ru.serega6531.packmate.exception.PcapFileNotFoundException;
import ru.serega6531.packmate.model.enums.Protocol;
import ru.serega6531.packmate.model.enums.SubscriptionMessageType;
import ru.serega6531.packmate.model.pojo.SubscriptionMessage;
@@ -15,28 +16,27 @@ import ru.serega6531.packmate.service.SubscriptionService;
import java.io.EOFException;
import java.io.File;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Arrays;
@Slf4j
public class FilePcapWorker extends AbstractPcapWorker {
private final File directory = new File("pcaps");
private final SubscriptionService subscriptionService;
private final File file;
public FilePcapWorker(ServicesService servicesService,
StreamService streamService,
SubscriptionService subscriptionService,
String localIpString,
InetAddress localIp,
String filename) throws UnknownHostException {
super(servicesService, streamService, localIpString);
super(servicesService, streamService, localIp);
this.subscriptionService = subscriptionService;
file = new File("pcaps", filename);
if (!file.exists()) {
log.info("Existing files: " + Arrays.toString(new File("pcaps").listFiles()));
throw new IllegalArgumentException("File " + file.getAbsolutePath() + " does not exist");
}
file = new File(directory, filename);
validateFileExists();
processorExecutorService = new InlineExecutorService();
}
@@ -86,4 +86,10 @@ public class FilePcapWorker extends AbstractPcapWorker {
public String getExecutorState() {
return "inline";
}
private void validateFileExists() {
if (!file.exists()) {
throw new PcapFileNotFoundException(file, directory);
}
}
}

View File

@@ -6,10 +6,13 @@ import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.pcap4j.core.PcapNativeException;
import org.pcap4j.core.PcapNetworkInterface;
import org.pcap4j.core.Pcaps;
import ru.serega6531.packmate.exception.PcapInterfaceNotFoundException;
import ru.serega6531.packmate.service.ServicesService;
import ru.serega6531.packmate.service.StreamService;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.List;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@@ -21,14 +24,14 @@ public class LivePcapWorker extends AbstractPcapWorker {
public LivePcapWorker(ServicesService servicesService,
StreamService streamService,
String localIpString,
InetAddress localIp,
String interfaceName) throws PcapNativeException, UnknownHostException {
super(servicesService, streamService, localIpString);
super(servicesService, streamService, localIp);
device = Pcaps.getDevByName(interfaceName);
if(device == null) {
log.info("Existing devices: {}", Pcaps.findAllDevs().stream().map(PcapNetworkInterface::getName).toList());
throw new IllegalArgumentException("Device " + interfaceName + " does not exist");
if (device == null) {
List<String> existingInterfaces = Pcaps.findAllDevs().stream().map(PcapNetworkInterface::getName).toList();
throw new PcapInterfaceNotFoundException(interfaceName, existingInterfaces);
}
BasicThreadFactory factory = new BasicThreadFactory.Builder()

View File

@@ -1,11 +1,10 @@
package ru.serega6531.packmate.pcap;
import org.pcap4j.core.PcapNativeException;
import ru.serega6531.packmate.model.enums.Protocol;
public class NoOpPcapWorker implements PcapWorker {
@Override
public void start() throws PcapNativeException {
public void start() {
}
@Override

View File

@@ -0,0 +1,45 @@
package ru.serega6531.packmate.properties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import ru.serega6531.packmate.model.enums.CaptureMode;
import ru.serega6531.packmate.model.enums.FakeAdminMode;
import java.net.InetAddress;
@ConfigurationProperties("packmate")
public record PackmateProperties(
CaptureMode captureMode,
String interfaceName,
String pcapFile,
InetAddress localIp,
WebProperties web,
TimeoutProperties timeout,
CleanupProperties cleanup,
boolean ignoreEmptyPackets
) {
public record WebProperties(
String accountLogin,
String accountPassword,
FakeAdminProperties fakeAdmin
) {}
public record FakeAdminProperties(
boolean enabled,
FakeAdminMode mode
) {}
public record TimeoutProperties(
int udpStreamTimeout,
int tcpStreamTimeout,
int checkInterval
){}
public record CleanupProperties(
boolean enabled,
int threshold,
int interval
){}
}

View File

@@ -1,11 +1,13 @@
package ru.serega6531.packmate.repository;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.*;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import ru.serega6531.packmate.model.Packet;
import ru.serega6531.packmate.model.Stream;
import javax.persistence.QueryHint;
import java.util.List;
public interface StreamRepository extends JpaRepository<Stream, Long>, JpaSpecificationExecutor<Stream> {
@@ -16,13 +18,12 @@ public interface StreamRepository extends JpaRepository<Stream, Long>, JpaSpecif
long deleteByEndTimestampBeforeAndFavoriteIsFalse(long threshold);
@Query("SELECT DISTINCT p FROM Packet p " +
@Query("SELECT p FROM Packet p " +
"LEFT JOIN FETCH p.matches " +
"WHERE p.stream.id = :streamId " +
"AND (:startingFrom IS NULL OR p.id > :startingFrom) " +
"ORDER BY p.id"
)
@QueryHints(@QueryHint(name = org.hibernate.jpa.QueryHints.HINT_PASS_DISTINCT_THROUGH, value = "false"))
List<Packet> getPackets(long streamId, Long startingFrom, Pageable pageable);
}

View File

@@ -0,0 +1,77 @@
package ru.serega6531.packmate.security;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import ru.serega6531.packmate.model.enums.FakeAdminMode;
import ru.serega6531.packmate.properties.PackmateProperties;
import java.io.IOException;
import java.util.Optional;
@Slf4j
@RequiredArgsConstructor
@Component
public class FakeAdminAuthFilter extends OncePerRequestFilter {
private final PackmateProperties properties;
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
if (!isFakeEnabled()) {
return true;
}
String path = request.getRequestURI();
return path.startsWith("/fake-admin")
|| path.startsWith("/api/fake")
|| path.startsWith("/fake/")
|| path.equals("/favicon.ico");
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if (!isFakeEnabled()) {
filterChain.doFilter(request, response);
return;
}
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
boolean isFakeAdmin = authentication != null && authentication.isAuthenticated()
&& authentication.getAuthorities().stream().anyMatch(a -> a.getAuthority().equals("ROLE_FAKE"));
if (isFakeAdmin) {
FakeAdminMode mode = Optional.ofNullable(properties.web().fakeAdmin())
.map(PackmateProperties.FakeAdminProperties::mode)
.orElse(FakeAdminMode.FUN);
String target = "/fake-admin/" + resolvePath(mode);
log.info("Redirecting fake admin to {}", target);
response.setStatus(HttpServletResponse.SC_TEMPORARY_REDIRECT);
response.setHeader(HttpHeaders.LOCATION, target);
return;
}
filterChain.doFilter(request, response);
}
private boolean isFakeEnabled() {
return Optional.ofNullable(properties.web().fakeAdmin())
.map(PackmateProperties.FakeAdminProperties::enabled)
.orElse(false);
}
private String resolvePath(FakeAdminMode mode) {
return switch (mode) {
case FAKE_PACKETS -> "packets";
case FUN -> "fun";
};
}
}

View File

@@ -0,0 +1,662 @@
package ru.serega6531.packmate.security;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.stereotype.Component;
import org.springframework.util.StreamUtils;
import java.io.IOException;
import java.net.URLConnection;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.nio.charset.StandardCharsets;
@Slf4j
@Component
public class FakeAdminResponder {
private final ObjectMapper mapper = new ObjectMapper();
private final List<String> encodedImages;
public FakeAdminResponder() {
this.encodedImages = loadImages();
}
private List<String> loadImages() {
try {
Resource[] resources = new PathMatchingResourcePatternResolver()
.getResources("classpath:/static/fake/images/*");
List<String> images = Arrays.stream(resources)
.map(resource -> {
try {
String contentType = URLConnection.guessContentTypeFromName(resource.getFilename());
if (contentType == null) {
contentType = "image/jpeg";
}
byte[] raw = StreamUtils.copyToByteArray(resource.getInputStream());
return "data:%s;base64,%s".formatted(
contentType,
Base64.getEncoder().encodeToString(raw));
} catch (IOException e) {
log.warn("Failed to load fake admin image {}", resource.getFilename(), e);
return null;
}
})
.filter(Objects::nonNull)
.toList();
if (images.isEmpty()) {
log.warn("No images found for fake admin fun mode");
}
return images;
} catch (IOException e) {
log.warn("Failed to load fake admin images", e);
return Collections.emptyList();
}
}
public String funPageHtml() {
String phrasesJson = toJson(getFunPhrases());
String imagesJson = toJson(encodedImages);
String phrasesB64 = Base64.getEncoder().encodeToString(phrasesJson.getBytes(StandardCharsets.UTF_8));
String imagesB64 = Base64.getEncoder().encodeToString(imagesJson.getBytes(StandardCharsets.UTF_8));
String template = """
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>0xb00b5 team Packmate // fake funwall</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&family=JetBrains+Mono:wght@500&display=swap');
:root {
--bg: #050512;
--accent: #59f3ff;
--accent-2: #ff5fd2;
--glass: rgba(9, 19, 45, 0.75);
--grid: rgba(89, 243, 255, 0.25);
}
* { box-sizing: border-box; }
body {
margin: 0;
padding: 0;
min-height: 100vh;
background: radial-gradient(circle at 20% 20%, rgba(255, 95, 210, 0.18), transparent 25%), radial-gradient(circle at 80% 0%, rgba(89, 243, 255, 0.2), transparent 30%), linear-gradient(135deg, #04040d 0%, #0a1024 45%, #050512 100%);
color: #e9f7ff;
font-family: 'Press Start 2P', 'JetBrains Mono', monospace;
display: flex;
align-items: center;
justify-content: center;
padding: 40px 18px 60px;
overflow: hidden;
}
.grid-bg {
position: fixed;
inset: 0;
background-image: linear-gradient(var(--grid) 1px, transparent 1px), linear-gradient(90deg, var(--grid) 1px, transparent 1px);
background-size: 80px 80px;
mask-image: radial-gradient(circle at 50% 20%, rgba(0,0,0,.8), transparent 75%);
opacity: 0.5;
z-index: 0;
}
.shell {
position: relative;
z-index: 1;
width: min(90vw, 1180px);
background: var(--glass);
border: 1px solid rgba(89, 243, 255, 0.4);
box-shadow: 0 0 40px rgba(89, 243, 255, 0.25), 0 0 24px rgba(255, 95, 210, 0.2);
border-radius: 18px;
padding: 32px;
backdrop-filter: blur(10px);
}
.shell::after {
content: '';
position: absolute;
inset: 12px;
border: 1px dashed rgba(255, 95, 210, 0.25);
border-radius: 14px;
pointer-events: none;
}
h1 {
margin: 0 0 20px;
font-size: 18px;
letter-spacing: 1.5px;
text-shadow: 0 0 8px rgba(89, 243, 255, 0.8);
}
.badge {
display: inline-block;
padding: 8px 14px;
border-radius: 10px;
border: 1px solid rgba(255, 95, 210, 0.35);
color: #ffcdf4;
box-shadow: inset 0 0 10px rgba(255, 95, 210, 0.35), 0 0 10px rgba(255, 95, 210, 0.35);
margin-bottom: 18px;
}
canvas {
width: 100%;
height: 420px;
border-radius: 14px;
border: 1px solid rgba(89, 243, 255, 0.35);
background: radial-gradient(circle at 30% 30%, rgba(255, 95, 210, 0.08), transparent 40%), #0b122a;
box-shadow: inset 0 0 30px rgba(0,0,0,0.35), 0 0 18px rgba(89, 243, 255, 0.12);
}
.typed {
margin-top: 22px;
font-size: 14px;
min-height: 24px;
letter-spacing: 0.8px;
color: #aaf6ff;
display: flex;
align-items: center;
gap: 10px;
}
#typed-text {
overflow: hidden;
white-space: nowrap;
}
.cursor {
display: inline-block;
width: 12px;
background: #aaf6ff;
animation: blink 0.8s infinite;
height: 14px;
}
@keyframes blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0.2; }
}
.footer-note {
margin-top: 18px;
font-size: 11px;
color: rgba(233, 247, 255, 0.65);
letter-spacing: 1px;
}
</style>
</head>
<body>
<div class="grid-bg"></div>
<div class="shell">
<h1>0xb00b5 team Packmate</h1>
<div class="badge">// @danosito</div>
<canvas id="snap-canvas"></canvas>
<audio id="fun-music" src="/fake/audio/Archive.Clue.mp3" preload="auto" loop></audio>
<div class="typed">
<span id="typed-text"></span><span class="cursor"></span>
</div>
<div class="footer-note">Hmmm where are the packets? IDK</div>
</div>
<script>
const phrases = JSON.parse(atob('__PHRASES_B64__'));
const images = JSON.parse(atob('__IMAGES_B64__'));
const canvas = document.getElementById('snap-canvas');
const ctx = canvas.getContext('2d');
const textEl = document.getElementById('typed-text');
const audioEl = document.getElementById('fun-music');
const pick = (list) => list[Math.floor(Math.random() * list.length)];
const specialLink = {
text: 'Your special guide to get flag!',
href: 'https://youtu.be/rrw-Pv3rc0E?si=-ZQmhZVxh4HF6luD'
};
if (audioEl) {
const ensurePlaying = () => {
if (!audioEl.dataset.started) {
audioEl.dataset.started = '1';
audioEl.volume = 0.4;
}
audioEl.play().catch(() => {});
};
ensurePlaying();
setInterval(() => {
if (audioEl.paused) {
audioEl.play().catch(() => {});
}
}, 2500);
}
function renderImage() {
const chosen = (images && images.length) ? pick(images) : '';
if (!chosen) {
ctx.fillStyle = '#0b122a';
ctx.fillRect(0, 0, canvas.width, canvas.height);
return;
}
const img = new Image();
img.onload = () => {
const maxW = 960;
const scale = Math.min(maxW / img.width, 1);
const w = img.width * scale;
const h = img.height * scale;
canvas.width = w;
canvas.height = h;
ctx.clearRect(0, 0, w, h);
ctx.drawImage(img, 0, 0, w, h);
setTimeout(() => snapAway(), 5000);
};
img.src = chosen;
}
function snapAway() {
let ticks = 0;
const interval = setInterval(() => {
for (let i = 0; i < 48; i++) {
const x = Math.random() * canvas.width;
const y = Math.random() * canvas.height;
const size = 4 + Math.random() * 10;
ctx.clearRect(x, y, size, size);
}
ticks++;
if (ticks > 80) {
clearInterval(interval);
setTimeout(renderImage, 400);
}
}, 28);
}
function typewriter() {
const phrase = pick(phrases);
const isSpecial = phrase === specialLink.text;
textEl.textContent = '';
let linkEl = null;
if (isSpecial) {
linkEl = document.createElement('a');
linkEl.href = specialLink.href;
linkEl.target = '_blank';
linkEl.rel = 'noreferrer noopener';
linkEl.style.color = '#aaf6ff';
textEl.appendChild(linkEl);
}
let i = 0;
const typeDelay = phrase.length ? 5000 / phrase.length : 120;
const typeInterval = setInterval(() => {
const target = isSpecial ? linkEl : textEl;
target.textContent += phrase.charAt(i);
i++;
if (i >= phrase.length) {
clearInterval(typeInterval);
setTimeout(() => erase(phrase, isSpecial), 3000);
}
}, typeDelay);
}
function erase(phrase, isSpecial) {
const eraseDelay = phrase.length ? 2000 / phrase.length : 80;
const eraser = setInterval(() => {
const target = isSpecial ? textEl.querySelector('a') : textEl;
if (!target) {
clearInterval(eraser);
setTimeout(typewriter, 200);
return;
}
target.textContent = target.textContent.slice(0, -1);
if (!target.textContent.length) {
if (isSpecial) {
target.remove();
}
clearInterval(eraser);
setTimeout(typewriter, 200);
}
}, eraseDelay);
}
renderImage();
typewriter();
</script>
</body>
</html>
""";
return template
.replace("__PHRASES_B64__", phrasesB64)
.replace("__IMAGES_B64__", imagesB64);
}
public String fakePacketsHtml() {
return """
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>0xb00b5 PM // packets</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/4.6.2/css/bootstrap.min.css">
<style>
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&family=JetBrains+Mono:wght@400;600&display=swap');
:root {
--bg: #05030a;
--bg-2: #0a0a18;
--panel: rgba(9, 7, 18, 0.9);
--panel-strong: rgba(14, 10, 24, 0.95);
--accent: #c66bff;
--accent-2: #7a1dff;
--text: #e6e0ff;
--muted: #9b90c8;
--mono: 'JetBrains Mono', 'Ubuntu Mono', monospace;
--pixel: 'Press Start 2P', 'JetBrains Mono', monospace;
}
* { box-sizing: border-box; scrollbar-width: none; }
*::-webkit-scrollbar { width: 0; height: 0; }
body {
margin: 0;
min-height: 100vh;
background:
radial-gradient(circle at 18% 24%, rgba(198, 107, 255, 0.18), transparent 25%),
radial-gradient(circle at 82% 12%, rgba(122, 29, 255, 0.12), transparent 25%),
linear-gradient(135deg, #020107 0%, #0a0820 50%, #03010b 100%);
color: var(--text);
font-family: var(--pixel);
letter-spacing: 0.6px;
overflow: hidden;
}
.bg-lines {
position: fixed; inset: 0; pointer-events: none; z-index: 0;
background-image: linear-gradient(rgba(122, 29, 255, 0.1) 1px, transparent 1px),
linear-gradient(90deg, rgba(198, 107, 255, 0.12) 1px, transparent 1px);
background-size: 120px 120px;
}
.app-shell {
position: relative;
z-index: 1;
}
.navbar {
background: var(--panel-strong);
border-bottom: 1px solid rgba(122, 29, 255, 0.45);
box-shadow: 0 10px 35px rgba(0, 0, 0, 0.55), 0 0 28px rgba(122, 29, 255, 0.35);
padding: 12px 18px;
}
.brand {
display: flex;
align-items: center;
gap: 10px;
color: var(--accent);
}
.brand .dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: linear-gradient(135deg, var(--accent), var(--accent-2));
box-shadow: 0 0 10px rgba(122, 29, 255, 0.65);
}
.metrics {
display: flex;
gap: 10px;
align-items: center;
margin-left: 12px;
flex-wrap: wrap;
}
.chip {
background: rgba(122, 29, 255, 0.12);
border: 1px solid rgba(122, 29, 255, 0.28);
color: var(--text);
padding: 8px 10px;
border-radius: 10px;
display: grid;
grid-template-columns: auto auto;
column-gap: 8px;
align-items: center;
font-size: 11px;
box-shadow: 0 0 14px rgba(122, 29, 255, 0.24);
}
.chip .label { color: var(--muted); }
.chip .value { color: var(--accent); }
.patterns-dropdown {
display: inline-flex;
width: 128px;
min-width: 128px;
max-width: 128px;
flex: 0 0 128px;
flex-shrink: 0;
box-sizing: border-box;
margin-left: 14px;
}
.patterns-dropdown > button {
display: inline-flex;
justify-content: center;
align-items: center;
font-family: var(--pixel);
font-size: 9.6px;
letter-spacing: 0.05px;
width: 128px;
min-width: 128px;
max-width: 128px;
padding-left: 8px;
padding-right: 8px;
white-space: nowrap;
}
.services-nav {
display: flex;
align-items: center;
gap: 4px;
margin-left: 14px;
flex-wrap: wrap;
}
.services-nav .nav-link {
padding: 6px 10px;
border-radius: 10px;
margin: 2px 0;
background: rgba(122, 29, 255, 0.12);
border: 1px solid rgba(122, 29, 255, 0.28);
color: var(--text);
font-size: 10px;
letter-spacing: 0.3px;
white-space: nowrap;
}
.layout {
display: grid;
grid-template-columns: 320px 1fr;
gap: 0;
min-height: calc(100vh - 72px);
}
.sidebar {
background: var(--panel);
border-right: 1px solid rgba(122, 29, 255, 0.25);
box-shadow: inset -10px 0 24px rgba(0, 0, 0, 0.35);
padding: 16px 12px;
overflow-y: auto;
}
.sidebar-title {
font-size: 10px;
text-transform: uppercase;
color: var(--muted);
margin-bottom: 10px;
}
.content {
background: var(--panel);
border-left: 1px solid rgba(122, 29, 255, 0.15);
padding: 18px 22px 26px;
box-shadow: inset 0 0 30px rgba(0, 0, 0, 0.45), 0 0 38px rgba(122, 29, 255, 0.18);
position: relative;
min-height: calc(100vh - 120px);
}
.content-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 14px;
}
.title {
font-size: 12px;
color: var(--text);
}
.packet-board {
background: rgba(9, 7, 18, 0.9);
border: 1px solid rgba(122, 29, 255, 0.35);
border-radius: 14px;
min-height: calc(100vh - 160px);
padding: 14px;
overflow: hidden;
}
.packet-feed {
font-family: var(--mono);
font-size: 11px;
max-height: calc(100vh - 200px);
overflow-y: auto;
padding-right: 6px;
}
.packet-line {
display: flex;
align-items: baseline;
gap: 10px;
padding: 6px 0;
border-bottom: 1px dashed rgba(122, 29, 255, 0.2);
color: var(--text);
}
.packet-line .pill {
display: inline-block;
padding: 2px 8px;
border-radius: 8px;
background: rgba(122, 29, 255, 0.2);
border: 1px solid rgba(122, 29, 255, 0.35);
font-size: 9px;
letter-spacing: 0.4px;
color: var(--accent);
}
.packet-line .meta {
color: var(--muted);
font-size: 10px;
}
.packet-line .bytes { color: #9ff8ff; }
.empty-slot { background: transparent; border: none; box-shadow: none; }
</style>
</head>
<body>
<div class="bg-lines"></div>
<div class="app-shell">
<nav class="navbar navbar-dark navbar-expand fixed-top">
<div class="d-flex align-items-center">
<span class="brand">
<span class="dot"></span>
0xb00b5 PM
</span>
</div>
<div class="metrics">
<span class="chip"><span class="label">SPM</span><span class="value" id="metric-spm">0</span></span>
<span class="chip"><span class="label">PPS</span><span class="value" id="metric-pps">0</span></span>
</div>
<div class="patterns-dropdown">
<button class="btn btn-dark btn-block" disabled>Patterns</button>
</div>
<div class="services-nav" id="nav-services"></div>
<span class="ml-auto text-monospace text-muted" style="font-size: 10px;">[Selected: none]</span>
</nav>
<div class="layout" style="margin-top: 64px;">
<aside class="sidebar">
<div class="sidebar-title">Packets</div>
<div class="packet-board">
<div class="packet-feed" id="packet-feed"></div>
</div>
</aside>
<main class="content empty-slot"></main>
</div>
</div>
<script>
const navServicesEl = document.getElementById('nav-services');
const feedEl = document.getElementById('packet-feed');
const spmEl = document.getElementById('metric-spm');
const ppsEl = document.getElementById('metric-pps');
let services = [];
let packetId = Math.floor(Math.random() * 5000) + 10;
fetch('/api/fake/services')
.then(r => r.json())
.then(data => {
services = data;
renderServices();
startFeed();
})
.catch(() => {
services = [{ name: 'ghost', port: 0, packetKind: 'tcp' }];
renderServices();
startFeed();
});
function renderServices() {
navServicesEl.innerHTML = '';
services.forEach((svc) => {
const link = document.createElement('a');
link.className = 'nav-link';
link.href = '#';
link.textContent = `${svc.name} #${svc.port}`;
navServicesEl.appendChild(link);
});
}
function spawnPacket() {
if (!services.length) return;
const svc = services[Math.floor(Math.random() * services.length)];
const proto = (svc.packetKind || 'tcp').toUpperCase();
const ts = new Date();
const clock = ts.toTimeString().slice(0, 8);
const bytes = Math.floor(Math.random() * 1800) + 40;
const line = document.createElement('div');
line.className = 'packet-line';
line.innerHTML = `
<span class="pill">${proto}</span>
<span class="meta">${clock}</span>
<span class="meta">:${svc.port} ${svc.name}</span>
<span class="bytes">${bytes} bytes</span>
<span class="meta">id #${packetId}</span>
`;
packetId++;
feedEl.prepend(line);
while (feedEl.children.length > 120) {
feedEl.removeChild(feedEl.lastChild);
}
updateMetrics();
}
function updateMetrics() {
const total = feedEl.children.length;
spmEl.textContent = Math.min(total, 999);
ppsEl.textContent = Math.floor(total / 2);
}
function startFeed() {
setInterval(spawnPacket, 55);
}
</script>
</body>
</html>
""";
}
private List<String> getFunPhrases() {
return List.of(
"Here's the flag. Are you ready? here it goes... Wait, no.",
"Wanna see the flag? send yours to @danosito:)",
"Hey, why are you here? go pentest our services",
"Hmmm i think <script>alert(\"You're stupid\")</script> might work..",
"Bip, boop, here was packet but codex ate it",
"Our LLM tockens ran out. Maybe you could give us some:)?",
":(){ :|:& };:",
"i think creds are admin:admin but i'm not sure...",
"Try eternalBlue, i think it would work",
"I think i defended this page well enough, here is flag: LLMDELETEDTHEFLAG=",
"Go open ida pro and reverse this text",
"I would give you our flags for free, but you are a bad person:(",
"b00b5 is not a fresh meat:(",
"marcus, send your packmate credits pls",
"Marcus, fuck off",
"Your special guide to get flag!"
);
}
private String toJson(List<String> data) {
try {
return mapper.writeValueAsString(data);
} catch (JsonProcessingException e) {
log.warn("Failed to convert data to json for fake admin", e);
return "[]";
}
}
}

View File

@@ -1,25 +1,31 @@
package ru.serega6531.packmate.service;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import ru.serega6531.packmate.model.CtfService;
import ru.serega6531.packmate.model.FoundPattern;
import ru.serega6531.packmate.model.Pattern;
import ru.serega6531.packmate.model.enums.PatternActionType;
import ru.serega6531.packmate.model.enums.PatternDirectionType;
import ru.serega6531.packmate.model.enums.SubscriptionMessageType;
import ru.serega6531.packmate.model.pojo.PatternCreateDto;
import ru.serega6531.packmate.model.pojo.PatternDto;
import ru.serega6531.packmate.model.pojo.PatternUpdateDto;
import ru.serega6531.packmate.model.pojo.SubscriptionMessage;
import ru.serega6531.packmate.repository.PatternRepository;
import javax.annotation.PostConstruct;
import java.time.Instant;
import java.util.*;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Service
@Slf4j
@@ -59,11 +65,11 @@ public class PatternService {
public Set<FoundPattern> findMatches(byte[] bytes, CtfService service, PatternDirectionType directionType, PatternActionType actionType) {
final List<Pattern> list = patterns.values().stream()
.filter(Pattern::isEnabled)
.filter(p -> p.getServiceId() == null || p.getServiceId() == service.getPort())
.filter(pattern -> pattern.isEnabled() && !pattern.isDeleted())
.filter(p -> p.getServiceId() == null || p.getServiceId().equals(service.getPort()))
.filter(p -> p.getActionType() == actionType)
.filter(p -> p.getDirectionType() == directionType || p.getDirectionType() == PatternDirectionType.BOTH)
.collect(Collectors.toList());
.toList();
return new PatternMatcher(bytes, list).findMatches();
}
@@ -88,15 +94,47 @@ public class PatternService {
}
}
public Pattern save(Pattern pattern) {
public void delete(int id) {
final Pattern pattern = find(id);
if (pattern != null) {
pattern.setDeleted(true);
final Pattern saved = repository.save(pattern);
patterns.put(id, saved);
log.info("Deleted pattern '{}' with value '{}'", pattern.getName(), pattern.getValue());
subscriptionService.broadcast(new SubscriptionMessage(SubscriptionMessageType.SAVE_PATTERN, toDto(saved)));
}
}
@Transactional
public PatternDto create(PatternCreateDto dto) {
Pattern pattern = fromDto(dto);
pattern.setEnabled(true);
pattern.setDeleted(false);
pattern.setSearchStartTimestamp(System.currentTimeMillis());
Pattern saved = save(pattern);
return toDto(saved);
}
@Transactional
public PatternDto update(int id, PatternUpdateDto dto) {
Pattern pattern = repository.findById(id).orElseThrow();
modelMapper.map(dto, pattern);
Pattern saved = save(pattern);
return toDto(saved);
}
private Pattern save(Pattern pattern) {
try {
PatternMatcher.compilePattern(pattern);
} catch (Exception e) {
throw new IllegalArgumentException(e.getMessage());
}
pattern.setSearchStartTimestamp(System.currentTimeMillis());
final Pattern saved = repository.save(pattern);
patterns.put(saved.getId(), saved);
@@ -121,12 +159,11 @@ public class PatternService {
}
}
public Pattern fromDto(PatternDto dto) {
public Pattern fromDto(PatternCreateDto dto) {
return modelMapper.map(dto, Pattern.class);
}
public PatternDto toDto(Pattern pattern) {
return modelMapper.map(pattern, PatternDto.class);
}
}

Some files were not shown because too many files have changed in this diff Show More