初始提交:恶意 IP 特征+行为融合检测系统

- 特征检测(规则/签名)与行为检测(24h 统计)单进程运行
- 融合名单按首次发现时间倒序,展示位置/归属(GeoIP + ASN)
- 钉钉融合推送(加签、24h 节流、去重)
- 页面状态灯、CSV 导出、Docker 镜像构建文件
This commit is contained in:
DeepFal committed 2026-10-18 05:03:05 +08:00
commit 2d76ed6672
42 files changed
+10645

No files matched your search

+168
View File
@@ -0,0 +1,168 @@
"""Tests for the judgement layer (ban candidate scoring)."""
import io
import json
import os
import sys
import tempfile
import unittest
from contextlib import redirect_stderr, redirect_stdout
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(HERE)
sys.path.insert(0, ROOT)
sys.path.insert(0, HERE)
from fake_graylog import JUDGE_MESSAGES, FakeGraylog # noqa: E402
from graylog_ops import cli # noqa: E402
class JudgeTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.server = FakeGraylog(messages=JUDGE_MESSAGES).start()
@classmethod
def tearDownClass(cls):
cls.server.stop()
def run_cli(self, *argv, json_mode=True):
args = ["--url", self.server.url, "--token", "testtoken"]
if json_mode:
args.append("--json")
args.extend(argv)
stdout, stderr = io.StringIO(), io.StringIO()
with redirect_stdout(stdout), redirect_stderr(stderr):
code = cli.main(args)
return code, stdout.getvalue(), stderr.getvalue()
def judged(self, *argv):
code, out, err = self.run_cli("judge", "--range", "300", *argv)
self.assertEqual(code, 0, err)
return json.loads(out)
# ------------------------------------------------------------ behaviour
def test_bruteforce_client_is_recommended_for_ban(self):
data = self.judged()
rows = {row["ip"]: row for row in data["candidates"]}
self.assertIn("203.0.113.66", rows)
row = rows["203.0.113.66"]
self.assertEqual(row["action"], "ban_1h")
self.assertGreaterEqual(row["score"], 75)
self.assertIn("auth_fail", row["signals"])
self.assertTrue(any("失败请求" in reason for reason in row["reasons"]))
def test_scanner_client_is_recommended_for_ban(self):
data = self.judged()
row = {item["ip"]: item for item in data["candidates"]}["192.0.2.99"]
self.assertEqual(row["action"], "ban_1h")
self.assertIn("scanner_ua", row["signals"])
self.assertIn("sensitive_path", row["signals"])
def test_busy_but_legitimate_client_is_not_banned(self):
data = self.judged("--all")
row = {item["ip"]: item for item in data["candidates"]}["198.51.100.200"]
self.assertIn(row["action"], ("ignore", "observe"))
self.assertFalse(row["evidence"])
def test_internal_addresses_are_excluded_by_default(self):
data = self.judged()
reasons = {row["ip"]: row["reason"] for row in data["excluded"]}
self.assertIn("10.180.5.5", reasons)
self.assertIn("内部", reasons["10.180.5.5"])
self.assertNotIn("10.180.5.5", {row["ip"] for row in data["candidates"]})
def test_include_internal_flag(self):
data = self.judged("--include-internal", "--all")
ips = {row["ip"] for row in data["candidates"]}
self.assertIn("10.180.5.5", ips)
def test_allowlist_file(self):
with tempfile.NamedTemporaryFile("w", suffix=".txt", delete=False) as handle:
handle.write("# 深信服 AD / VIP\n10.180.254.17\n203.0.113.66\n")
path = handle.name
try:
data = self.judged("--allow", path)
excluded = {row["ip"]: row["reason"] for row in data["excluded"]}
self.assertIn("203.0.113.66", excluded)
self.assertIn("白名单", excluded["203.0.113.66"])
self.assertNotIn("203.0.113.66", {row["ip"] for row in data["candidates"]})
finally:
os.unlink(path)
def test_explain_attaches_samples(self):
data = self.judged("--explain", "2")
sampled = [row for row in data["candidates"] if row.get("samples")]
self.assertTrue(sampled)
self.assertLessEqual(len(sampled[0]["samples"]), 3)
def test_verify_downgrades_automation_client(self):
"""A steady single-endpoint poller with 403s must not be auto-banned."""
automation = [{
"timestamp": "2026-10-17T15:%02d:%02d.000Z" % (index // 60, index % 60),
"source": "10.180.254.17",
"client_ip": "203.0.113.77",
"status": 403,
"request_method": "POST",
"request_uri": "/prod-api/biz/liveCourseChapter/msgSubscription",
"http_user_agent": "okhttp/4.9",
"message": '203.0.113.77 - "POST /prod-api/biz/x" 403',
} for index in range(40)]
server = FakeGraylog(messages=list(JUDGE_MESSAGES) + automation).start()
try:
stdout, stderr = io.StringIO(), io.StringIO()
with redirect_stdout(stdout), redirect_stderr(stderr):
code = cli.main(["--url", server.url, "--token", "testtoken",
"--json", "judge", "--range", "300",
"--verify", "10", "--all"])
self.assertEqual(code, 0, stderr.getvalue())
data = json.loads(stdout.getvalue())
row = {item["ip"]: item for item in data["candidates"]}["203.0.113.77"]
self.assertEqual(row["action"], "observe")
self.assertTrue(row["verify"]["automation_suspected"])
self.assertFalse(row["verify"]["login_like"])
finally:
server.stop()
def test_verify_keeps_login_bruteforce(self):
"""Failures against a login path stay bannable even if concentrated."""
data = self.judged("--verify", "5")
row = {item["ip"]: item for item in data["candidates"]}["203.0.113.66"]
self.assertEqual(row["action"], "ban_1h")
self.assertTrue(row["verify"]["login_like"])
def test_profile_roundtrip(self):
with tempfile.TemporaryDirectory() as directory:
profile = os.path.join(directory, "profile.json")
code, _, err = self.run_cli("discover", "--range", "3600",
"--save-profile", profile)
self.assertEqual(code, 0, err)
with open(profile, "r", encoding="utf-8") as handle:
saved = json.load(handle)
self.assertEqual(saved["fields"]["client_ip"], "client_ip")
self.assertEqual(saved["fields"]["status"], "status")
self.assertEqual(saved["fields"]["uri"], "request_uri")
data = self.judged("--profile", profile)
self.assertTrue(data["candidates"])
self.assertEqual(data["field"], "client_ip")
# -------------------------------------------------------------- output
def test_human_output(self):
code, out, err = self.run_cli("judge", json_mode=False)
self.assertEqual(code, 0, err)
self.assertIn("信号命中", out)
self.assertIn("本命令只读", out)
self.assertIn("ban_1h", out)
def test_signal_queries_are_reported(self):
data = self.judged()
names = {stat["name"]: stat for stat in data["signal_stats"]}
self.assertIn("auth_fail", names)
self.assertGreater(names["auth_fail"]["ips_matched"], 0)
self.assertGreater(names["scanner_ua"]["ips_matched"], 0)
self.assertIn("query", names["auth_fail"])
if __name__ == "__main__":
unittest.main()