初始提交:恶意 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

+103
View File
@@ -0,0 +1,103 @@
import os
import sys
import tempfile
import time
import unittest
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.dirname(HERE))
from graylog_ops import watch # noqa: E402
NOW = "2026-10-17T16:00:00Z"
def _behavior(ip, verdict="建议封禁", score=75):
return {
"ip": ip, "score": score, "verdict": verdict,
"signals": ["高失败率"], "reason_text": "24 小时内扫描了大量路径。",
"share_note": "", "behavior_type": "目录扫描",
"asn": "4538", "asn_org": "教育网", "shared_type": "education",
"total": 50, "pct4": 60.0, "uri_card": 20, "uri_templates": 15,
"ua_card": 1, "host_card": 2, "coverage": 100.0, "peak_share": 50.0,
"top_uri": "/.env", "top_ua": "curl/8.0",
"country_code": "CN", "country_name": "中国", "region": "北京",
"city": "Beijing",
}
class FusedTests(unittest.TestCase):
def test_feature_and_behavior_merge(self):
with tempfile.TemporaryDirectory() as directory:
store = watch.Store(os.path.join(directory, "ips.db"))
try:
store.upsert({"ip": "203.0.113.9", "tier": watch.TIER_MALICIOUS,
"score": 91.0, "total": 120, "window": 3600,
"rules": {"probe_java": 10},
"reasons": ["探测 10 次"]}, NOW)
store.sync_fused(NOW)
fused = store.fused_items()
self.assertEqual(len(fused), 1)
self.assertEqual(fused[0]["sources"], "feature")
self.assertEqual(fused[0]["first_banned_at"], NOW)
self.assertIn("特征命中", fused[0]["fused_reason"])
# behaviour-only ban is merged
store.replace_behavior_profiles([_behavior("198.51.100.9")])
store.sync_fused("2026-10-17T16:10:00Z")
merged = {item["ip"]: item for item in store.fused_items()}
self.assertIn("198.51.100.9", merged)
self.assertEqual(merged["198.51.100.9"]["sources"], "behavior")
self.assertEqual(merged["203.0.113.9"]["sources"], "feature")
# same IP on both sides becomes "both"
store.upsert({"ip": "198.51.100.9", "tier": watch.TIER_MALICIOUS,
"score": 80.0, "total": 50, "window": 3600,
"rules": {"probe_java": 6},
"reasons": ["探测 6 次"]}, "2026-10-17T16:11:00Z")
store.sync_fused("2026-10-17T16:12:00Z")
merged = {item["ip"]: item for item in store.fused_items()}
self.assertEqual(merged["198.51.100.9"]["sources"], "both")
# watch-only behaviour must not enter the block list
store.replace_behavior_profiles([
_behavior("192.0.2.44", verdict="仅告警(共享出口)")])
store.sync_fused("2026-10-17T16:20:00Z")
ips = {item["ip"] for item in store.fused_items()}
self.assertNotIn("192.0.2.44", ips)
self.assertIn("203.0.113.9", ips) # feature rows stay forever
finally:
store.close()
def test_push_throttle_24h(self):
with tempfile.TemporaryDirectory() as directory:
store = watch.Store(os.path.join(directory, "ips.db"))
try:
now = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
store.replace_behavior_profiles([_behavior("198.51.100.7")])
store.sync_fused(now)
# first sync suppresses historical rows; a fresh detection is due
with store.lock, store.conn:
store.conn.execute(
"UPDATE ip_fused SET last_pushed_at=NULL WHERE ip=?",
("198.51.100.7",))
due = store.due_pushes(active_hours=24, cooldown_hours=24)
self.assertEqual([row["ip"] for row in due], ["198.51.100.7"])
store.mark_pushed(["198.51.100.7"], now)
self.assertEqual(store.due_pushes(24, 24), []) # throttled
old = time.strftime("%Y-%m-%dT%H:%M:%SZ",
time.gmtime(time.time() - 25 * 3600))
with store.lock, store.conn:
store.conn.execute(
"UPDATE ip_fused SET last_pushed_at=? WHERE ip=?",
(old, "198.51.100.7"))
due = store.due_pushes(active_hours=24, cooldown_hours=24)
self.assertEqual([row["ip"] for row in due], ["198.51.100.7"])
finally:
store.close()
if __name__ == "__main__":
unittest.main()