diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8b6c31e..2479772 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,7 +3,29 @@
The release notes on GitHub are taken from this file: the section whose
heading is the version number.
-## 0.1.2
+## 0.2.0
+
+Findings
+- traffic66 now looks through the flows every 5 minutes and lists what
+ needs attention on a new **Findings** page: scans, port scans, password
+ guessing, lateral movement inside the network, unusual uploads to new
+ destinations, floods and traffic with addresses on threat lists. Each
+ finding is a sentence (who did what to whom, when, for how long) with the
+ numbers behind it and how the data was sampled.
+- The rules work on sampled sFlow and NetFlow. Tested with the demo's
+ attack sent through a switch sampling 1:4096: every step is found, each
+ as one finding; a day of normal traffic gives no findings apart from the
+ internet scanner.
+- **Dealt with** and **Not a problem** close a finding; "not a problem" is
+ never reported again. The overview shows the open findings first, a
+ host's details page lists the findings about it, and the side menu shows
+ how many high and medium findings are open.
+
+Fixes
+- A host's details page counted only part of the internal hosts it talked
+ to (the servers of internal conversations were missed).
+- Arabic and Urdu: ports read backwards ("tcp/445") and names ran into
+ their addresses.
Drill-down and naming
- **Show details** on any host, device or service opens a page about it:
diff --git a/README.md b/README.md
index 1c8cbe6..e5120e9 100644
--- a/README.md
+++ b/README.md
@@ -14,11 +14,14 @@ in a web UI and in a terminal UI.
capture from a network interface or mirror port.
- Checks its own numbers against interface counters (sFlow counters or
SNMP) and says why they differ when they do.
+- Finds scans, password guessing, lateral movement, unusual uploads, floods
+ and threat list traffic in the flows, also through sampling, and lists
+ them as findings to deal with.
- Top 66 lists, flow paths, countries and networks, threat list matches,
flow records, encapsulation (GRE, IPIP, VXLAN, GENEVE, MPLS).
- 13 languages in the web UI and the terminal UI.
-
+
All screenshots come from `traffic66 demo`, a simulated company network that you can run yourself (see [Try the demo](#1-try-the-demo)).
@@ -83,9 +86,11 @@ cd traffic66-windows-amd64
Open http://127.0.0.1:8066 and sign in as `admin` / `try66`. The demo builds
a small company network with a day of history and live traffic from four
-simulated devices, including two incidents to find: start on **Overview**,
-click a host in **Top clients**, choose **Show details**, and keep clicking
-from there. Stop it with Ctrl+C.
+simulated devices, including an attack: **Findings** shows each step of it
+(a scan, a port scan, password guessing, lateral movement, an upload to a
+control server) and a flood on the public website. Click **Details** on a
+finding, or start on **Overview**, click a host in **Top clients**, choose
+**Show details**, and keep clicking from there. Stop it with Ctrl+C.
Demo data is kept in `traffic66-demo` next to the program; delete that
folder to start the demo afresh.
@@ -622,7 +627,8 @@ Pages:
| Page | What it answers |
|---|---|
-| Overview | How much traffic now and compared with last week, by application; top clients and services |
+| Overview | How much traffic now and compared with last week, by application; open findings; top clients and services |
+| Findings | What needs attention: scans, password guessing, lateral movement, unusual uploads, floods and threat list traffic ([more](#findings)) |
| Top-N | One table of the top 66: by default conversations (client, server, service, country). Every column heading sorts; number columns (traffic, packets, average packet size, flows) rank all traffic in the range, so the smallest average packet size finds scanners and floods. **Group by** switches to applications, networks, segments, devices, encapsulation and VLAN |
| Flow paths | Which segment talks to which application in which country |
| Geo & networks | Traffic by country and by network (AS) |
@@ -648,9 +654,46 @@ The side menu shows how much disk the data uses and how much is free;
hover over the free space to see how much the kept days of detail need at
the current rate (estimated once there is a day of data).
+### Findings
+
+**Findings** lists what traffic66 found in the flows, most serious first. It
+checks the last 10 minutes every 5 minutes; something that goes on for an
+hour is one finding that grows, not a new one at every check.
+
+| Finding | What it means | Severity |
+|---|---|---|
+| Scan | One address sent small probes to many addresses on one port (TCP or ping) | High from inside your network, low from the internet |
+| Port scan | One address sent small probes to many ports of one host | High from inside, low from the internet |
+| Password guessing | Many short connections to a login service (SSH, RDP, SMB, databases and others) | High from inside, low from the internet |
+| Lateral movement | Inside your network, file sharing or remote administration sessions (SMB, RDP, SSH, WinRM, VNC) to hosts that never offered that service before | High |
+| Unusual upload | An internal host sent much more than it received (100 MB in 10 minutes, three times what it received) to an address it had not exchanged data with before | High |
+| Flood | 20,000 or more small packets per second to one address, ten times its usual rate | Medium |
+| Threat list | Traffic with an address on one of your threat lists | High when your host connected to it, low when the listed address knocked from outside |
+
+Each finding says who did what to whom, when and for how long, with the
+numbers behind it and how the data was sampled. **Details** opens the
+host's page, which also lists the findings about it. **Dealt with** closes
+a finding; if it happens again, a new one opens. **Not a problem** closes it
+for good: it is never reported again. The red number next to **Findings** in
+the side menu counts the open high and medium findings of the last 24 hours.
+
+Lateral movement and unusual uploads need to know what is normal, so they
+are reported once there is a day of history. On first start traffic66
+learns from the history it already has.
+
+With sampled data (sFlow, sampled NetFlow) the rules count what the samples
+show and ask for fewer of them, but then each must look like one short
+probe, so busy normal hosts do not trigger them. What sampling hides cannot
+be found: behind 1:4096 sampling, a scan of a few dozen hosts sends too few
+packets to be seen. The demo's attack goes through a switch that samples
+1:4096 and is found completely; a day of the demo's normal traffic produces
+no findings except the internet scanner knocking on the website.
+
+
+

-
+

diff --git a/cmd/traffic66/main.go b/cmd/traffic66/main.go
index 1757146..e997719 100644
--- a/cmd/traffic66/main.go
+++ b/cmd/traffic66/main.go
@@ -27,6 +27,7 @@ import (
"github.com/githubflyideas/traffic66/internal/auth"
"github.com/githubflyideas/traffic66/internal/capture"
"github.com/githubflyideas/traffic66/internal/collector"
+ "github.com/githubflyideas/traffic66/internal/detect"
"github.com/githubflyideas/traffic66/internal/dnsres"
"github.com/githubflyideas/traffic66/internal/enrich"
"github.com/githubflyideas/traffic66/internal/flow"
@@ -299,10 +300,12 @@ func serve(args []string, demo bool) {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
+ det := detect.New(st, inv, detect.Config{})
if demo {
backfillDemo(pipe, st)
}
go pipe.Run(ctx)
+ go det.Loop(ctx)
for _, part := range strings.Split(f.listen, ",") {
part = strings.TrimSpace(part)
@@ -380,7 +383,7 @@ func serve(args []string, demo bool) {
} else {
dns = dnsres.New(dnsres.Options{Upstream: f.dnsUpstream, PerSecond: f.dnsRate, TTL: f.dnsTTL})
}
- srv := &api.Server{Store: st, Pipe: pipe, Col: col, Inv: inv, ASN: asn, Thr: thr, DNS: dns, Static: web.FS(), Version: version,
+ 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.Capture = func() []api.CaptureInfo {
diff --git a/docs/README.ar.md b/docs/README.ar.md
index 4ecefd3..0fe9d60 100644
--- a/docs/README.ar.md
+++ b/docs/README.ar.md
@@ -16,11 +16,14 @@
اختياري من واجهة شبكة أو منفذ مرآة.
- يقارن أرقامه بعدّادات الواجهات (عدّادات sFlow أو SNMP) ويشرح سبب
الاختلاف حين يقع.
+- يكتشف في التدفقات عمليات المسح وتخمين كلمات المرور والتحرك الجانبي وعمليات
+ الرفع غير المعتادة والإغراق وحركة قوائم التهديدات، حتى عبر أخذ العينات،
+ ويعرضها اكتشافاتٍ تحتاج إلى معالجة.
- قوائم أعلى 66، ومسارات الحركة، والدول والشبكات، والتطابقات مع قوائم
التهديدات، وسجلات التدفق، والتغليف (GRE وIPIP وVXLAN وGENEVE وMPLS).
- 13 لغة في واجهة الويب والواجهة الطرفية.
-
+
جميع لقطات الشاشة مأخوذة من `traffic66 demo`، وهي شبكة شركة محاكاة يمكنك تشغيلها بنفسك (انظر [جرّب العرض التوضيحي](#1-try-the-demo)).
@@ -89,8 +92,11 @@ cd traffic66-windows-amd64
افتح http://127.0.0.1:8066 وسجّل الدخول باسم `admin` وكلمة المرور `try66`.
ينشئ العرض التوضيحي شبكة شركة صغيرة بسجلّ يوم كامل وحركة حية من أربعة أجهزة
-محاكاة، وفيها حادثتان عليك اكتشافهما: ابدأ من **نظرة عامة**، وانقر على
-مضيف في **أكثر العملاء**، واختر **عرض التفاصيل**، ثم تابع النقر من هناك. أوقفه بـ Ctrl+C. تُحفظ بيانات العرض في
+محاكاة، وفيها هجوم: تعرض **الاكتشافات** كل خطوة منه (مسح، ومسح للمنافذ، وتخمين
+لكلمات المرور، وتحرك جانبي، ورفع إلى خادم تحكم) وإغراقًا على الموقع العام.
+انقر **التفاصيل** على أحد الاكتشافات، أو ابدأ من **نظرة عامة**، وانقر على مضيف
+في **أكثر العملاء**، واختر **عرض التفاصيل**، ثم تابع النقر من هناك. أوقفه بـ
+Ctrl+C. تُحفظ بيانات العرض في
`traffic66-demo` بجوار البرنامج؛ احذف هذا المجلد لتبدأ العرض من جديد.
يستخدم العرض التوضيحي المنافذ نفسها التي يستخدمها التثبيت الفعلي (8066،
@@ -646,7 +652,8 @@ curl -L https://www.spamhaus.org/drop/drop.txt -o /threats/spamh
| الصفحة | ما الذي تجيب عنه |
|---|---|
-| نظرة عامة | حجم الحركة الآن ومقارنةً بالأسبوع الماضي حسب التطبيق؛ أبرز العملاء والخدمات |
+| نظرة عامة | حجم الحركة الآن ومقارنةً بالأسبوع الماضي حسب التطبيق؛ الاكتشافات المفتوحة؛ أبرز العملاء والخدمات |
+| الاكتشافات | ما يستدعي الانتباه: عمليات المسح، وتخمين كلمات المرور، والتحرك الجانبي، وعمليات الرفع غير المعتادة، والإغراق، وحركة قوائم التهديدات ([المزيد](#findings)) |
| Top-N | جدول واحد لأعلى 66: افتراضيًا المحادثات (العميل والخادم والخدمة والدولة). كل عنوان عمود يفرز؛ والأعمدة الرقمية (حركة المرور، الحزم، متوسط الحزمة، التدفقات) تعيد اختيار أعلى 66 من كل حركة المرور في الفترة، لذا يكشف أصغر متوسط حزمة عمليات المسح والإغراق. ويبدّل **التجميع حسب** إلى التطبيقات والشبكات والمقاطع والأجهزة وأنواع التغليف وشبكات VLAN |
| مسارات الحركة | أي مقطع يتواصل مع أي تطبيق في أي دولة |
| الجغرافيا والشبكات | الحركة حسب الدولة وحسب الشبكة (AS) |
@@ -671,9 +678,48 @@ curl -L https://www.spamhaus.org/drop/drop.txt -o /threats/spamh
مرّر المؤشر فوق المساحة الحرة لترى ما تحتاجه أيام التفاصيل المحفوظة بالمعدل
الحالي (يُقدَّر ذلك بعد توفر بيانات يوم كامل).
+
+
+### الاكتشافات
+
+تسرد **الاكتشافات** ما وجده traffic66 في التدفقات، الأخطر أولًا. يفحص آخر 10
+دقائق كل 5 دقائق؛ والشيء الذي يستمر ساعة هو اكتشاف واحد يكبر، لا اكتشاف جديد
+عند كل فحص.
+
+| الاكتشاف | معناه | الخطورة |
+|---|---|---|
+| مسح | عنوان واحد أرسل مجسّات صغيرة إلى عناوين كثيرة على منفذ واحد (TCP أو ping) | عالية من داخل شبكتك، منخفضة من الإنترنت |
+| مسح المنافذ | عنوان واحد أرسل مجسّات صغيرة إلى منافذ كثيرة لمضيف واحد | عالية من الداخل، منخفضة من الإنترنت |
+| تخمين كلمات المرور | اتصالات قصيرة كثيرة بخدمة تسجيل دخول (SSH وRDP وSMB وقواعد البيانات وغيرها) | عالية من الداخل، منخفضة من الإنترنت |
+| تحرك جانبي | داخل شبكتك، جلسات مشاركة ملفات أو إدارة عن بُعد (SMB وRDP وSSH وWinRM وVNC) إلى مضيفات لم تقدّم تلك الخدمة من قبل | عالية |
+| رفع غير معتاد | مضيف داخلي أرسل أكثر بكثير مما استقبل (100 MB في 10 دقائق، ثلاثة أضعاف ما استقبله) إلى عنوان لم يتبادل معه بيانات من قبل | عالية |
+| إغراق | 20,000 حزمة صغيرة أو أكثر في الثانية إلى عنوان واحد، عشرة أضعاف معدله المعتاد | متوسطة |
+| قائمة التهديدات | حركة مع عنوان في إحدى قوائم التهديدات لديك | عالية حين اتصل مضيفك به، منخفضة حين طرق العنوان المدرج من الخارج |
+
+يبيّن كل اكتشاف من فعل ماذا بمن، ومتى، وكم استمر، مع الأرقام التي يستند إليها
+وطريقة أخذ عينات البيانات. يفتح **التفاصيل** صفحة المضيف، التي تسرد أيضًا
+الاكتشافات المتعلقة به. ويغلق **تمت المعالجة** الاكتشاف؛ وإن تكرر الأمر فُتح
+اكتشاف جديد. أما **ليست مشكلة** فيغلقه نهائيًا: لا يُبلَّغ عنه مرة أخرى أبدًا.
+ويعدّ الرقم الأحمر بجوار **الاكتشافات** في القائمة الجانبية الاكتشافات
+المفتوحة ذات الخطورة العالية والمتوسطة في آخر 24 ساعة.
+
+يحتاج التحرك الجانبي والرفع غير المعتاد إلى معرفة ما هو طبيعي، لذا لا يُبلَّغ
+عنهما إلا بعد توفر سجلّ يوم كامل. وعند التشغيل الأول يتعلّم traffic66 من
+السجلّ المتوفر لديه.
+
+مع البيانات المأخوذة بالعينات (sFlow وNetFlow بالعينات) تعدّ القواعد ما تُظهره
+العينات وتطلب عددًا أقل منها، لكن يجب حينها أن تبدو كل عينة مجسًّا قصيرًا
+واحدًا، كي لا تُطلقها المضيفات الطبيعية المزدحمة. ما يخفيه أخذ العينات لا يمكن
+اكتشافه: خلف أخذ عينات بنسبة 1:4096، يرسل مسحٌ لبضع عشرات من المضيفات حزمًا
+أقل من أن تُرى. يمر هجوم العرض التوضيحي عبر محوّل يأخذ العينات بنسبة 1:4096
+ويُكتشف بالكامل؛ ولا ينتج يوم من الحركة الطبيعية في العرض التوضيحي أي اكتشافات
+سوى الماسح القادم من الإنترنت الذي يطرق الموقع.
+
+
+

-
+

diff --git a/docs/README.bn.md b/docs/README.bn.md
index de09049..79d4a58 100644
--- a/docs/README.bn.md
+++ b/docs/README.bn.md
@@ -14,11 +14,14 @@ database-এ রাখে, এবং দেখায় কে bandwidth ব্
network interface বা mirror port থেকে local capture-ও।
- নিজের সংখ্যাগুলো interface counter (sFlow counter বা SNMP)-এর সাথে মিলিয়ে
দেখে, আর পার্থক্য থাকলে কেন তা জানায়।
+- flow-এর মধ্যে scan, পাসওয়ার্ড অনুমান, lateral movement, অস্বাভাবিক upload,
+ flood আর threat list-এর ট্রাফিক খুঁজে বের করে, sampling-এর মধ্যেও, এবং
+ সেগুলো সামলানোর জন্য সন্দেহজনক কার্যকলাপের তালিকায় দেখায়।
- Top 66 তালিকা, flow-এর পথ, দেশ ও নেটওয়ার্ক, threat list-এর match, flow
record, encapsulation (GRE, IPIP, VXLAN, GENEVE, MPLS)।
- web UI ও terminal UI-তে 13টি ভাষা।
-
+
সব স্ক্রিনশট `traffic66 demo` থেকে নেওয়া, একটি simulated কোম্পানি নেটওয়ার্ক যা আপনি নিজেই চালাতে পারেন ([ডেমো চালিয়ে দেখুন](#1-try-the-demo) দেখুন)।
@@ -87,9 +90,13 @@ cd traffic66-windows-amd64
http://127.0.0.1:8066 খুলুন এবং `admin` / `try66` দিয়ে সাইন ইন করুন। ডেমো
একটি ছোট কোম্পানির নেটওয়ার্ক তৈরি করে, যাতে এক দিনের ইতিহাস আর চারটি
-simulated ডিভাইস থেকে live ট্রাফিক থাকে, সাথে খুঁজে বের করার মতো দুটি
-incident: **সারসংক্ষেপ** থেকে শুরু করুন, **শীর্ষ ক্লায়েন্ট**-এ একটি host-এ ক্লিক করুন,
-**বিস্তারিত দেখুন** বেছে নিন, তারপর ক্লিক করে করে এগোন। Ctrl+C দিয়ে বন্ধ করুন। ডেমোর ডেটা প্রোগ্রামের পাশে
+simulated ডিভাইস থেকে live ট্রাফিক থাকে, সাথে একটি আক্রমণ:
+**সন্দেহজনক কার্যকলাপ** তার প্রতিটি ধাপ দেখায় (একটি scan, একটি port scan,
+পাসওয়ার্ড অনুমান, lateral movement, একটি control server-এ upload) এবং public
+website-এ একটি flood। তালিকার কোনো এন্ট্রিতে **বিস্তারিত** ক্লিক করুন, অথবা
+**সারসংক্ষেপ** থেকে শুরু করুন, **শীর্ষ ক্লায়েন্ট**-এ একটি host-এ ক্লিক করুন,
+**বিস্তারিত দেখুন** বেছে নিন, তারপর ক্লিক করে করে এগোন। Ctrl+C দিয়ে বন্ধ
+করুন। ডেমোর ডেটা প্রোগ্রামের পাশে
`traffic66-demo`-তে থাকে; ডেমো নতুন করে শুরু করতে ওই ফোল্ডারটি মুছে দিন।
ডেমো আসল installation-এর মতো একই port ব্যবহার করে (8066, এবং UDP 6343,
@@ -653,7 +660,8 @@ application, দেশ, ডিভাইস — ক্লিক করা যা
| পেজ | কোন প্রশ্নের উত্তর দেয় |
|---|---|
-| সারসংক্ষেপ | এখন কত ট্রাফিক আর গত সপ্তাহের তুলনায় কত, application অনুযায়ী; শীর্ষ client ও service |
+| সারসংক্ষেপ | এখন কত ট্রাফিক আর গত সপ্তাহের তুলনায় কত, application অনুযায়ী; খোলা সন্দেহজনক কার্যকলাপ; শীর্ষ client ও service |
+| সন্দেহজনক কার্যকলাপ | কীসে নজর দিতে হবে: scan, পাসওয়ার্ড অনুমান, lateral movement, অস্বাভাবিক upload, flood আর threat list-এর ট্রাফিক ([আরও](#findings)) |
| Top-N | শীর্ষ 66-এর একটি table: ডিফল্টভাবে conversation (client, server, service, দেশ)। যেকোনো column heading-এ sort হয়; সংখ্যার column (ট্রাফিক, প্যাকেট, গড় প্যাকেট, flow) সময়সীমার সব ট্রাফিক থেকে শীর্ষ 66 আবার বাছে, তাই সবচেয়ে ছোট গড় প্যাকেট দিয়ে scan ও flood ধরা পড়ে। **গ্রুপের ভিত্তি**-তে application, নেটওয়ার্ক, segment, ডিভাইস, encapsulation ও VLAN-এ যাওয়া যায় |
| ট্রাফিকের পথ | কোন segment কোন দেশের কোন application-এর সাথে কথা বলে |
| ভূগোল ও নেটওয়ার্ক | দেশ ও নেটওয়ার্ক (AS) অনুযায়ী ট্রাফিক |
@@ -677,9 +685,50 @@ view-এর (পেজ, time range ও filter) link কপি করে, যা
ওপর hover করলে দেখা যায় বর্তমান হারে রাখা দিনগুলোর বিস্তারিত তথ্যের জন্য কতটা
জায়গা লাগবে (এক দিনের data জমা হলে হিসাব করা হয়)।
+
+
+### সন্দেহজনক কার্যকলাপ
+
+**সন্দেহজনক কার্যকলাপ** দেখায় traffic66 flow-এর মধ্যে কী পেয়েছে, সবচেয়ে
+গুরুতরগুলো আগে। এটি প্রতি 5 মিনিটে শেষ 10 মিনিট পরীক্ষা করে; যা এক ঘণ্টা ধরে
+চলে তা একটিই এন্ট্রি যা বাড়তে থাকে, প্রতিটি পরীক্ষায় নতুন নয়।
+
+| কার্যকলাপ | এর মানে | গুরুত্ব |
+|---|---|---|
+| স্ক্যান | একটি address এক port-এ অনেক address-এ ছোট probe পাঠিয়েছে (TCP বা ping) | আপনার নেটওয়ার্কের ভেতর থেকে হলে উচ্চ, ইন্টারনেট থেকে হলে নিম্ন |
+| পোর্ট স্ক্যান | একটি address একটি host-এর অনেক port-এ ছোট probe পাঠিয়েছে | ভেতর থেকে উচ্চ, ইন্টারনেট থেকে নিম্ন |
+| পাসওয়ার্ড অনুমান | একটি login service-এ (SSH, RDP, SMB, database এবং অন্যান্য) অনেক ছোট connection | ভেতর থেকে উচ্চ, ইন্টারনেট থেকে নিম্ন |
+| ল্যাটেরাল মুভমেন্ট | আপনার নেটওয়ার্কের ভেতরে, এমন host-এ file sharing বা remote administration session (SMB, RDP, SSH, WinRM, VNC) যারা আগে কখনো ওই service দেয়নি | উচ্চ |
+| অস্বাভাবিক আপলোড | একটি internal host যা পেয়েছে তার চেয়ে অনেক বেশি পাঠিয়েছে (10 মিনিটে 100 MB, পাওয়া ডেটার তিন গুণ) এমন address-এ যার সাথে আগে কখনো ডেটা আদান-প্রদান হয়নি | উচ্চ |
+| ফ্লাড | একটি address-এ প্রতি সেকেন্ডে 20,000 বা তার বেশি ছোট packet, তার স্বাভাবিক হারের দশ গুণ | মাঝারি |
+| হুমকি তালিকা | আপনার কোনো threat list-এর address-এর সাথে ট্রাফিক | আপনার host তার সাথে connect করলে উচ্চ, তালিকাভুক্ত address বাইরে থেকে কড়া নাড়লে নিম্ন |
+
+প্রতিটি এন্ট্রি জানায় কে কার সাথে কী করেছে, কখন এবং কতক্ষণ ধরে, পেছনের
+সংখ্যাগুলোসহ, আর ডেটা কীভাবে sample করা হয়েছিল। **বিস্তারিত** host-এর পেজ
+খোলে, যেখানে তার সম্পর্কে পাওয়া কার্যকলাপও তালিকাভুক্ত থাকে।
+**সমাধান হয়েছে** এন্ট্রিটি বন্ধ করে; আবার ঘটলে নতুন একটি খোলে। **সমস্যা নয়**
+এটিকে চিরতরে বন্ধ করে: এটি আর কখনো জানানো হয় না। পাশের menu-তে
+**সন্দেহজনক কার্যকলাপ**-এর পাশের লাল সংখ্যাটি শেষ 24 ঘণ্টার খোলা উচ্চ ও মাঝারি
+এন্ট্রি গোনে।
+
+ল্যাটেরাল মুভমেন্ট আর অস্বাভাবিক upload-এর জন্য জানা দরকার কোনটা স্বাভাবিক,
+তাই এক দিনের ইতিহাস জমা হলে তবেই এগুলো জানানো হয়। প্রথমবার চালু হলে traffic66
+আগে থেকে থাকা ইতিহাস থেকে শেখে।
+
+Sampled data (sFlow, sampled NetFlow)-এ নিয়মগুলো sample যা দেখায় তা গোনে এবং
+কম সংখ্যা চায়, কিন্তু তখন প্রতিটিকে একটি ছোট probe-এর মতো দেখাতে হয়, যাতে
+ব্যস্ত স্বাভাবিক host এগুলো চালু না করে। sampling যা লুকিয়ে ফেলে তা খুঁজে
+পাওয়া যায় না: 1:4096 sampling-এর পেছনে, কয়েক ডজন host-এর একটি scan এত কম
+packet পাঠায় যে দেখা যায় না। ডেমোর আক্রমণ এমন একটি switch দিয়ে যায় যা
+1:4096 হারে sample করে, এবং পুরোটাই ধরা পড়ে; ডেমোর এক দিনের স্বাভাবিক ট্রাফিক
+থেকে কোনো কার্যকলাপ ধরা পড়ে না, শুধু website-এ কড়া নাড়া internet scanner
+ছাড়া।
+
+
+

-
+

diff --git a/docs/README.es.md b/docs/README.es.md
index 457d6ba..33905dd 100644
--- a/docs/README.es.md
+++ b/docs/README.es.md
@@ -14,12 +14,15 @@ propios equipos, tanto en una interfaz web como en una interfaz de terminal.
local opcional desde una interfaz de red o un puerto espejo.
- Contrasta sus cifras con los contadores de interfaz (contadores sFlow o
SNMP) y explica por qué difieren cuando no coinciden.
+- Detecta en los flujos escaneos, adivinación de contraseñas, movimiento
+ lateral, subidas inusuales, inundaciones y tráfico de listas de amenazas,
+ también a través del muestreo, y los muestra como hallazgos que atender.
- Listas Top 66, rutas de tráfico, países y redes, coincidencias con listas
de amenazas, registros de flujo, encapsulación (GRE, IPIP, VXLAN, GENEVE,
MPLS).
- 13 idiomas en la interfaz web y en la de terminal.
-
+
Todas las capturas de pantalla proceden de `traffic66 demo`, una red de empresa simulada que puede ejecutar usted mismo (vea [Probar la demo](#1-try-the-demo)).
@@ -88,10 +91,13 @@ cd traffic66-windows-amd64
Abra http://127.0.0.1:8066 e inicie sesión como `admin` / `try66`. La demo
monta la red de una pequeña empresa con un día de historial y tráfico en
-vivo de cuatro equipos simulados, e incluye dos incidentes por descubrir:
-empiece en **Resumen**, haga clic en un host de **Principales clientes**,
-elija **Ver detalles** y siga haciendo clic a partir de ahí. Deténgala con
-Ctrl+C. Los datos de la demo se guardan en
+vivo de cuatro equipos simulados, e incluye un ataque: **Hallazgos**
+muestra cada uno de sus pasos (un escaneo, un escaneo de puertos,
+adivinación de contraseñas, movimiento lateral, una subida a un servidor de
+control) y una inundación contra el sitio web público. Haga clic en
+**Detalles** en un hallazgo, o empiece en **Resumen**, haga clic en un host
+de **Principales clientes**, elija **Ver detalles** y siga haciendo clic a
+partir de ahí. Deténgala con Ctrl+C. Los datos de la demo se guardan en
`traffic66-demo`, junto al programa; borre esa carpeta para empezar la demo
desde cero.
@@ -673,7 +679,8 @@ Páginas:
| Página | Qué responde |
|---|---|
-| Resumen | Cuánto tráfico hay ahora y frente a la semana pasada, por aplicación; principales clientes y servicios |
+| Resumen | Cuánto tráfico hay ahora y frente a la semana pasada, por aplicación; hallazgos abiertos; principales clientes y servicios |
+| Hallazgos | Qué requiere atención: escaneos, adivinación de contraseñas, movimiento lateral, subidas inusuales, inundaciones y tráfico de listas de amenazas ([más](#findings)) |
| Top-N | Una sola tabla de los 66 primeros: por defecto, conversaciones (cliente, servidor, servicio, país). Cualquier encabezado ordena; las columnas numéricas (tráfico, paquetes, paquete medio, flujos) clasifican todo el tráfico del periodo, así el menor paquete medio revela escaneos e inundaciones. **Agrupar por** cambia a aplicaciones, redes, segmentos, equipos, encapsulación y VLAN |
| Rutas de tráfico | Qué segmento habla con qué aplicación en qué país |
| Geografía y redes | Tráfico por país y por red (AS) |
@@ -699,9 +706,50 @@ pase el ratón por el espacio libre para ver cuánto necesitan, al ritmo
actual, los días de detalle que se conservan (se estima en cuanto hay un
día de datos).
+
+
+### Hallazgos
+
+**Hallazgos** enumera lo que traffic66 encontró en los flujos, empezando por
+lo más grave. Revisa los últimos 10 minutos cada 5 minutos; algo que dura
+una hora es un solo hallazgo que crece, no uno nuevo en cada revisión.
+
+| Hallazgo | Qué significa | Gravedad |
+|---|---|---|
+| Escaneo | Una dirección envió pequeñas sondas a muchas direcciones en un mismo puerto (TCP o ping) | Alta desde dentro de su red, baja desde internet |
+| Escaneo de puertos | Una dirección envió pequeñas sondas a muchos puertos de un mismo host | Alta desde dentro, baja desde internet |
+| Adivinación de contraseñas | Muchas conexiones cortas a un servicio de inicio de sesión (SSH, RDP, SMB, bases de datos y otros) | Alta desde dentro, baja desde internet |
+| Movimiento lateral | Dentro de su red, sesiones de compartición de archivos o de administración remota (SMB, RDP, SSH, WinRM, VNC) hacia hosts que nunca antes ofrecieron ese servicio | Alta |
+| Subida inusual | Un host interno envió mucho más de lo que recibió (100 MB en 10 minutos, el triple de lo recibido) a una dirección con la que no había intercambiado datos antes | Alta |
+| Inundación | 20.000 o más paquetes pequeños por segundo hacia una dirección, diez veces su ritmo habitual | Media |
+| Lista de amenazas | Tráfico con una dirección de una de sus listas de amenazas | Alta cuando su host se conectó a ella, baja cuando la dirección listada llamó desde fuera |
+
+Cada hallazgo dice quién hizo qué a quién, cuándo y durante cuánto tiempo,
+con las cifras que lo respaldan y cómo se muestrearon los datos.
+**Detalles** abre la página del host, que también enumera los hallazgos
+sobre él. **Resuelto** cierra un hallazgo; si vuelve a ocurrir, se abre uno
+nuevo. **No es un problema** lo cierra para siempre: no se vuelve a
+notificar. El número rojo junto a **Hallazgos** en el menú lateral cuenta
+los hallazgos abiertos de gravedad alta y media de las últimas 24 horas.
+
+El movimiento lateral y las subidas inusuales necesitan saber qué es
+normal, así que se notifican en cuanto hay un día de historial. En el
+primer arranque, traffic66 aprende del historial que ya tiene.
+
+Con datos muestreados (sFlow, NetFlow muestreado), las reglas cuentan lo que
+muestran las muestras y piden menos, pero entonces cada una debe parecer una
+sonda corta, de modo que los hosts normales con mucha actividad no las
+disparan. Lo que el muestreo oculta no se puede encontrar: tras un muestreo
+1:4096, un escaneo de unas decenas de hosts envía demasiado pocos paquetes
+para verse. El ataque de la demo pasa por un switch que muestrea a 1:4096 y
+se encuentra entero; un día de tráfico normal de la demo no produce ningún
+hallazgo salvo el escáner de internet que llama a la puerta del sitio web.
+
+
+

-
+

diff --git a/docs/README.fr.md b/docs/README.fr.md
index 76a16c3..f255ce5 100644
--- a/docs/README.fr.md
+++ b/docs/README.fr.md
@@ -16,12 +16,16 @@ terminal.
- Vérifie ses propres chiffres par rapport aux compteurs d'interface
(compteurs sFlow ou SNMP) et explique pourquoi ils diffèrent le cas
échéant.
+- Repère dans les flux les scans, les essais de mots de passe, les
+ mouvements latéraux, les envois inhabituels, les inondations et le trafic
+ des listes de menaces, y compris à travers l'échantillonnage, et les
+ présente comme des détections à traiter.
- Classements Top 66, chemins du trafic, pays et réseaux, correspondances
avec des listes de menaces, enregistrements de flux, encapsulation (GRE,
IPIP, VXLAN, GENEVE, MPLS).
- 13 langues dans l'interface web et dans l'interface terminal.
-
+
Toutes les captures d'écran proviennent de `traffic66 demo`, un réseau d'entreprise simulé que vous pouvez lancer vous-même (voir [Essayer la démo](#1-try-the-demo)).
@@ -91,7 +95,10 @@ cd traffic66-windows-amd64
Ouvrez http://127.0.0.1:8066 et connectez-vous avec `admin` / `try66`. La
démo construit le réseau d'une petite entreprise avec une journée
d'historique et du trafic en direct provenant de quatre équipements
-simulés, dont deux incidents à trouver : commencez par
+simulés, dont une attaque : **Détections** en montre chaque étape (un
+scan, un scan de ports, des essais de mots de passe, un mouvement latéral,
+un envoi vers un serveur de contrôle) ainsi qu'une inondation du site web
+public. Cliquez sur **Détails** sur une détection, ou commencez par
**Vue d'ensemble**, cliquez sur un hôte dans **Principaux clients**,
choisissez **Voir les détails** et continuez à cliquer de proche en
proche. Arrêtez-la avec Ctrl+C. Les données de la démo sont conservées dans
@@ -690,7 +697,8 @@ Pages :
| Page | À quoi elle répond |
|---|---|
-| Vue d'ensemble | Combien de trafic maintenant et par rapport à la semaine dernière, par application ; principaux clients et services |
+| Vue d'ensemble | Combien de trafic maintenant et par rapport à la semaine dernière, par application ; détections ouvertes ; principaux clients et services |
+| Détections | Ce qui demande votre attention : scans, essais de mots de passe, mouvements latéraux, envois inhabituels, inondations et trafic des listes de menaces ([plus](#findings)) |
| Top-N | Un seul tableau du top 66 : par défaut les conversations (client, serveur, service, pays). Chaque en-tête trie ; les colonnes numériques (trafic, paquets, paquet moyen, flux) classent tout le trafic de la période, si bien que le plus petit paquet moyen révèle scans et inondations. **Regrouper par** passe aux applications, réseaux, segments, équipements, encapsulation et VLAN |
| Chemins du trafic | Quel segment parle à quelle application dans quel pays |
| Géographie et réseaux | Trafic par pays et par réseau (AS) |
@@ -718,9 +726,55 @@ libre ; survolez l'espace libre pour voir ce dont les jours de détail
conservés ont besoin au rythme actuel (estimation disponible dès qu'il y a
une journée de données).
+
+
+### Détections
+
+**Détections** liste ce que traffic66 a trouvé dans les flux, le plus grave
+en premier. Il examine les 10 dernières minutes toutes les 5 minutes ; ce
+qui dure une heure forme une seule détection qui grandit, pas une nouvelle
+à chaque examen.
+
+| Détection | Ce que cela signifie | Gravité |
+|---|---|---|
+| Scan | Une adresse a envoyé de petites sondes à de nombreuses adresses sur un même port (TCP ou ping) | Élevée depuis votre réseau, faible depuis internet |
+| Scan de ports | Une adresse a envoyé de petites sondes à de nombreux ports d'un même hôte | Élevée depuis l'intérieur, faible depuis internet |
+| Essais de mots de passe | De nombreuses connexions courtes vers un service de connexion (SSH, RDP, SMB, bases de données et autres) | Élevée depuis l'intérieur, faible depuis internet |
+| Mouvement latéral | Dans votre réseau, des sessions de partage de fichiers ou d'administration à distance (SMB, RDP, SSH, WinRM, VNC) vers des hôtes qui n'avaient jamais offert ce service | Élevée |
+| Envoi inhabituel | Un hôte interne a envoyé bien plus qu'il n'a reçu (100 Mo en 10 minutes, trois fois ce qu'il a reçu) vers une adresse avec laquelle il n'avait jamais échangé de données | Élevée |
+| Inondation | 20 000 petits paquets par seconde ou plus vers une adresse, dix fois son débit habituel | Moyenne |
+| Liste de menaces | Trafic avec une adresse de l'une de vos listes de menaces | Élevée quand votre hôte s'y est connecté, faible quand l'adresse listée a frappé de l'extérieur |
+
+Chaque détection indique qui a fait quoi à qui, quand et pendant combien de
+temps, avec les chiffres à l'appui et la façon dont les données ont été
+échantillonnées. **Détails** ouvre la page de l'hôte, qui liste aussi les
+détections qui le concernent. **Traité** ferme une détection ; si cela se
+reproduit, une nouvelle s'ouvre. **Fausse alerte** la ferme définitivement :
+elle n'est plus jamais signalée. Le nombre rouge à côté de **Détections**
+dans le menu latéral compte les détections ouvertes de gravité élevée et
+moyenne des dernières 24 heures.
+
+Les mouvements latéraux et les envois inhabituels ont besoin de savoir ce
+qui est normal ; ils ne sont donc signalés qu'une fois qu'il y a une
+journée d'historique. Au premier démarrage, traffic66 apprend à partir de
+l'historique dont il dispose déjà.
+
+Avec des données échantillonnées (sFlow, NetFlow échantillonné), les règles
+comptent ce que montrent les échantillons et en demandent moins, mais
+chacun doit alors ressembler à une courte sonde, si bien que les hôtes
+normaux très actifs ne les déclenchent pas. Ce que l'échantillonnage cache
+ne peut pas être trouvé : derrière un échantillonnage 1:4096, un scan de
+quelques dizaines d'hôtes envoie trop peu de paquets pour être vu.
+L'attaque de la démo passe par un switch qui échantillonne à 1:4096 et est
+trouvée entièrement ; une journée de trafic normal de la démo ne produit
+aucune détection, hormis le scanner d'internet qui frappe à la porte du
+site web.
+
+
+

-
+

diff --git a/docs/README.hi.md b/docs/README.hi.md
index a01d24a..0e2bbd1 100644
--- a/docs/README.hi.md
+++ b/docs/README.hi.md
@@ -14,11 +14,14 @@ counters से मेल खाते हैं — web UI में भी औ
किसी network interface या mirror port से local capture भी।
- अपने आँकड़ों को interface counters (sFlow counters या SNMP) से मिलाकर
जाँचता है, और फ़र्क़ होने पर बताता है कि क्यों।
+- flows में scans, पासवर्ड का अनुमान, lateral movement, असामान्य uploads,
+ floods और threat list वाला ट्रैफ़िक ढूँढता है, sampling के बावजूद भी, और
+ उन्हें निपटाने के लिए संदिग्ध गतिविधियों की सूची में रखता है।
- Top 66 सूचियाँ, flow के रास्ते, देश और नेटवर्क, threat list के matches,
flow records, encapsulation (GRE, IPIP, VXLAN, GENEVE, MPLS)।
- web UI और terminal UI में 13 भाषाएँ।
-
+
सभी स्क्रीनशॉट `traffic66 demo` से लिए गए हैं, एक simulated कंपनी नेटवर्क जिसे आप ख़ुद चला सकते हैं ([डेमो चलाकर देखें](#1-try-the-demo) देखें)।
@@ -85,11 +88,14 @@ cd traffic66-windows-amd64
.\traffic66.exe demo -password try66
```
-http://127.0.0.1:8066 खोलें और `admin` / `try66` से साइन इन करें। डेमो एक
-छोटी कंपनी का नेटवर्क बनाता है, जिसमें एक दिन का इतिहास और चार simulated
-डिवाइसों से live ट्रैफ़िक होता है, और उसमें ढूँढने के लिए दो incidents भी हैं:
-**सारांश** से शुरू करें, **शीर्ष क्लाइंट** में किसी host पर क्लिक करें,
-**विवरण देखें** चुनें, और वहाँ से क्लिक करते हुए आगे बढ़ें। Ctrl+C से बंद करें। डेमो का डेटा प्रोग्राम के बगल में
+http://127.0.0.1:8066 खोलें और `admin` / `try66` से साइन इन करें। डेमो एक छोटी
+कंपनी का नेटवर्क बनाता है, जिसमें एक दिन का इतिहास और चार simulated डिवाइसों
+से live ट्रैफ़िक होता है, और उसमें एक हमला भी है: **संदिग्ध गतिविधि** उसका हर
+चरण दिखाता है (एक scan, एक port scan, पासवर्ड का अनुमान, lateral movement, एक
+control server पर upload) और public website पर एक flood भी। सूची की किसी
+प्रविष्टि पर **विवरण** क्लिक करें, या **सारांश** से शुरू करें,
+**शीर्ष क्लाइंट** में किसी host पर क्लिक करें, **विवरण देखें** चुनें, और वहाँ
+से क्लिक करते हुए आगे बढ़ें। Ctrl+C से बंद करें। डेमो का डेटा प्रोग्राम के बगल में
`traffic66-demo` में रहता है; डेमो नए सिरे से शुरू करने के लिए वह फ़ोल्डर
हटा दें।
@@ -656,7 +662,8 @@ application, देश, डिवाइस — पर क्लिक किय
| पेज | किस सवाल का जवाब देता है |
|---|---|
-| सारांश | अभी कितना ट्रैफ़िक है और पिछले हफ़्ते की तुलना में कितना, application के हिसाब से; top clients और services |
+| सारांश | अभी कितना ट्रैफ़िक है और पिछले हफ़्ते की तुलना में कितना, application के हिसाब से; खुली संदिग्ध गतिविधियाँ; top clients और services |
+| संदिग्ध गतिविधि | किस पर ध्यान देना है: scans, पासवर्ड का अनुमान, lateral movement, असामान्य uploads, floods और threat list वाला ट्रैफ़िक ([और जानें](#findings)) |
| Top-N | top 66 की एक table: डिफ़ॉल्ट रूप से conversations (client, server, service, देश)। हर column heading से sort होता है; संख्या वाले columns (ट्रैफ़िक, पैकेट, औसत पैकेट, flows) समय-सीमा के पूरे ट्रैफ़िक से top 66 फिर से चुनते हैं, इसलिए सबसे छोटे औसत पैकेट से scan और flood पकड़े जाते हैं। **इसके अनुसार समूह** से applications, नेटवर्क, segments, डिवाइस, encapsulation और VLAN पर जाएँ |
| ट्रैफ़िक के रास्ते | कौन-सा segment किस देश में किस application से बात करता है |
| भूगोल और नेटवर्क | देश के हिसाब से और नेटवर्क (AS) के हिसाब से ट्रैफ़िक |
@@ -680,9 +687,50 @@ side menu दिखाता है कि data कितनी disk इस्
ख़ाली जगह पर hover करें तो दिखता है कि मौजूदा दर पर रखे गए दिनों के detail को
कितनी जगह चाहिए (एक दिन का data होने के बाद अनुमान लगाया जाता है)।
+
+
+### संदिग्ध गतिविधि
+
+**संदिग्ध गतिविधि** वह सब दिखाता है जो traffic66 ने flows में पाया, सबसे गंभीर
+पहले। यह हर 5 मिनट पर पिछले 10 मिनट जाँचता है; जो चीज़ एक घंटे तक चलती है वह
+एक ही प्रविष्टि है जो बढ़ती जाती है, हर जाँच पर नई नहीं।
+
+| गतिविधि | इसका मतलब | गंभीरता |
+|---|---|---|
+| स्कैन | एक address ने एक ही port पर कई addresses को छोटे probes भेजे (TCP या ping) | आपके नेटवर्क के अंदर से हो तो उच्च, इंटरनेट से हो तो निम्न |
+| पोर्ट स्कैन | एक address ने एक host के कई ports पर छोटे probes भेजे | अंदर से उच्च, इंटरनेट से निम्न |
+| पासवर्ड का अनुमान | किसी login service (SSH, RDP, SMB, databases और अन्य) से कई छोटे connections | अंदर से उच्च, इंटरनेट से निम्न |
+| लेटरल मूवमेंट | आपके नेटवर्क के अंदर, file sharing या remote administration sessions (SMB, RDP, SSH, WinRM, VNC) ऐसे hosts से जिन्होंने पहले कभी वह service नहीं दी | उच्च |
+| असामान्य अपलोड | एक internal host ने जितना पाया उससे कहीं ज़्यादा भेजा (10 मिनट में 100 MB, पाए गए डेटा का तीन गुना) ऐसे address को जिससे उसने पहले कभी डेटा का लेन-देन नहीं किया | उच्च |
+| फ़्लड | एक address पर प्रति सेकंड 20,000 या उससे ज़्यादा छोटे packets, उसकी सामान्य दर का दस गुना | मध्यम |
+| ख़तरा सूची | आपकी किसी threat list के address के साथ ट्रैफ़िक | उच्च जब आपके host ने उससे connect किया, निम्न जब सूची वाला address बाहर से दस्तक दे रहा था |
+
+हर प्रविष्टि बताती है कि किसने किसके साथ क्या किया, कब और कितनी देर तक, इसके
+पीछे के आँकड़ों के साथ, और यह भी कि data कैसे sample किया गया था। **विवरण**
+host का पेज खोलता है, जिसमें उसके बारे में मिली गतिविधियाँ भी दिखती हैं।
+**निपटा दिया** प्रविष्टि को बंद करता है; अगर वही फिर होता है तो नई प्रविष्टि
+खुलती है। **कोई समस्या नहीं** उसे हमेशा के लिए बंद करता है: वह फिर कभी report
+नहीं होती। side menu में **संदिग्ध गतिविधि** के बगल का लाल नंबर पिछले 24 घंटों
+की खुली उच्च और मध्यम प्रविष्टियाँ गिनता है।
+
+लेटरल मूवमेंट और असामान्य uploads के लिए यह जानना ज़रूरी है कि सामान्य क्या
+है, इसलिए वे एक दिन का इतिहास होने के बाद ही report होते हैं। पहली बार शुरू
+होने पर traffic66 पहले से मौजूद इतिहास से सीखता है।
+
+Sampled data (sFlow, sampled NetFlow) के साथ नियम वही गिनते हैं जो samples
+दिखाते हैं और कम संख्या माँगते हैं, लेकिन तब हर sample एक छोटे probe जैसा
+दिखना चाहिए, ताकि व्यस्त सामान्य hosts इन्हें trigger न करें। जो sampling छिपा
+देता है वह पकड़ा नहीं जा सकता: 1:4096 sampling के पीछे, कुछ दर्जन hosts का
+scan इतने कम packets भेजता है कि दिखता नहीं। डेमो का हमला एक ऐसे switch से
+गुज़रता है जो 1:4096 पर sample करता है, और पूरा पकड़ा जाता है; डेमो के एक दिन
+के सामान्य ट्रैफ़िक से कोई गतिविधि नहीं निकलती, सिवाय website पर दस्तक देते
+internet scanner के।
+
+
+

-
+

diff --git a/docs/README.id.md b/docs/README.id.md
index 8662d0f..617df40 100644
--- a/docs/README.id.md
+++ b/docs/README.id.md
@@ -15,11 +15,14 @@ antarmuka terminal.
opsional capture lokal dari interface jaringan atau port mirror.
- Mencocokkan angkanya sendiri dengan counter interface (counter sFlow atau
SNMP) dan menjelaskan penyebabnya bila berbeda.
+- Menemukan pemindaian, tebakan kata sandi, pergerakan lateral, unggahan tidak
+ biasa, flood, dan trafik daftar ancaman di dalam flow, juga melalui
+ sampling, lalu mencantumkannya sebagai temuan untuk ditangani.
- Daftar Top 66, jalur trafik, negara dan jaringan, kecocokan dengan daftar
ancaman, catatan flow, enkapsulasi (GRE, IPIP, VXLAN, GENEVE, MPLS).
- 13 bahasa di antarmuka web dan antarmuka terminal.
-
+
Semua tangkapan layar berasal dari `traffic66 demo`, jaringan perusahaan simulasi yang bisa Anda jalankan sendiri (lihat [Coba demo](#1-try-the-demo)).
@@ -87,10 +90,13 @@ cd traffic66-windows-amd64
```
Buka http://127.0.0.1:8066 dan login sebagai `admin` / `try66`. Demo ini
-membangun jaringan kantor kecil dengan riwayat satu hari dan trafik live
-dari empat perangkat simulasi, lengkap dengan dua insiden yang bisa Anda
-cari: mulai dari **Ringkasan**, klik sebuah host di **Klien teratas**, pilih
-**Lihat detail**, lalu terus telusuri dengan mengeklik. Hentikan dengan Ctrl+C. Data demo disimpan di
+membangun jaringan kantor kecil dengan riwayat satu hari dan trafik live dari
+empat perangkat simulasi, lengkap dengan sebuah serangan: **Temuan**
+menampilkan setiap langkahnya (pemindaian, pemindaian port, tebakan kata
+sandi, pergerakan lateral, unggahan ke server kendali) serta flood ke situs
+web publik. Klik **Detail** pada sebuah temuan, atau mulai dari **Ringkasan**,
+klik sebuah host di **Klien teratas**, pilih **Lihat detail**, lalu terus
+telusuri dengan mengeklik. Hentikan dengan Ctrl+C. Data demo disimpan di
`traffic66-demo` di sebelah program; hapus folder itu untuk memulai demo
dari awal.
@@ -669,7 +675,8 @@ Halaman:
| Halaman | Pertanyaan yang dijawab |
|---|---|
-| Ringkasan | Berapa trafik sekarang dan dibanding minggu lalu, per aplikasi; klien dan layanan teratas |
+| Ringkasan | Berapa trafik sekarang dan dibanding minggu lalu, per aplikasi; temuan terbuka; klien dan layanan teratas |
+| Temuan | Apa yang perlu diperhatikan: pemindaian, tebakan kata sandi, pergerakan lateral, unggahan tidak biasa, flood, dan trafik daftar ancaman ([selengkapnya](#findings)) |
| Top-N | Satu tabel 66 teratas: secara default percakapan (klien, server, layanan, negara). Setiap judul kolom mengurutkan; kolom angka (lalu lintas, paket, rata-rata paket, flow) memilih ulang 66 teratas dari semua lalu lintas dalam rentang, sehingga rata-rata paket terkecil menemukan pemindaian dan banjir. **Kelompokkan menurut** beralih ke aplikasi, jaringan, segmen, perangkat, enkapsulasi, dan VLAN |
| Jalur trafik | Segmen mana berbicara dengan aplikasi apa di negara mana |
| Geografi & jaringan | Trafik per negara dan per jaringan (AS) |
@@ -698,9 +705,52 @@ masih kosong; arahkan kursor ke ruang kosong untuk melihat berapa yang
dibutuhkan detail untuk hari-hari yang disimpan pada laju saat ini
(diperkirakan setelah ada data satu hari).
+
+
+### Temuan
+
+**Temuan** mencantumkan apa yang ditemukan traffic66 di dalam flow, yang
+paling serius lebih dulu. Ia memeriksa 10 menit terakhir setiap 5 menit;
+sesuatu yang berlangsung satu jam adalah satu temuan yang terus bertambah,
+bukan temuan baru di setiap pemeriksaan.
+
+| Temuan | Artinya | Tingkat |
+|---|---|---|
+| Pemindaian | Satu alamat mengirim probe kecil ke banyak alamat pada satu port (TCP atau ping) | Tinggi dari dalam jaringan Anda, rendah dari internet |
+| Pemindaian port | Satu alamat mengirim probe kecil ke banyak port pada satu host | Tinggi dari dalam, rendah dari internet |
+| Tebak kata sandi | Banyak koneksi singkat ke layanan login (SSH, RDP, SMB, database, dan lainnya) | Tinggi dari dalam, rendah dari internet |
+| Pergerakan lateral | Di dalam jaringan Anda, sesi berbagi file atau administrasi jarak jauh (SMB, RDP, SSH, WinRM, VNC) ke host yang sebelumnya tidak pernah menyediakan layanan itu | Tinggi |
+| Unggahan tidak biasa | Host internal mengirim jauh lebih banyak daripada yang diterimanya (100 MB dalam 10 menit, tiga kali yang diterimanya) ke alamat yang belum pernah bertukar data dengannya | Tinggi |
+| Flood | 20,000 paket kecil atau lebih per detik ke satu alamat, sepuluh kali laju biasanya | Sedang |
+| Daftar ancaman | Trafik dengan alamat di salah satu daftar ancaman Anda | Tinggi bila host Anda terhubung ke alamat itu, rendah bila alamat dalam daftar mengetuk dari luar |
+
+Setiap temuan menyebutkan siapa melakukan apa terhadap siapa, kapan dan berapa
+lama, beserta angka di baliknya dan cara data di-sampling. **Detail** membuka
+halaman host, yang juga mencantumkan temuan tentang host itu.
+**Sudah ditangani** menutup temuan; jika terjadi lagi, temuan baru dibuka.
+**Bukan masalah** menutupnya untuk selamanya: temuan itu tidak akan pernah
+dilaporkan lagi. Angka merah di samping **Temuan** di menu samping menghitung
+temuan terbuka bertingkat tinggi dan sedang dalam 24 jam terakhir.
+
+Pergerakan lateral dan unggahan tidak biasa perlu tahu apa yang normal, jadi
+keduanya baru dilaporkan setelah ada riwayat satu hari. Saat pertama kali
+dijalankan, traffic66 belajar dari riwayat yang sudah ada.
+
+Dengan data hasil sampling (sFlow, NetFlow dengan sampling) aturan menghitung
+apa yang terlihat di sampel dan meminta jumlah yang lebih sedikit, tetapi
+setiap sampel harus tampak seperti satu probe singkat, sehingga host normal
+yang sibuk tidak memicunya. Apa yang disembunyikan sampling tidak bisa
+ditemukan: di balik sampling 1:4096, pemindaian terhadap beberapa puluh host
+mengirim terlalu sedikit paket untuk terlihat. Serangan dalam demo melewati
+switch yang melakukan sampling 1:4096 dan ditemukan seluruhnya; satu hari
+trafik normal demo tidak menghasilkan temuan apa pun kecuali pemindai internet
+yang mengetuk situs web.
+
+
+

-
+

diff --git a/docs/README.ja.md b/docs/README.ja.md
index 5c8de58..393d1aa 100644
--- a/docs/README.ja.md
+++ b/docs/README.ja.md
@@ -7,10 +7,11 @@ sFlow・NetFlow・IPFIX のフロー分析を 1 つのプログラムで行い
- Windows・Linux・macOS それぞれ実行ファイル 1 つだけです。データベースのインストールもランタイムも不要で、オフラインで動作します。
- sFlow v5、NetFlow v5、NetFlow v9、IPFIX を任意の UDP ポートで受信できます。ネットワークインターフェースやミラーポートでのローカルキャプチャにも対応しています。
- 自身の集計値をインターフェースカウンター(sFlow カウンターまたは SNMP)と照合し、ずれがあればその理由を示します。
+- フローからスキャン、パスワード総当たり、横展開、不審なアップロード、フラッド、脅威リストとの通信を見つけ出し(サンプリングされたフローでも可能)、対応すべき検知として一覧にします。
- Top 66 ランキング、フローの流れ、国とネットワーク、脅威リストとの一致、フローレコード、カプセル化(GRE、IPIP、VXLAN、GENEVE、MPLS)。
- Web UI とターミナル UI は 13 言語に対応しています。
-
+
スクリーンショットはすべて `traffic66 demo` で撮影したものです。これは自分で実行できる模擬的な社内ネットワークです([デモを試す](#1-try-the-demo) を参照)。
@@ -75,7 +76,7 @@ cd traffic66-windows-amd64
.\traffic66.exe demo -password try66
```
-http://127.0.0.1:8066 を開き、`admin` / `try66` でサインインします。デモでは小さな社内ネットワークが構築され、1 日分の履歴と、シミュレートした 4 台の機器からのライブトラフィックが流れます。中には見つけてほしいインシデントが 2 件仕込まれています。**概要** から始めて **上位クライアント** のホストをクリックし、**詳細を表示** を選んで、そこからさらにクリックでたどってください。Ctrl+C で停止します。デモデータはプログラムと同じ場所の `traffic66-demo` に保存されます。このフォルダーを削除するとデモを最初からやり直せます。
+http://127.0.0.1:8066 を開き、`admin` / `try66` でサインインします。デモでは小さな社内ネットワークが構築され、1 日分の履歴と、シミュレートした 4 台の機器からのライブトラフィックが流れます。中には攻撃が 1 件含まれています。**検知** にはその各段階(スキャン、ポートスキャン、パスワード総当たり、横展開、制御サーバーへのアップロード)と、公開 Web サイトへのフラッドが表示されます。検知の **詳細** をクリックするか、**概要** から始めて **上位クライアント** のホストをクリックし、**詳細を表示** を選んで、そこからさらにクリックでたどってください。Ctrl+C で停止します。デモデータはプログラムと同じ場所の `traffic66-demo` に保存されます。このフォルダーを削除するとデモを最初からやり直せます。
デモは本番インストールと同じポート(8066 と UDP 6343、2055、4739)を使います。本番環境と並べて動かす場合は、別のポートを指定してください:`traffic66 demo -password try66 -addr :8067 -listen ""`。
@@ -546,7 +547,8 @@ curl -L https://www.spamhaus.org/drop/drop.txt -o /threats/spamh
| ページ | 分かること |
|---|---|
-| 概要 | 現在のトラフィック量と先週との比較(アプリケーション別)、上位のクライアントとサービス |
+| 概要 | 現在のトラフィック量と先週との比較(アプリケーション別)、未対応の検知、上位のクライアントとサービス |
+| 検知 | 対応が必要なもの:スキャン、パスワード総当たり、横展開、不審なアップロード、フラッド、脅威リストとの通信([詳しく](#findings)) |
| Top-N | 上位 66 件を 1 つの表で表示。既定は会話(クライアント、サーバー、サービス、国)。どの列見出しでも並べ替えられます。数値の列(トラフィック、パケット数、平均パケット長、フロー数)は期間内の全トラフィックから上位 66 件を取り直すので、平均パケット長の小さい順でスキャンやフラッドが見つかります。**グループ化** でアプリケーション、ネットワーク、セグメント、機器、カプセル化、VLAN に切り替えられます |
| フローの流れ | どのセグメントがどの国のどのアプリケーションと通信しているか |
| 地域とネットワーク | 国別・ネットワーク(AS)別のトラフィック |
@@ -561,9 +563,33 @@ curl -L https://www.spamhaus.org/drop/drop.txt -o /threats/spamh
サイドメニューには、データが使っているディスク容量と空き容量が表示されます。空き容量にマウスを重ねると、現在のペースで明細を保持日数分残すのに必要な容量が分かります(1 日分のデータがたまると推定されます)。
+
+
+### 検知
+
+**検知** には、traffic66 がフローから見つけたものが深刻な順に表示されます。5 分ごとに直近 10 分間をチェックします。1 時間続く事象は、チェックのたびに新しい検知になるのではなく、1 件の検知として育っていきます。
+
+| 検知 | 意味 | 重大度 |
+|---|---|---|
+| スキャン | 1 つのアドレスが、多数のアドレスの同じポートに小さなプローブを送った(TCP または ping) | 社内ネットワークからなら高、インターネットからなら低 |
+| ポートスキャン | 1 つのアドレスが、1 台のホストの多数のポートに小さなプローブを送った | 社内からなら高、インターネットからなら低 |
+| パスワード総当たり | ログインサービス(SSH、RDP、SMB、データベースなど)への短い接続が多数 | 社内からなら高、インターネットからなら低 |
+| 横展開 | 社内ネットワーク内で、それまでそのサービスを提供したことのないホストへのファイル共有またはリモート管理のセッション(SMB、RDP、SSH、WinRM、VNC) | 高 |
+| 不審なアップロード | 内部ホストが、それまでデータをやり取りしたことのないアドレスへ、受信よりはるかに多く送信した(10 分間で 100 MB、受信量の 3 倍) | 高 |
+| フラッド | 1 つのアドレスへ毎秒 20,000 個以上の小さなパケット、普段の 10 倍のレート | 中 |
+| 脅威リスト | 脅威リストのいずれかに載っているアドレスとの通信 | 自分のホストから接続した場合は高、リスト上のアドレスが外から叩いてきた場合は低 |
+
+各検知には、誰が誰に何をしたか、いつ、どれだけの時間続いたかが、根拠となる数値とデータのサンプリング方法とともに示されます。**詳細** はそのホストのページを開きます。そこにもそのホストに関する検知が表示されます。**対応済み** は検知を閉じます。同じことが再び起きれば新しい検知が開きます。**問題なし** は検知を永久に閉じ、二度と報告されません。サイドメニューの **検知** の横にある赤い数字は、直近 24 時間の未対応の重大度「高」と「中」の検知の件数です。
+
+横展開と不審なアップロードは何が普段どおりかを知る必要があるため、1 日分の履歴がたまってから報告されます。初回起動時には、traffic66 はすでにある履歴から学習します。
+
+サンプリングされたデータ(sFlow、サンプリングされた NetFlow)では、ルールはサンプルに見えるものを数え、必要な件数を減らします。その代わり、各サンプルが 1 回の短いプローブに見えなければならないので、正常で忙しいホストが引っかかることはありません。サンプリングで隠れたものは見つけられません。1:4096 のサンプリングでは、数十台のホストへのスキャンは送るパケットが少なすぎて見えません。デモの攻撃は 1:4096 でサンプリングするスイッチを通りますが、すべて検知されます。デモの正常なトラフィックは 1 日分でも、Web サイトを叩くインターネット上のスキャナー以外の検知を生みません。
+
+
+

-
+

diff --git a/docs/README.ko.md b/docs/README.ko.md
index 41c230e..1ab72d5 100644
--- a/docs/README.ko.md
+++ b/docs/README.ko.md
@@ -12,11 +12,13 @@ sFlow, NetFlow, IPFIX 플로 분석을 하나의 프로그램으로 처리합니
인터페이스나 미러 포트에서 직접 로컬 캡처도 할 수 있습니다.
- 자체 집계 수치를 인터페이스 카운터(sFlow 카운터 또는 SNMP)와 대조하고, 차이가 나면 그
이유를 알려 줍니다.
+- 플로에서 스캔, 비밀번호 대입, 내부 확산, 비정상 업로드, 플러드, 위협 목록 트래픽을
+ 찾아내고(샘플링된 데이터에서도), 처리할 탐지 항목으로 보여 줍니다.
- Top 66 목록, 트래픽 경로, 국가와 네트워크, 위협 목록 일치 항목, 플로 레코드,
캡슐화(GRE, IPIP, VXLAN, GENEVE, MPLS).
- 웹 UI와 터미널 UI 모두 13개 언어를 지원합니다.
-
+
모든 스크린샷은 직접 실행해 볼 수 있는 가상의 회사 네트워크인 `traffic66 demo`에서 찍은 것입니다([데모 실행해 보기](#1-try-the-demo) 참조).
@@ -83,9 +85,11 @@ cd traffic66-windows-amd64
```
http://127.0.0.1:8066 을 열고 `admin` / `try66`으로 로그인합니다. 데모는 작은 회사
-네트워크를 만들어 하루치 이력과 시뮬레이션 장비 4대의 실시간 트래픽을 보여 주며, 찾아볼
-만한 사고 두 건이 들어 있습니다. **개요**에서 시작해 **상위 클라이언트**의 호스트를 클릭하고
-**상세 보기**를 고른 뒤, 거기서부터 계속 클릭해 보십시오. Ctrl+C로 중지합니다. 데모 데이터는 프로그램 옆의 `traffic66-demo`에
+네트워크를 만들어 하루치 이력과 시뮬레이션 장비 4대의 실시간 트래픽을 보여 주며, 공격
+하나가 들어 있습니다. **탐지**에 그 각 단계(스캔, 포트 스캔, 비밀번호 대입, 내부 확산,
+제어 서버로의 업로드)와 공개 웹사이트에 대한 플러드가 표시됩니다. 탐지 항목에서 **상세**를
+클릭하거나, **개요**에서 시작해 **상위 클라이언트**의 호스트를 클릭하고 **상세 보기**를 고른
+뒤, 거기서부터 계속 클릭해 보십시오. Ctrl+C로 중지합니다. 데모 데이터는 프로그램 옆의 `traffic66-demo`에
저장되며, 이 폴더를 삭제하면 데모를 처음부터 다시 시작할 수 있습니다.
데모는 실제 설치와 같은 포트(8066, UDP 6343, 2055, 4739)를 사용합니다. 실제 인스턴스와
@@ -633,7 +637,8 @@ curl -L https://www.spamhaus.org/drop/drop.txt -o /threats/spamh
| 페이지 | 알 수 있는 것 |
|---|---|
-| 개요 | 현재 트래픽 양과 지난주 대비 변화(애플리케이션별), 상위 클라이언트와 서비스 |
+| 개요 | 현재 트래픽 양과 지난주 대비 변화(애플리케이션별), 미처리 탐지, 상위 클라이언트와 서비스 |
+| 탐지 | 살펴봐야 할 것: 스캔, 비밀번호 대입, 내부 확산, 비정상 업로드, 플러드, 위협 목록 트래픽([자세히](#findings)) |
| Top-N | 상위 66개를 담은 하나의 표: 기본은 대화(클라이언트, 서버, 서비스, 국가). 모든 열 제목으로 정렬할 수 있고, 숫자 열(트래픽, 패킷, 평균 패킷 크기, 플로)은 기간 내 전체 트래픽에서 상위 66개를 다시 뽑으므로 평균 패킷 크기가 작은 순으로 스캔과 플러드를 찾을 수 있습니다. **그룹 기준**에서 애플리케이션, 네트워크, 세그먼트, 장비, 캡슐화, VLAN으로 바꿀 수 있습니다 |
| 트래픽 경로 | 어느 세그먼트가 어느 국가의 어느 애플리케이션과 통신하는지 |
| 지역 및 네트워크 | 국가별, 네트워크(AS)별 트래픽 |
@@ -657,9 +662,45 @@ curl -L https://www.spamhaus.org/drop/drop.txt -o /threats/spamh
올리면 현재 속도로 상세 데이터를 보관 일수만큼 유지하는 데 필요한 용량을 볼 수 있습니다
(하루치 데이터가 쌓이면 추정됨).
+
+
+### 탐지
+
+**탐지**는 traffic66이 플로에서 찾아낸 것을 심각한 순서로 보여 줍니다. 5분마다 최근
+10분을 검사하며, 한 시간 동안 이어지는 일은 검사할 때마다 새로 생기는 것이 아니라 하나의
+탐지 항목으로 계속 커집니다.
+
+| 탐지 | 의미 | 심각도 |
+|---|---|---|
+| 스캔 | 한 주소가 여러 주소의 같은 포트로 작은 탐색 패킷을 보냄(TCP 또는 ping) | 내부 네트워크에서 오면 높음, 인터넷에서 오면 낮음 |
+| 포트 스캔 | 한 주소가 한 호스트의 여러 포트로 작은 탐색 패킷을 보냄 | 내부에서 오면 높음, 인터넷에서 오면 낮음 |
+| 비밀번호 대입 | 로그인 서비스(SSH, RDP, SMB, 데이터베이스 등)로의 짧은 연결이 많음 | 내부에서 오면 높음, 인터넷에서 오면 낮음 |
+| 내부 확산 | 내부 네트워크 안에서, 그 서비스를 제공한 적이 없는 호스트로의 파일 공유 또는 원격 관리 세션(SMB, RDP, SSH, WinRM, VNC) | 높음 |
+| 비정상 업로드 | 내부 호스트가 이전에 데이터를 주고받은 적 없는 주소로, 받은 것보다 훨씬 많이 보냄(10분 동안 100 MB, 받은 양의 세 배) | 높음 |
+| 플러드 | 한 주소로 초당 20,000개 이상의 작은 패킷, 평소 속도의 열 배 | 중간 |
+| 위협 목록 | 위협 목록 중 하나에 있는 주소와의 트래픽 | 내 호스트가 그 주소에 연결했으면 높음, 목록의 주소가 밖에서 두드렸으면 낮음 |
+
+각 탐지 항목은 누가 누구에게 무엇을 했는지, 언제 얼마나 오래 했는지를 근거가 되는 수치와
+데이터 샘플링 방식과 함께 보여 줍니다. **상세**는 그 호스트의 페이지를 여는데, 거기에도 그
+호스트에 관한 탐지가 나옵니다. **처리 완료**는 탐지 항목을 닫으며, 같은 일이 다시 일어나면
+새 항목이 열립니다. **문제 없음**은 항목을 영구히 닫아 다시는 보고되지 않습니다. 사이드
+메뉴의 **탐지** 옆 빨간 숫자는 최근 24시간의 미처리 높음·중간 탐지 수입니다.
+
+내부 확산과 비정상 업로드는 무엇이 평소와 같은지 알아야 하므로, 하루치 이력이 쌓인 뒤부터
+보고됩니다. 처음 시작할 때 traffic66은 이미 있는 이력에서 학습합니다.
+
+샘플링된 데이터(sFlow, 샘플링된 NetFlow)에서는 규칙이 샘플에 보이는 것을 세고 더 적은
+수를 요구하지만, 그 대신 각 샘플이 짧은 탐색 하나처럼 보여야 하므로 바쁜 정상 호스트는
+걸리지 않습니다. 샘플링이 가린 것은 찾을 수 없습니다. 1:4096 샘플링 뒤에서는 호스트 수십
+대를 스캔해도 패킷이 너무 적어 보이지 않습니다. 데모의 공격은 1:4096으로 샘플링하는
+스위치를 거치지만 모두 탐지되며, 데모의 정상 트래픽은 하루치를 돌려도 웹사이트를 두드리는
+인터넷 스캐너 말고는 탐지를 만들지 않습니다.
+
+
+

-
+

diff --git a/docs/README.pt.md b/docs/README.pt.md
index 4c8b7de..30e764a 100644
--- a/docs/README.pt.md
+++ b/docs/README.pt.md
@@ -14,12 +14,15 @@ equipamentos, em uma interface web e em uma interface de terminal.
local opcional a partir de uma interface de rede ou porta espelho.
- Confere os próprios números com os contadores de interface (contadores
sFlow ou SNMP) e explica por que diferem quando há diferença.
+- Encontra nos fluxos varreduras, tentativas de senhas, movimento lateral,
+ envios incomuns, inundações e tráfego de listas de ameaças, inclusive
+ através da amostragem, e os lista como detecções a tratar.
- Listas Top 66, caminhos do tráfego, países e redes, ocorrências em listas
de ameaças, registros de fluxo, encapsulamento (GRE, IPIP, VXLAN, GENEVE,
MPLS).
- 13 idiomas na interface web e na interface de terminal.
-
+
Todas as capturas de tela vêm do `traffic66 demo`, uma rede corporativa simulada que você mesmo pode executar (veja [Testar a demo](#1-try-the-demo)).
@@ -88,7 +91,10 @@ cd traffic66-windows-amd64
Abra http://127.0.0.1:8066 e entre como `admin` / `try66`. A demo monta a
rede de uma pequena empresa com um dia de histórico e tráfego ao vivo de
-quatro equipamentos simulados, incluindo dois incidentes para encontrar:
+quatro equipamentos simulados, incluindo um ataque: **Detecções** mostra
+cada etapa dele (uma varredura, uma varredura de portas, tentativas de
+senhas, movimento lateral, um envio para um servidor de controle) e uma
+inundação contra o site público. Clique em **Detalhes** em uma detecção, ou
comece em **Visão geral**, clique em um host em **Principais clientes**,
escolha **Ver detalhes** e vá clicando a partir daí. Pare com Ctrl+C. Os dados da demo ficam em `traffic66-demo`, ao lado
do programa; apague essa pasta para recomeçar a demo do zero.
@@ -672,7 +678,8 @@ Páginas:
| Página | O que responde |
|---|---|
-| Visão geral | Quanto tráfego há agora e em comparação com a semana passada, por aplicação; principais clientes e serviços |
+| Visão geral | Quanto tráfego há agora e em comparação com a semana passada, por aplicação; detecções abertas; principais clientes e serviços |
+| Detecções | O que precisa de atenção: varreduras, tentativas de senhas, movimento lateral, envios incomuns, inundações e tráfego de listas de ameaças ([mais](#findings)) |
| Top-N | Uma única tabela dos 66 maiores: por padrão, conversas (cliente, servidor, serviço, país). Qualquer cabeçalho ordena; as colunas numéricas (tráfego, pacotes, pacote médio, fluxos) classificam todo o tráfego do período, então o menor pacote médio revela varreduras e inundações. **Agrupar por** muda para aplicações, redes, segmentos, equipamentos, encapsulamento e VLAN |
| Caminhos do tráfego | Qual segmento fala com qual aplicação em qual país |
| Geografia e redes | Tráfego por país e por rede (AS) |
@@ -698,9 +705,51 @@ O menu lateral mostra quanto disco os dados usam e quanto está livre; passe
o mouse sobre o espaço livre para ver quanto os dias de detalhe mantidos
precisam no ritmo atual (estimado assim que houver um dia de dados).
+
+
+### Detecções
+
+**Detecções** lista o que o traffic66 encontrou nos fluxos, as mais graves
+primeiro. Ele verifica os últimos 10 minutos a cada 5 minutos; algo que
+dura uma hora é uma única detecção que cresce, não uma nova a cada
+verificação.
+
+| Detecção | O que significa | Gravidade |
+|---|---|---|
+| Varredura | Um endereço enviou pequenas sondas a muitos endereços em uma mesma porta (TCP ou ping) | Alta de dentro da sua rede, baixa da internet |
+| Varredura de portas | Um endereço enviou pequenas sondas a muitas portas de um mesmo host | Alta de dentro, baixa da internet |
+| Tentativa de senhas | Muitas conexões curtas a um serviço de login (SSH, RDP, SMB, bancos de dados e outros) | Alta de dentro, baixa da internet |
+| Movimento lateral | Dentro da sua rede, sessões de compartilhamento de arquivos ou de administração remota (SMB, RDP, SSH, WinRM, VNC) para hosts que nunca ofereceram esse serviço antes | Alta |
+| Envio incomum | Um host interno enviou muito mais do que recebeu (100 MB em 10 minutos, três vezes o que recebeu) para um endereço com o qual não havia trocado dados antes | Alta |
+| Inundação | 20.000 ou mais pacotes pequenos por segundo para um endereço, dez vezes a taxa habitual dele | Média |
+| Lista de ameaças | Tráfego com um endereço de uma das suas listas de ameaças | Alta quando o seu host se conectou a ele, baixa quando o endereço listado bateu de fora |
+
+Cada detecção diz quem fez o quê a quem, quando e por quanto tempo, com os
+números por trás e como os dados foram amostrados. **Detalhes** abre a
+página do host, que também lista as detecções sobre ele. **Resolvido**
+fecha uma detecção; se acontecer de novo, uma nova é aberta.
+**Não é problema** a fecha de vez: ela nunca mais é relatada. O número
+vermelho ao lado de **Detecções** no menu lateral conta as detecções
+abertas de gravidade alta e média das últimas 24 horas.
+
+Movimento lateral e envios incomuns precisam saber o que é normal, então
+são relatados quando já houver um dia de histórico. Na primeira
+inicialização, o traffic66 aprende com o histórico que já tem.
+
+Com dados amostrados (sFlow, NetFlow amostrado), as regras contam o que as
+amostras mostram e pedem menos delas, mas então cada uma precisa parecer
+uma sonda curta, de modo que hosts normais movimentados não as disparam. O
+que a amostragem esconde não pode ser encontrado: com amostragem 1:4096,
+uma varredura de algumas dezenas de hosts envia pacotes de menos para ser
+vista. O ataque da demo passa por um switch que amostra a 1:4096 e é
+encontrado por completo; um dia de tráfego normal da demo não produz
+nenhuma detecção, exceto o scanner da internet batendo no site.
+
+
+

-
+

diff --git a/docs/README.ru.md b/docs/README.ru.md
index 793c3c4..20fb0a0 100644
--- a/docs/README.ru.md
+++ b/docs/README.ru.md
@@ -14,11 +14,14 @@
локальный захват с сетевого интерфейса или зеркального порта.
- Сверяет свои цифры со счётчиками интерфейсов (счётчики sFlow или SNMP) и,
если они расходятся, объясняет почему.
+- Находит в потоках сканирования, подбор паролей, боковое перемещение,
+ необычные выгрузки, флуд и трафик со списков угроз, в том числе при
+ сэмплировании, и выводит их списком обнаружений, требующих реакции.
- Списки Top 66, пути трафика, страны и сети, совпадения со списками угроз,
записи потоков, инкапсуляция (GRE, IPIP, VXLAN, GENEVE, MPLS).
- 13 языков в веб-интерфейсе и в терминальном интерфейсе.
-
+
Все скриншоты сделаны в `traffic66 demo` — смоделированной сети компании, которую можно запустить самому (см. [Попробовать демо](#1-try-the-demo)).
@@ -86,12 +89,14 @@ cd traffic66-windows-amd64
```
Откройте http://127.0.0.1:8066 и войдите как `admin` / `try66`. Демо
-разворачивает небольшую сеть компании с историей за сутки и живым трафиком
-от четырёх имитируемых устройств; в ней спрятаны два инцидента. Начните со
-страницы **Обзор**, щёлкните хост в **Основные клиенты**, выберите
-**Подробнее** и дальше идите по щелчкам.
-Остановка — Ctrl+C. Данные демо лежат в `traffic66-demo` рядом с
-программой; удалите эту папку, чтобы начать демо с нуля.
+разворачивает небольшую сеть компании с историей за сутки и живым трафиком от
+четырёх имитируемых устройств, в том числе с атакой: на странице
+**Обнаружения** видны все её шаги (сканирование, сканирование портов, подбор
+паролей, боковое перемещение, выгрузка на управляющий сервер) и флуд на
+публичный сайт. Нажмите **Подробнее** у обнаружения или начните со страницы
+**Обзор**, щёлкните хост в **Основные клиенты**, выберите **Подробнее** и
+дальше идите по щелчкам. Остановка — Ctrl+C. Данные демо лежат в
+`traffic66-demo` рядом с программой; удалите эту папку, чтобы начать демо с нуля.
Демо использует те же порты, что и рабочая установка (8066 и UDP 6343,
2055, 4739). Чтобы запустить его рядом с рабочей, задайте другие порты:
@@ -666,7 +671,8 @@ curl -L https://www.spamhaus.org/drop/drop.txt -o /threats/spamh
| Страница | На какой вопрос отвечает |
|---|---|
-| Обзор | Сколько трафика сейчас и по сравнению с прошлой неделей, по приложениям; основные клиенты и сервисы |
+| Обзор | Сколько трафика сейчас и по сравнению с прошлой неделей, по приложениям; открытые обнаружения; основные клиенты и сервисы |
+| Обнаружения | Что требует внимания: сканирования, подбор паролей, боковое перемещение, необычные выгрузки, флуд и трафик со списков угроз ([подробнее](#findings)) |
| Top-N | Одна таблица топ 66: по умолчанию сеансы (клиент, сервер, сервис, страна). Любой заголовок сортирует; числовые столбцы (трафик, пакеты, средний пакет, потоки) заново выбирают топ 66 из всего трафика за период, поэтому наименьший средний пакет находит сканирования и флуд. **Группировать по** переключает на приложения, сети, сегменты, устройства, инкапсуляцию и VLAN |
| Пути трафика | Какой сегмент обращается к какому приложению в какой стране |
| География и сети | Трафик по странам и по сетям (AS) |
@@ -693,9 +699,51 @@ curl -L https://www.spamhaus.org/drop/drop.txt -o /threats/spamh
нужно хранимым дням подробных данных при текущей нагрузке (оценка появляется,
когда накопятся данные за сутки).
+
+
+### Обнаружения
+
+На странице **Обнаружения** перечислено, что traffic66 нашёл в потоках, самое
+серьёзное сверху. Каждые 5 минут проверяются последние 10 минут; то, что
+длится час, — это одно обнаружение, которое растёт, а не новое при каждой
+проверке.
+
+| Обнаружение | Что это значит | Важность |
+|---|---|---|
+| Сканирование | Один адрес отправил мелкие пробы на множество адресов на один порт (TCP или ping) | Высокая изнутри вашей сети, низкая из интернета |
+| Сканирование портов | Один адрес отправил мелкие пробы на множество портов одного хоста | Высокая изнутри, низкая из интернета |
+| Подбор паролей | Множество коротких подключений к службе входа (SSH, RDP, SMB, базы данных и другие) | Высокая изнутри, низкая из интернета |
+| Боковое перемещение | Внутри вашей сети — сеансы общего доступа к файлам или удалённого администрирования (SMB, RDP, SSH, WinRM, VNC) к хостам, которые раньше никогда не предоставляли эту службу | Высокая |
+| Необычная выгрузка | Внутренний хост отправил намного больше, чем получил (100 MB за 10 минут, втрое больше полученного), на адрес, с которым раньше не обменивался данными | Высокая |
+| Флуд | 20,000 и более мелких пакетов в секунду на один адрес, в десять раз больше обычного | Средняя |
+| Список угроз | Трафик с адресом из одного из ваших списков угроз | Высокая, если ваш хост подключился к нему; низкая, если адрес из списка стучался снаружи |
+
+Каждое обнаружение говорит, кто что сделал и с кем, когда и как долго, с
+цифрами, на которых оно основано, и с тем, как сэмплировались данные.
+**Подробнее** открывает страницу хоста, где также перечислены обнаружения по
+нему. **Обработано** закрывает обнаружение; если это повторится, откроется
+новое. **Ложная тревога** закрывает его навсегда: о нём больше никогда не
+сообщается. Красное число рядом с **Обнаружения** в боковом меню — это
+открытые обнаружения высокой и средней важности за последние 24 часа.
+
+Боковому перемещению и необычным выгрузкам нужно знать, что нормально, поэтому
+о них сообщается, когда накопится история за сутки. При первом запуске
+traffic66 учится на уже имеющейся истории.
+
+На сэмплированных данных (sFlow, NetFlow с сэмплированием) правила считают то,
+что видно в выборке, и требуют меньше совпадений, но тогда каждое должно
+выглядеть как одна короткая проба, чтобы на обычных загруженных хостах они не
+срабатывали. То, что скрывает сэмплирование, найти нельзя: при сэмплировании
+1:4096 сканирование нескольких десятков хостов даёт слишком мало пакетов,
+чтобы его заметить. Атака в демо идёт через коммутатор с сэмплированием 1:4096
+и находится полностью; сутки обычного трафика демо не дают ни одного
+обнаружения, кроме интернет-сканера, стучащегося на сайт.
+
+
+

-
+

diff --git a/docs/README.ur.md b/docs/README.ur.md
index 99f0538..35387c8 100644
--- a/docs/README.ur.md
+++ b/docs/README.ur.md
@@ -16,11 +16,14 @@ embedded database میں رکھتا ہے، اور دکھاتا ہے کہ bandwid
کسی network interface یا mirror port سے local capture بھی۔
- اپنے اعداد کو interface counters (sFlow counters یا SNMP) سے ملا کر جانچتا
ہے، اور فرق ہو تو بتاتا ہے کہ کیوں۔
+- flows میں scans، پاس ورڈ کا اندازہ، lateral movement، غیر معمولی uploads،
+ floods اور threat list والی ٹریفک ڈھونڈتا ہے، sampling کے باوجود بھی، اور
+ انہیں نمٹانے کے لیے مشتبہ سرگرمیوں کی فہرست میں دکھاتا ہے۔
- Top 66 فہرستیں، flow کے راستے، ممالک اور نیٹ ورک، threat list کے matches،
flow records، encapsulation (GRE، IPIP، VXLAN، GENEVE، MPLS)۔
- web UI اور terminal UI میں 13 زبانیں۔
-
+
تمام اسکرین شاٹس `traffic66 demo` سے لیے گئے ہیں، ایک simulated کمپنی نیٹ ورک جسے آپ خود چلا سکتے ہیں ([ڈیمو چلا کر دیکھیں](#1-try-the-demo) دیکھیں)۔
@@ -89,10 +92,13 @@ cd traffic66-windows-amd64
http://127.0.0.1:8066 کھولیں اور `admin` / `try66` سے سائن اِن کریں۔ ڈیمو ایک
چھوٹی کمپنی کا نیٹ ورک بناتا ہے جس میں ایک دن کی ہسٹری اور چار simulated
-ڈیوائسز سے live ٹریفک ہوتی ہے، اور ڈھونڈنے کے لیے دو incidents بھی ہیں:
-**جائزہ** سے شروع کریں، **سرفہرست کلائنٹ** میں کسی host پر کلک کریں،
-**تفصیل دیکھیں** چنیں، اور وہاں سے کلک کرتے ہوئے آگے بڑھیں۔ Ctrl+C سے بند کریں۔ ڈیمو کا ڈیٹا پروگرام کے ساتھ `traffic66-demo`
-میں رہتا ہے؛ ڈیمو نئے سرے سے شروع کرنے کے لیے وہ فولڈر ڈیلیٹ کر دیں۔
+ڈیوائسز سے live ٹریفک ہوتی ہے، اور اس میں ایک حملہ بھی ہے: **مشتبہ سرگرمی** اس
+کا ہر مرحلہ دکھاتا ہے (ایک scan، ایک port scan، پاس ورڈ کا اندازہ، lateral
+movement، ایک control server پر upload) اور public website پر ایک flood بھی۔
+فہرست کے کسی اندراج پر **تفصیل** پر کلک کریں، یا **جائزہ** سے شروع کریں،
+**سرفہرست کلائنٹ** میں کسی host پر کلک کریں، **تفصیل دیکھیں** چنیں، اور وہاں
+سے کلک کرتے ہوئے آگے بڑھیں۔ Ctrl+C سے بند کریں۔ ڈیمو کا ڈیٹا پروگرام کے ساتھ
+`traffic66-demo` میں رہتا ہے؛ ڈیمو نئے سرے سے شروع کرنے کے لیے وہ فولڈر ڈیلیٹ کر دیں۔
ڈیمو وہی ports استعمال کرتا ہے جو اصل installation کرتی ہے (8066، اور UDP
6343، 2055، 4739)۔ اصل installation کے ساتھ ساتھ چلانے کے لیے اسے دوسرے ports
@@ -656,7 +662,8 @@ application، ملک، ڈیوائس — پر کلک کیا جا سکتا ہے:
| صفحہ | کس سوال کا جواب دیتا ہے |
|---|---|
-| جائزہ | ابھی کتنی ٹریفک ہے اور پچھلے ہفتے کے مقابلے میں کتنی، application کے حساب سے؛ سرفہرست clients اور services |
+| جائزہ | ابھی کتنی ٹریفک ہے اور پچھلے ہفتے کے مقابلے میں کتنی، application کے حساب سے؛ کھلی مشتبہ سرگرمیاں؛ سرفہرست clients اور services |
+| مشتبہ سرگرمی | کس چیز پر توجہ چاہیے: scans، پاس ورڈ کا اندازہ، lateral movement، غیر معمولی uploads، floods اور threat list والی ٹریفک ([مزید](#findings)) |
| ٹاپ N | سرفہرست 66 کی ایک table: ڈیفالٹ طور پر conversations (client، server، service، ملک)۔ ہر column heading سے sort ہوتا ہے؛ عددی columns (ٹریفک، پیکٹ، اوسط پیکٹ، flows) وقت کی حد کے پورے ٹریفک سے سرفہرست 66 دوبارہ چنتے ہیں، اس لیے سب سے چھوٹے اوسط پیکٹ سے scan اور flood پکڑے جاتے ہیں۔ **گروپ بندی بلحاظ** سے applications، نیٹ ورکس، segments، ڈیوائسز، encapsulation اور VLAN پر جائیں |
| ٹریفک کے راستے | کون سا segment کس ملک میں کس application سے بات کرتا ہے |
| مقامات اور نیٹ ورک | ملک کے حساب سے اور نیٹ ورک (AS) کے حساب سے ٹریفک |
@@ -680,9 +687,50 @@ time range اور filters) کا link کاپی کرتا ہے تاکہ آپ اسے
جگہ پر hover کریں تو پتا چلتا ہے کہ موجودہ رفتار پر رکھے گئے دنوں کی تفصیل کو کتنی
جگہ چاہیے (ایک دن کا data جمع ہونے کے بعد اندازہ لگایا جاتا ہے)۔
+
+
+### مشتبہ سرگرمی
+
+**مشتبہ سرگرمی** وہ سب دکھاتا ہے جو traffic66 نے flows میں پایا، سب سے سنگین
+پہلے۔ یہ ہر 5 منٹ پر پچھلے 10 منٹ جانچتا ہے؛ جو چیز ایک گھنٹے تک جاری رہے وہ
+ایک ہی اندراج ہے جو بڑھتا رہتا ہے، ہر جانچ پر نیا نہیں۔
+
+| سرگرمی | اس کا مطلب | شدت |
+|---|---|---|
+| اسکین | ایک address نے ایک ہی port پر بہت سے addresses کو چھوٹے probes بھیجے (TCP یا ping) | آپ کے نیٹ ورک کے اندر سے ہو تو زیادہ، انٹرنیٹ سے ہو تو کم |
+| پورٹ اسکین | ایک address نے ایک host کے بہت سے ports پر چھوٹے probes بھیجے | اندر سے زیادہ، انٹرنیٹ سے کم |
+| پاس ورڈ کا اندازہ | کسی login service (SSH، RDP، SMB، databases وغیرہ) سے بہت سے مختصر connections | اندر سے زیادہ، انٹرنیٹ سے کم |
+| لیٹرل موومنٹ | آپ کے نیٹ ورک کے اندر، file sharing یا remote administration sessions (SMB، RDP، SSH، WinRM، VNC) ایسے hosts سے جنہوں نے پہلے کبھی وہ service پیش نہیں کی | زیادہ |
+| غیر معمولی اپ لوڈ | ایک internal host نے جتنا وصول کیا اس سے کہیں زیادہ بھیجا (10 منٹ میں 100 MB، وصول شدہ کا تین گنا) ایسے address کو جس سے پہلے کبھی ڈیٹا کا تبادلہ نہیں ہوا تھا | زیادہ |
+| فلڈ | ایک address پر فی سیکنڈ 20,000 یا اس سے زیادہ چھوٹے packets، اس کی معمول کی رفتار کا دس گنا | درمیانہ |
+| خطرے کی فہرست | آپ کی کسی threat list کے address کے ساتھ ٹریفک | زیادہ جب آپ کے host نے اس سے connect کیا، کم جب فہرست والا address باہر سے دستک دے رہا تھا |
+
+ہر اندراج بتاتا ہے کہ کس نے کس کے ساتھ کیا کیا، کب اور کتنی دیر تک، اس کے
+پیچھے کے اعداد کے ساتھ، اور یہ کہ ڈیٹا کیسے sample کیا گیا تھا۔ **تفصیل** host
+کا صفحہ کھولتا ہے، جس میں اس کے بارے میں سرگرمیاں بھی درج ہوتی ہیں۔
+**نمٹا دیا** اندراج کو بند کرتا ہے؛ اگر وہی دوبارہ ہو تو نیا اندراج کھلتا ہے۔
+**مسئلہ نہیں** اسے ہمیشہ کے لیے بند کر دیتا ہے: وہ پھر کبھی رپورٹ نہیں ہوتا۔
+سائیڈ menu میں **مشتبہ سرگرمی** کے ساتھ والا سرخ نمبر پچھلے 24 گھنٹوں کے کھلے
+زیادہ اور درمیانہ شدت والے اندراج گنتا ہے۔
+
+لیٹرل موومنٹ اور غیر معمولی uploads کے لیے یہ جاننا ضروری ہے کہ معمول کیا ہے،
+اس لیے یہ ایک دن کی ہسٹری جمع ہونے کے بعد ہی رپورٹ ہوتے ہیں۔ پہلی بار شروع
+ہونے پر traffic66 پہلے سے موجود ہسٹری سے سیکھتا ہے۔
+
+Sampled data (sFlow، sampled NetFlow) کے ساتھ قواعد وہی گنتے ہیں جو samples
+دکھاتے ہیں اور کم تعداد مانگتے ہیں، لیکن پھر ہر ایک کو ایک مختصر probe جیسا
+دکھنا چاہیے، تاکہ مصروف عام hosts انہیں trigger نہ کریں۔ جو sampling چھپا دے
+وہ پکڑا نہیں جا سکتا: 1:4096 sampling کے پیچھے، چند درجن hosts کا scan اتنے کم
+packets بھیجتا ہے کہ نظر نہیں آتا۔ ڈیمو کا حملہ ایک ایسے switch سے گزرتا ہے جو
+1:4096 پر sample کرتا ہے، اور پورا پکڑا جاتا ہے؛ ڈیمو کی ایک دن کی عام ٹریفک
+سے کوئی سرگرمی نہیں نکلتی، سوائے website پر دستک دینے والے internet scanner
+کے۔
+
+
+

-
+

diff --git a/docs/README.zh.md b/docs/README.zh.md
index 36fd38b..fe1541c 100644
--- a/docs/README.zh.md
+++ b/docs/README.zh.md
@@ -7,10 +7,11 @@
- Windows、Linux、macOS 都只有一个可执行文件。无需安装数据库,无需运行时,可离线使用。
- 任意 UDP 端口均可接收 sFlow v5、NetFlow v5、NetFlow v9 和 IPFIX;也可选择在本机网卡或镜像端口上直接抓包。
- 用接口计数器(sFlow 计数器或 SNMP)校验自己的统计结果,对不上时会说明原因。
+- 从流数据中找出扫描、暴力破解、横向移动、异常上传、泛洪和威胁情报流量(采样数据同样适用),并列为待处理的发现。
- Top 66 排行、流向、国家与运营商网络、威胁情报命中、流记录、封装(GRE、IPIP、VXLAN、GENEVE、MPLS)。
- Web 界面和终端界面均支持 13 种语言。
-
+
所有截图均来自 `traffic66 demo`,这是一个你可以自己运行的模拟公司网络(见 [试用演示](#1-try-the-demo))。
@@ -75,7 +76,7 @@ cd traffic66-windows-amd64
.\traffic66.exe demo -password try66
```
-打开 http://127.0.0.1:8066,用 `admin` / `try66` 登录。演示模式会搭建一个小型公司网络,带一天的历史数据,外加四台模拟设备的实时流量,其中埋了两起异常事件等你去找:从 **概览** 开始,在 **Top 客户端** 中点击一台主机,选择 **查看详情**,然后继续一路点下去即可。按 Ctrl+C 停止。演示数据保存在程序旁边的 `traffic66-demo` 目录中,删除该目录即可从头开始。
+打开 http://127.0.0.1:8066,用 `admin` / `try66` 登录。演示模式会搭建一个小型公司网络,带一天的历史数据,外加四台模拟设备的实时流量,其中包括一次攻击:**发现** 列出了它的每一步(扫描、端口扫描、暴力破解、横向移动、向控制服务器上传数据),以及针对公司公开网站的一次泛洪。在某条发现上点击 **详情**,或者从 **概览** 开始,在 **Top 客户端** 中点击一台主机,选择 **查看详情**,然后继续一路点下去即可。按 Ctrl+C 停止。演示数据保存在程序旁边的 `traffic66-demo` 目录中,删除该目录即可从头开始。
演示模式使用与正式安装相同的端口(8066,以及 UDP 6343、2055、4739)。如果要和正式实例同时运行,请换用其他端口:`traffic66 demo -password try66 -addr :8067 -listen ""`。
@@ -546,7 +547,8 @@ curl -L https://www.spamhaus.org/drop/drop.txt -o /threats/spamh
| 页面 | 回答什么问题 |
|---|---|
-| 概览 | 当前流量多大、与上周相比如何,按应用拆分;主要客户端和服务 |
+| 概览 | 当前流量多大、与上周相比如何,按应用拆分;未处理的发现;主要客户端和服务 |
+| 发现 | 哪些需要处理:扫描、暴力破解、横向移动、异常上传、泛洪和威胁情报流量([详见](#findings)) |
| Top-N | 一张前 66 名的表:默认是会话(客户端、服务器、服务、国家)。每个列标题都能排序;数字列(流量、包数、平均包长、流数)按时间范围内的全部流量重新取前 66 名,按平均包长从小到大排能找出扫描和泛洪。**分组方式** 可切换到应用、网络、网段、设备、封装和 VLAN |
| 流向 | 哪个网段在访问哪个国家的哪个应用 |
| 地理与运营商 | 按国家和按网络(AS)统计的流量 |
@@ -561,9 +563,33 @@ curl -L https://www.spamhaus.org/drop/drop.txt -o /threats/spamh
侧边菜单显示数据占用了多少磁盘、还剩多少空间;鼠标悬停在剩余空间上,可以看到按当前速率保留各天明细所需的空间(有一天的数据后开始估算)。
+
+
+### 发现
+
+**发现** 列出 traffic66 在流数据中发现的问题,最严重的在前。它每 5 分钟检查一次最近 10 分钟的数据;持续一小时的行为是一条不断增长的发现,而不是每次检查都新开一条。
+
+| 发现 | 含义 | 严重程度 |
+|---|---|---|
+| 扫描 | 一个地址向许多地址的同一端口发送小探测包(TCP 或 ping) | 来自内网为高,来自互联网为低 |
+| 端口扫描 | 一个地址向一台主机的许多端口发送小探测包 | 来自内网为高,来自互联网为低 |
+| 暴力破解 | 对登录服务(SSH、RDP、SMB、数据库等)的大量短连接 | 来自内网为高,来自互联网为低 |
+| 横向移动 | 在内网中,向此前从未提供过该服务的主机发起文件共享或远程管理会话(SMB、RDP、SSH、WinRM、VNC) | 高 |
+| 异常上传 | 一台内部主机向此前从未与之交换过数据的地址发送的数据远多于接收的数据(10 分钟内 100 MB,且为接收量的三倍) | 高 |
+| 泛洪 | 每秒 20,000 个或更多小包发往同一地址,达到其平时速率的十倍 | 中 |
+| 威胁情报 | 与你的某个威胁情报列表中的地址有通信 | 你的主机主动连接它时为高,列表中的地址从外部试探时为低 |
+
+每条发现都说明谁在什么时间、持续多久对谁做了什么,并附上背后的数字以及数据的采样方式。**详情** 打开该主机的页面,那里也会列出与它相关的发现。**已处理** 关闭一条发现;如果再次发生,会新开一条。**不是问题** 则永久关闭它:以后不会再报告。侧边菜单中 **发现** 旁边的红色数字是最近 24 小时内未处理的高、中级发现的数量。
+
+横向移动和异常上传需要知道什么是正常的,因此要等有一天的历史数据后才会报告。首次启动时,traffic66 会从已有的历史数据中学习。
+
+对于采样数据(sFlow、采样的 NetFlow),规则按样本中看到的情况计数,要求的数量更少,但每个样本都必须看起来像一次短促的探测,因此繁忙的正常主机不会触发它们。被采样掩盖的东西无法发现:在 1:4096 采样下,扫描几十台主机发出的包太少,看不到。演示中的攻击经过一台以 1:4096 采样的交换机,仍被完整发现;演示中一天的正常流量除了敲打网站的互联网扫描器之外,不会产生任何发现。
+
+
+

-
+

diff --git a/docs/images/detail.png b/docs/images/detail.png
index b027778..b5a4950 100644
Binary files a/docs/images/detail.png and b/docs/images/detail.png differ
diff --git a/docs/images/findings.png b/docs/images/findings.png
new file mode 100644
index 0000000..d6538cf
Binary files /dev/null and b/docs/images/findings.png differ
diff --git a/docs/images/overview-zh.png b/docs/images/overview-zh.png
index 3f7bc6a..dcc6689 100644
Binary files a/docs/images/overview-zh.png and b/docs/images/overview-zh.png differ
diff --git a/docs/images/overview.png b/docs/images/overview.png
index 776dbe7..7273cf4 100644
Binary files a/docs/images/overview.png and b/docs/images/overview.png differ
diff --git a/internal/api/findings.go b/internal/api/findings.go
new file mode 100644
index 0000000..89fcda1
--- /dev/null
+++ b/internal/api/findings.go
@@ -0,0 +1,64 @@
+package api
+
+import (
+ "encoding/json"
+ "errors"
+ "net/http"
+
+ "github.com/githubflyideas/traffic66/internal/store"
+)
+
+// findings lists what the detection rules found in the time range, with
+// counts of open findings by severity and the detector's state.
+func (s *Server) findings(w http.ResponseWriter, r *http.Request) {
+ q, err := parseQuery(r)
+ if err != nil {
+ fail(w, err)
+ return
+ }
+ v := r.URL.Query()
+ status := v.Get("status")
+ if status == "" {
+ status = store.FindingOpen
+ }
+ fs, err := s.Store.Findings(store.FindingQuery{From: q.From, To: q.To, Status: status, IP: v.Get("ip"), Limit: limitParam(r, 500)})
+ if err != nil {
+ fail(w, err)
+ return
+ }
+ open, err := s.Store.Findings(store.FindingQuery{From: q.From, To: q.To, Status: store.FindingOpen, IP: v.Get("ip"), Limit: 1000})
+ if err != nil {
+ fail(w, err)
+ return
+ }
+ counts := map[int]int{1: 0, 2: 0, 3: 0}
+ for _, f := range open {
+ counts[f.Sev]++
+ }
+ out := map[string]any{"findings": fs, "open": map[string]int{"high": counts[3], "medium": counts[2], "low": counts[1]}}
+ if s.Det != nil {
+ out["detector"] = s.Det.Status()
+ }
+ writeJSON(w, http.StatusOK, out)
+}
+
+// setFindings marks findings as dealt with, as not a problem, or open again.
+func (s *Server) setFindings(w http.ResponseWriter, r *http.Request) {
+ var in struct {
+ IDs []int64 `json:"ids"`
+ Status string `json:"status"`
+ }
+ if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10)).Decode(&in); err != nil {
+ fail(w, err)
+ return
+ }
+ if len(in.IDs) == 0 {
+ fail(w, errors.New("no findings given"))
+ return
+ }
+ if err := s.Store.SetFindingStatus(in.IDs, in.Status); err != nil {
+ fail(w, err)
+ return
+ }
+ writeJSON(w, http.StatusOK, map[string]any{"ok": true})
+}
diff --git a/internal/api/findings_test.go b/internal/api/findings_test.go
new file mode 100644
index 0000000..a506268
--- /dev/null
+++ b/internal/api/findings_test.go
@@ -0,0 +1,79 @@
+package api
+
+import (
+ "encoding/json"
+ "net/http/httptest"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/githubflyideas/traffic66/internal/store"
+)
+
+func TestFindings(t *testing.T) {
+ st, err := store.Open(store.Options{Dir: t.TempDir()})
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer st.Close()
+ s := &Server{Store: st}
+ now := time.Now().UTC().Truncate(time.Minute)
+ scan := store.Finding{Kind: "scan", Sev: 3, Src: "10.1.1.1", Port: "445/tcp", First: now.Add(-20 * time.Minute), Last: now.Add(-10 * time.Minute),
+ Ev: map[string]any{"targets": 40.0, "examples": []string{"10.1.2.3"}}}
+ id, created, err := st.SaveFinding(scan, 30*time.Minute)
+ if err != nil || !created {
+ t.Fatal(created, err)
+ }
+ // the same scan ten minutes later extends the finding
+ scan.First, scan.Last, scan.Ev = now.Add(-10*time.Minute), now, map[string]any{"targets": 90.0, "examples": []string{"10.1.2.9"}}
+ if id2, created, err := st.SaveFinding(scan, 30*time.Minute); err != nil || created || id2 != id {
+ t.Fatal(id2, created, err)
+ }
+ st.SaveFinding(store.Finding{Kind: "threat", Sev: 1, Src: "192.0.2.66", Dst: "203.0.113.5", Port: "scanner", First: now.Add(-time.Hour), Last: now}, 30*time.Minute)
+
+ get := func(q string) map[string]any {
+ w := httptest.NewRecorder()
+ s.findings(w, httptest.NewRequest("GET", "/api/findings?range=24h"+q, nil))
+ if w.Code != 200 {
+ t.Fatalf("%s: %d %s", q, w.Code, w.Body)
+ }
+ var out map[string]any
+ json.Unmarshal(w.Body.Bytes(), &out)
+ return out
+ }
+ out := get("")
+ fs := out["findings"].([]any)
+ if len(fs) != 2 || out["open"].(map[string]any)["high"].(float64) != 1 {
+ t.Fatalf("findings %v", out)
+ }
+ first := fs[0].(map[string]any)
+ ev := first["ev"].(map[string]any)
+ if first["kind"] != "scan" || first["hits"].(float64) != 2 || ev["targets"].(float64) != 90 || len(ev["examples"].([]any)) != 2 {
+ t.Fatalf("merged finding %v", first)
+ }
+ if fs := get("&ip=10.1.2.9")["findings"].([]any); len(fs) != 1 {
+ t.Fatalf("by target: %v", fs)
+ }
+
+ post := func(body string) int {
+ w := httptest.NewRecorder()
+ s.setFindings(w, httptest.NewRequest("POST", "/api/findings", strings.NewReader(body)))
+ return w.Code
+ }
+ if c := post(`{"ids":[` + jsonInt(id) + `],"status":"false"}`); c != 200 {
+ t.Fatal(c)
+ }
+ if c := post(`{"ids":[1],"status":"bogus"}`); c != 400 {
+ t.Fatal("bad status accepted", c)
+ }
+ if fs := get("")["findings"].([]any); len(fs) != 1 {
+ t.Fatalf("open after marking false: %v", fs)
+ }
+ // a finding marked as not a problem is not reported again
+ scan.First, scan.Last = now.Add(time.Hour), now.Add(70*time.Minute)
+ if _, created, _ := st.SaveFinding(scan, 30*time.Minute); created {
+ t.Fatal("suppressed finding reopened")
+ }
+}
+
+func jsonInt(n int64) string { b, _ := json.Marshal(n); return string(b) }
diff --git a/internal/api/handlers.go b/internal/api/handlers.go
index a9d5c2d..71d9b17 100644
--- a/internal/api/handlers.go
+++ b/internal/api/handlers.go
@@ -545,7 +545,7 @@ func (s *Server) status(w http.ResponseWriter, r *http.Request) {
"disk_bytes": u.DiskBytes, "disk_free": free, "disk_need": need, "retention_days": s.Store.RetentionDays(), "oldest": u.Oldest.UnixMilli(),
"hot_rows": u.HotRows, "segments": u.Segments, "segment_rows": u.SegmentRows,
"dns_upstream": s.DNS.Upstream(), "dns_queries": s.DNS.Queries, "asn_ranges": s.ASN.Size(), "threat_lists": s.Thr.Lists(),
- "source_warnings": warn, "hosts": names, "skewed": s.Col.Skewed.Load(), "go": runtime.Version(),
+ "source_warnings": warn, "findings_open": s.Store.OpenFindings(2, time.Now().Add(-24*time.Hour)), "hosts": names, "skewed": s.Col.Skewed.Load(), "go": runtime.Version(),
})
}
diff --git a/internal/api/server.go b/internal/api/server.go
index 1d72993..896c060 100644
--- a/internal/api/server.go
+++ b/internal/api/server.go
@@ -17,6 +17,7 @@ import (
"time"
"github.com/githubflyideas/traffic66/internal/collector"
+ "github.com/githubflyideas/traffic66/internal/detect"
"github.com/githubflyideas/traffic66/internal/dnsres"
"github.com/githubflyideas/traffic66/internal/enrich"
"github.com/githubflyideas/traffic66/internal/pipeline"
@@ -32,6 +33,7 @@ type Server struct {
ASN *enrich.ASNDB
Thr *enrich.Threats
DNS *dnsres.Resolver
+ Det *detect.Detector
Static fs.FS
Version string
Demo bool
@@ -90,6 +92,8 @@ func (s *Server) Handler() http.Handler {
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/geo", s.getGeo)
api("POST /api/geo", s.putGeo)
static := http.FileServer(http.FS(s.Static))
diff --git a/internal/detect/detect.go b/internal/detect/detect.go
new file mode 100644
index 0000000..521f9aa
--- /dev/null
+++ b/internal/detect/detect.go
@@ -0,0 +1,652 @@
+// Package detect looks through recent flows for things worth a person's
+// attention: scans, password guessing, lateral movement inside the network,
+// large uploads to new destinations, floods and traffic with addresses on a
+// threat list. Each run looks at a short window of stored flows and records
+// what it finds as findings (see store.Finding).
+//
+// The rules work on sampled data (sFlow, sampled NetFlow): counts of
+// distinct targets, ports or connections are what was observed, and a rule
+// that needs many observations asks for fewer when the data is sampled but
+// then also requires each one to look like a single short probe. What
+// sampling hides (a scan of a few hosts behind 1:4096 sampling) is not
+// reported; the finding says how the data was sampled.
+package detect
+
+import (
+ "context"
+ "database/sql"
+ "fmt"
+ "log"
+ "net/netip"
+ "sort"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/githubflyideas/traffic66/internal/enrich"
+ "github.com/githubflyideas/traffic66/internal/store"
+)
+
+// Config holds the thresholds. Zero values take the defaults.
+type Config struct {
+ Window time.Duration // flows looked at per run (10 min)
+ Every time.Duration // time between runs (5 min)
+ Lag time.Duration // how long to wait for late flow reports (3 min)
+ Gap time.Duration // a sighting within this of the last one extends a finding (30 min)
+ Learn time.Duration // history needed before "new" can be judged (1 day)
+
+ ScanTargets, ScanTargetsSampled int // distinct hosts on one port (50 / 5)
+ PortScanPorts, PortScanSampled int // distinct ports on one host (30 / 5)
+ ProbeMaxAvg float64 // average frame size of probes, bytes (90)
+ BruteConns, BruteConnsSampled int // connections to one login service (30 / 5)
+ BruteMaxAvg float64 // average frame size of login attempts (300)
+ ExfilBytes float64 // upload to one new destination per window (100 MB)
+ ExfilRatio float64 // upload at least this times the download (3)
+ FloodPPS float64 // packets per second to one address (20,000)
+ FloodMaxAvg float64 // average frame size of a flood (120)
+ FloodRatio float64 // times the address's usual packet rate (10)
+ NewServiceClients int // a new service used by this many hosts is a new server, not lateral movement (5)
+}
+
+func (c *Config) defaults() {
+ def := func(p *time.Duration, v time.Duration) {
+ if *p <= 0 {
+ *p = v
+ }
+ }
+ def(&c.Window, 10*time.Minute)
+ def(&c.Every, 5*time.Minute)
+ def(&c.Lag, 3*time.Minute)
+ def(&c.Gap, 30*time.Minute)
+ def(&c.Learn, 24*time.Hour)
+ defi := func(p *int, v int) {
+ if *p <= 0 {
+ *p = v
+ }
+ }
+ defi(&c.ScanTargets, 50)
+ defi(&c.ScanTargetsSampled, 5)
+ defi(&c.PortScanPorts, 30)
+ defi(&c.PortScanSampled, 5)
+ defi(&c.BruteConns, 30)
+ defi(&c.BruteConnsSampled, 5)
+ defi(&c.NewServiceClients, 5)
+ deff := func(p *float64, v float64) {
+ if *p <= 0 {
+ *p = v
+ }
+ }
+ deff(&c.ProbeMaxAvg, 90)
+ deff(&c.BruteMaxAvg, 300)
+ deff(&c.ExfilBytes, 100e6)
+ deff(&c.ExfilRatio, 3)
+ deff(&c.FloodPPS, 20000)
+ deff(&c.FloodMaxAvg, 120)
+ deff(&c.FloodRatio, 10)
+}
+
+// Kinds of findings.
+const (
+ KindScan = "scan" // one source, many hosts, one port
+ KindPortScan = "portscan" // one source, one host, many ports
+ KindBrute = "brute" // many short connections to a login service
+ KindLateral = "lateral" // inside the network, to admin services never offered there before
+ KindExfil = "exfil" // large upload to a destination never used before
+ KindFlood = "flood" // packet flood to one address
+ KindThreat = "threat" // traffic with an address on a threat list
+)
+
+// Ports of login services (password guessing).
+var loginPorts = []int{21, 22, 23, 445, 1433, 1521, 3306, 3389, 5432, 5900, 5985, 5986, 6379, 27017}
+
+// sessionMinAvg is the average frame size, in bytes, above which traffic to
+// an admin service is a session that moved data: probes and failed logins
+// are smaller, and only real sessions teach or trigger the lateral rule.
+const sessionMinAvg = 250
+
+// Ports of remote administration and file sharing (lateral movement).
+var adminPorts = []int{22, 135, 139, 445, 3389, 5900, 5985, 5986}
+
+// Detector runs the rules against a store.
+type Detector struct {
+ St *store.Store
+ Inv *enrich.Inventory
+ Cfg Config
+
+ mu sync.Mutex
+ lastRun time.Time
+ lastErr string
+}
+
+// New makes a detector with cfg's thresholds (defaults where zero).
+func New(st *store.Store, inv *enrich.Inventory, cfg Config) *Detector {
+ cfg.defaults()
+ return &Detector{St: st, Inv: inv, Cfg: cfg}
+}
+
+// Status describes the detector for the UI.
+type Status struct {
+ LastRun time.Time `json:"last_run"`
+ LearningUntil time.Time `json:"learning_until"` // "new" rules wait until then; zero when done
+ Error string `json:"error,omitempty"`
+}
+
+func (d *Detector) Status() Status {
+ d.mu.Lock()
+ st := Status{LastRun: d.lastRun, Error: d.lastErr}
+ d.mu.Unlock()
+ if st.LastRun.IsZero() { // not run since start: the last window checked before
+ if t, err := time.Parse(time.RFC3339, d.St.Meta("detect_last")); err == nil {
+ st.LastRun = t
+ }
+ }
+ if o := d.oldest(); !o.IsZero() && time.Since(o) < d.Cfg.Learn {
+ st.LearningUntil = o.Add(d.Cfg.Learn)
+ }
+ return st
+}
+
+// Loop runs the rules every Cfg.Every until ctx ends, first catching up on
+// windows missed while traffic66 was not running (at most a day).
+func (d *Detector) Loop(ctx context.Context) {
+ d.CatchUp(time.Now().Add(-d.Cfg.Lag))
+ t := time.NewTicker(d.Cfg.Every)
+ defer t.Stop()
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ case now := <-t.C:
+ d.CatchUp(now.Add(-d.Cfg.Lag))
+ }
+ }
+}
+
+// CatchUp runs every window from the last run (or a day ago) up to end.
+func (d *Detector) CatchUp(end time.Time) {
+ end = end.UTC().Truncate(time.Minute)
+ from := end.Add(-24 * time.Hour)
+ if v := d.St.Meta("detect_last"); v != "" {
+ if t, err := time.Parse(time.RFC3339, v); err == nil && t.After(from) {
+ from = t
+ }
+ }
+ for t := from.Add(d.Cfg.Every); !t.After(end); t = t.Add(d.Cfg.Every) {
+ if _, err := d.Run(t); err != nil {
+ log.Printf("detect: %v", err)
+ }
+ }
+}
+
+// Run applies every rule to the window ending at end and records findings.
+// It returns how many new findings were opened.
+func (d *Detector) Run(end time.Time) (int, error) {
+ end = end.UTC().Truncate(time.Minute)
+ from := end.Add(-d.Cfg.Window)
+ w := &window{d: d, from: from, to: end, src: d.St.Source(from, end)}
+ if err := d.bootstrap(from); err != nil {
+ return 0, d.fail(err)
+ }
+ oldest := d.oldest()
+ learned := !oldest.IsZero() && from.Sub(oldest) >= d.Cfg.Learn
+ var all []store.Finding
+ for _, rule := range []func() ([]store.Finding, error){w.scans, w.portScans, w.brute, w.flood, w.threats} {
+ fs, err := rule()
+ if err != nil {
+ return 0, d.fail(err)
+ }
+ all = append(all, fs...)
+ }
+ if learned {
+ for _, rule := range []func() ([]store.Finding, error){w.lateral, w.exfil} {
+ fs, err := rule()
+ if err != nil {
+ return 0, d.fail(err)
+ }
+ all = append(all, fs...)
+ }
+ }
+ created := 0
+ for _, f := range all {
+ _, c, err := d.St.SaveFinding(f, d.Cfg.Gap)
+ if err != nil {
+ return created, d.fail(err)
+ }
+ if c {
+ created++
+ }
+ }
+ if err := w.learn(); err != nil {
+ return created, d.fail(err)
+ }
+ d.St.SetMeta("detect_last", end.Format(time.RFC3339))
+ d.mu.Lock()
+ d.lastRun, d.lastErr = time.Now(), ""
+ d.mu.Unlock()
+ return created, nil
+}
+
+func (d *Detector) fail(err error) error {
+ d.mu.Lock()
+ d.lastErr = err.Error()
+ d.mu.Unlock()
+ return err
+}
+
+// oldest is the time of the oldest stored flow.
+func (d *Detector) oldest() time.Time {
+ var a, b sql.NullTime
+ d.St.DB.QueryRow(`SELECT min(min_ts) FROM segments`).Scan(&a)
+ d.St.DB.QueryRow(`SELECT min(ts) FROM hot`).Scan(&b)
+ switch {
+ case a.Valid && b.Valid:
+ if b.Time.Before(a.Time) {
+ return b.Time
+ }
+ return a.Time
+ case a.Valid:
+ return a.Time
+ }
+ return b.Time
+}
+
+// bootstrap fills the "seen before" table from the stored history the first
+// time the detector runs, so a new installation does not need a day to learn
+// what an upgraded one already has.
+func (d *Detector) bootstrap(before time.Time) error {
+ if d.St.Meta("detect_seen") != "" {
+ return nil
+ }
+ // the last two days with data: enough to know the usual services and
+ // destinations, and cheap enough at high flow rates (it reads every
+ // stored flow of them)
+ var lastT sql.NullTime
+ if o := d.oldest(); !o.IsZero() && o.Before(before) {
+ d.St.DB.QueryRow(fmt.Sprintf(`SELECT max(ts) FROM %s WHERE ts < ?`, d.St.Source(o, before)), before.UTC()).Scan(&lastT)
+ }
+ last := before
+ if lastT.Valid && lastT.Time.Before(before) {
+ last = lastT.Time.Add(time.Minute)
+ }
+ w := &window{d: d, from: last.Add(-48 * time.Hour), to: before}
+ w.src = d.St.Source(w.from, w.to)
+ if err := w.learn(); err != nil {
+ return err
+ }
+ return d.St.SetMeta("detect_seen", before.Format(time.RFC3339))
+}
+
+// window is one run over [from, to).
+type window struct {
+ d *Detector
+ from, to time.Time
+ src string
+}
+
+func (w *window) base() string { return "ts >= ? AND ts < ? AND NOT dup" }
+func (w *window) args(extra ...any) []any {
+ return append([]any{w.from, w.to}, extra...)
+}
+
+func (w *window) internal(ip string) bool {
+ a, err := netip.ParseAddr(ip)
+ return err == nil && w.d.Inv.Internal(a)
+}
+
+func intList(v []int) string {
+ s := make([]string, len(v))
+ for i, x := range v {
+ s[i] = strconv.Itoa(x)
+ }
+ return strings.Join(s, ",")
+}
+
+func portLabel(port int, proto int) string {
+ switch proto {
+ case 1, 58:
+ return "icmp"
+ case 17:
+ return fmt.Sprintf("%d/udp", port)
+ }
+ return fmt.Sprintf("%d/tcp", port)
+}
+
+func split(s string) []string {
+ if s == "" {
+ return []string{}
+ }
+ return strings.Split(s, ",")
+}
+
+// scans: one source probing many hosts on one port (TCP SYN or ICMP echo).
+// Probes are small; on sampled data each observed probe must have gone to a
+// different host (at most three observed packets per host).
+func (w *window) scans() ([]store.Finding, error) {
+ c := w.d.Cfg
+ q := fmt.Sprintf(`SELECT src, dport, proto, count(DISTINCT dst) AS nd, sum(pkts), sum(wire), max(mult), min(ts), max(ts),
+ array_to_string(list_slice(list(DISTINCT dst), 1, 6), ',')
+ FROM %s WHERE %s AND proto IN (1, 6, 58)
+ GROUP BY src, dport, proto
+ HAVING sum(wire) <= sum(pkts) * CASE WHEN proto = 6 THEN %g ELSE 160 END
+ AND count(DISTINCT dst) >= CASE WHEN max(mult) > 1 THEN %d ELSE %d END
+ AND sum(pkts / greatest(mult, 1)) <= 3 * count(DISTINCT dst)
+ ORDER BY nd DESC LIMIT 100`, w.src, w.base(), c.ProbeMaxAvg, c.ScanTargetsSampled, c.ScanTargets)
+ rows, err := w.d.St.DB.Query(q, w.args()...)
+ if err != nil {
+ return nil, fmt.Errorf("scan rule: %w", err)
+ }
+ defer rows.Close()
+ var out []store.Finding
+ for rows.Next() {
+ var src, ex string
+ var port, proto, nd int
+ var pk, wire uint64
+ var mult float64
+ var first, last time.Time
+ if err := rows.Scan(&src, &port, &proto, &nd, &pk, &wire, &mult, &first, &last, &ex); err != nil {
+ return nil, err
+ }
+ sev := 1
+ if w.internal(src) {
+ sev = 3
+ }
+ out = append(out, store.Finding{Kind: KindScan, Sev: sev, Src: src, Port: portLabel(port, proto), First: first, Last: last.Add(time.Minute),
+ Ev: map[string]any{"targets": float64(nd), "pkts": float64(pk), "avg": float64(wire) / float64(max(pk, 1)), "sampling": mult, "examples": split(ex)}})
+ }
+ return out, rows.Err()
+}
+
+// portScans: one source probing many ports of one host. Only ports below
+// 32768 count, so the replies of a busy server to many client ports do not.
+func (w *window) portScans() ([]store.Finding, error) {
+ c := w.d.Cfg
+ q := fmt.Sprintf(`SELECT src, dst, count(DISTINCT dport) AS np, sum(pkts), sum(wire), max(mult), min(ts), max(ts),
+ array_to_string(list_slice(list(DISTINCT dport ORDER BY dport), 1, 8), ',')
+ FROM %s WHERE %s AND proto = 6 AND dport < 32768
+ GROUP BY src, dst
+ HAVING sum(wire) <= sum(pkts) * %g
+ AND count(DISTINCT dport) >= CASE WHEN max(mult) > 1 THEN %d ELSE %d END
+ AND sum(pkts / greatest(mult, 1)) <= 3 * count(DISTINCT dport)
+ ORDER BY np DESC LIMIT 100`, w.src, w.base(), c.ProbeMaxAvg, c.PortScanSampled, c.PortScanPorts)
+ rows, err := w.d.St.DB.Query(q, w.args()...)
+ if err != nil {
+ return nil, fmt.Errorf("port scan rule: %w", err)
+ }
+ defer rows.Close()
+ var out []store.Finding
+ for rows.Next() {
+ var src, dst, ex string
+ var np int
+ var pk, wire uint64
+ var mult float64
+ var first, last time.Time
+ if err := rows.Scan(&src, &dst, &np, &pk, &wire, &mult, &first, &last, &ex); err != nil {
+ return nil, err
+ }
+ sev := 1
+ if w.internal(src) {
+ sev = 3
+ }
+ out = append(out, store.Finding{Kind: KindPortScan, Sev: sev, Src: src, Dst: dst, First: first, Last: last.Add(time.Minute),
+ Ev: map[string]any{"ports": float64(np), "pkts": float64(pk), "avg": float64(wire) / float64(max(pk, 1)), "sampling": mult, "examples": split(ex)}})
+ }
+ return out, rows.Err()
+}
+
+// brute: many short connections from one source to one login service. Each
+// connection uses a new source port; on sampled data nearly every observed
+// packet must come from a different connection, which long sessions do not.
+func (w *window) brute() ([]store.Finding, error) {
+ c := w.d.Cfg
+ q := fmt.Sprintf(`SELECT src, dst, dport, count(DISTINCT sport) AS ns, sum(pkts), sum(wire), max(mult), min(ts), max(ts)
+ FROM %s WHERE %s AND proto = 6 AND dport IN (%s)
+ GROUP BY src, dst, dport
+ HAVING sum(wire) <= sum(pkts) * CASE WHEN max(mult) > 1 THEN least(%g, 200) ELSE %g END
+ AND count(DISTINCT sport) >= CASE WHEN max(mult) > 1 THEN %d ELSE %d END
+ AND (max(mult) <= 1 OR count(DISTINCT sport) >= 0.7 * sum(pkts / greatest(mult, 1)))
+ ORDER BY ns DESC LIMIT 100`, w.src, w.base(), intList(loginPorts), c.BruteMaxAvg, c.BruteMaxAvg, c.BruteConnsSampled, c.BruteConns)
+ rows, err := w.d.St.DB.Query(q, w.args()...)
+ if err != nil {
+ return nil, fmt.Errorf("password guessing rule: %w", err)
+ }
+ defer rows.Close()
+ var out []store.Finding
+ for rows.Next() {
+ var src, dst string
+ var port, ns int
+ var pk, wire uint64
+ var mult float64
+ var first, last time.Time
+ if err := rows.Scan(&src, &dst, &port, &ns, &pk, &wire, &mult, &first, &last); err != nil {
+ return nil, err
+ }
+ sev := 1
+ if w.internal(src) {
+ sev = 3
+ }
+ out = append(out, store.Finding{Kind: KindBrute, Sev: sev, Src: src, Dst: dst, Port: portLabel(port, 6), First: first, Last: last.Add(time.Minute),
+ Ev: map[string]any{"conns": float64(ns), "pkts": float64(pk), "avg": float64(wire) / float64(max(pk, 1)), "sampling": mult}})
+ }
+ return out, rows.Err()
+}
+
+// lateral: inside the network, connections to remote administration or file
+// sharing on hosts that never offered that service before (or first did in
+// the last day). A new service many hosts start using at once is a new
+// server, not an attacker, and is only learned.
+func (w *window) lateral() ([]store.Finding, error) {
+ c := w.d.Cfg
+ q := fmt.Sprintf(`SELECT h.client, h.server, h.svc_port, h.w, h.m, h.f, h.l FROM (
+ SELECT client, server, svc_port, sum(wire) AS w, max(mult) AS m, min(ts) AS f, max(ts) AS l
+ FROM %s WHERE %s AND dir = %d AND proto = 6 AND svc_port IN (%s)
+ GROUP BY client, server, svc_port HAVING sum(wire) > %d * sum(pkts)) h
+ LEFT JOIN seen s ON s.kind = 'svc' AND s.a = h.server AND s.b = '' AND s.port = h.svc_port
+ WHERE s.first_ts IS NULL OR s.first_ts >= ?`, w.src, w.base(), store.DirInternal, intList(adminPorts), sessionMinAvg)
+ rows, err := w.d.St.DB.Query(q, w.args(w.from.Add(-24*time.Hour))...)
+ if err != nil {
+ return nil, fmt.Errorf("lateral movement rule: %w", err)
+ }
+ defer rows.Close()
+ type hit struct {
+ client, server string
+ port int
+ wire uint64
+ mult float64
+ first, last time.Time
+ }
+ var hits []hit
+ users := map[string]map[string]bool{} // server/port -> clients
+ for rows.Next() {
+ var h hit
+ if err := rows.Scan(&h.client, &h.server, &h.port, &h.wire, &h.mult, &h.first, &h.last); err != nil {
+ return nil, err
+ }
+ hits = append(hits, h)
+ k := h.server + "/" + strconv.Itoa(h.port)
+ if users[k] == nil {
+ users[k] = map[string]bool{}
+ }
+ users[k][h.client] = true
+ }
+ if err := rows.Err(); err != nil {
+ return nil, err
+ }
+ type agg struct {
+ f store.Finding
+ targets []string
+ wire float64
+ }
+ by := map[string]*agg{}
+ for _, h := range hits {
+ if len(users[h.server+"/"+strconv.Itoa(h.port)]) >= c.NewServiceClients {
+ continue
+ }
+ k := h.client + "|" + strconv.Itoa(h.port)
+ a := by[k]
+ if a == nil {
+ a = &agg{f: store.Finding{Kind: KindLateral, Sev: 3, Src: h.client, Port: portLabel(h.port, 6), First: h.first, Last: h.last.Add(time.Minute)}}
+ by[k] = a
+ }
+ a.targets = append(a.targets, h.server)
+ a.wire += float64(h.wire)
+ if h.first.Before(a.f.First) {
+ a.f.First = h.first
+ }
+ if h.last.Add(time.Minute).After(a.f.Last) {
+ a.f.Last = h.last.Add(time.Minute)
+ }
+ a.f.Ev = map[string]any{"sampling": h.mult}
+ }
+ var out []store.Finding
+ for _, a := range by {
+ sort.Strings(a.targets)
+ a.f.Ev["targets"] = a.targets
+ a.f.Ev["hosts"] = float64(len(a.targets))
+ a.f.Ev["bytes"] = a.wire
+ out = append(out, a.f)
+ }
+ return out, nil
+}
+
+// exfil: an internal host uploading much more than it downloads to an
+// external address it had not exchanged data with before the last day.
+func (w *window) exfil() ([]store.Finding, error) {
+ c := w.d.Cfg
+ q := fmt.Sprintf(`SELECT h.client, h.server, h.up, h.down, h.m, h.f, h.l, h.cc, h.org FROM (
+ SELECT client, server, coalesce(sum(wire) FILTER (WHERE src = client), 0) AS up,
+ coalesce(sum(wire) FILTER (WHERE src = server), 0) AS down, max(mult) AS m, min(ts) AS f, max(ts) AS l,
+ any_value(peer_cc) AS cc, any_value(peer_org) AS org
+ FROM %s WHERE %s AND dir = %d
+ GROUP BY client, server
+ HAVING up >= %g AND up >= %g * down) h
+ LEFT JOIN seen s ON s.kind = 'up' AND s.a = h.client AND s.b = h.server AND s.port = 0
+ WHERE s.first_ts IS NULL OR s.first_ts >= ?`, w.src, w.base(), store.DirOutbound, c.ExfilBytes, c.ExfilRatio)
+ rows, err := w.d.St.DB.Query(q, w.args(w.from.Add(-24*time.Hour))...)
+ if err != nil {
+ return nil, fmt.Errorf("upload rule: %w", err)
+ }
+ defer rows.Close()
+ var out []store.Finding
+ for rows.Next() {
+ var cl, sv, cc, org string
+ var up, down uint64
+ var mult float64
+ var first, last time.Time
+ if err := rows.Scan(&cl, &sv, &up, &down, &mult, &first, &last, &cc, &org); err != nil {
+ return nil, err
+ }
+ out = append(out, store.Finding{Kind: KindExfil, Sev: 3, Src: cl, Dst: sv, First: first, Last: last.Add(time.Minute),
+ Ev: map[string]any{"up": float64(up), "down": float64(down), "sampling": mult, "cc": cc, "org": org}})
+ }
+ return out, rows.Err()
+}
+
+// flood: a high packet rate of small packets to one address, well above
+// what it usually receives.
+func (w *window) flood() ([]store.Finding, error) {
+ c := w.d.Cfg
+ secs := w.to.Sub(w.from).Seconds()
+ // only flows of small packets count, so a flood is seen even when the
+ // address also serves normal traffic
+ q := fmt.Sprintf(`SELECT dst, sum(pkts), sum(wire), count(DISTINCT src), max(mult), min(ts), max(ts), mode(dport), mode(proto)
+ FROM %s WHERE %s AND wire <= pkts * %g GROUP BY dst
+ HAVING sum(pkts) >= %g LIMIT 50`, w.src, w.base(), c.FloodMaxAvg, c.FloodPPS*secs)
+ rows, err := w.d.St.DB.Query(q, w.args()...)
+ if err != nil {
+ return nil, fmt.Errorf("flood rule: %w", err)
+ }
+ type cand struct {
+ dst string
+ pk, wire uint64
+ srcs int
+ mult float64
+ first, last time.Time
+ port, proto int
+ }
+ var cs []cand
+ for rows.Next() {
+ var x cand
+ if err := rows.Scan(&x.dst, &x.pk, &x.wire, &x.srcs, &x.mult, &x.first, &x.last, &x.port, &x.proto); err != nil {
+ rows.Close()
+ return nil, err
+ }
+ cs = append(cs, x)
+ }
+ rows.Close()
+ var out []store.Finding
+ for _, x := range cs {
+ // usual rate: packets the address exchanged per hour with data over
+ // the previous week, from the hourly summaries
+ var pk sql.NullFloat64
+ var hours int
+ w.d.St.DB.QueryRow(`SELECT sum(pkts), count(DISTINCT ts) FROM r_host WHERE ip = ? AND ts >= ? AND ts < ?`,
+ x.dst, w.from.Add(-7*24*time.Hour), w.from.Add(-time.Hour)).Scan(&pk, &hours)
+ usual := 0.0
+ if hours > 0 {
+ usual = pk.Float64 / (float64(hours) * 3600)
+ }
+ pps := float64(x.pk) / secs
+ if pps < c.FloodRatio*usual {
+ continue
+ }
+ out = append(out, store.Finding{Kind: KindFlood, Sev: 2, Dst: x.dst, Port: portLabel(x.port, x.proto), First: x.first, Last: x.last.Add(time.Minute),
+ Ev: map[string]any{"pps": pps, "usual": usual, "avg": float64(x.wire) / float64(max(x.pk, 1)), "sources": float64(x.srcs), "sampling": x.mult}})
+ }
+ return out, nil
+}
+
+// threats: traffic with an address on a threat list. An internal host
+// connecting to a listed address is serious; a listed address knocking on
+// the door from outside is everyday noise.
+func (w *window) threats() ([]store.Finding, error) {
+ q := fmt.Sprintf(`SELECT client, server, threat, any_value(dir), coalesce(sum(wire) FILTER (WHERE src = client), 0),
+ coalesce(sum(wire) FILTER (WHERE src = server), 0), sum(flows), max(mult), min(ts), max(ts)
+ FROM %s WHERE %s AND threat <> '' GROUP BY client, server, threat LIMIT 500`, w.src, w.base())
+ rows, err := w.d.St.DB.Query(q, w.args()...)
+ if err != nil {
+ return nil, fmt.Errorf("threat list rule: %w", err)
+ }
+ defer rows.Close()
+ var out []store.Finding
+ for rows.Next() {
+ var cl, sv, list string
+ var dir int
+ var up, down, flows uint64
+ var mult float64
+ var first, last time.Time
+ if err := rows.Scan(&cl, &sv, &list, &dir, &up, &down, &flows, &mult, &first, &last); err != nil {
+ return nil, err
+ }
+ sev := 2
+ switch dir {
+ case store.DirOutbound:
+ sev = 3
+ case store.DirInbound:
+ sev = 1
+ }
+ out = append(out, store.Finding{Kind: KindThreat, Sev: sev, Src: cl, Dst: sv, Port: list, First: first, Last: last.Add(time.Minute),
+ Ev: map[string]any{"list": list, "dir": float64(dir), "up": float64(up), "down": float64(down), "flows": float64(flows), "sampling": mult}})
+ }
+ return out, rows.Err()
+}
+
+// learn records which admin services and upload destinations the window
+// shows, for the "never before" rules, and forgets those unused for 30 days.
+func (w *window) learn() error {
+ db := w.d.St.DB
+ if _, err := db.Exec(fmt.Sprintf(`INSERT INTO seen SELECT 'svc', server, '', svc_port, min(ts), max(ts)
+ FROM %s WHERE %s AND dir = %d AND proto = 6 AND svc_port IN (%s) GROUP BY server, svc_port HAVING sum(wire) > %d * sum(pkts)
+ ON CONFLICT (kind, a, b, port) DO UPDATE SET last_ts = greatest(last_ts, excluded.last_ts)`,
+ w.src, w.base(), store.DirInternal, intList(adminPorts), sessionMinAvg), w.args()...); err != nil {
+ return fmt.Errorf("learning services: %w", err)
+ }
+ if _, err := db.Exec(fmt.Sprintf(`INSERT INTO seen SELECT 'up', client, server, 0, min(ts), max(ts)
+ FROM %s WHERE %s AND dir = %d GROUP BY client, server HAVING sum(wire) >= 1000000
+ ON CONFLICT (kind, a, b, port) DO UPDATE SET last_ts = greatest(last_ts, excluded.last_ts)`,
+ w.src, w.base(), store.DirOutbound), w.args()...); err != nil {
+ return fmt.Errorf("learning destinations: %w", err)
+ }
+ _, err := db.Exec(`DELETE FROM seen WHERE last_ts < ?`, w.to.Add(-30*24*time.Hour))
+ return err
+}
diff --git a/internal/detect/detect_test.go b/internal/detect/detect_test.go
new file mode 100644
index 0000000..da60827
--- /dev/null
+++ b/internal/detect/detect_test.go
@@ -0,0 +1,150 @@
+package detect
+
+import (
+ "slices"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/githubflyideas/traffic66/internal/enrich"
+ "github.com/githubflyideas/traffic66/internal/flow"
+ "github.com/githubflyideas/traffic66/internal/pipeline"
+ "github.com/githubflyideas/traffic66/internal/sim"
+ "github.com/githubflyideas/traffic66/internal/store"
+)
+
+// run simulates the demo network for the last hours (with or without the
+// attack), stores it and runs every detection window over it.
+func run(t *testing.T, attack bool, hours int) []store.Finding {
+ t.Helper()
+ st, err := store.Open(store.Options{Dir: t.TempDir(), RawDays: 30})
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() { st.Close() })
+ inv := enrich.NewInventory()
+ if err := inv.Parse(sim.Inventory()); err != nil {
+ t.Fatal(err)
+ }
+ asn, err := enrich.LoadTable(strings.NewReader(sim.ASNTable()))
+ if err != nil {
+ t.Fatal(err)
+ }
+ thr := enrich.NewThreats()
+ c2, scan := sim.ThreatList()
+ thr.Add("c2", strings.NewReader(c2))
+ thr.Add("scanner", strings.NewReader(scan))
+ p := pipeline.New(pipeline.Config{}, st, inv, asn, thr)
+ now := time.Now().UTC().Truncate(time.Minute)
+ // a normal day a week ago, as in the demo: what "before" looks like
+ week := 7 * 24 * time.Hour
+ old := sim.New(now, 6)
+ old.SessionScale = 40
+ i := 0
+ old.Backfill(now.Add(-24*time.Hour-week), now.Add(-week), false, func(r []flow.Record, _ []flow.IfCounters) {
+ p.Ingest(r)
+ if i++; i%60 == 0 {
+ p.FlushRows()
+ }
+ })
+ s := sim.New(now, 7)
+ s.Backfill(now.Add(-time.Duration(hours)*time.Hour), now.Add(-time.Minute), attack, func(r []flow.Record, _ []flow.IfCounters) {
+ p.Ingest(r)
+ if i++; i%30 == 0 {
+ p.FlushRows()
+ }
+ })
+ p.FlushRows()
+ p.FlushRollups()
+ if err := st.Seal(now); err != nil {
+ t.Fatal(err)
+ }
+ d := New(st, inv, Config{})
+ start := time.Now()
+ d.CatchUp(now)
+ t.Logf("detection over %d h: %v", hours, time.Since(start))
+ if e := d.Status().Error; e != "" {
+ t.Fatal(e)
+ }
+ fs, err := st.Findings(store.FindingQuery{From: now.Add(-48 * time.Hour), To: now.Add(time.Hour)})
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, f := range fs {
+ t.Logf("%-8s sev %d %s -> %s %s %v hits %d %v", f.Kind, f.Sev, f.Src, f.Dst, f.Port, f.Last.Sub(f.First), f.Hits, f.Ev)
+ }
+ return fs
+}
+
+func find(fs []store.Finding, kind, src, dst string) *store.Finding {
+ for i := range fs {
+ if fs[i].Kind == kind && (src == "" || fs[i].Src == src) && (dst == "" || fs[i].Dst == dst) {
+ return &fs[i]
+ }
+ }
+ return nil
+}
+
+// Every step of the demo attack is found, through 1:4096 sFlow sampling.
+func TestAttackFound(t *testing.T) {
+ if testing.Short() {
+ t.Skip("simulates hours of traffic")
+ }
+ fs := run(t, true, 7)
+ const pc, file, c2, web = "10.10.3.27", "10.20.0.15", "203.0.113.200", "203.0.113.5"
+ for _, want := range []struct{ kind, src, dst string }{
+ {KindScan, pc, ""}, {KindPortScan, pc, file}, {KindBrute, pc, file}, {KindLateral, pc, ""},
+ {KindExfil, pc, c2}, {KindThreat, pc, c2}, {KindFlood, "", web},
+ } {
+ f := find(fs, want.kind, want.src, want.dst)
+ if f == nil {
+ t.Errorf("no %s finding %s -> %s", want.kind, want.src, want.dst)
+ continue
+ }
+ if want.kind != KindFlood && f.Sev != 3 {
+ t.Errorf("%s: severity %d, want 3", want.kind, f.Sev)
+ }
+ }
+ if f := find(fs, KindLateral, pc, ""); f != nil {
+ got := f.Ev["targets"].([]any)
+ var ts []string
+ for _, x := range got {
+ ts = append(ts, x.(string))
+ }
+ for _, w := range []string{"10.10.1.23", "10.10.1.87", "10.10.2.14", "10.10.2.66", "10.10.4.9", "10.10.4.131"} {
+ if !slices.Contains(ts, w) {
+ t.Errorf("lateral targets %v lack %s", ts, w)
+ }
+ }
+ if len(ts) != 6 {
+ t.Errorf("lateral targets %v, want the 6 workstations", ts)
+ }
+ }
+ // one finding per step, not one per detection window
+ n := map[string]int{}
+ for _, f := range fs {
+ if f.Src == pc || f.Kind == KindFlood {
+ n[f.Kind+" "+f.Dst+" "+f.Port]++
+ }
+ }
+ for k, c := range n {
+ if c > 1 {
+ t.Errorf("%d %s findings, want 1", c, k)
+ }
+ }
+}
+
+// Normal traffic raises nothing; only the internet scanner knocking on the
+// public website is reported, as low severity.
+func TestNormalQuiet(t *testing.T) {
+ if testing.Short() {
+ t.Skip("simulates hours of traffic")
+ }
+ fs := run(t, false, 24)
+ for _, f := range fs {
+ if f.Src == "192.0.2.66" && f.Sev == 1 {
+ continue
+ }
+ t.Errorf("false finding: %s sev %d %s -> %s %s %v", f.Kind, f.Sev, f.Src, f.Dst, f.Port, f.Ev)
+ }
+}
diff --git a/internal/pipeline/pipeline_test.go b/internal/pipeline/pipeline_test.go
index d057252..c1dacfb 100644
--- a/internal/pipeline/pipeline_test.go
+++ b/internal/pipeline/pipeline_test.go
@@ -345,3 +345,26 @@ func TestDedupCap(t *testing.T) {
t.Fatalf("DedupFull %d, want 50", p.DedupFull.Load())
}
}
+
+// Internal hosts are counted on both sides of a conversation, once each.
+func TestTotalsInternalHosts(t *testing.T) {
+ p, st := setup(t)
+ now := time.Now().UTC().Truncate(time.Minute)
+ a, b, c := netip.MustParseAddr("10.1.1.1"), netip.MustParseAddr("10.1.1.2"), netip.MustParseAddr("10.1.1.3")
+ exp := netip.MustParseAddr("10.0.0.1")
+ var recs []flow.Record
+ for _, pr := range [][2]netip.Addr{{a, b}, {a, c}, {b, c}} { // a and b are clients and servers
+ recs = append(recs, flow.Record{Start: now.Add(-5 * time.Minute), End: now.Add(-5 * time.Minute), Src: pr[0], Dst: pr[1], SrcPort: 50000, DstPort: 445,
+ Proto: 6, Bytes: 1000, Packets: 2, Mult: 1, SamplingKnown: true, Exporter: exp, Source: flow.SrcIPFIX})
+ }
+ p.Ingest(recs)
+ p.FlushRows()
+ tot, err := st.Totals(store.Query{From: now.Add(-time.Hour), To: now})
+ if err != nil || tot.Hosts != 3 || tot.Peers != 0 {
+ t.Fatalf("totals %+v %v, want 3 hosts", tot, err)
+ }
+ tot, err = st.Totals(store.Query{From: now.Add(-time.Hour), To: now, Filters: []store.Filter{{Field: "ip", Value: "10.1.1.1"}}})
+ if err != nil || tot.Hosts != 3 {
+ t.Fatalf("one host's totals %+v %v, want itself and the 2 it talked to", tot, err)
+ }
+}
diff --git a/internal/sim/attack.go b/internal/sim/attack.go
new file mode 100644
index 0000000..fd810c0
--- /dev/null
+++ b/internal/sim/attack.go
@@ -0,0 +1,122 @@
+package sim
+
+import (
+ "math/rand/v2"
+ "net/netip"
+ "time"
+
+ "github.com/githubflyideas/traffic66/internal/flow"
+)
+
+// The demo's attack, a few hours before "now", as the sampled switch and the
+// unsampled firewall report it: the finance PC, already compromised, scans
+// the office and server networks for SMB, scans every port of the file
+// server, guesses RDP passwords on it, copies itself to six workstations over
+// SMB and then uploads data to its control server (the incident in model.go).
+// Later a botnet floods the public website with small UDP packets.
+//
+// Times are offsets before the incident start (now - 5 h).
+type attackPhase struct {
+ name string
+ start, end time.Duration // before incidentStart (start > end)
+}
+
+var phases = []attackPhase{
+ {"sweep", 90 * time.Minute, 80 * time.Minute},
+ {"portscan", 75 * time.Minute, 70 * time.Minute},
+ {"rdp", 65 * time.Minute, 50 * time.Minute},
+ {"smb", 40 * time.Minute, 20 * time.Minute},
+}
+
+// Workstations the finance PC copies itself to.
+var lateralTargets = []netip.Addr{
+ netip.MustParseAddr("10.10.1.23"), netip.MustParseAddr("10.10.1.87"), netip.MustParseAddr("10.10.2.14"),
+ netip.MustParseAddr("10.10.2.66"), netip.MustParseAddr("10.10.4.9"), netip.MustParseAddr("10.10.4.131"),
+}
+
+func (m *model) phase(name string, t time.Time) bool {
+ for _, p := range phases {
+ if p.name == name {
+ return !t.Before(m.incidentStart.Add(-p.start)) && t.Before(m.incidentStart.Add(-p.end))
+ }
+ }
+ return false
+}
+
+// flood: two hours before now, for ten minutes.
+func (m *model) flooding(t time.Time) bool {
+ s := m.incidentStart.Add(3 * time.Hour)
+ return !t.Before(s) && t.Before(s.Add(10*time.Minute))
+}
+
+// switchSample reports n packets of frame bytes from src to dst the way the
+// sampling switch does: each packet is sampled with probability 1/rate and
+// becomes one record. Interface counters see every packet.
+func (s *Sim) switchSample(t time.Time, dt time.Duration, n float64, frame float64, flags uint8,
+ pick func(r *rand.Rand) (src, dst netip.Addr, sport, dport uint16)) []flow.Record {
+ in, out := uint32(ifSwAccess), uint32(ifSwServers)
+ s.ctrIn[in] += n * frame
+ s.ctrOut[out] += n * frame
+ s.pktIn[in] += n
+ s.pktOut[out] += n
+ k := s.poisson(n / switchRate)
+ recs := make([]flow.Record, 0, k)
+ for i := 0; i < k; i++ {
+ src, dst, sp, dp := pick(s.rng)
+ at := t.Add(time.Duration(s.rng.Int64N(int64(dt))))
+ recs = append(recs, flow.Record{Start: at, End: at, Src: src, Dst: dst, SrcPort: sp, DstPort: dp, Proto: 6, TCPFlags: flags,
+ Bytes: uint64(frame), Packets: 1, L2: true, Sampling: switchRate, Mult: switchRate, SamplingKnown: true,
+ InIf: in, OutIf: out, Exporter: Switch, Source: flow.SrcSFlow, Observation: in, Direction: 255})
+ }
+ return recs
+}
+
+func (s *Sim) attackRecords(t time.Time, dt time.Duration) []flow.Record {
+ m := s.m
+ secs := dt.Seconds()
+ var recs []flow.Record
+ office := pool("10.10.0.0/16", 60000)
+ servers := pool("10.20.0.0/24", 240)
+ if m.phase("sweep", t) {
+ // SMB sweep: 2,000 SYN per second, one per address
+ recs = append(recs, s.switchSample(t, dt, 2000*secs, 64, 0x02, func(r *rand.Rand) (netip.Addr, netip.Addr, uint16, uint16) {
+ dst := office(r)
+ if r.IntN(10) == 0 {
+ dst = servers(r)
+ }
+ return financePC, dst, uint16(40000 + r.IntN(20000)), 445
+ })...)
+ }
+ if m.phase("portscan", t) {
+ // every TCP port of the file server, twice, in five minutes
+ recs = append(recs, s.switchSample(t, dt, 131072/300.0*secs, 64, 0x02, func(r *rand.Rand) (netip.Addr, netip.Addr, uint16, uint16) {
+ return financePC, fileServer, 51000, uint16(1 + r.IntN(65535))
+ })...)
+ }
+ if m.phase("rdp", t) {
+ // RDP password guessing: 10 attempts a second, ~15 small packets each
+ recs = append(recs, s.switchSample(t, dt, 150*secs, 170, 0x18, func(r *rand.Rand) (netip.Addr, netip.Addr, uint16, uint16) {
+ return financePC, fileServer, uint16(49152 + r.IntN(16000)), 3389
+ })...)
+ }
+ if m.phase("smb", t) {
+ // copying to six workstations over SMB, 30 Mbit/s in all
+ recs = append(recs, s.switchSample(t, dt, 30e6/8/1300*secs, 1318, 0x18, func(r *rand.Rand) (netip.Addr, netip.Addr, uint16, uint16) {
+ return financePC, lateralTargets[r.IntN(len(lateralTargets))], uint16(50000 + r.IntN(8)), 445
+ })...)
+ }
+ if m.flooding(t) {
+ // 60,000 packets a second of 46-byte UDP from 200 addresses, through
+ // the firewall (IPFIX, unsampled)
+ bots := pool("198.18.7.0/24", 200)
+ per := 60000 * secs / 200
+ for i := 0; i < 200; i++ {
+ src := bots(s.rng)
+ s.link(Firewall, ifFwOutside, ifFwDMZ, per, 64)
+ recs = append(recs, flow.Record{Start: t, End: t.Add(dt - time.Second), Src: src, Dst: dmzWeb, SrcPort: uint16(1024 + s.rng.IntN(60000)), DstPort: 80,
+ Proto: 17, Bytes: uint64(per * 46), Packets: uint64(per), Sampling: 1, Mult: 1, SamplingKnown: true,
+ InIf: ifFwOutside, OutIf: ifFwDMZ, Exporter: Firewall, Source: flow.SrcIPFIX})
+ }
+ }
+ return recs
+}
diff --git a/internal/sim/sim.go b/internal/sim/sim.go
index 4b4ac59..97e5562 100644
--- a/internal/sim/sim.go
+++ b/internal/sim/sim.go
@@ -296,6 +296,9 @@ func (s *Sim) Records(t time.Time, dt time.Duration, today bool) ([]flow.Record,
}
}
}
+ if today {
+ recs = append(recs, s.attackRecords(t, dt)...)
+ }
return recs, s.counters(t.Add(dt))
}
diff --git a/internal/store/findings.go b/internal/store/findings.go
new file mode 100644
index 0000000..069c645
--- /dev/null
+++ b/internal/store/findings.go
@@ -0,0 +1,226 @@
+package store
+
+import (
+ "database/sql"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "sort"
+ "strings"
+ "time"
+)
+
+// Finding is one thing the detection rules found: who (Src) did what (Kind)
+// to whom (Dst, Port), from First to Last, with the numbers that show it.
+type Finding struct {
+ ID int64 `json:"id"`
+ Kind string `json:"kind"`
+ Sev int `json:"sev"` // 1 low, 2 medium, 3 high
+ Src string `json:"src"`
+ Dst string `json:"dst"`
+ Port string `json:"port"`
+ First time.Time `json:"first"`
+ Last time.Time `json:"last"`
+ Hits int `json:"hits"` // detection runs that saw it
+ Ev map[string]any `json:"ev"`
+ Status string `json:"status"` // open, done, false
+}
+
+// Finding statuses.
+const (
+ FindingOpen = "open"
+ FindingDone = "done" // dealt with; a recurrence opens a new finding
+ FindingFalse = "false" // not a problem; never reported again
+)
+
+// SaveFinding records a finding. An open finding with the same kind, source,
+// target and port whose last sighting is less than gap before f.First is
+// extended instead of opening a new one, so a scan that goes on for an hour is
+// one finding. A finding marked as false is never reported again.
+func (s *Store) SaveFinding(f Finding, gap time.Duration) (id int64, created bool, err error) {
+ var n int
+ if err = s.DB.QueryRow(`SELECT count(*) FROM findings WHERE kind = ? AND src = ? AND dst = ? AND port = ? AND status = ?`,
+ f.Kind, f.Src, f.Dst, f.Port, FindingFalse).Scan(&n); err != nil {
+ return 0, false, err
+ }
+ if n > 0 {
+ return 0, false, nil
+ }
+ var (
+ first, last time.Time
+ hits, sev int
+ evs string
+ )
+ err = s.DB.QueryRow(`SELECT id, first_ts, last_ts, hits, sev, ev FROM findings
+ WHERE kind = ? AND src = ? AND dst = ? AND port = ? AND status = ? AND last_ts >= ?
+ ORDER BY last_ts DESC LIMIT 1`, f.Kind, f.Src, f.Dst, f.Port, FindingOpen, f.First.UTC().Add(-gap)).Scan(&id, &first, &last, &hits, &sev, &evs)
+ now := time.Now().UTC()
+ if errors.Is(err, sql.ErrNoRows) {
+ b, _ := json.Marshal(f.Ev)
+ err = s.DB.QueryRow(`INSERT INTO findings VALUES (nextval('finding_seq'), ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?) RETURNING id`,
+ f.Kind, f.Sev, f.Src, f.Dst, f.Port, f.First.UTC(), f.Last.UTC(), string(b), FindingOpen, now).Scan(&id)
+ return id, err == nil, err
+ }
+ if err != nil {
+ return 0, false, err
+ }
+ var old map[string]any
+ json.Unmarshal([]byte(evs), &old)
+ b, _ := json.Marshal(MergeEvidence(old, f.Ev))
+ if f.First.Before(first) {
+ first = f.First
+ }
+ if f.Last.After(last) {
+ last = f.Last
+ }
+ _, err = s.DB.Exec(`UPDATE findings SET first_ts = ?, last_ts = ?, hits = ?, sev = ?, ev = ?, updated = ? WHERE id = ?`,
+ first.UTC(), last.UTC(), hits+1, max(sev, f.Sev), string(b), now, id)
+ return id, false, err
+}
+
+// MergeEvidence combines two sightings of one finding: numbers keep the
+// larger value, lists are joined (at most 12 entries), anything else takes
+// the newer value.
+func MergeEvidence(old, nw map[string]any) map[string]any {
+ out := map[string]any{}
+ for k, v := range old {
+ out[k] = v
+ }
+ for k, v := range nw {
+ switch x := v.(type) {
+ case float64, int, int64, uint64:
+ a, b := num(out[k]), num(x)
+ if b > a {
+ out[k] = b
+ } else {
+ out[k] = a
+ }
+ case []string:
+ out[k] = union(out[k], x)
+ case []any:
+ var ss []string
+ for _, e := range x {
+ ss = append(ss, fmt.Sprint(e))
+ }
+ out[k] = union(out[k], ss)
+ default:
+ out[k] = v
+ }
+ }
+ return out
+}
+
+func num(v any) float64 {
+ switch x := v.(type) {
+ case float64:
+ return x
+ case int:
+ return float64(x)
+ case int64:
+ return float64(x)
+ case uint64:
+ return float64(x)
+ }
+ return 0
+}
+
+func union(old any, add []string) []string {
+ seen := map[string]bool{}
+ var out []string
+ put := func(s string) {
+ if !seen[s] && len(out) < 12 {
+ seen[s] = true
+ out = append(out, s)
+ }
+ }
+ switch x := old.(type) {
+ case []string:
+ for _, s := range x {
+ put(s)
+ }
+ case []any:
+ for _, s := range x {
+ put(fmt.Sprint(s))
+ }
+ }
+ for _, s := range add {
+ put(s)
+ }
+ sort.Strings(out)
+ return out
+}
+
+// FindingQuery selects findings seen in [From, To).
+type FindingQuery struct {
+ From, To time.Time
+ Status string // "" or "all": any; otherwise that status
+ IP string // involving this address
+ Limit int
+}
+
+// Findings returns findings, most severe and most recent first.
+func (s *Store) Findings(q FindingQuery) ([]Finding, error) {
+ conds := []string{"last_ts >= ?", "first_ts < ?"}
+ args := []any{q.From.UTC(), q.To.UTC()}
+ if q.Status != "" && q.Status != "all" {
+ conds = append(conds, "status = ?")
+ args = append(args, q.Status)
+ }
+ if q.IP != "" {
+ conds = append(conds, `(src = ? OR dst = ? OR contains(ev, ?))`)
+ args = append(args, q.IP, q.IP, `"`+q.IP+`"`)
+ }
+ if q.Limit <= 0 || q.Limit > 1000 {
+ q.Limit = 500
+ }
+ rows, err := s.DB.Query(fmt.Sprintf(`SELECT id, kind, sev, src, dst, port, first_ts, last_ts, hits, ev, status FROM findings
+ WHERE %s ORDER BY status = 'open' DESC, sev DESC, last_ts DESC LIMIT %d`, strings.Join(conds, " AND "), q.Limit), args...)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ out := []Finding{}
+ for rows.Next() {
+ var f Finding
+ var ev string
+ if err := rows.Scan(&f.ID, &f.Kind, &f.Sev, &f.Src, &f.Dst, &f.Port, &f.First, &f.Last, &f.Hits, &ev, &f.Status); err != nil {
+ return nil, err
+ }
+ json.Unmarshal([]byte(ev), &f.Ev)
+ out = append(out, f)
+ }
+ return out, rows.Err()
+}
+
+// SetFindingStatus marks findings as open, done or false.
+func (s *Store) SetFindingStatus(ids []int64, status string) error {
+ if status != FindingOpen && status != FindingDone && status != FindingFalse {
+ return fmt.Errorf("unknown status %q", status)
+ }
+ for _, id := range ids {
+ if _, err := s.DB.Exec(`UPDATE findings SET status = ?, updated = ? WHERE id = ?`, status, time.Now().UTC(), id); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// Meta reads a value from the meta table ("" when absent).
+func (s *Store) Meta(k string) string {
+ var v string
+ s.DB.QueryRow(`SELECT v FROM meta WHERE k = ?`, k).Scan(&v)
+ return v
+}
+
+// SetMeta stores a value in the meta table.
+func (s *Store) SetMeta(k, v string) error {
+ _, err := s.DB.Exec(`INSERT INTO meta VALUES (?, ?) ON CONFLICT (k) DO UPDATE SET v = excluded.v`, k, v)
+ return err
+}
+
+// OpenFindings counts open findings of at least severity sev seen since.
+func (s *Store) OpenFindings(sev int, since time.Time) int {
+ var n int
+ s.DB.QueryRow(`SELECT count(*) FROM findings WHERE status = ? AND sev >= ? AND last_ts >= ?`, FindingOpen, sev, since.UTC()).Scan(&n)
+ return n
+}
diff --git a/internal/store/query.go b/internal/store/query.go
index 432cb44..c845fd4 100644
--- a/internal/store/query.go
+++ b/internal/store/query.go
@@ -562,12 +562,19 @@ func (s *Store) Totals(q Query) (Totals, error) {
if err != nil {
return t, err
}
+ src := s.Source(q.From, q.To)
err = s.DB.QueryRow(fmt.Sprintf(`SELECT coalesce(sum(bytes),0), coalesce(sum(wire),0), coalesce(sum(pkts),0), coalesce(sum(flows),0),
- count(DISTINCT CASE WHEN cli_int THEN client END) + count(DISTINCT CASE WHEN srv_int AND NOT cli_int THEN server END),
count(DISTINCT CASE WHEN NOT srv_int THEN server WHEN NOT cli_int THEN client END),
count(DISTINCT nullif(peer_cc, ''))
- FROM %s WHERE %s`, s.Source(q.From, q.To), where), args...).
- Scan(&t.Bytes, &t.Wire, &t.Pkts, &t.Flows, &t.Hosts, &t.Peers, &t.Countries)
+ FROM %s WHERE %s`, src, where), args...).
+ Scan(&t.Bytes, &t.Wire, &t.Pkts, &t.Flows, &t.Peers, &t.Countries)
+ if err != nil {
+ return t, err
+ }
+ // internal addresses on either side, each counted once (a host is often
+ // a client in one conversation and a server in another)
+ err = s.DB.QueryRow(fmt.Sprintf(`SELECT count(*) FROM (SELECT client FROM %s WHERE %s AND cli_int UNION SELECT server FROM %s WHERE %s AND srv_int)`,
+ src, where, src, where), append(append([]any{}, args...), args...)...).Scan(&t.Hosts)
return t, err
}
diff --git a/internal/store/store.go b/internal/store/store.go
index af26c83..5d4bb75 100644
--- a/internal/store/store.go
+++ b/internal/store/store.go
@@ -79,6 +79,11 @@ var schema = []string{
`CREATE TABLE IF NOT EXISTS r_dim (ts TIMESTAMP, dim VARCHAR, val VARCHAR, bytes UBIGINT, wire UBIGINT, pkts UBIGINT, flows UBIGINT)`,
`CREATE TABLE IF NOT EXISTS ifc (ts TIMESTAMP, exporter VARCHAR, ifindex UINTEGER, speed UBIGINT, in_oct UBIGINT, out_oct UBIGINT, in_pkts UBIGINT, out_pkts UBIGINT)`,
`CREATE TABLE IF NOT EXISTS meta (k VARCHAR PRIMARY KEY, v VARCHAR)`,
+ // findings of the detection rules, and what the rules have seen before
+ `CREATE SEQUENCE IF NOT EXISTS finding_seq`,
+ `CREATE TABLE IF NOT EXISTS findings (id BIGINT PRIMARY KEY, kind VARCHAR, sev UTINYINT, src VARCHAR, dst VARCHAR, port VARCHAR,
+ first_ts TIMESTAMP, last_ts TIMESTAMP, hits INTEGER, ev VARCHAR, status VARCHAR, updated TIMESTAMP)`,
+ `CREATE TABLE IF NOT EXISTS seen (kind VARCHAR, a VARCHAR, b VARCHAR, port USMALLINT, first_ts TIMESTAMP, last_ts TIMESTAMP, PRIMARY KEY (kind, a, b, port))`,
}
// Options configure a Store.
diff --git a/internal/web/static/app.css b/internal/web/static/app.css
index 7e9dc5f..0ffe04e 100644
--- a/internal/web/static/app.css
+++ b/internal/web/static/app.css
@@ -113,9 +113,9 @@ td.rank{color:var(--ink-3);width:2.5em;text-align:end}
.nw{white-space:nowrap}
.sb{height:6px;border-radius:3px;background:var(--surface-2);min-width:60px;position:relative}
.sb span{position:absolute;inset-block:0;inset-inline-start:0;border-radius:3px;background:var(--accent)}
-.v{border:0;background:none;padding:0;cursor:pointer;color:var(--ink);text-align:start;border-bottom:1px dashed transparent;max-width:100%;overflow:hidden;text-overflow:ellipsis}
+.v{unicode-bidi:plaintext;border:0;background:none;padding:0;cursor:pointer;color:var(--ink);text-align:start;border-bottom:1px dashed transparent;max-width:100%;overflow:hidden;text-overflow:ellipsis}
.v:hover{border-bottom-color:var(--accent);color:var(--accent-ink)}
-.v .ip{color:var(--ink-3);font-size:12.5px;margin-inline-start:6px}
+.v .ip{unicode-bidi:isolate;color:var(--ink-3);font-size:12.5px;margin-inline-start:2px}
.tag{display:inline-block;font-size:11.5px;border-radius:4px;padding:0 5px;background:var(--surface-2);color:var(--ink-2);margin-inline-start:4px}
.movers{list-style:none;margin:0;padding:0}
@@ -189,3 +189,22 @@ th.sk .ar{font-size:10px;margin-inline-start:3px;opacity:.7}
.helpbox{background:var(--surface-2);border-radius:8px;padding:10px 12px;font-size:12.5px;color:var(--ink-2);margin:0 0 10px}
.helpbox pre{margin:6px 0 0;font-size:12px;white-space:pre-wrap}
.geo li{margin:3px 0}
+
+/* findings */
+.badge.crit{background:var(--crit);color:#fff}
+.sev{display:inline-block;min-width:52px;text-align:center;font-size:12px;font-weight:650;border-radius:4px;padding:1px 6px;white-space:nowrap}
+.sev.s3{background:var(--crit);color:#fff}.sev.s2{background:var(--warn);color:#1b2432}.sev.s1{background:var(--surface-2);color:var(--ink-2)}
+.fd td{vertical-align:top;padding-top:10px;padding-bottom:10px}
+.fd .what{font-size:14px;line-height:1.5}
+.fd .what .v{font-weight:600}
+.fd .ev{font-size:12.5px;color:var(--ink-3);margin-top:3px}
+.fd .kind{font-size:11.5px;color:var(--ink-3);text-transform:uppercase;letter-spacing:.03em;margin-bottom:2px}
+.fd .when{font-size:12.5px;color:var(--ink-2);white-space:nowrap}
+.fd .acts{white-space:nowrap;text-align:end}
+.fd .acts .btn{padding:2px 9px;font-size:12.5px;margin-inline-start:4px}
+.fd tr.closed .what,.fd tr.closed .sev{opacity:.55}
+.fd .stl{font-size:12px;color:var(--ink-3);margin-inline-start:6px}
+.fdhead{display:flex;gap:18px;align-items:center;flex-wrap:wrap}
+.fdhead .cnt{display:flex;align-items:center;gap:6px;font-size:13px}
+.fdhead .cnt b{font-size:20px;font-weight:650}
+.fdnote{font-size:12.5px;color:var(--ink-3);margin:10px 0 0}
diff --git a/internal/web/static/app.js b/internal/web/static/app.js
index 3597310..c94cb01 100644
--- a/internal/web/static/app.js
+++ b/internal/web/static/app.js
@@ -108,7 +108,7 @@ const asked = new Set();
function ipCell(ip, field = 'ip') {
if (!ip) return '—';
const n = names.get(ip);
- return ``;
+ return ``;
}
function V(field, val, label) {
return ``;
@@ -126,13 +126,13 @@ async function resolveNames() {
setTimeout(() => ips.forEach(ip => { if (!names.has(ip)) asked.delete(ip); }), 60000);
document.querySelectorAll('[data-ip]').forEach(el => {
const n = names.get(el.dataset.ip);
- if (n && !el.querySelector('.ip')) el.innerHTML = esc(n) + `${esc(el.dataset.ip)}`;
+ if (n && !el.querySelector('.ip')) el.innerHTML = esc(n) + ` ${esc(el.dataset.ip)}`;
});
} catch (e) {}
}
// ------------------------------------------------------------ state & API
-const VIEWS = ['overview', 'topn', 'sankey', 'geo', 'threats', 'records', 'ifaces', 'sources', 'detail'];
+const VIEWS = ['overview', 'findings', 'topn', 'sankey', 'geo', 'threats', 'records', '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() {
@@ -381,7 +381,7 @@ function errMsg(err) {
function errorBox(err) { return `
`}`)}
`;
tsChart($('#chDet'), {times: series.times, areas, lines});
+ bindFindings(el);
};
// ------------------------------------------------------------ shell
@@ -783,6 +889,7 @@ async function loadStatus() {
${t('self.free')} s.disk_free ? 'style="color:var(--crit)"' : ''} title="${esc(s.disk_need >= 0 ? t('self.need', {d: nf(s.retention_days), n: fmtBytes(s.disk_need)}) : t('self.need_later', {d: nf(s.retention_days)}))}">${fmtBytes(s.disk_free)}
${s.write_errors ? `!${nf(s.write_errors)}` : ''}`;
$('#srcBadge').hidden = !s.source_warnings; $('#srcBadge').textContent = s.source_warnings;
+ $('#findBadge').hidden = !s.findings_open; $('#findBadge').textContent = s.findings_open;
} catch (e) {}
}
@@ -827,7 +934,7 @@ async function init() {
setInterval(loadStatus, 10000);
setInterval(() => {
if (document.hidden || pop.style.display === 'block' || document.activeElement?.tagName === 'TEXTAREA' || document.activeElement?.id === 'q') return;
- if (state.v === 'sources' || state.v === 'records' || state.v === 'overview' || state.r === '15m' || state.r === '1h') render();
+ if (state.v === 'sources' || state.v === 'records' || state.v === 'overview' || state.v === 'findings' || state.r === '15m' || state.r === '1h') render();
}, 30000);
}
init();
diff --git a/internal/web/static/i18n/ar.json b/internal/web/static/i18n/ar.json
index 06d1ba4..bc48dbb 100644
--- a/internal/web/static/i18n/ar.json
+++ b/internal/web/static/i18n/ar.json
@@ -258,5 +258,54 @@
"src.ex_host": "حاسوب المالية",
"src.ex_net": "شبكة المكتب",
"src.ex_device": "الموجّه الرئيسي",
-"src.ex_iface": "وصلة مزوّد الخدمة"
+"src.ex_iface": "وصلة مزوّد الخدمة",
+"nav.findings": "الاكتشافات",
+"sev.high": "عالية",
+"sev.medium": "متوسطة",
+"sev.low": "منخفضة",
+"fd.open": "غير معالَجة",
+"fd.all": "الكل",
+"fd.none": "لم يُكتشف شيء في هذه الفترة.",
+"fd.none_open": "لا توجد اكتشافات غير معالَجة: لا شيء يحتاج إلى انتباه.",
+"fd.learning": "جارٍ تعلّم الوضع الطبيعي حتى {t}. بعد ذلك سيُبلَّغ عن الاتصالات بين مضيفيك وعمليات الرفع إلى وجهات جديدة.",
+"fd.last_run": "آخر فحص: {t}",
+"fd.note": "يجري الفحص كل 5 دقائق. مع البيانات المأخوذة بالعينات (sFlow وNetFlow بالعينات) تمثّل الأرقام ما تظهره العينات، وقد يمر مسح صغير دون أن يُرى.",
+"fd.view": "التفاصيل",
+"fd.done": "تمت المعالجة",
+"fd.false": "ليست مشكلة",
+"fd.reopen": "إعادة فتح",
+"fd.false_tip": "لن يُبلَّغ عن هذا مرة أخرى",
+"fd.st_done": "تمت المعالجة",
+"fd.st_false": "ليست مشكلة",
+"fd.min": "{n} دقيقة",
+"fd.hm": "{h} ساعة {m} دقيقة",
+"fd.kind.scan": "مسح",
+"fd.kind.portscan": "مسح المنافذ",
+"fd.kind.brute": "تخمين كلمات المرور",
+"fd.kind.lateral": "تحرك جانبي",
+"fd.kind.exfil": "رفع غير معتاد",
+"fd.kind.flood": "إغراق",
+"fd.kind.threat": "قائمة التهديدات",
+"fd.k.scan": "{src} استطلع ما لا يقل عن {n} عنوانًا على {port}",
+"fd.k.portscan": "{src} استطلع ما لا يقل عن {n} منفذًا على {dst}",
+"fd.k.brute": "{src} حاول مرارًا تسجيل الدخول إلى {dst} ({port})",
+"fd.k.lateral": "{src} فتح جلسات {port} مع {n} من المضيفين الذين لم يقدّموا هذه الخدمة من قبل",
+"fd.k.exfil": "{src} رفع {bytes} إلى {dst}، وهي وجهة لم يستخدمها من قبل",
+"fd.k.flood": "{dst} استقبل {pps} حزمة صغيرة في الثانية ({port})",
+"fd.k.threat_out": "{src} اتصل بـ {dst} المدرج في قائمة التهديدات «{list}»",
+"fd.k.threat_in": "{src}، المدرج في قائمة التهديدات «{list}»، اتصل بـ {dst}",
+"fd.e.sampling": "أخذ العينات 1:{n}",
+"fd.e.unsampled": "بدون أخذ عينات",
+"fd.e.pkts": "نحو {n} حزمة",
+"fd.e.avg": "{n} بايت لكل حزمة",
+"fd.e.examples": "مثل {list}",
+"fd.e.targets": "المضيفون: {list}",
+"fd.e.conns": "عدد الاتصالات المرصودة: {n}",
+"fd.e.bytes": "المنقول: {n}",
+"fd.e.up": "المُرسَل: {n}",
+"fd.e.down": "المُستقبَل: {n}",
+"fd.e.sources": "من {n} من العناوين",
+"fd.e.usual": "عادةً {n} حزمة في الثانية",
+"ov.findings_all": "كل الاكتشافات غير المعالَجة ({n})",
+"det.findings": "اكتشافات تخص هذا المضيف"
}
diff --git a/internal/web/static/i18n/bn.json b/internal/web/static/i18n/bn.json
index 751dba0..b0e687d 100644
--- a/internal/web/static/i18n/bn.json
+++ b/internal/web/static/i18n/bn.json
@@ -258,5 +258,54 @@
"src.ex_host": "হিসাব বিভাগের পিসি",
"src.ex_net": "অফিস LAN",
"src.ex_device": "কোর রাউটার",
-"src.ex_iface": "ISP আপলিংক"
+"src.ex_iface": "ISP আপলিংক",
+"nav.findings": "সন্দেহজনক কার্যকলাপ",
+"sev.high": "উচ্চ",
+"sev.medium": "মাঝারি",
+"sev.low": "নিম্ন",
+"fd.open": "অমীমাংসিত",
+"fd.all": "সব",
+"fd.none": "এই সময়ে কিছু পাওয়া যায়নি।",
+"fd.none_open": "কোনো অমীমাংসিত সন্দেহজনক কার্যকলাপ নেই: কিছুতে মনোযোগ দেওয়ার দরকার নেই।",
+"fd.learning": "{t} পর্যন্ত স্বাভাবিক অবস্থা শেখা হচ্ছে। এরপর আপনার হোস্টগুলোর মধ্যে সংযোগ এবং নতুন গন্তব্যে আপলোডের খবর দেওয়া হবে।",
+"fd.last_run": "শেষ যাচাই: {t}",
+"fd.note": "প্রতি ৫ মিনিটে যাচাই করা হয়। স্যাম্পল করা ডেটায় (sFlow, স্যাম্পল করা NetFlow) সংখ্যাগুলো স্যাম্পলে যা দেখা গেছে তা-ই, এবং ছোট স্ক্যান চোখ এড়িয়ে যেতে পারে।",
+"fd.view": "বিস্তারিত",
+"fd.done": "সমাধান হয়েছে",
+"fd.false": "সমস্যা নয়",
+"fd.reopen": "আবার খুলুন",
+"fd.false_tip": "এটি আর জানানো হবে না",
+"fd.st_done": "সমাধান হয়েছে",
+"fd.st_false": "সমস্যা নয়",
+"fd.min": "{n} মিনিট",
+"fd.hm": "{h} ঘণ্টা {m} মিনিট",
+"fd.kind.scan": "স্ক্যান",
+"fd.kind.portscan": "পোর্ট স্ক্যান",
+"fd.kind.brute": "পাসওয়ার্ড অনুমান",
+"fd.kind.lateral": "ল্যাটেরাল মুভমেন্ট",
+"fd.kind.exfil": "অস্বাভাবিক আপলোড",
+"fd.kind.flood": "ফ্লাড",
+"fd.kind.threat": "হুমকি তালিকা",
+"fd.k.scan": "{src} {port}-এ অন্তত {n}টি ঠিকানা যাচাই করেছে",
+"fd.k.portscan": "{src} {dst}-এর অন্তত {n}টি পোর্ট যাচাই করেছে",
+"fd.k.brute": "{src} বারবার {dst} ({port})-এ লগ ইন করার চেষ্টা করেছে",
+"fd.k.lateral": "{src} এমন {n}টি হোস্টে {port} সেশন খুলেছে যেগুলো আগে কখনো এই সেবা দেয়নি",
+"fd.k.exfil": "{src} {dst}-এ {bytes} আপলোড করেছে, যে গন্তব্য এটি আগে কখনো ব্যবহার করেনি",
+"fd.k.flood": "{dst} প্রতি সেকেন্ডে {pps}টি ছোট প্যাকেট পেয়েছে ({port})",
+"fd.k.threat_out": "{src} {dst}-এ সংযোগ করেছে, যা হুমকি তালিকা “{list}”-এ আছে",
+"fd.k.threat_in": "হুমকি তালিকা “{list}”-এ থাকা {src} {dst}-এ সংযোগ করেছে",
+"fd.e.sampling": "স্যাম্পলিং 1:{n}",
+"fd.e.unsampled": "স্যাম্পলিং ছাড়া",
+"fd.e.pkts": "প্রায় {n}টি প্যাকেট",
+"fd.e.avg": "প্যাকেটপ্রতি {n} বাইট",
+"fd.e.examples": "যেমন {list}",
+"fd.e.targets": "হোস্ট: {list}",
+"fd.e.conns": "{n}টি সংযোগ দেখা গেছে",
+"fd.e.bytes": "{n} স্থানান্তরিত",
+"fd.e.up": "{n} পাঠানো",
+"fd.e.down": "{n} পাওয়া",
+"fd.e.sources": "{n}টি ঠিকানা থেকে",
+"fd.e.usual": "সাধারণত প্রতি সেকেন্ডে {n}টি প্যাকেট",
+"ov.findings_all": "সব অমীমাংসিত সন্দেহজনক কার্যকলাপ ({n})",
+"det.findings": "এর সঙ্গে সম্পর্কিত সন্দেহজনক কার্যকলাপ"
}
diff --git a/internal/web/static/i18n/en.json b/internal/web/static/i18n/en.json
index 1939348..06c3571 100644
--- a/internal/web/static/i18n/en.json
+++ b/internal/web/static/i18n/en.json
@@ -258,5 +258,54 @@
"src.ex_host": "Finance PC",
"src.ex_net": "Office LAN",
"src.ex_device": "Core router",
-"src.ex_iface": "ISP uplink"
+"src.ex_iface": "ISP uplink",
+"nav.findings": "Findings",
+"sev.high": "High",
+"sev.medium": "Medium",
+"sev.low": "Low",
+"fd.open": "Open",
+"fd.all": "All",
+"fd.none": "Nothing was found in this time range.",
+"fd.none_open": "No open findings: nothing needs attention.",
+"fd.learning": "Learning what is normal until {t}. Connections between your hosts and uploads to new destinations are reported after that.",
+"fd.last_run": "Last checked {t}",
+"fd.note": "Checked every 5 minutes. With sampled data (sFlow, sampled NetFlow) the numbers are what the samples show, and a small scan can go unseen.",
+"fd.view": "Details",
+"fd.done": "Dealt with",
+"fd.false": "Not a problem",
+"fd.reopen": "Reopen",
+"fd.false_tip": "This will not be reported again",
+"fd.st_done": "dealt with",
+"fd.st_false": "not a problem",
+"fd.min": "{n} min",
+"fd.hm": "{h} h {m} min",
+"fd.kind.scan": "Scan",
+"fd.kind.portscan": "Port scan",
+"fd.kind.brute": "Password guessing",
+"fd.kind.lateral": "Lateral movement",
+"fd.kind.exfil": "Unusual upload",
+"fd.kind.flood": "Flood",
+"fd.kind.threat": "Threat list",
+"fd.k.scan": "{src} probed at least {n} addresses on {port}",
+"fd.k.portscan": "{src} probed at least {n} ports of {dst}",
+"fd.k.brute": "{src} tried to log in to {dst} ({port}) again and again",
+"fd.k.lateral": "{src} opened {port} sessions to {n} hosts that never offered this service before",
+"fd.k.exfil": "{src} uploaded {bytes} to {dst}, a destination it had not used before",
+"fd.k.flood": "{dst} received {pps} small packets per second ({port})",
+"fd.k.threat_out": "{src} connected to {dst}, which is on the threat list “{list}”",
+"fd.k.threat_in": "{src}, on the threat list “{list}”, connected to {dst}",
+"fd.e.sampling": "sampled 1:{n}",
+"fd.e.unsampled": "unsampled",
+"fd.e.pkts": "about {n} packets",
+"fd.e.avg": "{n} bytes per packet",
+"fd.e.examples": "for example {list}",
+"fd.e.targets": "hosts: {list}",
+"fd.e.conns": "{n} connections seen",
+"fd.e.bytes": "{n} moved",
+"fd.e.up": "{n} sent",
+"fd.e.down": "{n} received",
+"fd.e.sources": "from {n} addresses",
+"fd.e.usual": "usually {n} packets per second",
+"ov.findings_all": "All open findings ({n})",
+"det.findings": "Findings about it"
}
diff --git a/internal/web/static/i18n/es.json b/internal/web/static/i18n/es.json
index 7f3aa55..985c0aa 100644
--- a/internal/web/static/i18n/es.json
+++ b/internal/web/static/i18n/es.json
@@ -258,5 +258,54 @@
"src.ex_host": "PC de contabilidad",
"src.ex_net": "LAN de oficina",
"src.ex_device": "Router de núcleo",
-"src.ex_iface": "Enlace al ISP"
+"src.ex_iface": "Enlace al ISP",
+"nav.findings": "Hallazgos",
+"sev.high": "Alta",
+"sev.medium": "Media",
+"sev.low": "Baja",
+"fd.open": "Pendientes",
+"fd.all": "Todos",
+"fd.none": "No se encontró nada en este periodo.",
+"fd.none_open": "No hay hallazgos pendientes: nada requiere atención.",
+"fd.learning": "Aprendiendo lo que es normal hasta {t}. A partir de entonces se informará de las conexiones entre sus equipos y de las subidas a destinos nuevos.",
+"fd.last_run": "Última comprobación: {t}",
+"fd.note": "Se comprueba cada 5 minutos. Con datos muestreados (sFlow, NetFlow muestreado) las cifras son lo que muestran las muestras, y un escaneo pequeño puede pasar desapercibido.",
+"fd.view": "Detalles",
+"fd.done": "Resuelto",
+"fd.false": "No es un problema",
+"fd.reopen": "Reabrir",
+"fd.false_tip": "No se volverá a informar",
+"fd.st_done": "resuelto",
+"fd.st_false": "no es un problema",
+"fd.min": "{n} min",
+"fd.hm": "{h} h {m} min",
+"fd.kind.scan": "Escaneo",
+"fd.kind.portscan": "Escaneo de puertos",
+"fd.kind.brute": "Adivinación de contraseñas",
+"fd.kind.lateral": "Movimiento lateral",
+"fd.kind.exfil": "Subida inusual",
+"fd.kind.flood": "Inundación",
+"fd.kind.threat": "Lista de amenazas",
+"fd.k.scan": "{src} sondeó al menos {n} direcciones en {port}",
+"fd.k.portscan": "{src} sondeó al menos {n} puertos de {dst}",
+"fd.k.brute": "{src} intentó iniciar sesión en {dst} ({port}) una y otra vez",
+"fd.k.lateral": "{src} abrió sesiones {port} con {n} equipos que nunca antes habían ofrecido este servicio",
+"fd.k.exfil": "{src} subió {bytes} a {dst}, un destino que no había usado antes",
+"fd.k.flood": "{dst} recibió {pps} paquetes pequeños por segundo ({port})",
+"fd.k.threat_out": "{src} se conectó a {dst}, que está en la lista de amenazas «{list}»",
+"fd.k.threat_in": "{src}, que está en la lista de amenazas «{list}», se conectó a {dst}",
+"fd.e.sampling": "muestreo 1:{n}",
+"fd.e.unsampled": "sin muestreo",
+"fd.e.pkts": "unos {n} paquetes",
+"fd.e.avg": "{n} bytes por paquete",
+"fd.e.examples": "por ejemplo {list}",
+"fd.e.targets": "equipos: {list}",
+"fd.e.conns": "{n} conexiones vistas",
+"fd.e.bytes": "{n} transferidos",
+"fd.e.up": "{n} enviados",
+"fd.e.down": "{n} recibidos",
+"fd.e.sources": "desde {n} direcciones",
+"fd.e.usual": "normalmente {n} paquetes por segundo",
+"ov.findings_all": "Todos los hallazgos pendientes ({n})",
+"det.findings": "Hallazgos sobre este equipo"
}
diff --git a/internal/web/static/i18n/fr.json b/internal/web/static/i18n/fr.json
index 9dca33d..24729ac 100644
--- a/internal/web/static/i18n/fr.json
+++ b/internal/web/static/i18n/fr.json
@@ -258,5 +258,54 @@
"src.ex_host": "PC Compta",
"src.ex_net": "LAN du bureau",
"src.ex_device": "Routeur de cœur",
-"src.ex_iface": "Lien opérateur"
+"src.ex_iface": "Lien opérateur",
+"nav.findings": "Détections",
+"sev.high": "Élevée",
+"sev.medium": "Moyenne",
+"sev.low": "Faible",
+"fd.open": "À traiter",
+"fd.all": "Toutes",
+"fd.none": "Rien n'a été détecté sur cette période.",
+"fd.none_open": "Aucune détection à traiter : rien ne demande votre attention.",
+"fd.learning": "Apprentissage du comportement normal jusqu'à {t}. Les connexions entre vos hôtes et les envois vers de nouvelles destinations seront signalés ensuite.",
+"fd.last_run": "Dernière vérification : {t}",
+"fd.note": "Vérification toutes les 5 minutes. Avec des données échantillonnées (sFlow, NetFlow échantillonné), les chiffres reflètent ce que montrent les échantillons, et un petit scan peut passer inaperçu.",
+"fd.view": "Détails",
+"fd.done": "Traité",
+"fd.false": "Fausse alerte",
+"fd.reopen": "Rouvrir",
+"fd.false_tip": "Ne sera plus signalé",
+"fd.st_done": "traité",
+"fd.st_false": "fausse alerte",
+"fd.min": "{n} min",
+"fd.hm": "{h} h {m} min",
+"fd.kind.scan": "Scan",
+"fd.kind.portscan": "Scan de ports",
+"fd.kind.brute": "Essais de mots de passe",
+"fd.kind.lateral": "Mouvement latéral",
+"fd.kind.exfil": "Envoi inhabituel",
+"fd.kind.flood": "Inondation",
+"fd.kind.threat": "Liste de menaces",
+"fd.k.scan": "{src} a sondé au moins {n} adresses sur {port}",
+"fd.k.portscan": "{src} a sondé au moins {n} ports de {dst}",
+"fd.k.brute": "{src} a tenté à maintes reprises de se connecter à {dst} ({port})",
+"fd.k.lateral": "{src} a ouvert des sessions {port} vers {n} hôtes qui n'avaient jamais proposé ce service",
+"fd.k.exfil": "{src} a envoyé {bytes} vers {dst}, une destination qu'il n'avait jamais utilisée",
+"fd.k.flood": "{dst} a reçu {pps} petits paquets par seconde ({port})",
+"fd.k.threat_out": "{src} s'est connecté à {dst}, qui figure dans la liste de menaces « {list} »",
+"fd.k.threat_in": "{src}, qui figure dans la liste de menaces « {list} », s'est connecté à {dst}",
+"fd.e.sampling": "échantillonnage 1:{n}",
+"fd.e.unsampled": "non échantillonné",
+"fd.e.pkts": "environ {n} paquets",
+"fd.e.avg": "{n} octets par paquet",
+"fd.e.examples": "par exemple {list}",
+"fd.e.targets": "hôtes : {list}",
+"fd.e.conns": "{n} connexions observées",
+"fd.e.bytes": "{n} transférés",
+"fd.e.up": "{n} envoyés",
+"fd.e.down": "{n} reçus",
+"fd.e.sources": "depuis {n} adresses",
+"fd.e.usual": "habituellement {n} paquets par seconde",
+"ov.findings_all": "Toutes les détections à traiter ({n})",
+"det.findings": "Détections le concernant"
}
diff --git a/internal/web/static/i18n/hi.json b/internal/web/static/i18n/hi.json
index ffaf087..fceb3c0 100644
--- a/internal/web/static/i18n/hi.json
+++ b/internal/web/static/i18n/hi.json
@@ -258,5 +258,54 @@
"src.ex_host": "अकाउंट्स पीसी",
"src.ex_net": "ऑफ़िस LAN",
"src.ex_device": "कोर राउटर",
-"src.ex_iface": "ISP अपलिंक"
+"src.ex_iface": "ISP अपलिंक",
+"nav.findings": "संदिग्ध गतिविधि",
+"sev.high": "उच्च",
+"sev.medium": "मध्यम",
+"sev.low": "निम्न",
+"fd.open": "लंबित",
+"fd.all": "सभी",
+"fd.none": "इस अवधि में कुछ नहीं मिला।",
+"fd.none_open": "कोई लंबित संदिग्ध गतिविधि नहीं: किसी चीज़ पर ध्यान देने की ज़रूरत नहीं।",
+"fd.learning": "{t} तक सामान्य व्यवहार सीखा जा रहा है। इसके बाद आपके होस्ट के बीच कनेक्शन और नए गंतव्यों पर अपलोड की सूचना दी जाएगी।",
+"fd.last_run": "आख़िरी जाँच: {t}",
+"fd.note": "हर 5 मिनट में जाँच होती है। सैंपल किए गए डेटा (sFlow, सैंपल किया गया NetFlow) में आँकड़े वही हैं जो सैंपल में दिखे, और छोटा स्कैन छूट सकता है।",
+"fd.view": "विवरण",
+"fd.done": "निपटा दिया",
+"fd.false": "कोई समस्या नहीं",
+"fd.reopen": "फिर से खोलें",
+"fd.false_tip": "इसकी सूचना दोबारा नहीं दी जाएगी",
+"fd.st_done": "निपटा दिया गया",
+"fd.st_false": "कोई समस्या नहीं",
+"fd.min": "{n} मिनट",
+"fd.hm": "{h} घंटे {m} मिनट",
+"fd.kind.scan": "स्कैन",
+"fd.kind.portscan": "पोर्ट स्कैन",
+"fd.kind.brute": "पासवर्ड का अनुमान",
+"fd.kind.lateral": "लेटरल मूवमेंट",
+"fd.kind.exfil": "असामान्य अपलोड",
+"fd.kind.flood": "फ़्लड",
+"fd.kind.threat": "ख़तरा सूची",
+"fd.k.scan": "{src} ने {port} पर कम से कम {n} पतों की जाँच की",
+"fd.k.portscan": "{src} ने {dst} के कम से कम {n} पोर्ट की जाँच की",
+"fd.k.brute": "{src} ने बार-बार {dst} ({port}) पर लॉग इन करने की कोशिश की",
+"fd.k.lateral": "{src} ने ऐसे {n} होस्ट से {port} सेशन खोले जिन्होंने पहले कभी यह सेवा नहीं दी थी",
+"fd.k.exfil": "{src} ने {dst} पर {bytes} अपलोड किया, जिस गंतव्य का उसने पहले कभी उपयोग नहीं किया था",
+"fd.k.flood": "{dst} को प्रति सेकंड {pps} छोटे पैकेट मिले ({port})",
+"fd.k.threat_out": "{src} ने {dst} से कनेक्ट किया, जो ख़तरा सूची “{list}” में है",
+"fd.k.threat_in": "ख़तरा सूची “{list}” में मौजूद {src} ने {dst} से कनेक्ट किया",
+"fd.e.sampling": "सैंपलिंग 1:{n}",
+"fd.e.unsampled": "बिना सैंपलिंग",
+"fd.e.pkts": "लगभग {n} पैकेट",
+"fd.e.avg": "प्रति पैकेट {n} बाइट",
+"fd.e.examples": "उदाहरण: {list}",
+"fd.e.targets": "होस्ट: {list}",
+"fd.e.conns": "{n} कनेक्शन देखे गए",
+"fd.e.bytes": "{n} ट्रांसफ़र हुआ",
+"fd.e.up": "{n} भेजा गया",
+"fd.e.down": "{n} मिला",
+"fd.e.sources": "{n} पतों से",
+"fd.e.usual": "आमतौर पर प्रति सेकंड {n} पैकेट",
+"ov.findings_all": "सभी लंबित संदिग्ध गतिविधियाँ ({n})",
+"det.findings": "इससे जुड़ी संदिग्ध गतिविधि"
}
diff --git a/internal/web/static/i18n/id.json b/internal/web/static/i18n/id.json
index 7597fe1..dd793a7 100644
--- a/internal/web/static/i18n/id.json
+++ b/internal/web/static/i18n/id.json
@@ -258,5 +258,54 @@
"src.ex_host": "PC Keuangan",
"src.ex_net": "LAN Kantor",
"src.ex_device": "Router inti",
-"src.ex_iface": "Uplink ISP"
+"src.ex_iface": "Uplink ISP",
+"nav.findings": "Temuan",
+"sev.high": "Tinggi",
+"sev.medium": "Sedang",
+"sev.low": "Rendah",
+"fd.open": "Belum ditangani",
+"fd.all": "Semua",
+"fd.none": "Tidak ada yang ditemukan dalam rentang waktu ini.",
+"fd.none_open": "Tidak ada temuan yang belum ditangani: tidak ada yang perlu diperhatikan.",
+"fd.learning": "Mempelajari kondisi normal hingga {t}. Setelah itu, koneksi antar-host Anda dan unggahan ke tujuan baru akan dilaporkan.",
+"fd.last_run": "Terakhir diperiksa {t}",
+"fd.note": "Diperiksa setiap 5 menit. Dengan data sampling (sFlow, NetFlow dengan sampling), angka yang tampil adalah yang terlihat di sampel, dan pemindaian kecil bisa tidak terlihat.",
+"fd.view": "Detail",
+"fd.done": "Sudah ditangani",
+"fd.false": "Bukan masalah",
+"fd.reopen": "Buka lagi",
+"fd.false_tip": "Ini tidak akan dilaporkan lagi",
+"fd.st_done": "sudah ditangani",
+"fd.st_false": "bukan masalah",
+"fd.min": "{n} mnt",
+"fd.hm": "{h} jam {m} mnt",
+"fd.kind.scan": "Pemindaian",
+"fd.kind.portscan": "Pemindaian port",
+"fd.kind.brute": "Tebak kata sandi",
+"fd.kind.lateral": "Pergerakan lateral",
+"fd.kind.exfil": "Unggahan tidak biasa",
+"fd.kind.flood": "Flood",
+"fd.kind.threat": "Daftar ancaman",
+"fd.k.scan": "{src} memeriksa setidaknya {n} alamat pada {port}",
+"fd.k.portscan": "{src} memeriksa setidaknya {n} port milik {dst}",
+"fd.k.brute": "{src} berulang kali mencoba masuk ke {dst} ({port})",
+"fd.k.lateral": "{src} membuka sesi {port} ke {n} host yang sebelumnya tidak pernah menyediakan layanan ini",
+"fd.k.exfil": "{src} mengunggah {bytes} ke {dst}, tujuan yang belum pernah dipakainya",
+"fd.k.flood": "{dst} menerima {pps} paket kecil per detik ({port})",
+"fd.k.threat_out": "{src} terhubung ke {dst}, yang ada di daftar ancaman “{list}”",
+"fd.k.threat_in": "{src}, yang ada di daftar ancaman “{list}”, terhubung ke {dst}",
+"fd.e.sampling": "sampling 1:{n}",
+"fd.e.unsampled": "tanpa sampling",
+"fd.e.pkts": "sekitar {n} paket",
+"fd.e.avg": "{n} byte per paket",
+"fd.e.examples": "misalnya {list}",
+"fd.e.targets": "host: {list}",
+"fd.e.conns": "{n} koneksi terlihat",
+"fd.e.bytes": "{n} ditransfer",
+"fd.e.up": "{n} dikirim",
+"fd.e.down": "{n} diterima",
+"fd.e.sources": "dari {n} alamat",
+"fd.e.usual": "biasanya {n} paket per detik",
+"ov.findings_all": "Semua temuan yang belum ditangani ({n})",
+"det.findings": "Temuan tentang host ini"
}
diff --git a/internal/web/static/i18n/ja.json b/internal/web/static/i18n/ja.json
index 810234e..1c5b5d7 100644
--- a/internal/web/static/i18n/ja.json
+++ b/internal/web/static/i18n/ja.json
@@ -258,5 +258,54 @@
"src.ex_host": "経理PC",
"src.ex_net": "社内LAN",
"src.ex_device": "コアルーター",
-"src.ex_iface": "ISP上位回線"
+"src.ex_iface": "ISP上位回線",
+"nav.findings": "検知",
+"sev.high": "高",
+"sev.medium": "中",
+"sev.low": "低",
+"fd.open": "未対応",
+"fd.all": "すべて",
+"fd.none": "この期間には何も検知されていません。",
+"fd.none_open": "未対応の検知はありません。対応が必要なものはありません。",
+"fd.learning": "{t} まで平常時の状態を学習しています。ホスト間の新しい接続と新しい宛先へのアップロードは、その後から報告します。",
+"fd.last_run": "最終チェック:{t}",
+"fd.note": "5 分ごとにチェックします。サンプリングされたデータ(sFlow、サンプリング NetFlow)では、数値はサンプルに現れた分で、小規模なスキャンは見えないことがあります。",
+"fd.view": "詳細",
+"fd.done": "対応済み",
+"fd.false": "問題なし",
+"fd.reopen": "再オープン",
+"fd.false_tip": "今後これは報告されません",
+"fd.st_done": "対応済み",
+"fd.st_false": "問題なし",
+"fd.min": "{n} 分",
+"fd.hm": "{h} 時間 {m} 分",
+"fd.kind.scan": "スキャン",
+"fd.kind.portscan": "ポートスキャン",
+"fd.kind.brute": "パスワード総当たり",
+"fd.kind.lateral": "横展開",
+"fd.kind.exfil": "不審なアップロード",
+"fd.kind.flood": "フラッド",
+"fd.kind.threat": "脅威リスト",
+"fd.k.scan": "{src} が {port} で少なくとも {n} 個のアドレスを探索しました",
+"fd.k.portscan": "{src} が {dst} の少なくとも {n} 個のポートを探索しました",
+"fd.k.brute": "{src} が {dst}({port})へのログインを繰り返し試みました",
+"fd.k.lateral": "{src} が、これまでそのサービスを提供していなかった {n} 台のホストに {port} のセッションを開きました",
+"fd.k.exfil": "{src} が、これまで使ったことのない宛先 {dst} に {bytes} をアップロードしました",
+"fd.k.flood": "{dst} が毎秒 {pps} 個の小さなパケットを受信しました({port})",
+"fd.k.threat_out": "{src} が脅威リスト「{list}」に載っている {dst} に接続しました",
+"fd.k.threat_in": "脅威リスト「{list}」に載っている {src} が {dst} に接続しました",
+"fd.e.sampling": "サンプリング 1:{n}",
+"fd.e.unsampled": "サンプリングなし",
+"fd.e.pkts": "約 {n} パケット",
+"fd.e.avg": "1 パケット平均 {n} バイト",
+"fd.e.examples": "例:{list}",
+"fd.e.targets": "ホスト:{list}",
+"fd.e.conns": "{n} 接続を観測",
+"fd.e.bytes": "転送量 {n}",
+"fd.e.up": "送信 {n}",
+"fd.e.down": "受信 {n}",
+"fd.e.sources": "{n} 個のアドレスから",
+"fd.e.usual": "平常時は毎秒 {n} パケット",
+"ov.findings_all": "未対応の検知をすべて表示({n})",
+"det.findings": "このホストに関する検知"
}
diff --git a/internal/web/static/i18n/ko.json b/internal/web/static/i18n/ko.json
index 18e1308..913e468 100644
--- a/internal/web/static/i18n/ko.json
+++ b/internal/web/static/i18n/ko.json
@@ -258,5 +258,54 @@
"src.ex_host": "재무팀 PC",
"src.ex_net": "사무실 LAN",
"src.ex_device": "코어 라우터",
-"src.ex_iface": "ISP 업링크"
+"src.ex_iface": "ISP 업링크",
+"nav.findings": "탐지",
+"sev.high": "높음",
+"sev.medium": "중간",
+"sev.low": "낮음",
+"fd.open": "미처리",
+"fd.all": "전체",
+"fd.none": "이 기간에는 탐지된 것이 없습니다.",
+"fd.none_open": "미처리 탐지가 없습니다. 확인이 필요한 항목이 없습니다.",
+"fd.learning": "{t}까지 평소 상태를 학습하고 있습니다. 호스트 간의 새 연결과 새 목적지로의 업로드는 그 이후부터 보고합니다.",
+"fd.last_run": "마지막 확인: {t}",
+"fd.note": "5분마다 확인합니다. 샘플링된 데이터(sFlow, 샘플링된 NetFlow)에서는 수치가 샘플에 나타난 만큼이며, 소규모 스캔은 보이지 않을 수 있습니다.",
+"fd.view": "상세",
+"fd.done": "처리 완료",
+"fd.false": "문제 없음",
+"fd.reopen": "다시 열기",
+"fd.false_tip": "앞으로 이 항목은 보고되지 않습니다",
+"fd.st_done": "처리 완료",
+"fd.st_false": "문제 없음",
+"fd.min": "{n}분",
+"fd.hm": "{h}시간 {m}분",
+"fd.kind.scan": "스캔",
+"fd.kind.portscan": "포트 스캔",
+"fd.kind.brute": "비밀번호 대입",
+"fd.kind.lateral": "내부 확산",
+"fd.kind.exfil": "비정상 업로드",
+"fd.kind.flood": "플러드",
+"fd.kind.threat": "위협 목록",
+"fd.k.scan": "{src}에서 {port}로 최소 {n}개 주소를 탐색했습니다",
+"fd.k.portscan": "{src}에서 {dst}의 포트를 최소 {n}개 탐색했습니다",
+"fd.k.brute": "{src}에서 {dst}({port})로 로그인을 반복해서 시도했습니다",
+"fd.k.lateral": "{src}에서 이전에 이 서비스를 제공한 적 없는 호스트 {n}대로 {port} 세션을 열었습니다",
+"fd.k.exfil": "{src}에서 이전에 사용한 적 없는 목적지 {dst}로 {bytes}를 업로드했습니다",
+"fd.k.flood": "{dst}에 초당 {pps}개의 작은 패킷이 수신되었습니다({port})",
+"fd.k.threat_out": "{src}에서 위협 목록 “{list}”에 있는 {dst}에 연결했습니다",
+"fd.k.threat_in": "위협 목록 “{list}”에 있는 {src}에서 {dst}에 연결했습니다",
+"fd.e.sampling": "샘플링 1:{n}",
+"fd.e.unsampled": "샘플링 없음",
+"fd.e.pkts": "약 {n}개 패킷",
+"fd.e.avg": "패킷당 {n}바이트",
+"fd.e.examples": "예: {list}",
+"fd.e.targets": "호스트: {list}",
+"fd.e.conns": "연결 {n}개 관측",
+"fd.e.bytes": "전송량 {n}",
+"fd.e.up": "송신 {n}",
+"fd.e.down": "수신 {n}",
+"fd.e.sources": "{n}개 주소에서",
+"fd.e.usual": "평소 초당 {n}개 패킷",
+"ov.findings_all": "미처리 탐지 모두 보기({n})",
+"det.findings": "이 호스트에 대한 탐지"
}
diff --git a/internal/web/static/i18n/pt.json b/internal/web/static/i18n/pt.json
index ea9c52a..81ed4b3 100644
--- a/internal/web/static/i18n/pt.json
+++ b/internal/web/static/i18n/pt.json
@@ -258,5 +258,54 @@
"src.ex_host": "PC do Financeiro",
"src.ex_net": "Rede do escritório",
"src.ex_device": "Roteador de núcleo",
-"src.ex_iface": "Link da operadora"
+"src.ex_iface": "Link da operadora",
+"nav.findings": "Detecções",
+"sev.high": "Alta",
+"sev.medium": "Média",
+"sev.low": "Baixa",
+"fd.open": "Pendentes",
+"fd.all": "Todas",
+"fd.none": "Nada foi encontrado neste período.",
+"fd.none_open": "Nenhuma detecção pendente: nada precisa de atenção.",
+"fd.learning": "Aprendendo o que é normal até {t}. Depois disso, conexões entre seus hosts e envios para novos destinos serão relatados.",
+"fd.last_run": "Última verificação: {t}",
+"fd.note": "Verificado a cada 5 minutos. Com dados amostrados (sFlow, NetFlow amostrado), os números são o que as amostras mostram, e uma varredura pequena pode passar despercebida.",
+"fd.view": "Detalhes",
+"fd.done": "Resolvido",
+"fd.false": "Não é problema",
+"fd.reopen": "Reabrir",
+"fd.false_tip": "Não será relatado novamente",
+"fd.st_done": "resolvido",
+"fd.st_false": "não é problema",
+"fd.min": "{n} min",
+"fd.hm": "{h} h {m} min",
+"fd.kind.scan": "Varredura",
+"fd.kind.portscan": "Varredura de portas",
+"fd.kind.brute": "Tentativa de senhas",
+"fd.kind.lateral": "Movimento lateral",
+"fd.kind.exfil": "Envio incomum",
+"fd.kind.flood": "Inundação",
+"fd.kind.threat": "Lista de ameaças",
+"fd.k.scan": "{src} sondou pelo menos {n} endereços em {port}",
+"fd.k.portscan": "{src} sondou pelo menos {n} portas de {dst}",
+"fd.k.brute": "{src} tentou fazer login em {dst} ({port}) repetidas vezes",
+"fd.k.lateral": "{src} abriu sessões {port} com {n} hosts que nunca tinham oferecido esse serviço",
+"fd.k.exfil": "{src} enviou {bytes} para {dst}, um destino que não tinha usado antes",
+"fd.k.flood": "{dst} recebeu {pps} pacotes pequenos por segundo ({port})",
+"fd.k.threat_out": "{src} se conectou a {dst}, que está na lista de ameaças “{list}”",
+"fd.k.threat_in": "{src}, que está na lista de ameaças “{list}”, se conectou a {dst}",
+"fd.e.sampling": "amostragem 1:{n}",
+"fd.e.unsampled": "sem amostragem",
+"fd.e.pkts": "cerca de {n} pacotes",
+"fd.e.avg": "{n} bytes por pacote",
+"fd.e.examples": "por exemplo {list}",
+"fd.e.targets": "hosts: {list}",
+"fd.e.conns": "{n} conexões vistas",
+"fd.e.bytes": "{n} transferidos",
+"fd.e.up": "{n} enviados",
+"fd.e.down": "{n} recebidos",
+"fd.e.sources": "de {n} endereços",
+"fd.e.usual": "normalmente {n} pacotes por segundo",
+"ov.findings_all": "Todas as detecções pendentes ({n})",
+"det.findings": "Detecções sobre este host"
}
diff --git a/internal/web/static/i18n/ru.json b/internal/web/static/i18n/ru.json
index dd142a2..21aa58f 100644
--- a/internal/web/static/i18n/ru.json
+++ b/internal/web/static/i18n/ru.json
@@ -258,5 +258,54 @@
"src.ex_host": "ПК бухгалтерии",
"src.ex_net": "Офисная сеть",
"src.ex_device": "Ядровой маршрутизатор",
-"src.ex_iface": "Аплинк провайдера"
+"src.ex_iface": "Аплинк провайдера",
+"nav.findings": "Обнаружения",
+"sev.high": "Высокая",
+"sev.medium": "Средняя",
+"sev.low": "Низкая",
+"fd.open": "Открытые",
+"fd.all": "Все",
+"fd.none": "За этот период ничего не обнаружено.",
+"fd.none_open": "Открытых обнаружений нет: ничего не требует внимания.",
+"fd.learning": "До {t} изучается обычное поведение. После этого будут сообщаться соединения между вашими узлами и выгрузки на новые адреса.",
+"fd.last_run": "Последняя проверка: {t}",
+"fd.note": "Проверка каждые 5 минут. При сэмплированных данных (sFlow, сэмплированный NetFlow) числа отражают то, что видно в выборке, и небольшое сканирование может остаться незамеченным.",
+"fd.view": "Подробнее",
+"fd.done": "Обработано",
+"fd.false": "Ложная тревога",
+"fd.reopen": "Открыть снова",
+"fd.false_tip": "Об этом больше не будет сообщений",
+"fd.st_done": "обработано",
+"fd.st_false": "ложная тревога",
+"fd.min": "{n} мин",
+"fd.hm": "{h} ч {m} мин",
+"fd.kind.scan": "Сканирование",
+"fd.kind.portscan": "Сканирование портов",
+"fd.kind.brute": "Подбор паролей",
+"fd.kind.lateral": "Боковое перемещение",
+"fd.kind.exfil": "Необычная выгрузка",
+"fd.kind.flood": "Флуд",
+"fd.kind.threat": "Список угроз",
+"fd.k.scan": "{src} опросил не менее {n} адресов на {port}",
+"fd.k.portscan": "{src} опросил не менее {n} портов на {dst}",
+"fd.k.brute": "{src} раз за разом пытался войти на {dst} ({port})",
+"fd.k.lateral": "{src} открыл сеансы {port} к узлам, которые раньше не предоставляли этот сервис: {n}",
+"fd.k.exfil": "{src} выгрузил {bytes} на {dst} — адрес, который раньше не использовал",
+"fd.k.flood": "{dst} получал {pps} мелких пакетов в секунду ({port})",
+"fd.k.threat_out": "{src} подключался к {dst} из списка угроз «{list}»",
+"fd.k.threat_in": "{src} из списка угроз «{list}» подключался к {dst}",
+"fd.e.sampling": "сэмплирование 1:{n}",
+"fd.e.unsampled": "без сэмплирования",
+"fd.e.pkts": "около {n} пакетов",
+"fd.e.avg": "{n} байт на пакет",
+"fd.e.examples": "например, {list}",
+"fd.e.targets": "узлы: {list}",
+"fd.e.conns": "соединений: {n}",
+"fd.e.bytes": "передано {n}",
+"fd.e.up": "отправлено {n}",
+"fd.e.down": "получено {n}",
+"fd.e.sources": "адресов-источников: {n}",
+"fd.e.usual": "обычно пакетов в секунду: {n}",
+"ov.findings_all": "Все открытые обнаружения ({n})",
+"det.findings": "Обнаружения по этому узлу"
}
diff --git a/internal/web/static/i18n/ur.json b/internal/web/static/i18n/ur.json
index 9f20913..ded01b8 100644
--- a/internal/web/static/i18n/ur.json
+++ b/internal/web/static/i18n/ur.json
@@ -258,5 +258,54 @@
"src.ex_host": "اکاؤنٹس پی سی",
"src.ex_net": "دفتر LAN",
"src.ex_device": "کور راؤٹر",
-"src.ex_iface": "ISP اپ لنک"
+"src.ex_iface": "ISP اپ لنک",
+"nav.findings": "مشتبہ سرگرمی",
+"sev.high": "زیادہ",
+"sev.medium": "درمیانہ",
+"sev.low": "کم",
+"fd.open": "زیرِ التوا",
+"fd.all": "سب",
+"fd.none": "اس مدت میں کچھ نہیں ملا۔",
+"fd.none_open": "کوئی زیرِ التوا مشتبہ سرگرمی نہیں: کسی چیز پر توجہ کی ضرورت نہیں۔",
+"fd.learning": "{t} تک معمول کی حالت سیکھی جا رہی ہے۔ اس کے بعد آپ کے ہوسٹس کے درمیان کنکشن اور نئی منزلوں پر اپ لوڈ کی اطلاع دی جائے گی۔",
+"fd.last_run": "آخری جانچ: {t}",
+"fd.note": "ہر 5 منٹ بعد جانچ ہوتی ہے۔ سیمپل شدہ ڈیٹا (sFlow، سیمپل شدہ NetFlow) میں اعداد وہی ہیں جو سیمپل میں نظر آئے، اور چھوٹا اسکین نظر سے رہ سکتا ہے۔",
+"fd.view": "تفصیل",
+"fd.done": "نمٹا دیا",
+"fd.false": "مسئلہ نہیں",
+"fd.reopen": "دوبارہ کھولیں",
+"fd.false_tip": "اس کی دوبارہ اطلاع نہیں دی جائے گی",
+"fd.st_done": "نمٹا دیا گیا",
+"fd.st_false": "مسئلہ نہیں",
+"fd.min": "{n} منٹ",
+"fd.hm": "{h} گھنٹے {m} منٹ",
+"fd.kind.scan": "اسکین",
+"fd.kind.portscan": "پورٹ اسکین",
+"fd.kind.brute": "پاس ورڈ کا اندازہ",
+"fd.kind.lateral": "لیٹرل موومنٹ",
+"fd.kind.exfil": "غیر معمولی اپ لوڈ",
+"fd.kind.flood": "فلڈ",
+"fd.kind.threat": "خطرے کی فہرست",
+"fd.k.scan": "{src} نے {port} پر کم از کم {n} پتوں کی جانچ کی",
+"fd.k.portscan": "{src} نے {dst} کے کم از کم {n} پورٹس کی جانچ کی",
+"fd.k.brute": "{src} نے بار بار {dst} ({port}) پر لاگ ان کرنے کی کوشش کی",
+"fd.k.lateral": "{src} نے ایسے {n} ہوسٹس سے {port} سیشن کھولے جو پہلے کبھی یہ سروس فراہم نہیں کرتے تھے",
+"fd.k.exfil": "{src} نے {dst} پر {bytes} اپ لوڈ کیا، ایک ایسی منزل جو اس نے پہلے استعمال نہیں کی تھی",
+"fd.k.flood": "{dst} کو فی سیکنڈ {pps} چھوٹے پیکٹ موصول ہوئے ({port})",
+"fd.k.threat_out": "{src} نے {dst} سے کنکشن کیا، جو خطرے کی فہرست “{list}” میں ہے",
+"fd.k.threat_in": "خطرے کی فہرست “{list}” میں موجود {src} نے {dst} سے کنکشن کیا",
+"fd.e.sampling": "سیمپلنگ 1:{n}",
+"fd.e.unsampled": "بغیر سیمپلنگ",
+"fd.e.pkts": "تقریباً {n} پیکٹ",
+"fd.e.avg": "فی پیکٹ {n} بائٹ",
+"fd.e.examples": "مثلاً {list}",
+"fd.e.targets": "ہوسٹ: {list}",
+"fd.e.conns": "{n} کنکشن دیکھے گئے",
+"fd.e.bytes": "{n} منتقل ہوا",
+"fd.e.up": "{n} بھیجا گیا",
+"fd.e.down": "{n} موصول ہوا",
+"fd.e.sources": "{n} پتوں سے",
+"fd.e.usual": "عموماً فی سیکنڈ {n} پیکٹ",
+"ov.findings_all": "تمام زیرِ التوا مشتبہ سرگرمیاں ({n})",
+"det.findings": "اس سے متعلق مشتبہ سرگرمی"
}
diff --git a/internal/web/static/i18n/zh.json b/internal/web/static/i18n/zh.json
index f949916..ed583ba 100644
--- a/internal/web/static/i18n/zh.json
+++ b/internal/web/static/i18n/zh.json
@@ -258,5 +258,54 @@
"src.ex_host": "财务电脑",
"src.ex_net": "办公网",
"src.ex_device": "核心路由器",
-"src.ex_iface": "运营商上联"
+"src.ex_iface": "运营商上联",
+"nav.findings": "发现",
+"sev.high": "高",
+"sev.medium": "中",
+"sev.low": "低",
+"fd.open": "未处理",
+"fd.all": "全部",
+"fd.none": "这个时间范围内没有发现。",
+"fd.none_open": "没有未处理的发现,目前不需要关注什么。",
+"fd.learning": "正在学习正常情况,直到 {t}。之后才会报告主机之间的新连接和传往新目的地的上传。",
+"fd.last_run": "上次检查:{t}",
+"fd.note": "每 5 分钟检查一次。抽样数据(sFlow、抽样 NetFlow)的数字是样本里看到的,小规模的扫描可能看不到。",
+"fd.view": "详情",
+"fd.done": "已处理",
+"fd.false": "不是问题",
+"fd.reopen": "重新打开",
+"fd.false_tip": "以后不再报告这一条",
+"fd.st_done": "已处理",
+"fd.st_false": "不是问题",
+"fd.min": "{n} 分钟",
+"fd.hm": "{h} 小时 {m} 分钟",
+"fd.kind.scan": "扫描",
+"fd.kind.portscan": "端口扫描",
+"fd.kind.brute": "暴力破解",
+"fd.kind.lateral": "横向移动",
+"fd.kind.exfil": "异常上传",
+"fd.kind.flood": "泛洪",
+"fd.kind.threat": "威胁情报",
+"fd.k.scan": "{src} 探测了至少 {n} 个地址的 {port}",
+"fd.k.portscan": "{src} 探测了 {dst} 的至少 {n} 个端口",
+"fd.k.brute": "{src} 反复尝试登录 {dst}({port})",
+"fd.k.lateral": "{src} 向 {n} 台以前从未提供该服务的主机建立了 {port} 会话",
+"fd.k.exfil": "{src} 向以前从未使用过的目的地 {dst} 上传了 {bytes}",
+"fd.k.flood": "{dst} 每秒收到 {pps} 个小包({port})",
+"fd.k.threat_out": "{src} 连接了威胁情报列表“{list}”中的 {dst}",
+"fd.k.threat_in": "威胁情报列表“{list}”中的 {src} 连接了 {dst}",
+"fd.e.sampling": "抽样 1:{n}",
+"fd.e.unsampled": "未抽样",
+"fd.e.pkts": "约 {n} 个包",
+"fd.e.avg": "平均每包 {n} 字节",
+"fd.e.examples": "例如 {list}",
+"fd.e.targets": "主机:{list}",
+"fd.e.conns": "观察到 {n} 个连接",
+"fd.e.bytes": "传输 {n}",
+"fd.e.up": "发送 {n}",
+"fd.e.down": "接收 {n}",
+"fd.e.sources": "来自 {n} 个地址",
+"fd.e.usual": "平时每秒 {n} 个包",
+"ov.findings_all": "全部未处理的发现({n})",
+"det.findings": "关于它的发现"
}
diff --git a/internal/web/static/index.html b/internal/web/static/index.html
index c065f72..f723eab 100644
--- a/internal/web/static/index.html
+++ b/internal/web/static/index.html
@@ -14,6 +14,7 @@