- 特征检测(规则/签名)与行为检测(24h 统计)单进程运行 - 融合名单按首次发现时间倒序,展示位置/归属(GeoIP + ASN) - 钉钉融合推送(加签、24h 节流、去重) - 页面状态灯、CSV 导出、Docker 镜像构建文件
104 lines
4.7 KiB
Python
104 lines
4.7 KiB
Python
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()
|