413 lines
13 KiB
Python
413 lines
13 KiB
Python
"""
|
||
Template-based natural language feature descriptions.
|
||
|
||
Uses value-range → text mappings to generate Chinese natural language
|
||
descriptions of cluster features. Consumed by cluster_overview.html and
|
||
entity_profile.html for human-readable cluster summaries.
|
||
|
||
Data sources
|
||
------------
|
||
- ``analysis.tls_ref.get_tls_ref_dict()`` : ``field_code → {meaning, data_type}``
|
||
- ``ClusterFeature`` model rows : feature_name, mean, std, distinguishing_score
|
||
|
||
Usage::
|
||
|
||
from analysis.nl_describe import describe_feature, describe_cluster
|
||
|
||
text = describe_feature("8ack", 720.5, std_dev=2.1)
|
||
# "中等数据包 (720 bytes),比全局均值高2.1个标准差"
|
||
|
||
summary = describe_cluster(features, top_n=5)
|
||
# "此簇特征: 低延迟(23ms), 大数据包(1520 bytes), ..."
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from typing import Optional, Sequence
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Category mapping: field_code → semantic category
|
||
# ---------------------------------------------------------------------------
|
||
|
||
CATEGORY_EXACT: dict[str, str] = {
|
||
# ── latency ──
|
||
"4dur": "latency",
|
||
"2tmo": "latency",
|
||
"8dbd": "latency",
|
||
# ── payload bytes ──
|
||
"8ack": "packet_bytes",
|
||
"8byt": "packet_bytes",
|
||
# ── packet count ──
|
||
"8pak": "packet_count",
|
||
"8ppk": "packet_count",
|
||
# ── port ──
|
||
":prs": "port",
|
||
":prd": "port",
|
||
# ── TLS version ──
|
||
"0ver": "tls_version",
|
||
# ── key size ──
|
||
"4ksz": "key_size",
|
||
# ── cipher code ──
|
||
"0cph": "cipher_code",
|
||
# ── curve code ──
|
||
"0crv": "curve_code",
|
||
# ── session / offset ──
|
||
"8seq": "session_offset",
|
||
"8ses": "session_offset",
|
||
"8did": "session_offset",
|
||
# ── IP protocol ──
|
||
"1ipp": "ip_protocol",
|
||
# ── TLS random ──
|
||
"0rnt": "tls_random_time",
|
||
"0rnd": "tls_random",
|
||
# ── database refs ──
|
||
"4dbn": "database_ref",
|
||
"4srs": "device_ref",
|
||
# ── recovery flags ──
|
||
"cnrs": "recovery",
|
||
"isrs": "recovery",
|
||
}
|
||
|
||
# Substring-based matching (checked after exact match fails)
|
||
CATEGORY_SUBSTR: list[tuple[str, str]] = [
|
||
("latd", "latitude"),
|
||
("lond", "longitude"),
|
||
("orgn", "organization"),
|
||
("ispn", "isp"),
|
||
("anon", "anonymity"),
|
||
("doma", "domain"),
|
||
("city", "city"),
|
||
("_lat", "latitude"),
|
||
("_lon", "longitude"),
|
||
("latitude", "latitude"),
|
||
("longitude", "longitude"),
|
||
("duration", "latency"),
|
||
("delay", "latency"),
|
||
("bytes", "packet_bytes"),
|
||
("packets", "packet_count"),
|
||
("count", "count"),
|
||
("ratio", "ratio"),
|
||
("tls_ver", "tls_version"),
|
||
("version", "tls_version"),
|
||
]
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# TLS version code → human description
|
||
# ---------------------------------------------------------------------------
|
||
|
||
TLS_VERSION_MAP: dict[int, str] = {
|
||
0x0000: "SSL 2.0",
|
||
0x0300: "SSL 3.0",
|
||
0x0301: "TLS 1.0",
|
||
0x0302: "TLS 1.1",
|
||
0x0303: "TLS 1.2",
|
||
0x0304: "TLS 1.3",
|
||
0x7F00: "TLS 1.3 (draft)",
|
||
0x7F12: "TLS 1.3 (draft 18)",
|
||
0xFEFF: "DTLS 1.0",
|
||
}
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Templates: category → [(predicate, template_string), ...]
|
||
#
|
||
# Template variables available:
|
||
# {v} — value (formatted to 1 decimal unless overridden)
|
||
# {v:.0f} — value with custom format
|
||
# {sv} — signed value (e.g. "+2.1" for positive zscore)
|
||
# {av} — absolute value (e.g. "2.1")
|
||
# {z:.1f} — z-score signed (preserved in deviation suffix)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
TEMPLATES: dict[str, list[tuple]] = {
|
||
"latency": [
|
||
(lambda v: v > 1000, "极高延迟 ({v:.0f}ms)"),
|
||
(lambda v: v > 500, "高延迟 ({v:.0f}ms)"),
|
||
(lambda v: v > 200, "中等延迟 ({v:.0f}ms)"),
|
||
(lambda v: v > 50, "较低延迟 ({v:.0f}ms)"),
|
||
(lambda v: True, "低延迟 ({v:.0f}ms)"),
|
||
],
|
||
"packet_bytes": [
|
||
(lambda v: v > 1500, "大数据包 ({v:.0f} bytes, 超过MTU)"),
|
||
(lambda v: v > 1000, "较大数据包 ({v:.0f} bytes)"),
|
||
(lambda v: v > 400, "中等数据包 ({v:.0f} bytes)"),
|
||
(lambda v: True, "小数据包 ({v:.0f} bytes)"),
|
||
],
|
||
"packet_count": [
|
||
(lambda v: v > 200, "大量数据包 ({v:.0f} 个)"),
|
||
(lambda v: v > 50, "中等数据包数 ({v:.0f} 个)"),
|
||
(lambda v: True, "少量数据包 ({v:.0f} 个)"),
|
||
],
|
||
"port": [
|
||
(lambda v: v <= 1024, "系统端口 ({v:.0f})"),
|
||
(lambda v: v <= 49151, "注册端口 ({v:.0f})"),
|
||
(lambda v: True, "动态/私有端口 ({v:.0f})"),
|
||
],
|
||
"tls_version": [
|
||
(lambda v: 0x0304 <= v <= 0x0304, "TLS 1.3 (最新)"),
|
||
(lambda v: 0x0303 <= v < 0x0304, "TLS 1.2 (主流)"),
|
||
(lambda v: 0x0302 <= v < 0x0303, "TLS 1.1 (已弃用)"),
|
||
(lambda v: 0x0301 <= v < 0x0302, "TLS 1.0 (过时)"),
|
||
(lambda v: 0x0300 <= v < 0x0301, "SSL 3.0 (不安全)"),
|
||
(lambda v: v < 0x0300, "SSL 2.0 (极不安全)"),
|
||
(lambda v: v > 0x0304, "DTLS/实验性TLS (0x{v:.0X})"),
|
||
(lambda v: True, "未知TLS版本 (0x{v:.0X})"),
|
||
],
|
||
"key_size": [
|
||
(lambda v: v >= 384, "高强度密钥 ({v:.0f} bits)"),
|
||
(lambda v: v >= 256, "强密钥 ({v:.0f} bits)"),
|
||
(lambda v: v >= 128, "标准密钥 ({v:.0f} bits)"),
|
||
(lambda v: v >= 64, "弱密钥 ({v:.0f} bits)"),
|
||
(lambda v: True, "极弱密钥 ({v:.0f} bits)"),
|
||
],
|
||
"ip_protocol": [
|
||
(lambda v: abs(v - 6) < 0.5, "TCP 协议 ({v:.0f})"),
|
||
(lambda v: abs(v - 17) < 0.5, "UDP 协议 ({v:.0f})"),
|
||
(lambda v: abs(v - 1) < 0.5, "ICMP 协议 ({v:.0f})"),
|
||
(lambda v: abs(v - 2) < 0.5, "IGMP 协议 ({v:.0f})"),
|
||
(lambda v: True, "IP 协议 #{v:.0f}"),
|
||
],
|
||
"session_offset": [
|
||
(lambda v: v > 10000, "大型会话 (偏移 {v:.0f})"),
|
||
(lambda v: v > 1000, "中型会话 (偏移 {v:.0f})"),
|
||
(lambda v: True, "小型会话 (偏移 {v:.0f})"),
|
||
],
|
||
"latitude": [
|
||
(lambda v: v > 60, "高纬度地区 ({v:.1f}°)"),
|
||
(lambda v: v > 23.5, "中纬度地区 — 北温带 ({v:.1f}°)"),
|
||
(lambda v: v >= -23.5, "低纬度地区 — 热带 ({v:.1f}°)"),
|
||
(lambda v: v >= -60, "中纬度地区 — 南温带 ({v:.1f}°)"),
|
||
(lambda v: True, "高纬度地区 — 南极 ({v:.1f}°)"),
|
||
],
|
||
"longitude": [
|
||
(lambda v: v > 120 or v < -120, "远东地区 ({v:.1f}°)"),
|
||
(lambda v: v > 60 or v < -60, "中亚/东欧地区 ({v:.1f}°)"),
|
||
(lambda v: v > 0 or v < 0, "西欧/非洲地区 ({v:.1f}°)"),
|
||
(lambda v: True, "本初子午线附近 ({v:.1f}°)"),
|
||
],
|
||
"cipher_code": [
|
||
(lambda v: True, "加密套件代码 0x{v:.0X}"),
|
||
],
|
||
"curve_code": [
|
||
(lambda v: True, "椭圆曲线代码 0x{v:.0X}"),
|
||
],
|
||
"tls_random_time": [
|
||
(lambda v: True, "TLS 随机时间戳 ({v:.0f})"),
|
||
],
|
||
"tls_random": [
|
||
(lambda v: True, "TLS 随机值"),
|
||
],
|
||
"database_ref": [
|
||
(lambda v: True, "数据库引用 #{v:.0f}"),
|
||
],
|
||
"device_ref": [
|
||
(lambda v: True, "设备标识 #{v:.0f}"),
|
||
],
|
||
"recovery": [
|
||
(lambda v: v >= 0.5, "可恢复流"),
|
||
(lambda v: True, "不可恢复流"),
|
||
],
|
||
"organization": [
|
||
(lambda v: True, "组织指标 ({v:.1f})"),
|
||
],
|
||
"isp": [
|
||
(lambda v: True, "ISP 指标 ({v:.1f})"),
|
||
],
|
||
"anonymity": [
|
||
(lambda v: True, "匿名状态指标 ({v:.1f})"),
|
||
],
|
||
"domain": [
|
||
(lambda v: True, "域名指标 ({v:.1f})"),
|
||
],
|
||
"city": [
|
||
(lambda v: True, "城市指标 ({v:.1f})"),
|
||
],
|
||
"count": [
|
||
(lambda v: v > 100, "计数很高 ({v:.0f})"),
|
||
(lambda v: v > 50, "计数较高 ({v:.0f})"),
|
||
(lambda v: True, "计数正常 ({v:.0f})"),
|
||
],
|
||
"ratio": [
|
||
(lambda v: v > 0.8, "占比极高 ({v:.1%})"),
|
||
(lambda v: v > 0.5, "占比偏高 ({v:.1%})"),
|
||
(lambda v: v >= 0.0, "占比较低 ({v:.1%})"),
|
||
(lambda v: True, "占比 ({v:.1%})"),
|
||
],
|
||
}
|
||
|
||
# Default template for unrecognised categories / fallback
|
||
DEFAULT_TEMPLATES: list[tuple] = [
|
||
(lambda v: True, "{meaning} = {v:.2f}"),
|
||
]
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Public API
|
||
# ---------------------------------------------------------------------------
|
||
|
||
def _resolve_category(feature_name: str) -> str | None:
|
||
"""Map a ``feature_name`` (column name) to a template category string.
|
||
|
||
Tries exact match against ``CATEGORY_EXACT`` first, then substring
|
||
match against ``CATEGORY_SUBSTR``. Returns ``None`` if no match.
|
||
"""
|
||
# Exact match
|
||
cat = CATEGORY_EXACT.get(feature_name)
|
||
if cat is not None:
|
||
return cat
|
||
|
||
# Substring match
|
||
lowered = feature_name.lower()
|
||
for sub, cat_label in CATEGORY_SUBSTR:
|
||
if sub in lowered:
|
||
return cat_label
|
||
|
||
return None
|
||
|
||
|
||
def _lookup_meaning(feature_name: str) -> str:
|
||
"""Return the Chinese meaning of *feature_name* from the TLS reference DB.
|
||
|
||
Falls back to *feature_name* itself when no entry is found.
|
||
"""
|
||
try:
|
||
from analysis.tls_ref import get_tls_ref_dict
|
||
|
||
ref = get_tls_ref_dict()
|
||
entry = ref.get(feature_name)
|
||
if entry is not None:
|
||
return entry.get("meaning", feature_name)
|
||
except Exception:
|
||
pass
|
||
return feature_name
|
||
|
||
|
||
def describe_feature(
|
||
feature_name: str,
|
||
value: float,
|
||
std_dev: float | None = None,
|
||
cluster_std: float | None = None,
|
||
) -> str:
|
||
"""Generate a Chinese natural-language description for a single feature.
|
||
|
||
Parameters
|
||
----------
|
||
feature_name : str
|
||
Column name (e.g. ``"8ack"``, ``":ips.latd"``).
|
||
value : float
|
||
The feature's mean value in the current context (cluster mean).
|
||
std_dev : float | None
|
||
Optional Z-score magnitude (``abs(distinguishing_score)``). When
|
||
provided and >= 1.0, appends a deviation suffix.
|
||
cluster_std : float | None
|
||
Optional within-cluster standard deviation. Describes how spread
|
||
the values are within this cluster (high = scattered, low = tight).
|
||
|
||
Returns
|
||
-------
|
||
str
|
||
Natural-language description string.
|
||
"""
|
||
meaning = _lookup_meaning(feature_name)
|
||
category = _resolve_category(feature_name)
|
||
|
||
# Select templates
|
||
tmpl_list = TEMPLATES.get(category) if category else None
|
||
if tmpl_list is None:
|
||
tmpl_list = DEFAULT_TEMPLATES
|
||
|
||
# Evaluate predicates
|
||
text = ""
|
||
for predicate, template in tmpl_list:
|
||
try:
|
||
if predicate(value):
|
||
text = template.format(v=value, sv=_signed(value), av=abs(value),
|
||
meaning=meaning)
|
||
break
|
||
except Exception:
|
||
continue
|
||
|
||
if not text:
|
||
text = f"{meaning} = {value:.2f}"
|
||
|
||
# Append deviation context
|
||
if std_dev is not None and std_dev >= 1.0:
|
||
text += f",比全局均值高{std_dev:.1f}个标准差"
|
||
|
||
# Append within-cluster variance context
|
||
if cluster_std is not None and cluster_std > 0:
|
||
if cluster_std > 2.0:
|
||
text += "(簇内离散度高)"
|
||
elif cluster_std < 0.3:
|
||
text += "(簇内高度集中)"
|
||
|
||
return text
|
||
|
||
|
||
def describe_cluster(
|
||
features: Sequence[dict],
|
||
top_n: int = 5,
|
||
) -> str:
|
||
"""Generate a 2-3 sentence natural-language summary for a cluster.
|
||
|
||
Parameters
|
||
----------
|
||
features : list[dict]
|
||
Each dict must include:
|
||
|
||
- ``feature_name`` : str
|
||
- ``mean`` : float (cluster mean)
|
||
- ``distinguishing_score`` : float (Z-score; may be signed)
|
||
|
||
Optional keys used when present:
|
||
|
||
- ``std`` : float (cluster std dev)
|
||
|
||
top_n : int
|
||
Number of most-distinguishing features to include (default 5).
|
||
|
||
Returns
|
||
-------
|
||
str
|
||
A short paragraph describing the cluster's distinguishing traits.
|
||
|
||
Example:
|
||
``"低延迟 (23ms),比全局均值高3.1个标准差; 大数据包 (1520 bytes),比全局均值高2.3个标准差; 系统端口 (443)"``
|
||
"""
|
||
if not features:
|
||
return "无显著区分特征"
|
||
|
||
# Sort by |distinguishing_score| descending
|
||
ranked = sorted(
|
||
features,
|
||
key=lambda f: abs(f.get("distinguishing_score", 0.0)),
|
||
reverse=True,
|
||
)[:top_n]
|
||
|
||
sentences: list[str] = []
|
||
for f in ranked:
|
||
name = f.get("feature_name", "")
|
||
mean = f.get("mean")
|
||
score = abs(f.get("distinguishing_score", 0.0))
|
||
cstd = f.get("std")
|
||
|
||
if mean is None or name == "":
|
||
continue
|
||
|
||
desc = describe_feature(name, float(mean), std_dev=score if score >= 1.0 else None,
|
||
cluster_std=float(cstd) if cstd is not None else None)
|
||
sentences.append(desc)
|
||
|
||
if not sentences:
|
||
return "无显著区分特征"
|
||
|
||
return ";".join(sentences) + "。"
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Helpers
|
||
# ---------------------------------------------------------------------------
|
||
|
||
def _signed(value: float) -> str:
|
||
"""Return a signed string representation, e.g. ``"+2.1"``."""
|
||
return f"{value:+.1f}"
|