From c3b3d0a0c73e6a82bbd5c1156c88479e9f983fb3 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 2 Oct 2026 20:07:03 +0900 Subject: [PATCH] Offline analysis: pcap/pcapng files in a sandbox apart from the live data - internal/pcapfile reads pcap (us/ns, both byte orders) and pcapng (IDB resolution, EPB/SPB/OPB); no compressed files. - internal/sandbox: up to 3 files of at most 50 MB, checked while they stream to disk; packets become flows (60 s active / 15 s idle) in a database of their own under /sandbox; detection rules run over the capture; Delete removes files and data. Each file is a device. - API: ds=sb serves overview/topn/sankey/series/records/threats/findings from the sandbox; /api/sandbox, /api/sandbox/files. - Web UI: Offline analysis page, orange bar while capture files are shown, Back to live data; 13 languages. - Demo: an example capture with a port scan, SMB sweep and RDP guessing. - README x13, CHANGELOG 0.3.1. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XEUxFQp8ivofZLm1o6KiXm --- CHANGELOG.md | 13 + README.md | 16 +- cmd/traffic66/main.go | 26 ++ docs/README.ar.md | 16 +- docs/README.bn.md | 16 +- docs/README.es.md | 16 +- docs/README.fr.md | 16 +- docs/README.hi.md | 16 +- docs/README.id.md | 16 +- docs/README.ja.md | 16 +- docs/README.ko.md | 16 +- docs/README.pt.md | 16 +- docs/README.ru.md | 16 +- docs/README.ur.md | 16 +- docs/README.zh.md | 16 +- docs/images/sandbox.png | Bin 0 -> 123884 bytes internal/api/sandbox.go | 80 +++++ internal/api/sandbox_test.go | 68 ++++ internal/api/server.go | 21 +- internal/pcapfile/pcapfile.go | 282 ++++++++++++++++ internal/pcapfile/pcapfile_test.go | 111 +++++++ internal/pcapfile/write.go | 40 +++ internal/sandbox/flows.go | 134 ++++++++ internal/sandbox/sample.go | 135 ++++++++ internal/sandbox/sandbox.go | 516 +++++++++++++++++++++++++++++ internal/sandbox/sandbox_test.go | 134 ++++++++ internal/web/static/app.css | 8 + internal/web/static/app.js | 119 ++++++- internal/web/static/i18n/ar.json | 37 ++- internal/web/static/i18n/bn.json | 37 ++- internal/web/static/i18n/en.json | 37 ++- internal/web/static/i18n/es.json | 37 ++- internal/web/static/i18n/fr.json | 37 ++- internal/web/static/i18n/hi.json | 37 ++- internal/web/static/i18n/id.json | 37 ++- internal/web/static/i18n/ja.json | 37 ++- internal/web/static/i18n/ko.json | 37 ++- internal/web/static/i18n/pt.json | 37 ++- internal/web/static/i18n/ru.json | 37 ++- internal/web/static/i18n/ur.json | 37 ++- internal/web/static/i18n/zh.json | 37 ++- internal/web/static/index.html | 3 + 42 files changed, 2336 insertions(+), 43 deletions(-) create mode 100644 docs/images/sandbox.png create mode 100644 internal/api/sandbox.go create mode 100644 internal/api/sandbox_test.go create mode 100644 internal/pcapfile/pcapfile.go create mode 100644 internal/pcapfile/pcapfile_test.go create mode 100644 internal/pcapfile/write.go create mode 100644 internal/sandbox/flows.go create mode 100644 internal/sandbox/sample.go create mode 100644 internal/sandbox/sandbox.go create mode 100644 internal/sandbox/sandbox_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index d0b9457..ccbda52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,19 @@ heading is the version number. ## 0.3.1 +Offline analysis +- New page **Offline analysis**: upload packet captures (.pcap, .pcapng + from Wireshark or tcpdump; up to 3 files of at most 50 MB). They are + turned into flows in a database of their own, apart from the live data. + **Analyse** shows them on every page (overview, Top 66, traffic details, + findings, flow paths, map, flow records) with an orange bar naming the + files; **Back to live data** returns. **Delete** removes the files and + their data. +- The detection rules run over the capture: scans, port scans and password + guessing are found in it. +- The demo includes an example capture with an attack. + +Pages - Traffic details opens on servers only; tabs switch to clients, both ends side by side, and services (13 languages). diff --git a/README.md b/README.md index da1b22e..2e52026 100644 --- a/README.md +++ b/README.md @@ -652,6 +652,7 @@ Pages: | Geo & networks | A world map of traffic by country; the networks (AS) traffic came from and went to, over time in bits/s and packets/s; traffic by country and by network | | Threat intel | Hosts that talked to addresses on your threat lists, and how much they sent | | Flow records | How many flow records there were and when (a bar per interval), and the records themselves, newest first, page by page, with selectable columns | +| Offline analysis | Packet captures (pcap, pcapng) analysed apart from the live data ([more](#offline-analysis)) | | Interface check | Traffic of every interface over time (ingress and egress, bits/s and packets/s), and flow numbers next to the interface counters, worst first, with reasons | | Sources | Devices, sampling, loss, collectors, SNMP, the countries and networks database, the logo, and **Names** | @@ -734,6 +735,19 @@ The same overview in Chinese; every page is available in 13 languages: ![Overview in Chinese](docs/images/overview-zh.png) +### Offline analysis + +**Offline analysis** looks at packet captures from Wireshark or tcpdump with the same pages as the live data, without mixing them in: + +1. **Upload capture files…**: `.pcap` or `.pcapng`, not compressed. Up to 3 files, each at most 50 MB. The files are turned into flows in a database of their own (`/sandbox/`); the live data, its numbers and findings are not touched. +2. **Analyse**: every page (overview, Top 66, traffic details, findings, flow paths, map, flow records) now shows the capture files over their whole time. An orange bar names the files; **Back to live data** returns. Each file appears as a device, so the **Device** box shows one file at a time. +3. The detection rules run over the capture: scans, port scans and password guessing are listed under **Findings**. Rules that need a day of history (lateral movement, unusual uploads) do not apply to a capture. +4. **Delete** removes a file and its data; **Delete all** removes everything. + +The demo includes an example capture with an attack in it. + +![Offline analysis: capture files with their packets, flows and time](docs/images/sandbox.png) + ## 10. Terminal UI ``` @@ -889,7 +903,7 @@ The data directory holds everything: | `password` | login passwords (hashed) | | `inventory.txt` | names (**Sources → Names**) | | `logo.png` (or `.svg`, `.jpg`, `.webp`, `.gif`) | your logo (**Sources → Logo**), if you uploaded one | -| `country.mmdb`, `asn.mmdb`, `both.mmdb`, `asn.tsv.gz`, `dbip-country.mmdb`, `dbip-asn.mmdb`, `threats/` | countries and networks databases you added or downloaded, and threat lists | +| `country.mmdb`, `asn.mmdb`, `both.mmdb`, `asn.tsv.gz`, `dbip-country.mmdb`, `dbip-asn.mmdb`, `threats/`, `sandbox/` | countries and networks databases you added or downloaded, and threat lists | **How long data is kept**: flow detail 30 days, summaries (overview and long time ranges) 400 days. Older data is deleted automatically, checked every 5 diff --git a/cmd/traffic66/main.go b/cmd/traffic66/main.go index 882b417..9236e1f 100644 --- a/cmd/traffic66/main.go +++ b/cmd/traffic66/main.go @@ -4,6 +4,7 @@ package main import ( + "bytes" "context" "crypto/rand" "encoding/hex" @@ -32,6 +33,7 @@ import ( "github.com/githubflyideas/traffic66/internal/enrich" "github.com/githubflyideas/traffic66/internal/flow" "github.com/githubflyideas/traffic66/internal/pipeline" + "github.com/githubflyideas/traffic66/internal/sandbox" "github.com/githubflyideas/traffic66/internal/sim" "github.com/githubflyideas/traffic66/internal/snmp" "github.com/githubflyideas/traffic66/internal/store" @@ -387,6 +389,11 @@ func serve(args []string, demo bool) { srv := &api.Server{Store: st, Pipe: pipe, Col: col, Inv: inv, ASN: asn, Thr: thr, DNS: dns, Det: det, Static: web.FS(), Version: version, Demo: demo, Check: checker.Check, Exists: checker.Exists, LocalTok: tok, DataDir: f.data, Started: time.Now()} srv.SNMP = poller.Status + srv.SB = sandbox.New(filepath.Join(f.data, "sandbox"), inv, asn, thr) + defer srv.SB.Close() + if demo { + demoSample(f.data, srv.SB) + } srv.Capture = func() []api.CaptureInfo { var out []api.CaptureInfo for _, c := range caps { @@ -480,6 +487,25 @@ func prepareDemo(dir string) { write("threats/scanner.txt", scan) } +// demoSample puts the example capture into the sandbox once, so that offline +// analysis can be tried at once; deleting it keeps it deleted. +func demoSample(dir string, sb *sandbox.Sandbox) { + mark := filepath.Join(dir, "sandbox-sample") + if _, err := os.Stat(mark); err == nil { + return + } + var buf bytes.Buffer + if err := sandbox.Sample(&buf, time.Now().Add(-2*time.Hour).Truncate(time.Minute)); err != nil { + log.Printf("demo: example capture: %v", err) + return + } + if _, err := sb.Add(sandbox.SampleName, &buf, true); err != nil { + log.Printf("demo: example capture: %v", err) + return + } + os.WriteFile(mark, nil, 0o644) +} + func backfillDemo(pipe *pipeline.Pipeline, st *store.Store) { var n int64 st.DB.QueryRow(`SELECT (SELECT count(*) FROM hot) + (SELECT count(*) FROM segments)`).Scan(&n) diff --git a/docs/README.ar.md b/docs/README.ar.md index 69f74ed..b606783 100644 --- a/docs/README.ar.md +++ b/docs/README.ar.md @@ -663,6 +663,7 @@ curl -L https://www.spamhaus.org/drop/drop.txt -o /threats/spamh | الجغرافيا والشبكات | خريطة العالم للحركة حسب الدولة؛ الشبكات (AS) التي جاءت منها الحركة وذهبت إليها، عبر الزمن بوحدة bits/s وpackets/s؛ والحركة حسب الدولة وحسب الشبكة | | معلومات التهديدات | المضيفات التي تواصلت مع عناوين في قوائم تهديداتك، وكم أرسلت | | سجلات التدفق | كم سجل تدفق كان هناك ومتى (شريط لكل فترة)، والسجلات نفسها، الأحدث أولًا، صفحةً صفحة، مع أعمدة قابلة للاختيار | +| التحليل دون اتصال | تحليل ملفات التقاط الحزم (pcap وpcapng) بمعزل عن البيانات الحية ([المزيد](#التحليل-دون-اتصال)) | | مطابقة الواجهات | حركة كل واجهة عبر الزمن (الدخول والخروج، bits/s وpackets/s)، وأرقام التدفقات بجوار عدّادات الواجهات، الأسوأ أولًا، مع الأسباب | | المصادر | الأجهزة، وأخذ العينات، والفقد، والمستقبِلات، وSNMP، وقاعدة بيانات الدول والشبكات، والشعار، و**الأسماء** | @@ -747,6 +748,19 @@ curl -L https://www.spamhaus.org/drop/drop.txt -o /threats/spamh +### التحليل دون اتصال + +يعرض **التحليل دون اتصال** ملفات التقاط Wireshark أو tcpdump بالصفحات نفسها المستخدمة للبيانات الحية، دون خلطها بها: + +1. **رفع ملفات التقاط…**: ‎`.pcap` أو ‎`.pcapng` غير مضغوطة. حتى 3 ملفات، كل منها 50 MB كحد أقصى. تتحول الملفات إلى تدفقات في قاعدة بيانات خاصة بها (`/sandbox/`)، فلا تتأثر البيانات الحية وأرقامها واكتشافاتها. +2. **تحليل**: تعرض كل الصفحات (نظرة عامة، أعلى 66، تفاصيل الحركة، الاكتشافات، مسارات الحركة، الخريطة، سجلات التدفق) الملفات على امتداد وقتها كله. يذكر شريط برتقالي أسماء الملفات، و**العودة إلى البيانات الحية** يعيدك. يظهر كل ملف كجهاز، فيعرض مربع **الجهاز** ملفاً واحداً في كل مرة. +3. تعمل قواعد الكشف على الالتقاط: تظهر عمليات المسح ومسح المنافذ وتخمين كلمات المرور في **الاكتشافات**. القواعد التي تحتاج إلى يوم من السجل (الحركة الجانبية، الرفع غير المعتاد) لا تنطبق على الالتقاط. +4. **حذف** يحذف ملفاً وبياناته، و**حذف الكل** يحذف كل شيء. + +يتضمن العرض التجريبي ملف التقاط نموذجياً فيه هجوم. + +![التحليل دون اتصال: ملفات الالتقاط مع حزمها وتدفقاتها ووقتها](images/sandbox.png) + ## 10. الواجهة الطرفية ``` @@ -917,7 +931,7 @@ traffic66 -data /var/lib/traffic66 -listen "sflow=:6343,netflow=:2055,ipfix=:473 | `password` | كلمات مرور تسجيل الدخول (مُجزّأة) | | `inventory.txt` | الأسماء (**المصادر ← الأسماء**) | | `logo.png` (أو `.svg`، `.jpg`، `.webp`، `.gif`) | شعارك (**المصادر ← الشعار**)، إن كنت قد رفعته | -| `country.mmdb`، `asn.mmdb`، `both.mmdb`، `asn.tsv.gz`، `dbip-country.mmdb`، `dbip-asn.mmdb`، `threats/` | قواعد بيانات الدول والشبكات وقوائم التهديدات التي أضفتها | +| `country.mmdb`، `asn.mmdb`، `both.mmdb`، `asn.tsv.gz`، `dbip-country.mmdb`، `dbip-asn.mmdb`، `threats/`، `sandbox/` | قواعد بيانات الدول والشبكات وقوائم التهديدات التي أضفتها | **مدة الاحتفاظ بالبيانات**: تفاصيل التدفقات 30 يومًا، والملخصات (النظرة العامة والفترات الطويلة) 400 يوم. تُحذف البيانات الأقدم تلقائيًا، ويُفحص ذلك كل 5 دقائق؛ لا يُحذف شيء غير ذلك ولا يوجد حد آخر. غيّر مدة التفاصيل diff --git a/docs/README.bn.md b/docs/README.bn.md index 5961b96..673140c 100644 --- a/docs/README.bn.md +++ b/docs/README.bn.md @@ -670,6 +670,7 @@ application, দেশ, ডিভাইস — ক্লিক করা যা | ভূগোল ও নেটওয়ার্ক | দেশ অনুযায়ী ট্রাফিকের বিশ্ব মানচিত্র; যেসব নেটওয়ার্ক (AS) থেকে ট্রাফিক এসেছে ও যেখানে গেছে, সময়ের সাথে bits/s ও packets/s-এ; দেশ ও নেটওয়ার্ক অনুযায়ী ট্রাফিক | | হুমকির তথ্য | যেসব host আপনার threat list-এর address-এর সাথে কথা বলেছে, এবং কতটা পাঠিয়েছে | | ফ্লো রেকর্ড | কতগুলো flow record ছিল এবং কখন (প্রতি interval-এ একটি bar), আর record-গুলো নিজেই, নতুনগুলো আগে, পেজ ধরে ধরে, বেছে নেওয়া যায় এমন column সহ | +| অফলাইন বিশ্লেষণ | pcap, pcapng ক্যাপচার লাইভ ডেটা থেকে আলাদা করে বিশ্লেষণ ([আরও](#অফলাইন-বিশ্লেষণ)) | | ইন্টারফেস মিলানো | সময়ের সাথে প্রতিটি interface-এর ট্রাফিক (ingress ও egress, bits/s ও packets/s), আর interface counter-এর পাশে flow-এর সংখ্যা, সবচেয়ে খারাপগুলো আগে, কারণসহ | | উৎস | ডিভাইস, sampling, loss, collector, SNMP, দেশ ও নেটওয়ার্ক ডেটাবেস, লোগো, এবং **নাম** | @@ -755,6 +756,19 @@ packet পাঠায় যে দেখা যায় না। ডেম +### অফলাইন বিশ্লেষণ + +**অফলাইন বিশ্লেষণ** Wireshark বা tcpdump-এর ক্যাপচার লাইভ ডেটার মতো একই পেজে দেখায়, তবে মেশায় না: + +1. **ক্যাপচার ফাইল আপলোড করুন…**: `.pcap` বা `.pcapng`, সংকুচিত নয়। সর্বোচ্চ 3টি ফাইল, প্রতিটি 50 MB পর্যন্ত। ফাইলগুলো ফ্লো-তে রূপান্তরিত হয়ে আলাদা ডেটাবেসে (`/sandbox/`) যায়; লাইভ ডেটা, তার হিসাব ও ফলাফল বদলায় না। +2. **বিশ্লেষণ**: সব পেজ (সংক্ষেপ, Top 66, ট্রাফিকের বিস্তারিত, ফলাফল, ফ্লো পথ, মানচিত্র, ফ্লো রেকর্ড) ক্যাপচারের পুরো সময় দেখায়। কমলা ব্যানারে ফাইলের নাম থাকে; **লাইভ ডেটায় ফিরুন** দিয়ে ফিরুন। প্রতিটি ফাইল একটি ডিভাইস হিসেবে দেখায়, তাই **ডিভাইস** বক্সে একটি করে ফাইল দেখা যায়। +3. শনাক্তকরণের নিয়মগুলো ক্যাপচারেও চলে: স্ক্যান, পোর্ট স্ক্যান ও পাসওয়ার্ড অনুমান **সন্দেহজনক কার্যকলাপ**-এ আসে। যেসব নিয়মে এক দিনের ইতিহাস লাগে (ল্যাটারাল মুভমেন্ট, অস্বাভাবিক আপলোড) সেগুলো ক্যাপচারে প্রযোজ্য নয়। +4. **মুছুন** একটি ফাইল ও তার ডেটা মুছে দেয়; **সব মুছুন** সব মুছে দেয়। + +ডেমোতে আক্রমণসহ একটি উদাহরণ ক্যাপচার আছে। + +![অফলাইন বিশ্লেষণ: ক্যাপচার ফাইল, তাদের প্যাকেট, ফ্লো ও সময়](images/sandbox.png) + ## 10. Terminal UI ``` @@ -924,7 +938,7 @@ traffic66 -data /var/lib/traffic66 -listen "sflow=:6343,netflow=:2055,ipfix=:473 | `password` | login পাসওয়ার্ড (hashed) | | `inventory.txt` | নাম (**উৎস → নাম**) | | `logo.png` (বা `.svg`, `.jpg`, `.webp`, `.gif`) | আপনার লোগো (**উৎস → লোগো**), যদি আপলোড করে থাকেন | -| `country.mmdb`, `asn.mmdb`, `both.mmdb`, `asn.tsv.gz`, `dbip-country.mmdb`, `dbip-asn.mmdb`, `threats/` | আপনার যোগ করা দেশ ও নেটওয়ার্ক database এবং threat list | +| `country.mmdb`, `asn.mmdb`, `both.mmdb`, `asn.tsv.gz`, `dbip-country.mmdb`, `dbip-asn.mmdb`, `threats/`, `sandbox/` | আপনার যোগ করা দেশ ও নেটওয়ার্ক database এবং threat list | **ডেটা কতদিন রাখা হয়**: flow detail 30 দিন, summary (overview ও দীর্ঘ সময়সীমা) 400 দিন। এর চেয়ে পুরোনো ডেটা নিজে থেকেই মুছে যায়, প্রতি 5 মিনিটে যাচাই হয়; এ ছাড়া কিছু মোছা হয় না এবং অন্য কোনো সীমা নেই। detail-এর মেয়াদ diff --git a/docs/README.es.md b/docs/README.es.md index e7309f6..12ba861 100644 --- a/docs/README.es.md +++ b/docs/README.es.md @@ -686,6 +686,7 @@ Páginas: | Geografía y redes | Un mapa del mundo del tráfico por país; las redes (AS) de las que vino y a las que fue el tráfico, en el tiempo en bits/s y paquetes/s; tráfico por país y por red | | Inteligencia de amenazas | Hosts que hablaron con direcciones de sus listas de amenazas y cuánto enviaron | | Registros de flujo | Cuántos registros de flujo hubo y cuándo (una barra por intervalo), y los registros mismos, del más reciente al más antiguo, página a página, con columnas seleccionables | +| Análisis offline | Capturas de paquetes (pcap, pcapng) analizadas aparte de los datos en vivo ([más](#análisis-offline)) | | Verificación de interfaces | Tráfico de cada interfaz en el tiempo (entrada y salida, bits/s y paquetes/s), y cifras de flujo junto a los contadores de interfaz, de peor a mejor, con motivos | | Fuentes | Equipos, muestreo, pérdidas, colectores, SNMP, la base de datos de países y redes, el logotipo y **Nombres** | @@ -775,6 +776,19 @@ El mismo resumen en chino; todas las páginas están disponibles en 13 idiomas: +### Análisis offline + +**Análisis offline** muestra capturas de Wireshark o tcpdump con las mismas páginas que los datos en vivo, sin mezclarlas: + +1. **Subir archivos de captura…**: `.pcap` o `.pcapng`, sin comprimir. Hasta 3 archivos, cada uno de 50 MB como máximo. Los archivos se convierten en flujos en una base de datos propia (`/sandbox/`); los datos en vivo, sus cifras y hallazgos no se tocan. +2. **Analizar**: todas las páginas (resumen, Top 66, detalles del tráfico, hallazgos, rutas, mapa, registros de flujo) muestran los archivos durante todo su tiempo. Una barra naranja nombra los archivos; **Volver a los datos en vivo** vuelve. Cada archivo aparece como un dispositivo, así que el cuadro **Dispositivo** muestra un archivo cada vez. +3. Las reglas de detección se aplican a la captura: barridos, escaneos de puertos y adivinación de contraseñas aparecen en **Hallazgos**. Las reglas que necesitan un día de historial (movimiento lateral, subidas inusuales) no se aplican a una captura. +4. **Eliminar** borra un archivo y sus datos; **Eliminar todo** lo borra todo. + +La demo incluye una captura de ejemplo con un ataque. + +![Análisis offline: archivos de captura con sus paquetes, flujos y tiempo](images/sandbox.png) + ## 10. Interfaz de terminal ``` @@ -948,7 +962,7 @@ El directorio de datos lo contiene todo: | `password` | contraseñas de acceso (con hash) | | `inventory.txt` | nombres (**Fuentes → Nombres**) | | `logo.png` (o `.svg`, `.jpg`, `.webp`, `.gif`) | su logotipo (**Fuentes → Logotipo**), si subió uno | -| `country.mmdb`, `asn.mmdb`, `both.mmdb`, `asn.tsv.gz`, `dbip-country.mmdb`, `dbip-asn.mmdb`, `threats/` | bases de datos de países y redes y listas de amenazas que haya añadido | +| `country.mmdb`, `asn.mmdb`, `both.mmdb`, `asn.tsv.gz`, `dbip-country.mmdb`, `dbip-asn.mmdb`, `threats/`, `sandbox/` | bases de datos de países y redes y listas de amenazas que haya añadido | **Cuánto tiempo se guardan los datos**: el detalle de flujos 30 días; los resúmenes (vista general y periodos largos) 400 días. Lo más antiguo se borra automáticamente, con una comprobación cada 5 minutos; no se borra nada más ni hay diff --git a/docs/README.fr.md b/docs/README.fr.md index 72a235e..1e74798 100644 --- a/docs/README.fr.md +++ b/docs/README.fr.md @@ -704,6 +704,7 @@ Pages : | Géographie et réseaux | Une carte du monde du trafic par pays ; les réseaux (AS) d'où venait et où allait le trafic, dans le temps en bits/s et paquets/s ; trafic par pays et par réseau | | Menaces | Hôtes ayant communiqué avec des adresses de vos listes de menaces, et volume envoyé | | Enregistrements de flux | Combien d'enregistrements de flux il y a eu et quand (une barre par intervalle), et les enregistrements eux-mêmes, du plus récent au plus ancien, page par page, avec colonnes au choix | +| Analyse hors ligne | Captures de paquets (pcap, pcapng) analysées à part des données en direct ([plus](#analyse-hors-ligne)) | | Contrôle des interfaces | Trafic de chaque interface dans le temps (entrée et sortie, bits/s et paquets/s), et chiffres de flux à côté des compteurs d'interface, les pires en premier, avec les causes | | Sources | Équipements, échantillonnage, pertes, collecteurs, SNMP, la base de données pays et réseaux, le logo et **Noms** | @@ -800,6 +801,19 @@ La même vue d'ensemble en chinois ; toutes les pages sont disponibles en 13 lan +### Analyse hors ligne + +**Analyse hors ligne** affiche des captures Wireshark ou tcpdump avec les mêmes pages que les données en direct, sans les mélanger : + +1. **Importer des fichiers de capture…** : `.pcap` ou `.pcapng`, non compressés. Jusqu'à 3 fichiers de 50 Mo au plus chacun. Les fichiers sont transformés en flux dans une base à part (`/sandbox/`) ; les données en direct, leurs chiffres et détections ne sont pas touchés. +2. **Analyser** : toutes les pages (vue d'ensemble, Top 66, détails du trafic, détections, chemins, carte, enregistrements de flux) montrent les fichiers sur toute leur durée. Une barre orange nomme les fichiers ; **Retour aux données en direct** revient. Chaque fichier apparaît comme un équipement : le champ **Équipement** montre un fichier à la fois. +3. Les règles de détection s'appliquent à la capture : balayages, scans de ports et essais de mots de passe apparaissent dans **Détections**. Les règles qui demandent un jour d'historique (mouvement latéral, envois inhabituels) ne s'appliquent pas à une capture. +4. **Supprimer** efface un fichier et ses données ; **Tout supprimer** efface tout. + +La démo contient une capture d'exemple avec une attaque. + +![Analyse hors ligne : fichiers de capture avec leurs paquets, flux et période](images/sandbox.png) + ## 10. Interface terminal ``` @@ -976,7 +990,7 @@ Le répertoire de données contient tout : | `password` | mots de passe de connexion (hachés) | | `inventory.txt` | noms (**Sources → Noms**) | | `logo.png` (ou `.svg`, `.jpg`, `.webp`, `.gif`) | votre logo (**Sources → Logo**), si vous en avez importé un | -| `country.mmdb`, `asn.mmdb`, `both.mmdb`, `asn.tsv.gz`, `dbip-country.mmdb`, `dbip-asn.mmdb`, `threats/` | bases de données pays et réseaux et listes de menaces que vous avez ajoutées | +| `country.mmdb`, `asn.mmdb`, `both.mmdb`, `asn.tsv.gz`, `dbip-country.mmdb`, `dbip-asn.mmdb`, `threats/`, `sandbox/` | bases de données pays et réseaux et listes de menaces que vous avez ajoutées | **Durée de conservation** : le détail des flux 30 jours, les résumés (vue d'ensemble et longues périodes) 400 jours. Les données plus anciennes sont supprimées automatiquement, vérifiées toutes les 5 minutes ; rien d'autre n'est diff --git a/docs/README.hi.md b/docs/README.hi.md index 57cc00a..f6c679d 100644 --- a/docs/README.hi.md +++ b/docs/README.hi.md @@ -671,6 +671,7 @@ application, देश, डिवाइस — पर क्लिक किय | भूगोल और नेटवर्क | देश के अनुसार ट्रैफ़िक का विश्व मानचित्र; वे नेटवर्क (AS) जहाँ से ट्रैफ़िक आया और जहाँ गया, समय के साथ bits/s और packets/s में; देश के हिसाब से और नेटवर्क के हिसाब से ट्रैफ़िक | | ख़तरे की जानकारी | वे hosts जिन्होंने आपकी threat lists के addresses से बात की, और उन्होंने कितना भेजा | | फ़्लो रिकॉर्ड | कितने flow records थे और कब (हर interval के लिए एक bar), और ख़ुद records, सबसे नए पहले, पेज-दर-पेज, चुने जा सकने वाले columns के साथ | +| ऑफ़लाइन विश्लेषण | pcap, pcapng कैप्चर का लाइव डेटा से अलग विश्लेषण ([और](#ऑफ़लाइन-विश्लेषण)) | | इंटरफ़ेस मिलान | समय के साथ हर interface का ट्रैफ़िक (ingress और egress, bits/s और packets/s), और interface counters के बगल में flow के आँकड़े, सबसे ख़राब पहले, कारणों के साथ | | स्रोत | डिवाइस, sampling, loss, collectors, SNMP, देश और नेटवर्क डेटाबेस, लोगो, और **नाम** | @@ -758,6 +759,19 @@ internet scanner के। +### ऑफ़लाइन विश्लेषण + +**ऑफ़लाइन विश्लेषण** Wireshark या tcpdump के कैप्चर को लाइव डेटा वाले पेजों पर ही दिखाता है, बिना उन्हें मिलाए: + +1. **कैप्चर फ़ाइलें अपलोड करें…**: `.pcap` या `.pcapng`, संपीड़ित नहीं। अधिकतम 3 फ़ाइलें, हर एक 50 MB तक। फ़ाइलें फ़्लो में बदलकर अलग डेटाबेस (`/sandbox/`) में जाती हैं; लाइव डेटा, उसके आँकड़े और निष्कर्ष नहीं बदलते। +2. **विश्लेषण करें**: सभी पेज (अवलोकन, Top 66, ट्रैफ़िक विवरण, निष्कर्ष, फ़्लो पथ, मानचित्र, फ़्लो रिकॉर्ड) कैप्चर फ़ाइलों का पूरा समय दिखाते हैं। नारंगी पट्टी फ़ाइलों के नाम बताती है; **लाइव डेटा पर लौटें** से लौटें। हर फ़ाइल एक डिवाइस की तरह दिखती है, इसलिए **डिवाइस** बॉक्स से एक-एक फ़ाइल देखी जा सकती है। +3. पहचान के नियम कैप्चर पर भी चलते हैं: स्कैन, पोर्ट स्कैन और पासवर्ड अनुमान **संदिग्ध गतिविधि** में दिखते हैं। जिन नियमों को एक दिन का इतिहास चाहिए (लेटरल मूवमेंट, असामान्य अपलोड) वे कैप्चर पर लागू नहीं होते। +4. **हटाएँ** एक फ़ाइल और उसका डेटा हटाता है; **सब हटाएँ** सब कुछ हटाता है। + +डेमो में हमले वाला एक उदाहरण कैप्चर शामिल है। + +![ऑफ़लाइन विश्लेषण: कैप्चर फ़ाइलें, उनके पैकेट, फ़्लो और समय](images/sandbox.png) + ## 10. Terminal UI ``` @@ -927,7 +941,7 @@ traffic66 -data /var/lib/traffic66 -listen "sflow=:6343,netflow=:2055,ipfix=:473 | `password` | login पासवर्ड (hashed) | | `inventory.txt` | नाम (**स्रोत → नाम**) | | `logo.png` (या `.svg`, `.jpg`, `.webp`, `.gif`) | आपका लोगो (**स्रोत → लोगो**), अगर आपने अपलोड किया है | -| `country.mmdb`, `asn.mmdb`, `both.mmdb`, `asn.tsv.gz`, `dbip-country.mmdb`, `dbip-asn.mmdb`, `threats/` | आपके जोड़े हुए देश और नेटवर्क databases और threat lists | +| `country.mmdb`, `asn.mmdb`, `both.mmdb`, `asn.tsv.gz`, `dbip-country.mmdb`, `dbip-asn.mmdb`, `threats/`, `sandbox/` | आपके जोड़े हुए देश और नेटवर्क databases और threat lists | **डेटा कितने समय तक रखा जाता है**: flow detail 30 दिन, summaries (overview और लंबी समय-सीमाएँ) 400 दिन। इससे पुराना डेटा अपने आप हटता है, हर 5 मिनट में जाँच होती है; इसके अलावा कुछ नहीं हटता और कोई और सीमा नहीं है। diff --git a/docs/README.id.md b/docs/README.id.md index 0ba97d6..dacbb57 100644 --- a/docs/README.id.md +++ b/docs/README.id.md @@ -683,6 +683,7 @@ Halaman: | Geografi & jaringan | Peta dunia trafik per negara; jaringan (AS) asal dan tujuan trafik, dari waktu ke waktu dalam bit/s dan paket/s; trafik per negara dan per jaringan | | Intel ancaman | Host yang berkomunikasi dengan alamat di daftar ancaman Anda, dan berapa banyak yang mereka kirim | | Catatan flow | Berapa banyak catatan flow dan kapan (satu batang per interval), dan catatannya sendiri, terbaru di atas, per halaman, dengan kolom yang bisa dipilih | +| Analisis offline | Tangkapan paket (pcap, pcapng) dianalisis terpisah dari data langsung ([selengkapnya](#analisis-offline)) | | Pencocokan antarmuka | Trafik setiap interface dari waktu ke waktu (masuk dan keluar, bit/s dan paket/s), dan angka flow di samping counter interface, yang paling buruk di atas, beserta alasannya | | Sumber | Perangkat, sampling, loss, collector, SNMP, basis data negara dan jaringan, logo, dan **Nama** | @@ -777,6 +778,19 @@ Ringkasan yang sama dalam bahasa Tionghoa; setiap halaman tersedia dalam 13 baha +### Analisis offline + +**Analisis offline** menampilkan tangkapan Wireshark atau tcpdump dengan halaman yang sama seperti data langsung, tanpa mencampurnya: + +1. **Unggah file tangkapan…**: `.pcap` atau `.pcapng`, tidak dikompresi. Maksimal 3 file, masing-masing paling besar 50 MB. File diubah menjadi flow di basis data tersendiri (`/sandbox/`); data langsung, angka, dan temuannya tidak tersentuh. +2. **Analisis**: semua halaman (ringkasan, Top 66, detail trafik, temuan, jalur, peta, catatan flow) menampilkan file untuk seluruh waktunya. Bilah oranye menyebut nama file; **Kembali ke data langsung** kembali. Tiap file tampil sebagai perangkat, jadi kotak **Perangkat** menampilkan satu file sekaligus. +3. Aturan deteksi dijalankan pada tangkapan: pemindaian, pemindaian port, dan tebakan kata sandi muncul di **Temuan**. Aturan yang perlu riwayat sehari (pergerakan lateral, unggahan tak biasa) tidak berlaku untuk tangkapan. +4. **Hapus** menghapus satu file dan datanya; **Hapus semua** menghapus semuanya. + +Demo menyertakan contoh tangkapan berisi serangan. + +![Analisis offline: file tangkapan beserta paket, flow, dan waktunya](images/sandbox.png) + ## 10. Antarmuka terminal ``` @@ -948,7 +962,7 @@ Semua tersimpan di direktori data: | `password` | kata sandi login (dalam bentuk hash) | | `inventory.txt` | nama (**Sumber → Nama**) | | `logo.png` (atau `.svg`, `.jpg`, `.webp`, `.gif`) | logo Anda (**Sumber → Logo**), jika Anda mengunggahnya | -| `country.mmdb`, `asn.mmdb`, `both.mmdb`, `asn.tsv.gz`, `dbip-country.mmdb`, `dbip-asn.mmdb`, `threats/` | basis data negara dan jaringan serta daftar ancaman yang Anda tambahkan | +| `country.mmdb`, `asn.mmdb`, `both.mmdb`, `asn.tsv.gz`, `dbip-country.mmdb`, `dbip-asn.mmdb`, `threats/`, `sandbox/` | basis data negara dan jaringan serta daftar ancaman yang Anda tambahkan | **Berapa lama data disimpan**: detail flow 30 hari, ringkasan (ikhtisar dan rentang waktu panjang) 400 hari. Data yang lebih lama dihapus otomatis, diperiksa setiap 5 menit; tidak ada yang lain dihapus dan tidak ada batas lain. diff --git a/docs/README.ja.md b/docs/README.ja.md index 1f8d62d..63fa012 100644 --- a/docs/README.ja.md +++ b/docs/README.ja.md @@ -562,6 +562,7 @@ curl -L https://www.spamhaus.org/drop/drop.txt -o /threats/spamh | 地域とネットワーク | 国別トラフィックの世界地図、トラフィックの送信元・送信先のネットワーク(AS)の推移(bits/s と packets/s)、国別・ネットワーク別のトラフィック | | 脅威インテル | 脅威リスト上のアドレスと通信したホストと、その送信量 | | フローレコード | フローレコードの数とその時期(間隔ごとの棒グラフ)と、レコードそのもの(新しい順、ページ送り、表示列は選択可能) | +| オフライン分析 | pcap・pcapng のキャプチャをライブデータと分けて分析 ([詳しく](#オフライン分析)) | | インターフェース照合 | 全インターフェースのトラフィックの推移(受信と送信、bits/s と packets/s)と、フローの数値とインターフェースカウンターの対比(差の大きい順、理由付き) | | 受信状況 | 機器、サンプリング、ロス、コレクター、SNMP、国・ネットワークデータベース、ロゴ、**名前** | @@ -613,6 +614,19 @@ curl -L https://www.spamhaus.org/drop/drop.txt -o /threats/spamh +### オフライン分析 + +**オフライン分析** では Wireshark や tcpdump のキャプチャを、ライブデータと混ぜずに同じページで見られます: + +1. **キャプチャファイルをアップロード…**:`.pcap` または `.pcapng`(圧縮不可)。最大 3 ファイル、各 50 MB まで。ファイルはフローに変換されて専用のデータベース(`/sandbox/`)に入り、ライブデータや集計、検出結果には影響しません。 +2. **分析**:全ページ(概要、Top 66、トラフィック詳細、検出、フローパス、地図、フローレコード)がキャプチャの全期間を表示します。オレンジの帯にファイル名が出て、**ライブデータに戻る** で戻ります。各ファイルは 1 台の機器として扱われるので、**機器** 欄で 1 ファイルずつ見られます。 +3. 検出ルールもキャプチャに対して動き、スキャン、ポートスキャン、パスワード推測が **検知** に出ます。1 日分の履歴が必要なルール(横展開、異常なアップロード)はキャプチャには適用されません。 +4. **削除** でファイルとそのデータを、**すべて削除** ですべてを削除します。 + +デモには攻撃を含むサンプルキャプチャが入っています。 + +![オフライン分析:キャプチャファイルとパケット数、フロー数、時間](images/sandbox.png) + ## 10. ターミナル UI ``` @@ -759,7 +773,7 @@ traffic66 -data /var/lib/traffic66 -listen "sflow=:6343,netflow=:2055,ipfix=:473 | `password` | ログインパスワード(ハッシュ化) | | `inventory.txt` | 名前(**受信状況 → 名前**) | | `logo.png`(または `.svg`、`.jpg`、`.webp`、`.gif`) | アップロードしたロゴ(**受信状況 → ロゴ**)。アップロードした場合のみ | -| `country.mmdb`, `asn.mmdb`, `both.mmdb`, `asn.tsv.gz`, `dbip-country.mmdb`, `dbip-asn.mmdb`, `threats/` | 追加した国・ネットワークデータベースと脅威リスト | +| `country.mmdb`, `asn.mmdb`, `both.mmdb`, `asn.tsv.gz`, `dbip-country.mmdb`, `dbip-asn.mmdb`, `threats/`, `sandbox/` | 追加した国・ネットワークデータベースと脅威リスト | **データの保存期間**:フロー明細は 30 日、集計(概要と長い期間)は 400 日です。それより古いデータは自動で削除され、5 分ごとに確認します。それ以外は何も削除せず、ほかの制限もありません。明細の保存日数は `-retention-days` で変更でき、何日でも指定できます(例:`-retention-days 365`)。ディスク使用量もそれに応じて増えます。保存日数分が入りきらないときは、サイドメニューの **空き** が赤くなります。ディスクが満杯になると、空きができるまで新しいフローは保存できません。 diff --git a/docs/README.ko.md b/docs/README.ko.md index 04d5180..e817d9e 100644 --- a/docs/README.ko.md +++ b/docs/README.ko.md @@ -647,6 +647,7 @@ curl -L https://www.spamhaus.org/drop/drop.txt -o /threats/spamh | 지역 및 네트워크 | 국가별 트래픽 세계 지도, 트래픽이 오간 네트워크(AS)의 시간에 따른 변화(bits/s와 packets/s), 국가별, 네트워크별 트래픽 | | 위협 정보 | 위협 목록에 있는 주소와 통신한 호스트와 그 전송량 | | 플로 레코드 | 플로 레코드가 얼마나, 언제 있었는지(간격마다 막대 하나), 그리고 레코드 자체, 최신순, 페이지 단위, 표시할 열 선택 가능 | +| 오프라인 분석 | pcap·pcapng 캡처를 실시간 데이터와 따로 분석 ([자세히](#오프라인-분석)) | | 인터페이스 대조 | 모든 인터페이스의 시간에 따른 트래픽(수신과 송신, bits/s와 packets/s), 그리고 플로 수치와 인터페이스 카운터 비교, 차이가 큰 순, 원인 포함 | | 수집 상태 | 장비, 샘플링, 유실, 수집기, SNMP, 국가 및 네트워크 데이터베이스, 로고, **이름** | @@ -726,6 +727,19 @@ curl -L https://www.spamhaus.org/drop/drop.txt -o /threats/spamh +### 오프라인 분석 + +**오프라인 분석**는 Wireshark나 tcpdump 캡처를 실시간 데이터와 섞지 않고 같은 페이지로 보여 줍니다: + +1. **캡처 파일 업로드…**: `.pcap` 또는 `.pcapng`(압축 불가). 최대 3개 파일, 각각 50 MB 이하. 파일은 플로로 바뀌어 별도 데이터베이스(`/sandbox/`)에 들어가며, 실시간 데이터와 집계, 탐지 결과에는 영향이 없습니다. +2. **분석**: 모든 페이지(개요, Top 66, 트래픽 상세, 탐지, 플로 경로, 지도, 플로 레코드)가 캡처의 전체 기간을 보여 줍니다. 주황색 막대에 파일 이름이 나오고, **실시간 데이터로 돌아가기**로 돌아갑니다. 각 파일은 장비 하나로 나타나므로 **장비** 칸으로 파일을 하나씩 볼 수 있습니다. +3. 탐지 규칙도 캡처에 대해 실행되어 스캔, 포트 스캔, 비밀번호 추측이 **탐지**에 나옵니다. 하루치 이력이 필요한 규칙(내부 확산, 비정상 업로드)은 캡처에 적용되지 않습니다. +4. **삭제**는 파일과 데이터를, **모두 삭제**는 전부를 지웁니다. + +데모에는 공격이 담긴 예제 캡처가 들어 있습니다. + +![오프라인 분석: 캡처 파일과 패킷, 플로, 시간](images/sandbox.png) + ## 10. 터미널 UI ``` @@ -889,7 +903,7 @@ traffic66 -data /var/lib/traffic66 -listen "sflow=:6343,netflow=:2055,ipfix=:473 | `password` | 로그인 비밀번호(해시) | | `inventory.txt` | 이름(**수집 상태 → 이름**) | | `logo.png`(또는 `.svg`, `.jpg`, `.webp`, `.gif`) | 업로드한 경우 내 로고(**수집 상태 → 로고**) | -| `country.mmdb`, `asn.mmdb`, `both.mmdb`, `asn.tsv.gz`, `dbip-country.mmdb`, `dbip-asn.mmdb`, `threats/` | 추가한 국가 및 네트워크 데이터베이스와 위협 목록 | +| `country.mmdb`, `asn.mmdb`, `both.mmdb`, `asn.tsv.gz`, `dbip-country.mmdb`, `dbip-asn.mmdb`, `threats/`, `sandbox/` | 추가한 국가 및 네트워크 데이터베이스와 위협 목록 | **데이터 보존 기간**: 플로 상세는 30일, 요약(개요와 긴 기간)은 400일입니다. 그보다 오래된 데이터는 자동으로 삭제되며 5분마다 확인합니다. 그 밖에는 아무것도 삭제하지 않고 다른 제한도 없습니다. 상세 보존 일수는 `-retention-days`로 바꿀 수 있고 diff --git a/docs/README.pt.md b/docs/README.pt.md index 9dc4d35..cb38982 100644 --- a/docs/README.pt.md +++ b/docs/README.pt.md @@ -686,6 +686,7 @@ Páginas: | Geografia e redes | Um mapa-múndi do tráfego por país; as redes (AS) de onde o tráfego veio e para onde foi, ao longo do tempo em bits/s e pacotes/s; tráfego por país e por rede | | Ameaças | Hosts que se comunicaram com endereços das suas listas de ameaças, e quanto enviaram | | Registros de fluxo | Quantos registros de fluxo houve e quando (uma barra por intervalo), e os próprios registros, dos mais recentes para os mais antigos, página a página, com colunas selecionáveis | +| Análise offline | Capturas de pacotes (pcap, pcapng) analisadas separadas dos dados ao vivo ([mais](#análise-offline)) | | Conferência de interfaces | Tráfego de cada interface ao longo do tempo (entrada e saída, bits/s e pacotes/s), e números de fluxo ao lado dos contadores de interface, piores primeiro, com os motivos | | Fontes | Equipamentos, amostragem, perdas, coletores, SNMP, o banco de dados de países e redes, o logotipo e **Nomes** | @@ -776,6 +777,19 @@ A mesma visão geral em chinês; todas as páginas estão disponíveis em 13 idi +### Análise offline + +**Análise offline** mostra capturas do Wireshark ou tcpdump com as mesmas páginas dos dados ao vivo, sem misturá-las: + +1. **Enviar arquivos de captura…**: `.pcap` ou `.pcapng`, sem compressão. Até 3 arquivos, cada um com no máximo 50 MB. Os arquivos viram fluxos num banco próprio (`/sandbox/`); os dados ao vivo, seus números e detecções não são afetados. +2. **Analisar**: todas as páginas (visão geral, Top 66, detalhes do tráfego, detecções, caminhos, mapa, registros de fluxo) mostram os arquivos durante todo o seu período. Uma barra laranja nomeia os arquivos; **Voltar aos dados ao vivo** volta. Cada arquivo aparece como um dispositivo, então o campo **Equipamento** mostra um arquivo por vez. +3. As regras de detecção rodam sobre a captura: varreduras, varreduras de portas e tentativas de senha aparecem em **Detecções**. Regras que precisam de um dia de histórico (movimento lateral, envios incomuns) não se aplicam a uma captura. +4. **Excluir** apaga um arquivo e seus dados; **Excluir tudo** apaga tudo. + +A demo inclui uma captura de exemplo com um ataque. + +![Análise offline: arquivos de captura com pacotes, fluxos e período](images/sandbox.png) + ## 10. Interface de terminal ``` @@ -947,7 +961,7 @@ O diretório de dados guarda tudo: | `password` | senhas de login (em hash) | | `inventory.txt` | nomes (**Fontes → Nomes**) | | `logo.png` (ou `.svg`, `.jpg`, `.webp`, `.gif`) | seu logotipo (**Fontes → Logotipo**), se você enviou um | -| `country.mmdb`, `asn.mmdb`, `both.mmdb`, `asn.tsv.gz`, `dbip-country.mmdb`, `dbip-asn.mmdb`, `threats/` | bancos de dados de países e redes e listas de ameaças que você adicionou | +| `country.mmdb`, `asn.mmdb`, `both.mmdb`, `asn.tsv.gz`, `dbip-country.mmdb`, `dbip-asn.mmdb`, `threats/`, `sandbox/` | bancos de dados de países e redes e listas de ameaças que você adicionou | **Por quanto tempo os dados ficam**: o detalhe de fluxos 30 dias; os resumos (visão geral e períodos longos) 400 dias. O que for mais antigo é apagado automaticamente, com verificação a cada 5 minutos; nada mais é apagado e diff --git a/docs/README.ru.md b/docs/README.ru.md index c2bcd08..457b38f 100644 --- a/docs/README.ru.md +++ b/docs/README.ru.md @@ -679,6 +679,7 @@ curl -L https://www.spamhaus.org/drop/drop.txt -o /threats/spamh | География и сети | Карта мира с трафиком по странам; сети (AS), откуда трафик пришёл и куда ушёл, во времени в бит/с и пакетах/с; трафик по странам и по сетям | | Угрозы | Хосты, обращавшиеся к адресам из ваших списков угроз, и сколько они отправили | | Записи потоков | Сколько было записей потоков и когда (столбик на интервал), и сами записи, новые сверху, постранично, с выбором столбцов | +| Офлайн-анализ | Захваты пакетов (pcap, pcapng) анализируются отдельно от живых данных ([подробнее](#офлайн-анализ)) | | Сверка интерфейсов | Трафик каждого интерфейса во времени (вход и выход, бит/с и пакеты/с) и цифры по потокам рядом со счётчиками интерфейсов, худшие сверху, с причинами | | Источники | Устройства, сэмплирование, потери, коллекторы, SNMP, база стран и сетей, логотип и **Названия** | @@ -770,6 +771,19 @@ traffic66 учится на уже имеющейся истории. +### Офлайн-анализ + +**Офлайн-анализ** показывает захваты Wireshark или tcpdump на тех же страницах, что и живые данные, не смешивая их: + +1. **Загрузить файлы захвата…**: `.pcap` или `.pcapng`, без сжатия. До 3 файлов, каждый не больше 50 МБ. Файлы превращаются в потоки в отдельной базе (`/sandbox/`); живые данные, их статистика и находки не затрагиваются. +2. **Анализ**: все страницы (обзор, Top 66, детали трафика, находки, пути, карта, записи потоков) показывают файлы за всё их время. Оранжевая полоса называет файлы; **Вернуться к живым данным** возвращает обратно. Каждый файл выглядит как устройство, поэтому поле **Устройство** показывает по одному файлу. +3. Правила обнаружения работают и на захвате: сканирования, сканирования портов и подбор паролей появляются в **Обнаружения**. Правила, которым нужен день истории (перемещение внутри сети, необычные выгрузки), к захвату не применяются. +4. **Удалить** удаляет файл и его данные; **Удалить всё** удаляет всё. + +В демо есть пример захвата с атакой. + +![Офлайн-анализ: файлы захвата с пакетами, потоками и временем](images/sandbox.png) + ## 10. Терминальный интерфейс ``` @@ -941,7 +955,7 @@ traffic66 -data /var/lib/traffic66 -listen "sflow=:6343,netflow=:2055,ipfix=:473 | `password` | пароли для входа (в виде хешей) | | `inventory.txt` | названия (**Источники → Названия**) | | `logo.png` (или `.svg`, `.jpg`, `.webp`, `.gif`) | ваш логотип (**Источники → Логотип**), если вы его загрузили | -| `country.mmdb`, `asn.mmdb`, `both.mmdb`, `asn.tsv.gz`, `dbip-country.mmdb`, `dbip-asn.mmdb`, `threats/` | добавленные вами базы стран и сетей и списки угроз | +| `country.mmdb`, `asn.mmdb`, `both.mmdb`, `asn.tsv.gz`, `dbip-country.mmdb`, `dbip-asn.mmdb`, `threats/`, `sandbox/` | добавленные вами базы стран и сетей и списки угроз | **Сколько хранятся данные**: детализация потоков 30 дней, сводки (обзор и длинные периоды) 400 дней. Более старые данные удаляются автоматически, проверка каждые 5 минут; больше ничего не удаляется и других ограничений нет. diff --git a/docs/README.ur.md b/docs/README.ur.md index 92960b6..c12ae9e 100644 --- a/docs/README.ur.md +++ b/docs/README.ur.md @@ -672,6 +672,7 @@ application، ملک، ڈیوائس — پر کلک کیا جا سکتا ہے: | مقامات اور نیٹ ورک | ملک کے لحاظ سے ٹریفک کا دنیا کا نقشہ؛ وہ نیٹ ورکس (AS) جہاں سے ٹریفک آئی اور جہاں گئی، وقت کے ساتھ bits/s اور packets/s میں؛ ملک کے حساب سے اور نیٹ ورک کے حساب سے ٹریفک | | خطرے کی معلومات | وہ hosts جنہوں نے آپ کی threat lists کے addresses سے بات کی، اور کتنا بھیجا | | فلو ریکارڈ | کتنے flow records تھے اور کب (ہر interval کے لیے ایک bar)، اور خود records، نئے پہلے، صفحہ بہ صفحہ، منتخب کیے جا سکنے والے columns کے ساتھ | +| آف لائن تجزیہ | pcap، pcapng کیپچر کا لائیو ڈیٹا سے الگ تجزیہ ([مزید](#آف-لائن-تجزیہ)) | | انٹرفیس جانچ | وقت کے ساتھ ہر interface کی ٹریفک (ingress اور egress، bits/s اور packets/s)، اور interface counters کے ساتھ flow کے اعداد، بدترین پہلے، وجوہات کے ساتھ | | ذرائع | ڈیوائسز، sampling، loss، collectors، SNMP، ممالک اور نیٹ ورکس کا ڈیٹابیس، لوگو، اور **نام** | @@ -759,6 +760,19 @@ packets بھیجتا ہے کہ نظر نہیں آتا۔ ڈیمو کا حملہ +### آف لائن تجزیہ + +**آف لائن تجزیہ** Wireshark یا tcpdump کے کیپچر کو لائیو ڈیٹا والے صفحات پر ہی دکھاتا ہے، انہیں ملائے بغیر: + +1. **کیپچر فائلیں اپ لوڈ کریں…**: ‎`.pcap` یا ‎`.pcapng`، کمپریس شدہ نہیں۔ زیادہ سے زیادہ 3 فائلیں، ہر ایک 50 MB تک۔ فائلیں فلو میں بدل کر الگ ڈیٹابیس (`/sandbox/`) میں جاتی ہیں؛ لائیو ڈیٹا، اس کے اعداد اور نتائج متاثر نہیں ہوتے۔ +2. **تجزیہ کریں**: تمام صفحات (جائزہ، Top 66، ٹریفک کی تفصیل، نتائج، فلو راستے، نقشہ، فلو ریکارڈز) کیپچر فائلوں کا پورا وقت دکھاتے ہیں۔ نارنجی پٹی فائلوں کے نام بتاتی ہے؛ **لائیو ڈیٹا پر واپس** سے واپس جائیں۔ ہر فائل ایک ڈیوائس کی طرح دکھتی ہے، اس لیے **آلہ** خانے سے ایک ایک فائل دیکھی جا سکتی ہے۔ +3. شناخت کے قواعد کیپچر پر بھی چلتے ہیں: اسکین، پورٹ اسکین اور پاس ورڈ کا اندازہ **مشتبہ سرگرمی** میں آتے ہیں۔ جن قواعد کو ایک دن کی تاریخ چاہیے (لیٹرل موومنٹ، غیر معمولی اپ لوڈ) وہ کیپچر پر لاگو نہیں ہوتے۔ +4. **حذف کریں** ایک فائل اور اس کا ڈیٹا ہٹاتا ہے؛ **سب حذف کریں** سب کچھ ہٹاتا ہے۔ + +ڈیمو میں حملے والی ایک مثال کیپچر شامل ہے۔ + +![آف لائن تجزیہ: کیپچر فائلیں، ان کے پیکٹ، فلو اور وقت](images/sandbox.png) + ## 10. Terminal UI ``` @@ -927,7 +941,7 @@ traffic66 -data /var/lib/traffic66 -listen "sflow=:6343,netflow=:2055,ipfix=:473 | `password` | login پاس ورڈز (hashed) | | `inventory.txt` | نام (**ذرائع → نام**) | | `logo.png` (یا `.svg`، `.jpg`، `.webp`، `.gif`) | آپ کا لوگو (**ذرائع → لوگو**)، اگر آپ نے اپ لوڈ کیا ہو | -| `country.mmdb`، `asn.mmdb`، `both.mmdb`، `asn.tsv.gz`، `dbip-country.mmdb`، `dbip-asn.mmdb`، `threats/` | آپ کے شامل کردہ ممالک اور نیٹ ورکس کے databases اور threat lists | +| `country.mmdb`، `asn.mmdb`، `both.mmdb`، `asn.tsv.gz`، `dbip-country.mmdb`، `dbip-asn.mmdb`، `threats/`، `sandbox/` | آپ کے شامل کردہ ممالک اور نیٹ ورکس کے databases اور threat lists | **ڈیٹا کتنی دیر رکھا جاتا ہے**: flow detail 30 دن، summaries (overview اور لمبی مدتیں) 400 دن۔ اس سے پرانا ڈیٹا خود بخود حذف ہوتا ہے، ہر 5 منٹ میں جانچ ہوتی ہے؛ اس کے علاوہ کچھ حذف نہیں ہوتا اور کوئی اور حد نہیں۔ detail diff --git a/docs/README.zh.md b/docs/README.zh.md index f87b331..fe2cdb5 100644 --- a/docs/README.zh.md +++ b/docs/README.zh.md @@ -562,6 +562,7 @@ curl -L https://www.spamhaus.org/drop/drop.txt -o /threats/spamh | 地理与运营商 | 世界地图上按国家显示的流量;流量来自和去往哪些网络(AS),以 bits/s 和 packets/s 显示随时间的变化;按国家和按网络统计的流量 | | 威胁情报 | 与威胁情报列表中地址有通信的主机,以及它们发送了多少流量 | | 流记录 | 有多少条流记录、出现在什么时候(每个间隔一根柱),以及流记录本身,最新的在前,分页显示,可选择显示列 | +| 离线分析 | 在实时数据之外分析抓包文件(pcap、pcapng) ([详情](#离线分析)) | | 接口对账 | 每个接口的流量随时间变化(入向和出向,bits/s 和 packets/s),以及流统计与接口计数器并列对比,差异最大的在前,附原因 | | 接入 | 设备、采样、丢包、采集器、SNMP、国家和运营商数据库、Logo,以及 **名称** | @@ -613,6 +614,19 @@ curl -L https://www.spamhaus.org/drop/drop.txt -o /threats/spamh +### 离线分析 + +**离线分析** 用和实时数据相同的页面查看 Wireshark 或 tcpdump 的抓包文件,但不混进实时数据: + +1. **上传抓包文件…**:`.pcap` 或 `.pcapng`,不支持压缩包。最多 3 个文件,每个不超过 50 MB。文件被转换成流,存进单独的数据库(`/sandbox/`),实时数据、统计和发现都不受影响。 +2. **分析**:所有页面(概览、Top 66、流量明细、发现、流向、地图、流记录)都改为显示抓包文件的整个时间段。橙色横条列出文件名,点 **返回实时数据** 返回。每个文件显示为一台设备,用 **设备** 筛选框可以只看一个文件。 +3. 检测规则会在抓包上运行:扫描、端口扫描和密码爆破会列在 **发现** 里。需要一天历史的规则(横向移动、异常上传)不适用于抓包。 +4. **删除** 删除一个文件及其数据;**全部删除** 删除全部。 + +demo 自带一个包含攻击过程的示例抓包。 + +![离线分析:抓包文件及其包数、流数和时间](images/sandbox.png) + ## 10. 终端界面 ``` @@ -759,7 +773,7 @@ traffic66 -data /var/lib/traffic66 -listen "sflow=:6343,netflow=:2055,ipfix=:473 | `password` | 登录密码(哈希) | | `inventory.txt` | 名称(**接入 → 名称**) | | `logo.png`(或 `.svg`、`.jpg`、`.webp`、`.gif`) | 你的 Logo(**接入 → Logo**),如果上传过 | -| `country.mmdb`, `asn.mmdb`, `both.mmdb`, `asn.tsv.gz`, `dbip-country.mmdb`, `dbip-asn.mmdb`, `threats/` | 你添加的国家和运营商数据库以及威胁情报列表 | +| `country.mmdb`, `asn.mmdb`, `both.mmdb`, `asn.tsv.gz`, `dbip-country.mmdb`, `dbip-asn.mmdb`, `threats/`, `sandbox/` | 你添加的国家和运营商数据库以及威胁情报列表 | **数据保留多久**:流明细 30 天,汇总(总览和长时间范围)400 天。更早的数据自动删除,每 5 分钟检查一次;除此之外不删任何数据,也没有其他限制。用 `-retention-days` 修改明细保留天数,任意天数都可以,例如 `-retention-days 365`。占用的磁盘随之增长:保留天数放不下时,侧边栏的 **可用** 会变红。磁盘写满后,新的流无法保存,直到腾出空间。 diff --git a/docs/images/sandbox.png b/docs/images/sandbox.png new file mode 100644 index 0000000000000000000000000000000000000000..730ecfecae41543ea8b37bd6f66f6da6cad702bf GIT binary patch literal 123884 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

; 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

  • q~_nX#VR< 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 sandbox.MaxFileSize { + writeJSON(w, http.StatusRequestEntityTooLarge, map[string]any{"error": "too_big", "max": sandbox.MaxFileSize}) + return + } + f, err := s.SB.Add(r.URL.Query().Get("name"), r.Body, false) + if errors.Is(err, sandbox.ErrLimit) { + writeJSON(w, http.StatusRequestEntityTooLarge, map[string]any{"error": err.Error(), "max": sandbox.MaxFileSize, "max_files": sandbox.MaxFiles}) + return + } + if errors.Is(err, sandbox.ErrNotCapture) { + writeJSON(w, http.StatusUnsupportedMediaType, map[string]any{"error": err.Error(), "code": "not_capture"}) + return + } + if err != nil { + fail(w, err) + return + } + writeJSON(w, http.StatusOK, map[string]any{"file": f, "sandbox": s.SB.Info()}) +} + +// deleteSandbox deletes one file (?name=) or all of them, and the data. +func (s *Server) deleteSandbox(w http.ResponseWriter, r *http.Request) { + if s.SB == nil { + fail(w, errors.New("offline analysis is not available")) + return + } + if err := s.SB.Delete(r.URL.Query().Get("name")); err != nil { + fail(w, err) + return + } + writeJSON(w, http.StatusOK, s.SB.Info()) +} diff --git a/internal/api/sandbox_test.go b/internal/api/sandbox_test.go new file mode 100644 index 0000000..1a17bba --- /dev/null +++ b/internal/api/sandbox_test.go @@ -0,0 +1,68 @@ +package api + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http/httptest" + "path/filepath" + "testing" + "time" + + "github.com/githubflyideas/traffic66/internal/enrich" + "github.com/githubflyideas/traffic66/internal/sandbox" + "github.com/githubflyideas/traffic66/internal/store" +) + +// ds=sb answers from the sandbox; without it the live database answers and +// has none of the capture's data. +func TestSandboxData(t *testing.T) { + dir := t.TempDir() + st, err := store.Open(store.Options{Dir: filepath.Join(dir, "live")}) + if err != nil { + t.Fatal(err) + } + defer st.Close() + sb := sandbox.New(filepath.Join(dir, "sandbox"), enrich.NewInventory(), enrich.NewASNDB(), enrich.NewThreats()) + defer sb.Close() + s := &Server{Store: st, SB: sb, Inv: enrich.NewInventory(), ASN: enrich.NewASNDB(), Thr: enrich.NewThreats()} + h := s.data((*Server).findings) + end := time.Now().Add(-3 * time.Hour).Truncate(time.Minute) + q := fmt.Sprintf("from=%d&to=%d", end.Add(-time.Hour).UnixMilli(), end.Add(time.Hour).UnixMilli()) + w := httptest.NewRecorder() + h(w, httptest.NewRequest("GET", "/api/findings?ds=sb&"+q, nil)) + if w.Code != 400 { + t.Fatalf("empty sandbox: %d %s", w.Code, w.Body) + } + var buf bytes.Buffer + sandbox.Sample(&buf, end) + w = httptest.NewRecorder() + s.putSandboxFile(w, httptest.NewRequest("POST", "/api/sandbox/files?name=a.pcap", &buf)) + if w.Code != 200 { + t.Fatalf("upload: %d %s", w.Code, w.Body) + } + for i := 0; i < 300 && !sb.Info().Ready; i++ { + time.Sleep(100 * time.Millisecond) + } + for sb.Info().Busy { + time.Sleep(100 * time.Millisecond) + } + count := func(ds string) int { + w := httptest.NewRecorder() + h(w, httptest.NewRequest("GET", "/api/findings?"+ds+q, nil)) + var out struct{ Findings []any } + json.Unmarshal(w.Body.Bytes(), &out) + return len(out.Findings) + } + if n := count("ds=sb&"); n != 3 { + t.Errorf("sandbox findings: %d", n) + } + if n := count(""); n != 0 { + t.Errorf("live findings: %d", n) + } + w = httptest.NewRecorder() + s.putSandboxFile(w, httptest.NewRequest("POST", "/api/sandbox/files?name=b.txt", bytes.NewReader([]byte("hello there")))) + if w.Code != 415 { + t.Errorf("text file: %d", w.Code) + } +} diff --git a/internal/api/server.go b/internal/api/server.go index cdc4416..0c33375 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -21,6 +21,7 @@ import ( "github.com/githubflyideas/traffic66/internal/dnsres" "github.com/githubflyideas/traffic66/internal/enrich" "github.com/githubflyideas/traffic66/internal/pipeline" + "github.com/githubflyideas/traffic66/internal/sandbox" "github.com/githubflyideas/traffic66/internal/store" ) @@ -34,6 +35,7 @@ type Server struct { Thr *enrich.Threats DNS *dnsres.Resolver Det *detect.Detector + SB *sandbox.Sandbox // offline analysis of capture files Static fs.FS Version string Demo bool @@ -82,20 +84,23 @@ func (s *Server) Handler() http.Handler { mux.HandleFunc("GET /logo", s.logo) api := func(pattern string, h http.HandlerFunc) { mux.Handle(pattern, s.auth(h)) } api("GET /api/status", s.status) - api("GET /api/overview", s.overview) - api("GET /api/topn", s.topn) - api("GET /api/sankey", s.sankey) - api("GET /api/series", s.series) - api("GET /api/records", s.records) - api("GET /api/threats", s.threats) + api("GET /api/overview", s.data((*Server).overview)) + api("GET /api/topn", s.data((*Server).topn)) + api("GET /api/sankey", s.data((*Server).sankey)) + api("GET /api/series", s.data((*Server).series)) + api("GET /api/records", s.data((*Server).records)) + api("GET /api/threats", s.data((*Server).threats)) api("GET /api/ifaces", s.ifaces) api("GET /api/recon", s.recon) api("GET /api/sources", s.sources) api("POST /api/resolve", s.resolve) api("GET /api/inventory", s.getInventory) api("POST /api/inventory", s.putInventory) - api("GET /api/findings", s.findings) - api("POST /api/findings", s.setFindings) + api("GET /api/findings", s.data((*Server).findings)) + api("POST /api/findings", s.data((*Server).setFindings)) + api("GET /api/sandbox", s.getSandbox) + api("POST /api/sandbox/files", s.putSandboxFile) + api("DELETE /api/sandbox/files", s.deleteSandbox) api("POST /api/logo", s.putLogo) api("DELETE /api/logo", s.deleteLogo) api("GET /api/geo", s.getGeo) diff --git a/internal/pcapfile/pcapfile.go b/internal/pcapfile/pcapfile.go new file mode 100644 index 0000000..f0154bc --- /dev/null +++ b/internal/pcapfile/pcapfile.go @@ -0,0 +1,282 @@ +// Package pcapfile reads packet capture files: classic pcap (microsecond and +// nanosecond, either byte order) and pcapng, optionally gzip-compressed, as +// written by tcpdump, Wireshark and dumpcap. +package pcapfile + +import ( + "bufio" + "encoding/binary" + "errors" + "fmt" + "io" + "math" + "time" +) + +// Link types handled by the flow builder. +const ( + LinkNull = 0 + LinkEthernet = 1 + LinkRaw = 101 + LinkLoop = 108 + LinkSLL = 113 + LinkIPv4 = 228 + LinkIPv6 = 229 + LinkSLL2 = 276 +) + +// Packet is one captured packet. Data is valid until the next call to Next. +type Packet struct { + Time time.Time + Data []byte // captured bytes (may be shorter than the packet) + WireLen int // length of the packet on the wire + Link int // link type of the interface it was captured on +} + +// ErrFormat is returned for files that are neither pcap nor pcapng. +var ErrFormat = errors.New("not a pcap or pcapng file") + +// maxPacket bounds one packet record; larger lengths mean a damaged file. +const maxPacket = 1 << 18 + +// Reader reads packets from a capture file. +type Reader struct { + r *bufio.Reader + ng bool + bo binary.ByteOrder + buf []byte + Count uint64 + + // pcap + link int + nanos bool + + // pcapng + ifaces []iface + lastTS time.Time +} + +type iface struct { + link int + res float64 // seconds per timestamp unit +} + +// Format names the format of a file from its first bytes ("pcap", +// "pcapng") or returns "". +func Format(b []byte) string { + if len(b) < 4 { + return "" + } + switch binary.LittleEndian.Uint32(b) { + case 0xa1b2c3d4, 0xd4c3b2a1, 0xa1b23c4d, 0x4d3cb2a1: + return "pcap" + case 0x0a0d0d0a: + return "pcapng" + } + return "" +} + +// NewReader reads the file header from r. +func NewReader(r io.Reader) (*Reader, error) { + br := bufio.NewReaderSize(r, 1<<16) + head, err := br.Peek(4) + if err != nil { + return nil, ErrFormat + } + rd := &Reader{r: br, buf: make([]byte, 0, 1<<16)} + switch Format(head) { + case "pcap": + return rd, rd.pcapHeader() + case "pcapng": + rd.ng = true + return rd, nil + } + return nil, ErrFormat +} + +func (rd *Reader) read(n int) ([]byte, error) { + if cap(rd.buf) < n { + rd.buf = make([]byte, n) + } + b := rd.buf[:n] + _, err := io.ReadFull(rd.r, b) + return b, err +} + +func (rd *Reader) pcapHeader() error { + h, err := rd.read(24) + if err != nil { + return ErrFormat + } + switch binary.LittleEndian.Uint32(h) { + case 0xa1b2c3d4: + rd.bo = binary.LittleEndian + case 0xa1b23c4d: + rd.bo, rd.nanos = binary.LittleEndian, true + case 0xd4c3b2a1: + rd.bo = binary.BigEndian + case 0x4d3cb2a1: + rd.bo, rd.nanos = binary.BigEndian, true + } + // the low 16 bits are the link type; the high bits may carry an FCS flag + rd.link = int(rd.bo.Uint32(h[20:]) & 0xffff) + return nil +} + +// Next returns the next packet, or io.EOF at the end of the file. +func (rd *Reader) Next(p *Packet) error { + if rd.ng { + return rd.nextNG(p) + } + h, err := rd.read(16) + if err != nil { + if err == io.ErrUnexpectedEOF { + return io.EOF // a capture cut off mid-header + } + return err + } + sec, frac := rd.bo.Uint32(h), rd.bo.Uint32(h[4:]) + incl, orig := rd.bo.Uint32(h[8:]), rd.bo.Uint32(h[12:]) + if incl > maxPacket { + return fmt.Errorf("packet %d: damaged record (%d bytes)", rd.Count+1, incl) + } + ns := int64(frac) * 1000 + if rd.nanos { + ns = int64(frac) + } + data, err := rd.read(int(incl)) + if err != nil { + return io.EOF + } + rd.Count++ + *p = Packet{Time: time.Unix(int64(sec), ns).UTC(), Data: data, WireLen: int(max(orig, incl)), Link: rd.link} + return nil +} + +func (rd *Reader) nextNG(p *Packet) error { + for { + h, err := rd.read(8) + if err != nil { + if err == io.ErrUnexpectedEOF { + return io.EOF + } + return err + } + typ := binary.LittleEndian.Uint32(h) + if typ == 0x0a0d0d0a { // section header: learn the byte order + if err := rd.section(h); err != nil { + return err + } + continue + } + if rd.bo == nil { + return ErrFormat + } + typ = rd.bo.Uint32(h) + blen := rd.bo.Uint32(h[4:]) + if blen < 12 || blen > maxPacket+64 || blen%4 != 0 { + return fmt.Errorf("block after packet %d: damaged (%d bytes)", rd.Count, blen) + } + body, err := rd.read(int(blen - 8)) + if err != nil { + return io.EOF + } + body = body[:len(body)-4] // trailing length + switch typ { + case 1: // interface description + if len(body) < 8 { + continue + } + ifc := iface{link: int(rd.bo.Uint16(body)), res: 1e-6} + rd.options(body[8:], func(code uint16, v []byte) { + if code == 9 && len(v) >= 1 { // if_tsresol + if v[0]&0x80 != 0 { + ifc.res = math.Pow(2, -float64(v[0]&0x7f)) + } else { + ifc.res = math.Pow(10, -float64(v[0])) + } + } + }) + rd.ifaces = append(rd.ifaces, ifc) + case 6, 2: // enhanced packet, obsolete packet + if len(body) < 20 { + continue + } + id := rd.bo.Uint32(body) + if typ == 2 { + id = uint32(rd.bo.Uint16(body)) + } + ts := uint64(rd.bo.Uint32(body[4:]))<<32 | uint64(rd.bo.Uint32(body[8:])) + incl, orig := rd.bo.Uint32(body[12:]), rd.bo.Uint32(body[16:]) + if int(id) >= len(rd.ifaces) || 20+int(incl) > len(body) { + continue + } + ifc := rd.ifaces[id] + t := tsTime(ts, ifc.res) + rd.lastTS = t + rd.Count++ + *p = Packet{Time: t, Data: body[20 : 20+incl], WireLen: int(max(orig, incl)), Link: ifc.link} + return nil + case 3: // simple packet: no timestamp, interface 0 + if len(body) < 4 || len(rd.ifaces) == 0 { + continue + } + orig := rd.bo.Uint32(body) + data := body[4:] + if int(orig) < len(data) { + data = data[:orig] + } + rd.Count++ + *p = Packet{Time: rd.lastTS, Data: data, WireLen: int(max(orig, uint32(len(data)))), Link: rd.ifaces[0].link} + return nil + } + } +} + +func (rd *Reader) section(h []byte) error { + blen := h[4:8] + bom, err := rd.r.Peek(4) + if err != nil { + return ErrFormat + } + switch binary.LittleEndian.Uint32(bom) { + case 0x1a2b3c4d: + rd.bo = binary.LittleEndian + case 0x4d3c2b1a: + rd.bo = binary.BigEndian + default: + return ErrFormat + } + n := rd.bo.Uint32(blen) + if n < 28 || n > 1<<20 { + return ErrFormat + } + if _, err := rd.read(int(n - 8)); err != nil { + return ErrFormat + } + rd.ifaces = nil // interfaces are per section + return nil +} + +func (rd *Reader) options(b []byte, fn func(code uint16, v []byte)) { + for len(b) >= 4 { + code, n := rd.bo.Uint16(b), int(rd.bo.Uint16(b[2:])) + if code == 0 || 4+n > len(b) { + return + } + fn(code, b[4:4+n]) + b = b[4+(n+3)&^3:] + } +} + +func tsTime(ts uint64, res float64) time.Time { + if res == 1e-6 { + return time.UnixMicro(int64(ts)).UTC() + } + if res == 1e-9 { + return time.Unix(0, int64(ts)).UTC() + } + sec := float64(ts) * res + s := math.Floor(sec) + return time.Unix(int64(s), int64((sec-s)*1e9)).UTC() +} diff --git a/internal/pcapfile/pcapfile_test.go b/internal/pcapfile/pcapfile_test.go new file mode 100644 index 0000000..00ff26f --- /dev/null +++ b/internal/pcapfile/pcapfile_test.go @@ -0,0 +1,111 @@ +package pcapfile + +import ( + "bytes" + "encoding/binary" + "io" + "testing" + "time" +) + +func readAll(t *testing.T, b []byte) []Packet { + t.Helper() + rd, err := NewReader(bytes.NewReader(b)) + if err != nil { + t.Fatal(err) + } + var out []Packet + for { + var p Packet + err := rd.Next(&p) + if err == io.EOF { + return out + } + if err != nil { + t.Fatal(err) + } + p.Data = append([]byte(nil), p.Data...) + out = append(out, p) + } +} + +func TestPcapRoundTrip(t *testing.T) { + var buf bytes.Buffer + w, _ := NewWriter(&buf, LinkEthernet) + t0 := time.Date(2026, 9, 1, 10, 0, 0, 123456000, time.UTC) + w.Write(t0, []byte{1, 2, 3}, 60) + w.Write(t0.Add(time.Second), []byte{4, 5}, 2) + for _, b := range [][]byte{buf.Bytes()} { + ps := readAll(t, b) + if len(ps) != 2 || !ps[0].Time.Equal(t0) || ps[0].WireLen != 60 || ps[1].Link != LinkEthernet || !bytes.Equal(ps[1].Data, []byte{4, 5}) { + t.Fatalf("%+v", ps) + } + } + // cut off in the middle of a packet: the complete ones are kept + if ps := readAll(t, buf.Bytes()[:buf.Len()-1]); len(ps) != 1 { + t.Fatalf("truncated: %d packets", len(ps)) + } +} + +// pcapng written by hand: section header, two interfaces (the second with +// nanosecond resolution), enhanced and simple packet blocks, an unknown block. +func TestPcapng(t *testing.T) { + le := binary.LittleEndian + var b bytes.Buffer + block := func(typ uint32, body []byte) { + for len(body)%4 != 0 { + body = append(body, 0) + } + n := uint32(12 + len(body)) + b.Write(le.AppendUint32(le.AppendUint32(nil, typ), n)) + b.Write(body) + b.Write(le.AppendUint32(nil, n)) + } + shb := le.AppendUint32(nil, 0x1a2b3c4d) + shb = append(shb, 1, 0, 0, 0) + shb = append(shb, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff) + block(0x0a0d0d0a, shb) + idb := func(link uint16, opts []byte) []byte { + x := le.AppendUint16(nil, link) + x = append(x, 0, 0) + x = le.AppendUint32(x, 0) + return append(x, opts...) + } + block(1, idb(LinkEthernet, nil)) + block(1, idb(LinkRaw, []byte{9, 0, 1, 0, 9, 0, 0, 0, 0, 0, 0, 0})) // if_tsresol=9, end + epb := func(id uint32, ts uint64, data []byte, orig uint32) []byte { + x := le.AppendUint32(nil, id) + x = le.AppendUint32(x, uint32(ts>>32)) + x = le.AppendUint32(x, uint32(ts)) + x = le.AppendUint32(x, uint32(len(data))) + x = le.AppendUint32(x, orig) + return append(x, data...) + } + t0 := time.Date(2026, 9, 1, 10, 0, 0, 0, time.UTC) + block(6, epb(0, uint64(t0.UnixMicro()), []byte{1, 2, 3}, 100)) + block(99, []byte{1, 2, 3, 4}) + block(6, epb(1, uint64(t0.Add(time.Second+7).UnixNano()), []byte{0x45, 0}, 2)) + block(3, append(le.AppendUint32(nil, 5), 9, 9, 9, 9, 9, 9, 9)) + ps := readAll(t, b.Bytes()) + if len(ps) != 3 { + t.Fatalf("%d packets", len(ps)) + } + if !ps[0].Time.Equal(t0) || ps[0].WireLen != 100 || ps[0].Link != LinkEthernet { + t.Errorf("first %+v", ps[0]) + } + if !ps[1].Time.Equal(t0.Add(time.Second+7)) || ps[1].Link != LinkRaw { + t.Errorf("second %+v", ps[1]) + } + if len(ps[2].Data) != 5 || !ps[2].Time.Equal(ps[1].Time) { + t.Errorf("simple %+v", ps[2]) + } +} + +func TestNotACapture(t *testing.T) { + gz := []byte{0x1f, 0x8b, 8, 0, 0, 0, 0, 0, 0, 3} + for _, b := range [][]byte{[]byte("hello world, this is text"), gz, []byte("ustar\x00")} { + if _, err := NewReader(bytes.NewReader(b)); err != ErrFormat { + t.Fatalf("%q: %v", b, err) + } + } +} diff --git a/internal/pcapfile/write.go b/internal/pcapfile/write.go new file mode 100644 index 0000000..7b1e22d --- /dev/null +++ b/internal/pcapfile/write.go @@ -0,0 +1,40 @@ +package pcapfile + +import ( + "encoding/binary" + "io" + "time" +) + +// Writer writes a classic pcap file (microsecond timestamps, little endian). +type Writer struct { + w io.Writer + hdr [16]byte +} + +// NewWriter writes the file header for link type link. +func NewWriter(w io.Writer, link int) (*Writer, error) { + var h [24]byte + binary.LittleEndian.PutUint32(h[0:], 0xa1b2c3d4) + binary.LittleEndian.PutUint16(h[4:], 2) + binary.LittleEndian.PutUint16(h[6:], 4) + binary.LittleEndian.PutUint32(h[16:], 65535) + binary.LittleEndian.PutUint32(h[20:], uint32(link)) + _, err := w.Write(h[:]) + return &Writer{w: w}, err +} + +// Write writes one packet of wireLen bytes on the wire, of which data was +// captured. +func (w *Writer) Write(t time.Time, data []byte, wireLen int) error { + us := t.UnixMicro() + binary.LittleEndian.PutUint32(w.hdr[0:], uint32(us/1e6)) + binary.LittleEndian.PutUint32(w.hdr[4:], uint32(us%1e6)) + binary.LittleEndian.PutUint32(w.hdr[8:], uint32(len(data))) + binary.LittleEndian.PutUint32(w.hdr[12:], uint32(max(wireLen, len(data)))) + if _, err := w.w.Write(w.hdr[:]); err != nil { + return err + } + _, err := w.w.Write(data) + return err +} diff --git a/internal/sandbox/flows.go b/internal/sandbox/flows.go new file mode 100644 index 0000000..4e3fbf9 --- /dev/null +++ b/internal/sandbox/flows.go @@ -0,0 +1,134 @@ +package sandbox + +import ( + "encoding/binary" + "net/netip" + "time" + + "github.com/githubflyideas/traffic66/internal/flow" + "github.com/githubflyideas/traffic66/internal/packet" + "github.com/githubflyideas/traffic66/internal/pcapfile" +) + +// builder turns packets into flow records the way local capture does +// (60 s active, 15 s idle timeout), but on the packets' own timestamps. +type builder struct { + exporter netip.Addr + domain uint32 + table map[fkey]*fentry + swept time.Time + emit func([]flow.Record) + skipped uint64 // packets without an IP header +} + +type fkey struct { + src, dst netip.Addr + sport, dport uint16 + proto uint8 + vlan uint16 + encap flow.Encap + l2 bool +} + +type fentry struct { + first, last time.Time + pkts, bytes uint64 + flags uint8 +} + +const ( + activeTimeout = 60 * time.Second + idleTimeout = 15 * time.Second + maxEntries = 500000 +) + +func newBuilder(exporter netip.Addr, domain uint32, emit func([]flow.Record)) *builder { + return &builder{exporter: exporter, domain: domain, table: map[fkey]*fentry{}, emit: emit} +} + +// decode reads the headers of a packet captured on link type link. +func decode(p *pcapfile.Packet, r *flow.Record) (l2 bool, ok bool) { + b := p.Data + switch p.Link { + case pcapfile.LinkEthernet: + return true, packet.Ethernet(b, r) + case pcapfile.LinkRaw, pcapfile.LinkIPv4, pcapfile.LinkIPv6, 12, 14: + return false, packet.IP(b, r) + case pcapfile.LinkNull, pcapfile.LinkLoop: + if len(b) < 4 { + return false, false + } + return false, packet.IP(b[4:], r) + case pcapfile.LinkSLL: + if len(b) < 16 { + return false, false + } + return false, ipType(binary.BigEndian.Uint16(b[14:])) && packet.IP(b[16:], r) + case pcapfile.LinkSLL2: + if len(b) < 20 { + return false, false + } + return false, ipType(binary.BigEndian.Uint16(b)) && packet.IP(b[20:], r) + } + return false, false +} + +func ipType(t uint16) bool { return t == 0x0800 || t == 0x86dd } + +func (b *builder) add(p *pcapfile.Packet) { + var r flow.Record + l2, ok := decode(p, &r) + if !ok { + b.skipped++ + return + } + k := fkey{r.Src, r.Dst, r.SrcPort, r.DstPort, r.Proto, r.VLAN, r.Encap, l2} + e := b.table[k] + if e == nil { + if len(b.table) >= maxEntries { + b.sweep(p.Time, true) + } + e = &fentry{first: p.Time} + b.table[k] = e + } + if p.Time.After(e.last) { + e.last = p.Time + } + e.pkts++ + n := uint64(p.WireLen) + if l2 { + n += 4 // captured frames exclude the FCS + } + e.bytes += n + e.flags |= r.TCPFlags + if p.Time.Sub(b.swept) >= time.Second { + b.sweep(p.Time, false) + } +} + +// sweep emits flows that ended (idle) or ran long enough (active); all +// emits everything. +func (b *builder) sweep(now time.Time, all bool) { + b.swept = now + var out []flow.Record + for k, e := range b.table { + idle := now.Sub(e.last) >= idleTimeout + active := now.Sub(e.first) >= activeTimeout + if !all && !idle && !active { + continue + } + if e.pkts > 0 { + out = append(out, flow.Record{Start: e.first, End: e.last, Src: k.src, Dst: k.dst, SrcPort: k.sport, DstPort: k.dport, + Proto: k.proto, VLAN: k.vlan, Encap: k.encap, TCPFlags: e.flags, Bytes: e.bytes, Packets: e.pkts, L2: k.l2, + Sampling: 1, Mult: 1, SamplingKnown: true, Exporter: b.exporter, Domain: b.domain, Source: flow.SrcCapture, Direction: 255}) + } + if idle || all { + delete(b.table, k) + } else { + *e = fentry{first: now, last: now} + } + } + if len(out) > 0 { + b.emit(out) + } +} diff --git a/internal/sandbox/sample.go b/internal/sandbox/sample.go new file mode 100644 index 0000000..eae1e35 --- /dev/null +++ b/internal/sandbox/sample.go @@ -0,0 +1,135 @@ +package sandbox + +import ( + "bufio" + "encoding/binary" + "io" + "math/rand/v2" + "net/netip" + "sort" + "time" + + "github.com/githubflyideas/traffic66/internal/pcapfile" +) + +// SampleName is the file name of the demo's example capture. +const SampleName = "example-office-attack.pcap" + +// Sample writes the demo's example capture: 15 minutes of an office network +// ending at end, with an attack inside it. A compromised PC (10.10.3.27) +// scans every low port of the file server, sweeps the office for SMB, then +// guesses RDP passwords on the file server. Only headers are captured (snap length 54), +// so the file stays small while the traffic sizes are real. +func Sample(w io.Writer, end time.Time) error { + rng := rand.New(rand.NewPCG(66, 6)) + start := end.Add(-15 * time.Minute) + type pkt struct { + t time.Time + src, dst netip.Addr + sport, dport uint16 + proto, flags uint8 + size int + } + var ps []pkt + at := func(from, d time.Duration) time.Time { + return start.Add(from + time.Duration(rng.Int64N(int64(d)))) + } + ip := func(s string) netip.Addr { return netip.MustParseAddr(s) } + office := func() netip.Addr { + return netip.AddrFrom4([4]byte{10, 10, byte(1 + rng.IntN(4)), byte(2 + rng.IntN(250))}) + } + // ordinary traffic: web, video calls, DNS, file shares + servers := []struct { + a string + port uint16 + proto uint8 + mbit float64 + }{ + {"142.250.196.110", 443, 6, 6}, {"151.101.1.140", 443, 6, 3}, {"13.107.42.14", 443, 6, 4}, + {"104.16.132.229", 443, 6, 2}, {"52.84.150.39", 443, 6, 3}, {"170.114.52.2", 8801, 17, 2.5}, + {"114.114.114.114", 53, 17, 0.05}, {"8.8.8.8", 53, 17, 0.05}, {"10.20.0.15", 445, 6, 5}, + } + clients := make([]netip.Addr, 40) + for i := range clients { + clients[i] = office() + } + for _, s := range servers { + srv := ip(s.a) + n := int(s.mbit * 1e6 / 8 / 1200 * 900 / 50) // packets in 15 minutes, at 1/50 of the rate + for i := 0; i < n; i++ { + c := clients[rng.IntN(len(clients))] + // one session per client and server + sport := uint16(49152 + (int(c.As4()[2])*251+int(c.As4()[3])+int(s.port))%16000) + size := 1200 + rng.IntN(300) + if s.port == 53 { + size = 80 + rng.IntN(60) + } + t := at(0, 15*time.Minute) + if rng.IntN(5) == 0 { // requests and acks upstream + ps = append(ps, pkt{t, c, srv, sport, s.port, s.proto, 0x18, 66 + rng.IntN(400)}) + } else { + ps = append(ps, pkt{t, srv, c, s.port, sport, s.proto, 0x18, size}) + } + } + } + pc, fileServer := ip("10.10.3.27"), ip("10.20.0.15") + // minute 3-5: SMB sweep of the office, one SYN per address + for i := 0; i < 600; i++ { + ps = append(ps, pkt{at(3*time.Minute, 2*time.Minute), pc, netip.AddrFrom4([4]byte{10, 10, byte(1 + i/250), byte(2 + i%250)}), + uint16(40000 + rng.IntN(20000)), 445, 6, 0x02, 60}) + } + // minute 1-2: every port up to 1024 on the file server + for p := 1; p <= 1024; p++ { + ps = append(ps, pkt{at(time.Minute, time.Minute), pc, fileServer, 51000, uint16(p), 6, 0x02, 60}) + } + // minute 12-15: RDP password guessing, 300 short connections + for i := 0; i < 300; i++ { + t := at(12*time.Minute, 3*time.Minute-200*time.Millisecond) + sport := uint16(50000 + i) + for j := 0; j < 8; j++ { + ps = append(ps, pkt{t.Add(time.Duration(j) * 20 * time.Millisecond), pc, fileServer, sport, 3389, 6, 0x18, 90 + rng.IntN(150)}) + } + } + sort.Slice(ps, func(i, j int) bool { return ps[i].t.Before(ps[j].t) }) + + bw := bufio.NewWriter(w) + pw, err := pcapfile.NewWriter(bw, pcapfile.LinkEthernet) + if err != nil { + return err + } + buf := make([]byte, 54) + for _, p := range ps { + n := frame(buf, p.src, p.dst, p.sport, p.dport, p.proto, p.flags, p.size) + if err := pw.Write(p.t, buf[:n], p.size); err != nil { + return err + } + } + return bw.Flush() +} + +// frame writes Ethernet, IPv4 and TCP/UDP headers for a packet of size +// bytes on the wire and returns how many header bytes were written. +func frame(b []byte, src, dst netip.Addr, sport, dport uint16, proto, flags uint8, size int) int { + clear(b) + copy(b[0:6], []byte{0, 0x1b, 0x21, 1, 2, 3}) + copy(b[6:12], []byte{0, 0x1b, 0x21, 4, 5, 6}) + binary.BigEndian.PutUint16(b[12:], 0x0800) + ipb := b[14:] + ipb[0] = 0x45 + binary.BigEndian.PutUint16(ipb[2:], uint16(max(size-14, 40))) + ipb[8] = 64 + ipb[9] = proto + s4, d4 := src.As4(), dst.As4() + copy(ipb[12:16], s4[:]) + copy(ipb[16:20], d4[:]) + l4 := ipb[20:] + binary.BigEndian.PutUint16(l4[0:], sport) + binary.BigEndian.PutUint16(l4[2:], dport) + if proto == 17 { + binary.BigEndian.PutUint16(l4[4:], uint16(max(size-34, 8))) + return 14 + 20 + 8 + } + l4[12] = 5 << 4 + l4[13] = flags + return 14 + 20 + 20 +} diff --git a/internal/sandbox/sandbox.go b/internal/sandbox/sandbox.go new file mode 100644 index 0000000..461aa35 --- /dev/null +++ b/internal/sandbox/sandbox.go @@ -0,0 +1,516 @@ +// Package sandbox imports packet capture files into a database of their own, +// apart from the live data: the same pages and detection rules then work on +// the capture, and deleting the sandbox removes both the files and the +// database. +package sandbox + +import ( + "encoding/json" + "errors" + "fmt" + "hash/fnv" + "io" + "log" + "net/netip" + "os" + "path/filepath" + "regexp" + "strings" + "sync" + "time" + + "github.com/githubflyideas/traffic66/internal/detect" + "github.com/githubflyideas/traffic66/internal/enrich" + "github.com/githubflyideas/traffic66/internal/flow" + "github.com/githubflyideas/traffic66/internal/pcapfile" + "github.com/githubflyideas/traffic66/internal/pipeline" + "github.com/githubflyideas/traffic66/internal/store" +) + +// Limits of the free edition. +var ( + MaxFiles = 3 + MaxFileSize = int64(50 << 20) +) + +// File is one capture file in the sandbox. +type File struct { + Name string `json:"name"` + Size int64 `json:"size"` + Added time.Time `json:"added"` + Sample bool `json:"sample,omitempty"` // the demo's example; not counted against the limits + Packets uint64 `json:"packets"` + Skipped uint64 `json:"skipped"` // packets without an IP header + Flows uint64 `json:"flows"` + First time.Time `json:"first"` + Last time.Time `json:"last"` + Status string `json:"status"` // waiting, importing, done, error + Error string `json:"error,omitempty"` + Exporter string `json:"exporter"` // the "device" the file's flows appear under +} + +// Sandbox holds the uploaded files and their database. +type Sandbox struct { + Dir string + Inv *enrich.Inventory // the live inventory: names of networks and hosts + ASN *enrich.ASNDB + Thr *enrich.Threats + + mu sync.Mutex + files []*File + st *store.Store + det *detect.Detector + inv *enrich.Inventory // live inventory plus a device per file + busy bool + gen int // bumped by every rebuild; stale imports stop + wake chan struct{} +} + +// ErrLimit is returned when a file would exceed the limits. +var ErrLimit = errors.New("limit") + +// ErrNotCapture is returned for files that are not pcap or pcapng. +var ErrNotCapture = errors.New("not a capture file: use .pcap or .pcapng as saved by Wireshark or tcpdump (not compressed)") + +// New opens the sandbox in dir (creating nothing until a file is added) and +// starts its import worker. +func New(dir string, inv *enrich.Inventory, asn *enrich.ASNDB, thr *enrich.Threats) *Sandbox { + sb := &Sandbox{Dir: dir, Inv: inv, ASN: asn, Thr: thr, wake: make(chan struct{}, 1)} + sb.load() + go sb.worker() + if len(sb.files) > 0 { + sb.mu.Lock() + if sb.st == nil { // database missing: rebuild from the files + for _, f := range sb.files { + f.Status = "waiting" + } + } + sb.mu.Unlock() + sb.kick() + } + return sb +} + +func (sb *Sandbox) pcapDir() string { return filepath.Join(sb.Dir, "files") } +func (sb *Sandbox) dbDir() string { return filepath.Join(sb.Dir, "db") } +func (sb *Sandbox) indexPath() string { + return filepath.Join(sb.Dir, "files.json") +} + +func (sb *Sandbox) load() { + b, err := os.ReadFile(sb.indexPath()) + if err != nil { + return + } + var fs []*File + if json.Unmarshal(b, &fs) != nil { + return + } + for _, f := range fs { + if _, err := os.Stat(filepath.Join(sb.pcapDir(), f.Name)); err == nil { + sb.files = append(sb.files, f) + } + } + if _, err := os.Stat(filepath.Join(sb.dbDir(), "traffic66.duckdb")); err == nil { + if err := sb.open(); err != nil { + log.Printf("sandbox: %v", err) + } + } + for _, f := range sb.files { + if f.Status != "done" && f.Status != "error" { + f.Status = "waiting" + } + } +} + +func (sb *Sandbox) save() { + b, _ := json.MarshalIndent(sb.files, "", " ") + os.MkdirAll(sb.Dir, 0o755) + tmp := sb.indexPath() + ".tmp" + if os.WriteFile(tmp, b, 0o644) == nil { + os.Rename(tmp, sb.indexPath()) + } +} + +// open opens (or creates) the database; sb.mu held or not yet shared. +func (sb *Sandbox) open() error { + st, err := store.Open(store.Options{Dir: sb.dbDir(), MemoryFraction: 0.05, Threads: 2}) + if err != nil { + return err + } + sb.st = st + sb.refreshInventory() + sb.det = detect.New(st, sb.inv, detect.Config{}) + return nil +} + +// refreshInventory names each file's exporter after the file. +func (sb *Sandbox) refreshInventory() { + inv := enrich.NewInventory() + var b strings.Builder + if sb.Inv != nil { + b.WriteString(sb.Inv.Text()) + b.WriteString("\n") + } + for _, f := range sb.files { + fmt.Fprintf(&b, "device %s %s\n", f.Exporter, f.Name) + } + if err := inv.Parse(b.String()); err != nil { + log.Printf("sandbox: inventory: %v", err) + } + sb.inv = inv + if sb.det != nil { + sb.det.Inv = inv + } +} + +// Store returns the sandbox database, or nil when there is none. +func (sb *Sandbox) Store() (*store.Store, *detect.Detector, *enrich.Inventory) { + sb.mu.Lock() + defer sb.mu.Unlock() + return sb.st, sb.det, sb.inv +} + +// Info describes the sandbox for the UI. +type Info struct { + Files []File `json:"files"` + Busy bool `json:"busy"` + First time.Time `json:"first"` + Last time.Time `json:"last"` + MaxFiles int `json:"max_files"` + MaxFileSize int64 `json:"max_file_size"` + Ready bool `json:"ready"` // has data to look at +} + +func (sb *Sandbox) Info() Info { + sb.mu.Lock() + defer sb.mu.Unlock() + in := Info{Files: []File{}, Busy: sb.busy, MaxFiles: MaxFiles, MaxFileSize: MaxFileSize} + for _, f := range sb.files { + in.Files = append(in.Files, *f) + if f.Flows == 0 { + continue + } + if in.First.IsZero() || f.First.Before(in.First) { + in.First = f.First + } + if f.Last.After(in.Last) { + in.Last = f.Last + } + in.Ready = sb.st != nil + } + return in +} + +var unsafeName = regexp.MustCompile(`[^\p{L}\p{N}._ -]+`) + +// cleanName makes an uploaded file name safe and unique. +func (sb *Sandbox) cleanName(name string) string { + name = filepath.Base(strings.ReplaceAll(name, "\\", "/")) + name = strings.TrimSpace(unsafeName.ReplaceAllString(name, "_")) + if name == "" || name == "." || name == ".." || strings.HasPrefix(name, ".") { + name = "capture.pcap" + } + if len(name) > 100 { + name = name[len(name)-100:] + } + base, ext := name, filepath.Ext(name) + base = strings.TrimSuffix(base, ext) + for i := 2; ; i++ { + taken := false + for _, f := range sb.files { + if f.Name == name { + taken = true + } + } + if !taken { + return name + } + name = fmt.Sprintf("%s-%d%s", base, i, ext) + } +} + +// Add stores an uploaded capture file and queues it for import. The file is +// checked while it streams to disk: at most MaxFileSize bytes, and it must +// start like a pcap or pcapng file. +func (sb *Sandbox) Add(name string, r io.Reader, sample bool) (*File, error) { + sb.mu.Lock() + n := 0 + for _, f := range sb.files { + if !f.Sample { + n++ + } + } + if !sample && n >= MaxFiles { + sb.mu.Unlock() + return nil, fmt.Errorf("%w: at most %d files; delete one first", ErrLimit, MaxFiles) + } + name = sb.cleanName(name) + // reserve the name while the upload runs + f := &File{Name: name, Added: time.Now().UTC(), Status: "uploading", Sample: sample} + sb.files = append(sb.files, f) + sb.mu.Unlock() + fail := func(err error) (*File, error) { + sb.mu.Lock() + sb.files = slicesDelete(sb.files, f) + sb.mu.Unlock() + os.Remove(filepath.Join(sb.pcapDir(), name)) + return nil, err + } + if err := os.MkdirAll(sb.pcapDir(), 0o755); err != nil { + return fail(err) + } + head := make([]byte, 4) + if _, err := io.ReadFull(r, head); err != nil || pcapfile.Format(head) == "" { + return fail(ErrNotCapture) + } + path := filepath.Join(sb.pcapDir(), name) + out, err := os.Create(path) + if err != nil { + return fail(err) + } + out.Write(head) + limit := MaxFileSize + if sample { + limit = 1 << 30 + } + written, err := io.Copy(out, io.LimitReader(r, limit-4+1)) + out.Close() + if err != nil { + return fail(err) + } + if written+4 > limit { + return fail(fmt.Errorf("%w: a file may be at most %d MB", ErrLimit, MaxFileSize>>20)) + } + sb.mu.Lock() + f.Size = written + 4 + f.Status = "waiting" + f.Exporter = sb.exporterFor(f) + sb.save() + sb.refreshInventory() + sb.mu.Unlock() + sb.kick() + return f, nil +} + +// exporterFor picks a free address 127.0.1.n for a file's flows. +func (sb *Sandbox) exporterFor(f *File) string { + used := map[string]bool{} + for _, o := range sb.files { + used[o.Exporter] = true + } + for i := 1; ; i++ { + a := fmt.Sprintf("127.0.1.%d", i) + if !used[a] { + return a + } + } +} + +func slicesDelete(fs []*File, f *File) []*File { + out := fs[:0] + for _, x := range fs { + if x != f { + out = append(out, x) + } + } + return out +} + +// Delete removes one file (name) or everything (name ""). The database is +// rebuilt from the remaining files. +func (sb *Sandbox) Delete(name string) error { + sb.mu.Lock() + var keep []*File + found := name == "" + for _, f := range sb.files { + if name == "" || f.Name == name { + found = true + if f.Status == "uploading" { + keep = append(keep, f) + continue + } + os.Remove(filepath.Join(sb.pcapDir(), f.Name)) + continue + } + keep = append(keep, f) + } + if !found { + sb.mu.Unlock() + return errors.New("no such file") + } + sb.files = keep + sb.gen++ + st := sb.st + sb.st, sb.det = nil, nil + for _, f := range sb.files { + if f.Status != "uploading" { + f.Status, f.Error, f.Packets, f.Skipped, f.Flows, f.First, f.Last = "waiting", "", 0, 0, 0, time.Time{}, time.Time{} + } + } + sb.save() + sb.refreshInventory() + sb.mu.Unlock() + if st != nil { + st.Close() + } + if err := os.RemoveAll(sb.dbDir()); err != nil { + return err + } + if len(keep) == 0 { + os.RemoveAll(sb.Dir) + } + sb.kick() + return nil +} + +func (sb *Sandbox) kick() { + select { + case sb.wake <- struct{}{}: + default: + } +} + +func (sb *Sandbox) worker() { + for range sb.wake { + for sb.importNext() { + } + } +} + +// importNext imports one waiting file; it reports whether it did. +func (sb *Sandbox) importNext() bool { + sb.mu.Lock() + var f *File + for _, x := range sb.files { + if x.Status == "waiting" { + f = x + break + } + } + if f == nil { + sb.mu.Unlock() + return false + } + if sb.st == nil { + if err := sb.open(); err != nil { + f.Status, f.Error = "error", err.Error() + sb.save() + sb.mu.Unlock() + return true + } + } + f.Status = "importing" + sb.busy = true + gen, st, det, inv := sb.gen, sb.st, sb.det, sb.inv + sb.mu.Unlock() + + res, err := sb.importFile(gen, f, st, inv) + if err == nil { + // the rules over the whole capture, window by window + every := det.Cfg.Every + for t := res.first.Truncate(every).Add(every); !t.After(res.last.Add(every)); t = t.Add(every) { + if _, err := det.Run(t); err != nil { + log.Printf("sandbox: detect: %v", err) + break + } + } + } + sb.mu.Lock() + defer sb.mu.Unlock() + sb.busy = false + if gen != sb.gen { // deleted meanwhile + return true + } + f.Packets, f.Skipped, f.Flows, f.First, f.Last = res.packets, res.skipped, res.flows, res.first, res.last + switch { + case err != nil: + f.Status, f.Error = "error", err.Error() + case res.flows == 0: + f.Status, f.Error = "error", "no IP packets found" + default: + f.Status = "done" + } + sb.save() + log.Printf("sandbox: %s: %d packets, %d flows, %s – %s (%v)", f.Name, res.packets, res.flows, + res.first.Format(time.RFC3339), res.last.Format(time.RFC3339), err) + return true +} + +type result struct { + packets, skipped, flows uint64 + first, last time.Time +} + +func (sb *Sandbox) importFile(gen int, f *File, st *store.Store, inv *enrich.Inventory) (result, error) { + var res result + in, err := os.Open(filepath.Join(sb.pcapDir(), f.Name)) + if err != nil { + return res, err + } + defer in.Close() + rd, err := pcapfile.NewReader(in) + if err != nil { + return res, err + } + p := pipeline.New(pipeline.Config{}, st, inv, sb.ASN, sb.Thr) + h := fnv.New32a() + h.Write([]byte(f.Name)) + exp, _ := netip.ParseAddr(f.Exporter) + n := 0 + b := newBuilder(exp, h.Sum32(), func(recs []flow.Record) { + res.flows += uint64(len(recs)) + p.Ingest(recs) + if n++; n%50 == 0 { + p.FlushRows() + } + }) + var pk pcapfile.Packet + for { + err := rd.Next(&pk) + if err == io.EOF { + break + } + if err != nil { + // keep what was read before the damage + log.Printf("sandbox: %s: %v", f.Name, err) + break + } + if pk.Time.Year() < 1995 || pk.Time.After(time.Now().Add(24*time.Hour)) { + res.skipped++ + continue + } + if res.first.IsZero() || pk.Time.Before(res.first) { + res.first = pk.Time + } + if pk.Time.After(res.last) { + res.last = pk.Time + } + b.add(&pk) + if rd.Count%100000 == 0 { + sb.mu.Lock() + stale := gen != sb.gen + f.Packets = rd.Count + sb.mu.Unlock() + if stale { + return res, errors.New("deleted") + } + } + } + b.sweep(res.last.Add(time.Hour), true) + p.FlushRows() + p.FlushRollups() + res.packets, res.skipped = rd.Count, res.skipped+b.skipped + return res, nil +} + +// Close closes the database. +func (sb *Sandbox) Close() { + sb.mu.Lock() + st := sb.st + sb.st = nil + sb.mu.Unlock() + if st != nil { + st.Close() + } +} diff --git a/internal/sandbox/sandbox_test.go b/internal/sandbox/sandbox_test.go new file mode 100644 index 0000000..c9d5545 --- /dev/null +++ b/internal/sandbox/sandbox_test.go @@ -0,0 +1,134 @@ +package sandbox + +import ( + "bytes" + "errors" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/githubflyideas/traffic66/internal/enrich" + "github.com/githubflyideas/traffic66/internal/store" +) + +func wait(t *testing.T, sb *Sandbox) Info { + t.Helper() + for i := 0; i < 600; i++ { + in := sb.Info() + pending := false + for _, f := range in.Files { + if f.Status != "done" && f.Status != "error" { + pending = true + } + } + if !pending && !in.Busy { + return in + } + time.Sleep(100 * time.Millisecond) + } + t.Fatal("import did not finish") + return Info{} +} + +// The example capture is imported into its own database, the attack in it +// is found, and deleting removes everything. +func TestSampleImport(t *testing.T) { + dir := filepath.Join(t.TempDir(), "sandbox") + inv := enrich.NewInventory() + inv.Parse("net 10.10.0.0/16 Office\nnet 10.20.0.0/24 Servers\nhost 10.20.0.15 File server\n") + sb := New(dir, inv, enrich.NewASNDB(), enrich.NewThreats()) + defer sb.Close() + var buf bytes.Buffer + end := time.Date(2026, 9, 20, 11, 0, 0, 0, time.UTC) + if err := Sample(&buf, end); err != nil { + t.Fatal(err) + } + t.Logf("sample: %d bytes", buf.Len()) + if _, err := sb.Add(SampleName, bytes.NewReader(buf.Bytes()), true); err != nil { + t.Fatal(err) + } + in := wait(t, sb) + f := in.Files[0] + if f.Status != "done" || f.Flows == 0 || !in.Ready { + t.Fatalf("%+v", f) + } + if f.First.Before(end.Add(-16*time.Minute)) || f.Last.After(end) { + t.Errorf("span %v – %v", f.First, f.Last) + } + st, _, _ := sb.Store() + fs, err := st.Findings(store.FindingQuery{From: f.First.Add(-time.Hour), To: f.Last.Add(time.Hour)}) + if err != nil { + t.Fatal(err) + } + kinds := map[string]bool{} + for _, x := range fs { + kinds[x.Kind] = true + t.Logf("%s %s -> %s %s sev %d", x.Kind, x.Src, x.Dst, x.Port, x.Sev) + } + for _, k := range []string{"scan", "portscan", "brute"} { + if !kinds[k] { + t.Errorf("no %s finding", k) + } + } + var wire float64 + st.DB.QueryRow(`SELECT sum(wire) FROM hot`).Scan(&wire) + if wire < 20e6 { + t.Errorf("traffic %.0f bytes", wire) + } + if err := sb.Delete(""); err != nil { + t.Fatal(err) + } + if _, err := os.Stat(dir); !os.IsNotExist(err) { + t.Errorf("sandbox directory left: %v", err) + } + if st, _, _ := sb.Store(); st != nil { + t.Error("database still open") + } +} + +func TestLimits(t *testing.T) { + old := MaxFileSize + MaxFileSize = 1000 + defer func() { MaxFileSize = old }() + sb := New(t.TempDir(), enrich.NewInventory(), enrich.NewASNDB(), enrich.NewThreats()) + defer sb.Close() + var small bytes.Buffer + Sample(&small, time.Now()) + head := small.Bytes()[:800] + if _, err := sb.Add("x.pcap", bytes.NewReader(small.Bytes()), false); !errors.Is(err, ErrLimit) { + t.Fatalf("big file: %v", err) + } + if _, err := sb.Add("x.txt", strings.NewReader("hello world"), false); err == nil || errors.Is(err, ErrLimit) { + t.Fatalf("text file: %v", err) + } + for i := 0; i < 3; i++ { + if _, err := sb.Add("a.pcap", bytes.NewReader(head), false); err != nil { + t.Fatal(err) + } + } + if _, err := sb.Add("d.pcap", bytes.NewReader(head), false); !errors.Is(err, ErrLimit) { + t.Fatalf("fourth file: %v", err) + } + in := wait(t, sb) + names := []string{} + for _, f := range in.Files { + names = append(names, f.Name+":"+f.Exporter) + } + if strings.Join(names, " ") != "a.pcap:127.0.1.1 a-2.pcap:127.0.1.2 a-3.pcap:127.0.1.3" { + t.Errorf("files %v", names) + } + if err := sb.Delete("a-2.pcap"); err != nil { + t.Fatal(err) + } + if in := wait(t, sb); len(in.Files) != 2 { + t.Errorf("%d files after delete", len(in.Files)) + } + if _, err := sb.Add("../../etc/passwd.pcap", bytes.NewReader(head), false); err != nil { + t.Fatal(err) + } + if _, err := os.Stat(filepath.Join(sb.Dir, "files", "passwd.pcap")); err != nil { + t.Errorf("name not cleaned: %v", err) + } +} diff --git a/internal/web/static/app.css b/internal/web/static/app.css index 7556836..9e0a7e4 100644 --- a/internal/web/static/app.css +++ b/internal/web/static/app.css @@ -243,3 +243,11 @@ th.sk .ar{font-size:10px;margin-inline-start:3px;opacity:.7} .attrib{font-size:11.5px;margin:8px 0 0} .attrib a{color:inherit} h3.subhead{font-size:14px;font-weight:650;margin:20px 0 4px} +.sbbar{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin:0 20px 8px;padding:8px 12px;border-radius:8px;background:#fff4e0;border:1px solid #f0c27a;font-size:13px} +.sbbar[hidden]{display:none} +.sbtag{font-weight:700;color:#8a4b00} +.sbfiles{font-weight:600} +.sbbar .btn{margin-inline-start:auto} +.sbexplain{font-size:13px;color:var(--ink-2);margin:0 0 12px} +.sbactions{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-top:12px} +label.btn.disabled{opacity:.5;cursor:not-allowed!important} diff --git a/internal/web/static/app.js b/internal/web/static/app.js index 1c21a1e..8671162 100644 --- a/internal/web/static/app.js +++ b/internal/web/static/app.js @@ -140,7 +140,7 @@ async function resolveNames() { } // ------------------------------------------------------------ state & API -const VIEWS = ['overview', 'findings', 'topn', 'traffic', 'sankey', 'geo', 'threats', 'records', 'ifaces', 'sources', 'detail']; +const VIEWS = ['overview', 'findings', 'topn', 'traffic', 'sankey', 'geo', 'threats', 'records', 'sandbox', 'ifaces', 'sources', 'detail']; const RANGES = ['15m', '1h', '6h', '24h', '7d', '30d']; const state = {v: 'overview', r: '24h', f: [], dim: 'conv', ifc: null, ifdir: 'in', sort: {k: 'wire', asc: false}, det: null}; function readHash() { @@ -151,6 +151,7 @@ function readHash() { state.tm = p.get('tm') === 'table' || (p.get('v') === 'topn' && p.get('dim')) ? 'table' : 'talkers'; state.sk = ['segment', 'conv'].includes(p.get('by')) ? p.get('by') : 'host'; state.side = ['client', 'both', 'service'].includes(p.get('side')) ? p.get('side') : 'server'; + state.ds = p.get('ds') === 'sb' ? 'sb' : ''; const dd = p.get('d'); if (dd && dd.includes(':')) state.det = {f: dd.slice(0, dd.indexOf(':')), v: dd.slice(dd.indexOf(':') + 1)}; state.f = (p.get('f') || '').split(',').filter(Boolean).map(s => { const neg = s[0] === '!'; if (neg) s = s.slice(1); @@ -160,6 +161,7 @@ function readHash() { } function writeHash(push) { const p = new URLSearchParams({v: state.v, r: state.r}); + if (state.ds) p.set('ds', state.ds); if (state.v === 'topn') { if (state.tm === 'table') { p.set('tm', 'table'); p.set('dim', state.dim); } } if (state.v === 'sankey' && state.sk !== 'host') p.set('by', state.sk); if (state.v === 'traffic' && state.side !== 'server') p.set('side', state.side); @@ -168,8 +170,13 @@ function writeHash(push) { const h = '#' + p.toString(); if (push && h !== location.hash) history.pushState(null, '', h); else history.replaceState(null, '', h); } +// pages whose data can come from the sandbox (uploaded capture files) +const SB_DATA = new Set(['overview', 'topn', 'sankey', 'series', 'records', 'threats', 'findings']); +const inSB = () => state.ds === 'sb' && sbInfo.ready; +let sbInfo = {files: [], ready: false}; async function api(path, extra = {}, filters = state.f) { - const p = new URLSearchParams({range: state.r, ...extra}); + if (inSB() && (path === 'ifaces' || path === 'recon')) return {ifaces: []}; + const p = new URLSearchParams(inSB() && SB_DATA.has(path) ? {ds: 'sb', ...sbRange(), ...extra} : {range: state.r, ...extra}); if (filters.length) p.set('f', JSON.stringify(filters.map(x => ({f: x.f, v: x.v, neg: x.neg})))); const res = await fetch('/api/' + path + '?' + p); if (res.status === 401) { showLogin(); throw new Error('login'); } @@ -177,7 +184,13 @@ async function api(path, extra = {}, filters = state.f) { if (!res.ok) { const e = new Error(j.error || res.statusText); e.kind = j.kind; throw e; } return j; } -const spanMs = () => ({'15m': 9e5, '1h': 36e5, '6h': 216e5, '24h': 864e5, '7d': 6048e5, '30d': 2592e6})[state.r]; +// the sandbox's time range: the whole capture, on whole minutes +function sbRange() { + const a = Math.floor(Date.parse(sbInfo.first) / 6e4) * 6e4, b = Math.ceil((Date.parse(sbInfo.last) + 1) / 6e4) * 6e4; + return {from: a, to: Math.max(b, a + 6e4)}; +} +const spanMs = () => { if (inSB()) { const r = sbRange(); return r.to - r.from; } return ({'15m': 9e5, '1h': 36e5, '6h': 216e5, '24h': 864e5, '7d': 6048e5, '30d': 2592e6})[state.r]; }; +const rangeLabel = () => inSB() ? t('sb.range') : t('range.' + state.r); // ------------------------------------------------------------ tooltip & charts const tip = $('#tip'); @@ -372,7 +385,7 @@ function renderFilters() { // values; choosing one replaces any filter on that field, emptying it // removes the filter. const FBAR = [['exporter', 'field.exporter'], ['client', 'field.client'], ['server', 'field.server'], ['port', 'col.service']]; -const FBAR_HIDDEN = new Set(['findings', 'sources']); +const FBAR_HIDDEN = new Set(['findings', 'sources', 'sandbox']); function renderFbar() { const bar = $('#fbar'); bar.hidden = FBAR_HIDDEN.has(state.v); @@ -456,7 +469,7 @@ views.overview = async (el) => { el.innerHTML = `
    ${t('kpi.now')}
    ${fmtBps(d.now_bps)}
    ${change == null ? ' ' : esc(t(basis === 'week' ? 'kpi.vs_week' : 'kpi.vs_prev', {p: fmtPct(change, 0)}))}
    -
    ${t('kpi.total', {r: t('range.' + state.r)})}
    ${fmtBytes(tot.wire)}
    ${esc(peakTxt)}
    +
    ${t('kpi.total', {r: rangeLabel()})}
    ${fmtBytes(tot.wire)}
    ${esc(peakTxt)}
    ${t('kpi.hosts')}
    ${nf(tot.hosts)}
     
    ${t('kpi.peers')}
    ${nf(tot.peers)}
    ${esc(t('kpi.countries', {n: nf(tot.countries)}))}
    ${t('kpi.accuracy')}
    ${acc.n}
    ${acc.kind ? status(acc.kind, acc.d) : `${esc(acc.d)}`}
    @@ -559,7 +572,7 @@ function bindFindings(el) { el.querySelectorAll('[data-go]').forEach(b => b.onclick = () => go(b.dataset.go)); el.querySelectorAll('[data-fd-view]').forEach(b => b.onclick = () => showDetail('ip', b.dataset.fdView)); el.querySelectorAll('[data-fd-set]').forEach(b => b.onclick = async () => { - const res = await fetch('/api/findings', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ids: [+b.dataset.id], status: b.dataset.fdSet})}); + const res = await fetch('/api/findings' + (inSB() ? '?ds=sb' : ''), {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ids: [+b.dataset.id], status: b.dataset.fdSet})}); if (!res.ok) { const j = await res.json().catch(() => ({})); toast(j.error || res.statusText); return; } loadStatus(); render(); }); @@ -888,6 +901,67 @@ views.geo = async (el) => { asCharts.forEach(([by, m], i) => fillSeries('asr' + i, ad[i], by, m)); }; +// ------------------------------------------------------------ offline analysis +// Capture files are imported into a database of their own; "Analyse" shows +// them on all the other pages, apart from the live data. +let sbMsg = ''; +views.sandbox = async (el) => { + await loadSB(); + const fs = sbInfo.files || [], own = fs.filter(f => !f.sample).length, mb = Math.round(sbInfo.max_file_size / 1048576); + const df = new Intl.DateTimeFormat(LANG, {dateStyle: 'short', timeStyle: 'medium'}); + const pending = fs.some(f => f.status !== 'done' && f.status !== 'error') || sbInfo.busy; + const rows = fs.map(f => `${esc(f.name)}${f.sample ? ` ${t('sb.sample')}` : ''}${fmtBytes(f.size)} + ${f.packets ? nf(f.packets) : '—'}${f.flows ? nf(f.flows) : '—'} + ${f.flows ? esc(df.format(new Date(f.first))) + ' – ' + esc(df.format(new Date(f.last))) : ''} + ${f.status === 'error' ? status('bad', f.error || t('sb.st_error')) : f.status === 'done' ? status('ok', t('sb.st_done')) : `${t('sb.st_' + f.status)}…`} + `).join(''); + el.innerHTML = `
    ${panel('c12', t('sb.title'), t('sb.sub'), `

    ${esc(t('sb.explain'))}

    + + ${rows || ``}
    ${t('sb.col_file')}${t('sb.col_size')}${t('sb.col_packets')}${t('sb.col_flows')}${t('sb.col_span')}${t('sb.col_status')}
    ${t('sb.empty')}
    +
    + + ${fs.length ? `` : ''}
    +

    ${esc(t('sb.limits', {n: sbInfo.max_files, mb}))} ${esc(t('sb.formats'))}

    `)}
    `; + // the last upload problem stays shown after the page refreshes + const msg = (ok, text) => { sbMsg = ok === false ? text : ''; const m = $('#sbMsg'); m.style.color = ok === null ? 'var(--ink-3)' : ok ? 'var(--good)' : 'var(--crit)'; m.textContent = text; }; + if (sbMsg) msg(false, sbMsg); + $('#sbGo').onclick = () => { state.ds = 'sb'; go('overview'); }; + el.querySelectorAll('[data-sbdel]').forEach(b => b.onclick = async () => { + if (!confirm(t('sb.confirm', {f: b.dataset.sbdel}))) return; + await fetch('/api/sandbox/files?name=' + encodeURIComponent(b.dataset.sbdel), {method: 'DELETE'}); + render(); + }); + if ($('#sbAll')) $('#sbAll').onclick = async () => { + if (!confirm(t('sb.confirm_all'))) return; + await fetch('/api/sandbox/files', {method: 'DELETE'}); + state.ds = ''; render(); + }; + $('#sbFile').onchange = async e => { + const files = [...e.target.files]; + msg(null, ''); + let left = sbInfo.max_files - own; + for (const f of files) { + if (left <= 0) { msg(false, t('sb.too_many', {n: sbInfo.max_files})); break; } + if (f.size > sbInfo.max_file_size) { msg(false, t('sb.too_big', {f: f.name, mb})); continue; } + const ok = await new Promise(done => { + const x = new XMLHttpRequest(); + x.open('POST', '/api/sandbox/files?name=' + encodeURIComponent(f.name)); + x.upload.onprogress = ev => ev.lengthComputable && msg(null, t('sb.uploading', {f: f.name, p: Math.round(100 * ev.loaded / ev.total)})); + x.onload = () => { + if (x.status === 200) return done(true); + let j = {}; try { j = JSON.parse(x.responseText); } catch (err) {} + msg(false, x.status === 413 ? (j.max_files && !/MB/.test(j.error) ? t('sb.too_many', {n: sbInfo.max_files}) : t('sb.too_big', {f: f.name, mb})) : j.code === 'not_capture' ? t('sb.not_capture', {f: f.name}) : (j.error || x.statusText)); done(false); + }; + x.onerror = () => { msg(false, t('sb.failed')); done(false); }; + x.send(f); + }); + if (ok) left--; + } + render(); + }; + if (pending) setTimeout(() => { if (state.v === 'sandbox') render(); }, 1500); +}; + views.threats = async (el) => { const d = await api('threats', {limit: 66}); const rows = d.rows || [], lists = d.lists || {}; @@ -935,7 +1009,7 @@ views.records = async (el) => { const pager = `
    ${esc(t('rec.range', {a: nf(total ? page * per + 1 : 0), b: nf(Math.min(total, page * per + rows.length)), n: nf(total)}))}
    `; - el.innerHTML = `
    ${t('rec.title')}
    ${nf(total)}
    ${esc(t('rec.in', {r: t('range.' + state.r)}))}
    + el.innerHTML = `
    ${t('rec.title')}
    ${nf(total)}
    ${esc(t('rec.in', {r: rangeLabel()}))}

    ${t('rec.title')}

    ${esc(t('rec.sorted'))}
    @@ -1126,7 +1200,7 @@ views.detail = async (el) => {

    ${title}

    -
    ${t('kpi.total', {r: t('range.' + state.r)})}
    ${fmtBytes(tot.wire)}
    ${d.peak_at ? esc(t('kpi.peak', {v: fmtBps(d.peak_bps), t: fmtTime(d.peak_at, spanMs())})) : ' '}
    +
    ${t('kpi.total', {r: rangeLabel()})}
    ${fmtBytes(tot.wire)}
    ${d.peak_at ? esc(t('kpi.peak', {v: fmtBps(d.peak_bps), t: fmtTime(d.peak_at, spanMs())})) : ' '}
    ${t('kpi.now')}
    ${fmtBps(d.now_bps)}
     
    ${t(isIP ? 'det.peers' : 'det.clients')}
    ${nf(isIP ? tot.peers + tot.hosts - 1 : tot.hosts)}
     
    ${t('kpi.peers')}
    ${nf(tot.peers)}
    ${esc(t('kpi.countries', {n: nf(tot.countries)}))}
    @@ -1147,7 +1221,34 @@ views.detail = async (el) => { }; // ------------------------------------------------------------ shell +// The sandbox bar replaces the time range while capture files are shown. +function renderSB() { + const on = inSB() && !['ifaces', 'sources'].includes(state.v); + $('#sbbar').hidden = !on; + $('#range').hidden = on; + $('#refresh').hidden = on; + document.querySelectorAll('#nav [data-v=ifaces], #nav [data-v=sources]').forEach(b => b.hidden = inSB()); + if (!on) return; + const r = sbRange(), span = r.to - r.from; + const df = new Intl.DateTimeFormat(LANG, {dateStyle: 'medium', timeStyle: 'short'}), tf = new Intl.DateTimeFormat(LANG, {timeStyle: 'short'}); + const sameDay = new Date(r.from).toDateString() === new Date(r.to).toDateString(); + const names = sbInfo.files.filter(f => f.status === 'done').map(f => f.name); + $('#sbbar').innerHTML = `${t('sb.banner')}${names.map(esc).join(' · ')} + ${esc(df.format(r.from))} – ${esc(sameDay ? tf.format(r.to) : df.format(r.to))} (${esc(fmtDur(span))}) + `; + $('#sbBack').onclick = () => { state.ds = ''; render(); }; +} +function fmtDur(ms) { + const m = Math.round(ms / 6e4); + return m < 120 ? t('sb.min', {n: nf(m)}) : t('sb.hours', {n: nf(m / 60, 1)}); +} +async function loadSB() { + try { const res = await fetch('/api/sandbox'); if (res.ok) sbInfo = await res.json(); } catch (e) {} + if (state.ds && !sbInfo.ready) state.ds = ''; +} + async function render(push) { + renderSB(); renderFilters(); renderFbar(); writeHash(push); @@ -1221,7 +1322,7 @@ async function init() { const res = await fetch('/api/status'); if (res.status === 401) { showLogin(); return; } $('#app').hidden = false; - await loadStatus(); + await Promise.all([loadStatus(), loadSB()]); render(); setInterval(loadStatus, 10000); setInterval(() => { diff --git a/internal/web/static/i18n/ar.json b/internal/web/static/i18n/ar.json index 942577a..0b667ef 100644 --- a/internal/web/static/i18n/ar.json +++ b/internal/web/static/i18n/ar.json @@ -376,5 +376,40 @@ "geo.map_none": "لا حركة", "geo.data_from": "بيانات الدول والشبكات:", "geo.map_from": "الخريطة:", -"td.both": "الطرفان" +"td.both": "الطرفان", +"nav.sandbox": "التحليل دون اتصال", +"sb.title": "ملفات الالتقاط", +"sb.sub": "حلّل ملفات pcap بمعزل عن البيانات الحية", +"sb.explain": "ارفع ملفات التقاط من Wireshark أو tcpdump. تُحفظ في قاعدة بيانات منفصلة فلا تتأثر البيانات الحية وأرقامها واكتشافاتها. يعرض زر «تحليل» الملفات في كل الصفحات (نظرة عامة، أعلى 66، الاكتشافات، مسارات الحركة، الخريطة، سجلات التدفق)، ويحذف زر «حذف» الملفات وبياناتها.", +"sb.col_file": "الملف", +"sb.col_size": "الحجم", +"sb.col_packets": "الحزم", +"sb.col_flows": "التدفقات", +"sb.col_span": "الوقت", +"sb.col_status": "الحالة", +"sb.st_uploading": "جارٍ الرفع", +"sb.st_waiting": "في الانتظار", +"sb.st_importing": "جارٍ الاستيراد", +"sb.st_done": "تم الاستيراد", +"sb.st_error": "فشل", +"sb.sample": "مثال", +"sb.delete": "حذف", +"sb.delete_all": "حذف الكل", +"sb.confirm": "حذف {f} وبياناته؟", +"sb.confirm_all": "حذف كل ملفات الالتقاط وبياناتها؟", +"sb.analyze": "تحليل", +"sb.upload": "رفع ملفات التقاط…", +"sb.uploading": "جارٍ رفع {f}: {p}%", +"sb.failed": "فشل الرفع", +"sb.too_big": "{f} أكبر من {mb} MB", +"sb.too_many": "{n} ملفات كحد أقصى؛ احذف واحداً أولاً", +"sb.limits": "حتى {n} ملفات، كل منها {mb} MB كحد أقصى.", +"sb.formats": "الصيغ: ‎.pcap و‎.pcapng (غير مضغوطة).", +"sb.empty": "لا توجد ملفات التقاط بعد.", +"sb.banner": "التحليل دون اتصال", +"sb.back": "العودة إلى البيانات الحية", +"sb.range": "الالتقاط كاملاً", +"sb.min": "{n} دقيقة", +"sb.hours": "{n} ساعة", +"sb.not_capture": "{f} ليس ملف التقاط. استخدم ‎.pcap أو ‎.pcapng محفوظاً من Wireshark أو tcpdump." } diff --git a/internal/web/static/i18n/bn.json b/internal/web/static/i18n/bn.json index db553ee..2c46cf9 100644 --- a/internal/web/static/i18n/bn.json +++ b/internal/web/static/i18n/bn.json @@ -376,5 +376,40 @@ "geo.map_none": "ট্রাফিক নেই", "geo.data_from": "দেশ ও নেটওয়ার্ক ডেটা:", "geo.map_from": "মানচিত্র:", -"td.both": "দুই প্রান্ত" +"td.both": "দুই প্রান্ত", +"nav.sandbox": "অফলাইন বিশ্লেষণ", +"sb.title": "ক্যাপচার ফাইল", +"sb.sub": "লাইভ ডেটা থেকে আলাদা করে pcap ফাইল বিশ্লেষণ", +"sb.explain": "Wireshark বা tcpdump-এর ক্যাপচার ফাইল আপলোড করুন। এগুলো আলাদা ডেটাবেসে যায়: লাইভ ডেটা, তার হিসাব ও ফলাফল বদলায় না। “বিশ্লেষণ” সব পেজে (সংক্ষেপ, Top 66, ফলাফল, ফ্লো পথ, মানচিত্র, ফ্লো রেকর্ড) ফাইলগুলো দেখায়; “মুছুন” ফাইল ও তার ডেটা মুছে দেয়।", +"sb.col_file": "ফাইল", +"sb.col_size": "আকার", +"sb.col_packets": "প্যাকেট", +"sb.col_flows": "ফ্লো", +"sb.col_span": "সময়", +"sb.col_status": "অবস্থা", +"sb.st_uploading": "আপলোড হচ্ছে", +"sb.st_waiting": "অপেক্ষায়", +"sb.st_importing": "ইমপোর্ট হচ্ছে", +"sb.st_done": "ইমপোর্ট হয়েছে", +"sb.st_error": "ব্যর্থ", +"sb.sample": "উদাহরণ", +"sb.delete": "মুছুন", +"sb.delete_all": "সব মুছুন", +"sb.confirm": "{f} ও তার ডেটা মুছবেন?", +"sb.confirm_all": "সব ক্যাপচার ফাইল ও তাদের ডেটা মুছবেন?", +"sb.analyze": "বিশ্লেষণ", +"sb.upload": "ক্যাপচার ফাইল আপলোড করুন…", +"sb.uploading": "{f} আপলোড হচ্ছে: {p}%", +"sb.failed": "আপলোড ব্যর্থ", +"sb.too_big": "{f} {mb} MB-এর বেশি", +"sb.too_many": "সর্বোচ্চ {n}টি ফাইল; আগে একটি মুছুন", +"sb.limits": "সর্বোচ্চ {n}টি ফাইল, প্রতিটি {mb} MB পর্যন্ত।", +"sb.formats": "ফরম্যাট: .pcap ও .pcapng (সংকুচিত নয়)।", +"sb.empty": "এখনও কোনো ক্যাপচার ফাইল নেই।", +"sb.banner": "অফলাইন বিশ্লেষণ", +"sb.back": "লাইভ ডেটায় ফিরুন", +"sb.range": "পুরো ক্যাপচার", +"sb.min": "{n} মিনিট", +"sb.hours": "{n} ঘণ্টা", +"sb.not_capture": "{f} ক্যাপচার ফাইল নয়। Wireshark বা tcpdump-এ সংরক্ষিত .pcap বা .pcapng ব্যবহার করুন।" } diff --git a/internal/web/static/i18n/en.json b/internal/web/static/i18n/en.json index a576efb..d4e239b 100644 --- a/internal/web/static/i18n/en.json +++ b/internal/web/static/i18n/en.json @@ -376,5 +376,40 @@ "geo.map_none": "no traffic", "geo.data_from": "Countries and networks:", "geo.map_from": "Map:", -"td.both": "Both ends" +"td.both": "Both ends", +"nav.sandbox": "Offline analysis", +"sb.title": "Capture files", +"sb.sub": "Analyse pcap files apart from the live data", +"sb.explain": "Upload packet captures from Wireshark or tcpdump. They go into a separate database: the live data, its numbers and findings are not touched. Analyse shows the files on every page (overview, Top 66, findings, flow paths, map, flow records); Delete removes the files and their data.", +"sb.col_file": "File", +"sb.col_size": "Size", +"sb.col_packets": "Packets", +"sb.col_flows": "Flows", +"sb.col_span": "Time", +"sb.col_status": "Status", +"sb.st_uploading": "uploading", +"sb.st_waiting": "waiting", +"sb.st_importing": "importing", +"sb.st_done": "Imported", +"sb.st_error": "Failed", +"sb.sample": "example", +"sb.delete": "Delete", +"sb.delete_all": "Delete all", +"sb.confirm": "Delete {f} and its data?", +"sb.confirm_all": "Delete all capture files and their data?", +"sb.analyze": "Analyse", +"sb.upload": "Upload capture files…", +"sb.uploading": "Uploading {f}: {p}%", +"sb.failed": "Upload failed", +"sb.too_big": "{f} is larger than {mb} MB", +"sb.too_many": "At most {n} files; delete one first", +"sb.limits": "Up to {n} files, each at most {mb} MB.", +"sb.formats": "Formats: .pcap and .pcapng (not compressed).", +"sb.empty": "No capture files yet.", +"sb.banner": "Offline analysis", +"sb.back": "Back to live data", +"sb.range": "whole capture", +"sb.min": "{n} min", +"sb.hours": "{n} h", +"sb.not_capture": "{f} is not a capture file. Use .pcap or .pcapng as saved by Wireshark or tcpdump." } diff --git a/internal/web/static/i18n/es.json b/internal/web/static/i18n/es.json index 08fc575..502e418 100644 --- a/internal/web/static/i18n/es.json +++ b/internal/web/static/i18n/es.json @@ -376,5 +376,40 @@ "geo.map_none": "sin tráfico", "geo.data_from": "Países y redes:", "geo.map_from": "Mapa:", -"td.both": "Ambos extremos" +"td.both": "Ambos extremos", +"nav.sandbox": "Análisis offline", +"sb.title": "Archivos de captura", +"sb.sub": "Analizar archivos pcap aparte de los datos en vivo", +"sb.explain": "Suba capturas de Wireshark o tcpdump. Van a una base de datos aparte: los datos en vivo, sus cifras y hallazgos no se tocan. Analizar muestra los archivos en todas las páginas (resumen, Top 66, hallazgos, rutas, mapa, registros); Eliminar borra los archivos y sus datos.", +"sb.col_file": "Archivo", +"sb.col_size": "Tamaño", +"sb.col_packets": "Paquetes", +"sb.col_flows": "Flujos", +"sb.col_span": "Tiempo", +"sb.col_status": "Estado", +"sb.st_uploading": "subiendo", +"sb.st_waiting": "en espera", +"sb.st_importing": "importando", +"sb.st_done": "Importado", +"sb.st_error": "Error", +"sb.sample": "ejemplo", +"sb.delete": "Eliminar", +"sb.delete_all": "Eliminar todo", +"sb.confirm": "¿Eliminar {f} y sus datos?", +"sb.confirm_all": "¿Eliminar todos los archivos de captura y sus datos?", +"sb.analyze": "Analizar", +"sb.upload": "Subir archivos de captura…", +"sb.uploading": "Subiendo {f}: {p}%", +"sb.failed": "Error al subir", +"sb.too_big": "{f} supera {mb} MB", +"sb.too_many": "Como máximo {n} archivos; elimine uno primero", +"sb.limits": "Hasta {n} archivos, cada uno de {mb} MB como máximo.", +"sb.formats": "Formatos: .pcap y .pcapng (sin comprimir).", +"sb.empty": "Aún no hay archivos de captura.", +"sb.banner": "Análisis offline", +"sb.back": "Volver a los datos en vivo", +"sb.range": "toda la captura", +"sb.min": "{n} min", +"sb.hours": "{n} h", +"sb.not_capture": "{f} no es un archivo de captura. Use .pcap o .pcapng guardado con Wireshark o tcpdump." } diff --git a/internal/web/static/i18n/fr.json b/internal/web/static/i18n/fr.json index 006b3ca..1e95fe1 100644 --- a/internal/web/static/i18n/fr.json +++ b/internal/web/static/i18n/fr.json @@ -376,5 +376,40 @@ "geo.map_none": "aucun trafic", "geo.data_from": "Pays et réseaux :", "geo.map_from": "Carte :", -"td.both": "Les deux côtés" +"td.both": "Les deux côtés", +"nav.sandbox": "Analyse hors ligne", +"sb.title": "Fichiers de capture", +"sb.sub": "Analyser des fichiers pcap à part des données en direct", +"sb.explain": "Importez des captures Wireshark ou tcpdump. Elles vont dans une base séparée : les données en direct, leurs chiffres et détections ne sont pas touchés. Analyser affiche les fichiers sur toutes les pages (vue d'ensemble, Top 66, détections, chemins, carte, enregistrements) ; Supprimer efface les fichiers et leurs données.", +"sb.col_file": "Fichier", +"sb.col_size": "Taille", +"sb.col_packets": "Paquets", +"sb.col_flows": "Flux", +"sb.col_span": "Période", +"sb.col_status": "État", +"sb.st_uploading": "envoi", +"sb.st_waiting": "en attente", +"sb.st_importing": "import", +"sb.st_done": "Importé", +"sb.st_error": "Échec", +"sb.sample": "exemple", +"sb.delete": "Supprimer", +"sb.delete_all": "Tout supprimer", +"sb.confirm": "Supprimer {f} et ses données ?", +"sb.confirm_all": "Supprimer tous les fichiers de capture et leurs données ?", +"sb.analyze": "Analyser", +"sb.upload": "Importer des fichiers de capture…", +"sb.uploading": "Envoi de {f} : {p} %", +"sb.failed": "Échec de l'envoi", +"sb.too_big": "{f} dépasse {mb} Mo", +"sb.too_many": "{n} fichiers au plus ; supprimez-en un d'abord", +"sb.limits": "Jusqu'à {n} fichiers de {mb} Mo au plus chacun.", +"sb.formats": "Formats : .pcap et .pcapng (non compressés).", +"sb.empty": "Aucun fichier de capture.", +"sb.banner": "Analyse hors ligne", +"sb.back": "Retour aux données en direct", +"sb.range": "toute la capture", +"sb.min": "{n} min", +"sb.hours": "{n} h", +"sb.not_capture": "{f} n'est pas un fichier de capture. Utilisez un .pcap ou .pcapng enregistré par Wireshark ou tcpdump." } diff --git a/internal/web/static/i18n/hi.json b/internal/web/static/i18n/hi.json index 4a38501..d7f22d7 100644 --- a/internal/web/static/i18n/hi.json +++ b/internal/web/static/i18n/hi.json @@ -376,5 +376,40 @@ "geo.map_none": "कोई ट्रैफ़िक नहीं", "geo.data_from": "देश और नेटवर्क डेटा:", "geo.map_from": "नक़्शा:", -"td.both": "दोनों छोर" +"td.both": "दोनों छोर", +"nav.sandbox": "ऑफ़लाइन विश्लेषण", +"sb.title": "कैप्चर फ़ाइलें", +"sb.sub": "लाइव डेटा से अलग pcap फ़ाइलों का विश्लेषण", +"sb.explain": "Wireshark या tcpdump की कैप्चर फ़ाइलें अपलोड करें। वे अलग डेटाबेस में जाती हैं: लाइव डेटा, उसके आँकड़े और निष्कर्ष नहीं बदलते। “विश्लेषण करें” सभी पेजों (अवलोकन, Top 66, निष्कर्ष, फ़्लो पथ, मानचित्र, फ़्लो रिकॉर्ड) पर फ़ाइलें दिखाता है; “हटाएँ” फ़ाइलें और उनका डेटा मिटा देता है।", +"sb.col_file": "फ़ाइल", +"sb.col_size": "आकार", +"sb.col_packets": "पैकेट", +"sb.col_flows": "फ़्लो", +"sb.col_span": "समय", +"sb.col_status": "स्थिति", +"sb.st_uploading": "अपलोड हो रही", +"sb.st_waiting": "कतार में", +"sb.st_importing": "आयात हो रही", +"sb.st_done": "आयात हो गई", +"sb.st_error": "विफल", +"sb.sample": "उदाहरण", +"sb.delete": "हटाएँ", +"sb.delete_all": "सब हटाएँ", +"sb.confirm": "{f} और उसका डेटा हटाएँ?", +"sb.confirm_all": "सभी कैप्चर फ़ाइलें और उनका डेटा हटाएँ?", +"sb.analyze": "विश्लेषण करें", +"sb.upload": "कैप्चर फ़ाइलें अपलोड करें…", +"sb.uploading": "{f} अपलोड हो रही है: {p}%", +"sb.failed": "अपलोड विफल", +"sb.too_big": "{f} {mb} MB से बड़ी है", +"sb.too_many": "अधिकतम {n} फ़ाइलें; पहले एक हटाएँ", +"sb.limits": "अधिकतम {n} फ़ाइलें, हर एक {mb} MB तक।", +"sb.formats": "फ़ॉर्मेट: .pcap और .pcapng (संपीड़ित नहीं)।", +"sb.empty": "अभी कोई कैप्चर फ़ाइल नहीं।", +"sb.banner": "ऑफ़लाइन विश्लेषण", +"sb.back": "लाइव डेटा पर लौटें", +"sb.range": "पूरा कैप्चर", +"sb.min": "{n} मिनट", +"sb.hours": "{n} घंटे", +"sb.not_capture": "{f} कैप्चर फ़ाइल नहीं है। Wireshark या tcpdump से सहेजी .pcap या .pcapng इस्तेमाल करें।" } diff --git a/internal/web/static/i18n/id.json b/internal/web/static/i18n/id.json index f1629ce..e9e15ed 100644 --- a/internal/web/static/i18n/id.json +++ b/internal/web/static/i18n/id.json @@ -376,5 +376,40 @@ "geo.map_none": "tanpa lalu lintas", "geo.data_from": "Negara dan jaringan:", "geo.map_from": "Peta:", -"td.both": "Kedua sisi" +"td.both": "Kedua sisi", +"nav.sandbox": "Analisis offline", +"sb.title": "File tangkapan", +"sb.sub": "Analisis file pcap terpisah dari data langsung", +"sb.explain": "Unggah tangkapan paket dari Wireshark atau tcpdump. File masuk ke basis data terpisah: data langsung, angkanya, dan temuannya tidak tersentuh. Analisis menampilkan file di semua halaman (ringkasan, Top 66, temuan, jalur, peta, catatan flow); Hapus menghapus file dan datanya.", +"sb.col_file": "File", +"sb.col_size": "Ukuran", +"sb.col_packets": "Paket", +"sb.col_flows": "Flow", +"sb.col_span": "Waktu", +"sb.col_status": "Status", +"sb.st_uploading": "mengunggah", +"sb.st_waiting": "menunggu", +"sb.st_importing": "mengimpor", +"sb.st_done": "Diimpor", +"sb.st_error": "Gagal", +"sb.sample": "contoh", +"sb.delete": "Hapus", +"sb.delete_all": "Hapus semua", +"sb.confirm": "Hapus {f} dan datanya?", +"sb.confirm_all": "Hapus semua file tangkapan dan datanya?", +"sb.analyze": "Analisis", +"sb.upload": "Unggah file tangkapan…", +"sb.uploading": "Mengunggah {f}: {p}%", +"sb.failed": "Gagal mengunggah", +"sb.too_big": "{f} lebih dari {mb} MB", +"sb.too_many": "Maksimal {n} file; hapus satu dulu", +"sb.limits": "Maksimal {n} file, masing-masing paling besar {mb} MB.", +"sb.formats": "Format: .pcap dan .pcapng (tidak dikompresi).", +"sb.empty": "Belum ada file tangkapan.", +"sb.banner": "Analisis offline", +"sb.back": "Kembali ke data langsung", +"sb.range": "seluruh tangkapan", +"sb.min": "{n} mnt", +"sb.hours": "{n} jam", +"sb.not_capture": "{f} bukan file tangkapan. Gunakan .pcap atau .pcapng yang disimpan Wireshark atau tcpdump." } diff --git a/internal/web/static/i18n/ja.json b/internal/web/static/i18n/ja.json index d86b926..ddf4e2a 100644 --- a/internal/web/static/i18n/ja.json +++ b/internal/web/static/i18n/ja.json @@ -376,5 +376,40 @@ "geo.map_none": "通信なし", "geo.data_from": "国・ネットワークのデータ:", "geo.map_from": "地図:", -"td.both": "両端" +"td.both": "両端", +"nav.sandbox": "オフライン分析", +"sb.title": "キャプチャファイル", +"sb.sub": "pcap ファイルをライブデータと分けて分析", +"sb.explain": "Wireshark や tcpdump のキャプチャファイルをアップロードします。別のデータベースに入るため、ライブデータや集計、検出結果には影響しません。「分析」を押すと全ページ(概要、Top 66、検出、フローパス、地図、フローレコード)にファイルの内容が表示されます。「削除」でファイルとデータを消します。", +"sb.col_file": "ファイル", +"sb.col_size": "サイズ", +"sb.col_packets": "パケット", +"sb.col_flows": "フロー", +"sb.col_span": "時間", +"sb.col_status": "状態", +"sb.st_uploading": "アップロード中", +"sb.st_waiting": "待機中", +"sb.st_importing": "取り込み中", +"sb.st_done": "取り込み済み", +"sb.st_error": "失敗", +"sb.sample": "サンプル", +"sb.delete": "削除", +"sb.delete_all": "すべて削除", +"sb.confirm": "{f} とそのデータを削除しますか?", +"sb.confirm_all": "すべてのキャプチャファイルとデータを削除しますか?", +"sb.analyze": "分析", +"sb.upload": "キャプチャファイルをアップロード…", +"sb.uploading": "{f} をアップロード中:{p}%", +"sb.failed": "アップロードに失敗しました", +"sb.too_big": "{f} は {mb} MB を超えています", +"sb.too_many": "ファイルは最大 {n} 個です。先に 1 つ削除してください", +"sb.limits": "最大 {n} ファイル、各 {mb} MB まで。", +"sb.formats": "形式:.pcap と .pcapng(圧縮ファイルは不可)。", +"sb.empty": "キャプチャファイルはまだありません。", +"sb.banner": "オフライン分析", +"sb.back": "ライブデータに戻る", +"sb.range": "キャプチャ全体", +"sb.min": "{n} 分", +"sb.hours": "{n} 時間", +"sb.not_capture": "{f} はキャプチャファイルではありません。Wireshark や tcpdump で保存した .pcap か .pcapng を使ってください。" } diff --git a/internal/web/static/i18n/ko.json b/internal/web/static/i18n/ko.json index 6ebdcd5..96f9503 100644 --- a/internal/web/static/i18n/ko.json +++ b/internal/web/static/i18n/ko.json @@ -376,5 +376,40 @@ "geo.map_none": "트래픽 없음", "geo.data_from": "국가·네트워크 데이터:", "geo.map_from": "지도:", -"td.both": "양쪽" +"td.both": "양쪽", +"nav.sandbox": "오프라인 분석", +"sb.title": "캡처 파일", +"sb.sub": "실시간 데이터와 따로 pcap 파일 분석", +"sb.explain": "Wireshark나 tcpdump 캡처 파일을 올립니다. 별도 데이터베이스에 들어가므로 실시간 데이터, 집계, 탐지 결과에는 영향이 없습니다. '분석'을 누르면 모든 페이지(개요, Top 66, 탐지, 플로 경로, 지도, 플로 레코드)에 파일 내용이 나오고, '삭제'는 파일과 데이터를 지웁니다.", +"sb.col_file": "파일", +"sb.col_size": "크기", +"sb.col_packets": "패킷", +"sb.col_flows": "플로", +"sb.col_span": "시간", +"sb.col_status": "상태", +"sb.st_uploading": "업로드 중", +"sb.st_waiting": "대기 중", +"sb.st_importing": "가져오는 중", +"sb.st_done": "가져옴", +"sb.st_error": "실패", +"sb.sample": "예제", +"sb.delete": "삭제", +"sb.delete_all": "모두 삭제", +"sb.confirm": "{f}와(과) 그 데이터를 삭제할까요?", +"sb.confirm_all": "모든 캡처 파일과 데이터를 삭제할까요?", +"sb.analyze": "분석", +"sb.upload": "캡처 파일 업로드…", +"sb.uploading": "{f} 업로드 중: {p}%", +"sb.failed": "업로드 실패", +"sb.too_big": "{f}이(가) {mb} MB를 넘습니다", +"sb.too_many": "파일은 최대 {n}개입니다. 먼저 하나를 삭제하세요", +"sb.limits": "최대 {n}개 파일, 각각 {mb} MB 이하.", +"sb.formats": "형식: .pcap, .pcapng(압축 파일 불가).", +"sb.empty": "아직 캡처 파일이 없습니다.", +"sb.banner": "오프라인 분석", +"sb.back": "실시간 데이터로 돌아가기", +"sb.range": "캡처 전체", +"sb.min": "{n}분", +"sb.hours": "{n}시간", +"sb.not_capture": "{f}은(는) 캡처 파일이 아닙니다. Wireshark나 tcpdump로 저장한 .pcap 또는 .pcapng를 쓰세요." } diff --git a/internal/web/static/i18n/pt.json b/internal/web/static/i18n/pt.json index a4eeead..6754c62 100644 --- a/internal/web/static/i18n/pt.json +++ b/internal/web/static/i18n/pt.json @@ -376,5 +376,40 @@ "geo.map_none": "sem tráfego", "geo.data_from": "Países e redes:", "geo.map_from": "Mapa:", -"td.both": "Ambas as pontas" +"td.both": "Ambas as pontas", +"nav.sandbox": "Análise offline", +"sb.title": "Arquivos de captura", +"sb.sub": "Analisar arquivos pcap separados dos dados ao vivo", +"sb.explain": "Envie capturas do Wireshark ou tcpdump. Elas vão para um banco separado: os dados ao vivo, seus números e detecções não são afetados. Analisar mostra os arquivos em todas as páginas (visão geral, Top 66, detecções, caminhos, mapa, registros); Excluir apaga os arquivos e seus dados.", +"sb.col_file": "Arquivo", +"sb.col_size": "Tamanho", +"sb.col_packets": "Pacotes", +"sb.col_flows": "Fluxos", +"sb.col_span": "Período", +"sb.col_status": "Status", +"sb.st_uploading": "enviando", +"sb.st_waiting": "na fila", +"sb.st_importing": "importando", +"sb.st_done": "Importado", +"sb.st_error": "Falhou", +"sb.sample": "exemplo", +"sb.delete": "Excluir", +"sb.delete_all": "Excluir tudo", +"sb.confirm": "Excluir {f} e seus dados?", +"sb.confirm_all": "Excluir todos os arquivos de captura e seus dados?", +"sb.analyze": "Analisar", +"sb.upload": "Enviar arquivos de captura…", +"sb.uploading": "Enviando {f}: {p}%", +"sb.failed": "Falha no envio", +"sb.too_big": "{f} passa de {mb} MB", +"sb.too_many": "No máximo {n} arquivos; exclua um primeiro", +"sb.limits": "Até {n} arquivos, cada um com no máximo {mb} MB.", +"sb.formats": "Formatos: .pcap e .pcapng (sem compressão).", +"sb.empty": "Nenhum arquivo de captura ainda.", +"sb.banner": "Análise offline", +"sb.back": "Voltar aos dados ao vivo", +"sb.range": "captura inteira", +"sb.min": "{n} min", +"sb.hours": "{n} h", +"sb.not_capture": "{f} não é um arquivo de captura. Use .pcap ou .pcapng salvo pelo Wireshark ou tcpdump." } diff --git a/internal/web/static/i18n/ru.json b/internal/web/static/i18n/ru.json index de529f8..b4a8f41 100644 --- a/internal/web/static/i18n/ru.json +++ b/internal/web/static/i18n/ru.json @@ -376,5 +376,40 @@ "geo.map_none": "нет трафика", "geo.data_from": "Страны и сети:", "geo.map_from": "Карта:", -"td.both": "Обе стороны" +"td.both": "Обе стороны", +"nav.sandbox": "Офлайн-анализ", +"sb.title": "Файлы захвата", +"sb.sub": "Анализ pcap-файлов отдельно от живых данных", +"sb.explain": "Загрузите захваты Wireshark или tcpdump. Они попадают в отдельную базу: живые данные, их статистика и находки не затрагиваются. «Анализ» показывает файлы на всех страницах (обзор, Top 66, находки, пути, карта, записи потоков); «Удалить» стирает файлы и их данные.", +"sb.col_file": "Файл", +"sb.col_size": "Размер", +"sb.col_packets": "Пакеты", +"sb.col_flows": "Потоки", +"sb.col_span": "Время", +"sb.col_status": "Состояние", +"sb.st_uploading": "загрузка", +"sb.st_waiting": "в очереди", +"sb.st_importing": "импорт", +"sb.st_done": "Импортирован", +"sb.st_error": "Ошибка", +"sb.sample": "пример", +"sb.delete": "Удалить", +"sb.delete_all": "Удалить всё", +"sb.confirm": "Удалить {f} и его данные?", +"sb.confirm_all": "Удалить все файлы захвата и их данные?", +"sb.analyze": "Анализ", +"sb.upload": "Загрузить файлы захвата…", +"sb.uploading": "Загрузка {f}: {p}%", +"sb.failed": "Не удалось загрузить", +"sb.too_big": "{f} больше {mb} МБ", +"sb.too_many": "Не больше {n} файлов; сначала удалите один", +"sb.limits": "До {n} файлов, каждый не больше {mb} МБ.", +"sb.formats": "Форматы: .pcap и .pcapng (без сжатия).", +"sb.empty": "Файлов захвата пока нет.", +"sb.banner": "Офлайн-анализ", +"sb.back": "Вернуться к живым данным", +"sb.range": "весь захват", +"sb.min": "{n} мин", +"sb.hours": "{n} ч", +"sb.not_capture": "{f} — не файл захвата. Используйте .pcap или .pcapng, сохранённый Wireshark или tcpdump." } diff --git a/internal/web/static/i18n/ur.json b/internal/web/static/i18n/ur.json index 5afa08f..7fb6a97 100644 --- a/internal/web/static/i18n/ur.json +++ b/internal/web/static/i18n/ur.json @@ -376,5 +376,40 @@ "geo.map_none": "ٹریفک نہیں", "geo.data_from": "ممالک اور نیٹ ورکس کا ڈیٹا:", "geo.map_from": "نقشہ:", -"td.both": "دونوں سرے" +"td.both": "دونوں سرے", +"nav.sandbox": "آف لائن تجزیہ", +"sb.title": "کیپچر فائلیں", +"sb.sub": "لائیو ڈیٹا سے الگ pcap فائلوں کا تجزیہ", +"sb.explain": "Wireshark یا tcpdump کی کیپچر فائلیں اپ لوڈ کریں۔ یہ الگ ڈیٹابیس میں جاتی ہیں: لائیو ڈیٹا، اس کے اعداد اور نتائج متاثر نہیں ہوتے۔ «تجزیہ کریں» تمام صفحات (جائزہ، Top 66، نتائج، فلو راستے، نقشہ، فلو ریکارڈز) پر فائلیں دکھاتا ہے؛ «حذف کریں» فائلیں اور ان کا ڈیٹا مٹا دیتا ہے۔", +"sb.col_file": "فائل", +"sb.col_size": "سائز", +"sb.col_packets": "پیکٹ", +"sb.col_flows": "فلو", +"sb.col_span": "وقت", +"sb.col_status": "حالت", +"sb.st_uploading": "اپ لوڈ ہو رہی ہے", +"sb.st_waiting": "قطار میں", +"sb.st_importing": "امپورٹ ہو رہی ہے", +"sb.st_done": "امپورٹ ہو گئی", +"sb.st_error": "ناکام", +"sb.sample": "مثال", +"sb.delete": "حذف کریں", +"sb.delete_all": "سب حذف کریں", +"sb.confirm": "{f} اور اس کا ڈیٹا حذف کریں؟", +"sb.confirm_all": "تمام کیپچر فائلیں اور ان کا ڈیٹا حذف کریں؟", +"sb.analyze": "تجزیہ کریں", +"sb.upload": "کیپچر فائلیں اپ لوڈ کریں…", +"sb.uploading": "{f} اپ لوڈ ہو رہی ہے: {p}%", +"sb.failed": "اپ لوڈ ناکام", +"sb.too_big": "{f} {mb} MB سے بڑی ہے", +"sb.too_many": "زیادہ سے زیادہ {n} فائلیں؛ پہلے ایک حذف کریں", +"sb.limits": "زیادہ سے زیادہ {n} فائلیں، ہر ایک {mb} MB تک۔", +"sb.formats": "فارمیٹ: ‎.pcap اور ‎.pcapng (کمپریس شدہ نہیں)۔", +"sb.empty": "ابھی کوئی کیپچر فائل نہیں۔", +"sb.banner": "آف لائن تجزیہ", +"sb.back": "لائیو ڈیٹا پر واپس", +"sb.range": "پورا کیپچر", +"sb.min": "{n} منٹ", +"sb.hours": "{n} گھنٹے", +"sb.not_capture": "{f} کیپچر فائل نہیں ہے۔ Wireshark یا tcpdump سے محفوظ ‎.pcap یا ‎.pcapng استعمال کریں۔" } diff --git a/internal/web/static/i18n/zh.json b/internal/web/static/i18n/zh.json index 26cb958..4adec8f 100644 --- a/internal/web/static/i18n/zh.json +++ b/internal/web/static/i18n/zh.json @@ -376,5 +376,40 @@ "geo.map_none": "无流量", "geo.data_from": "国家和运营商数据:", "geo.map_from": "地图:", -"td.both": "两端" +"td.both": "两端", +"nav.sandbox": "离线分析", +"sb.title": "抓包文件", +"sb.sub": "在实时数据之外分析 pcap 文件", +"sb.explain": "上传 Wireshark 或 tcpdump 的抓包文件。它们进入一个单独的数据库,不影响实时数据、统计和发现。点“分析”后,所有页面(概览、Top 66、发现、流向、地图、流记录)都显示这些文件的内容;“删除”会删除文件及其数据。", +"sb.col_file": "文件", +"sb.col_size": "大小", +"sb.col_packets": "包数", +"sb.col_flows": "流数", +"sb.col_span": "时间", +"sb.col_status": "状态", +"sb.st_uploading": "上传中", +"sb.st_waiting": "排队中", +"sb.st_importing": "导入中", +"sb.st_done": "已导入", +"sb.st_error": "失败", +"sb.sample": "示例", +"sb.delete": "删除", +"sb.delete_all": "全部删除", +"sb.confirm": "删除 {f} 及其数据?", +"sb.confirm_all": "删除所有抓包文件及其数据?", +"sb.analyze": "分析", +"sb.upload": "上传抓包文件…", +"sb.uploading": "正在上传 {f}:{p}%", +"sb.failed": "上传失败", +"sb.too_big": "{f} 超过 {mb} MB", +"sb.too_many": "最多 {n} 个文件,请先删除一个", +"sb.limits": "最多 {n} 个文件,每个不超过 {mb} MB。", +"sb.formats": "格式:.pcap 和 .pcapng(不支持压缩包)。", +"sb.empty": "还没有抓包文件。", +"sb.banner": "离线分析", +"sb.back": "返回实时数据", +"sb.range": "整个抓包", +"sb.min": "{n} 分钟", +"sb.hours": "{n} 小时", +"sb.not_capture": "{f} 不是抓包文件。请使用 Wireshark 或 tcpdump 保存的 .pcap 或 .pcapng。" } diff --git a/internal/web/static/index.html b/internal/web/static/index.html index 055513f..8614e56 100644 --- a/internal/web/static/index.html +++ b/internal/web/static/index.html @@ -22,6 +22,7 @@
    +
    @@ -43,6 +44,7 @@

    +
    @@ -56,6 +58,7 @@

    +