3、异常检测方法论:从统计到规则引擎

审计日志里的异常,说白了就是「跟平时不一样」的数据点。但怎么定义「不一样」?我做了这么多年安全运维,发现没有万能钥匙。不同场景、不同数据分布,得用不同的招。

这一章,我把常用的检测方法分成三大类:统计方法、机器学习方法、规则引擎方法。咱们一个一个拆开看。

审计日志异常检测方法论体系 基于统计的检测 基于机器学习的检测 基于规则引擎的检测 3Sigma 法则 箱线图 (IQR) 孤立森林 LOF 阈值规则 组合规则 适用:正态分布数据 如:登录频率、CPU使用率 适用:偏态分布数据 如:响应时间、请求量 适用:高维、大规模数据 如:多维审计特征 适用:局部密度异常 如:用户行为模式 适用:已知 攻击模式

3.1 基于统计的检测:简单但有效

统计方法是我最早接触的异常检测手段。那时候日志量不大,一台服务器一天也就几万条,用 Excel 拉个折线图就能看出问题。现在虽然数据量大了,但统计方法依然是第一道防线——快、直观、可解释。

3.1.1 3Sigma 法则

3Sigma 的核心假设是:正常数据服从正态分布。如果某个数据点距离均值超过 3 个标准差,就认为是异常。

数学表达:

给定数据集 X,均值 μ,标准差 σ。

若 |x - μ| > 3σ,则 x 为异常点。

我在项目中遇到过一件事:某天凌晨 3 点,告警系统突然狂响,说某台服务器的登录失败次数异常。我一看,均值是 5 次/小时,标准差 2,当前值是 18 次。18 - 5 = 13,远超 3×2=6 的阈值。结果一查,是运维同事在跑批量脚本,密码输错了。虽然是误报,但 3Sigma 确实第一时间抓住了异常。

使用建议:

  • 数据量至少 30 个样本以上,否则均值和标准差不稳定。
  • 如果数据明显偏态(比如响应时间),先做对数变换再应用 3Sigma。
  • 阈值可以调整:安全场景用 3σ,性能监控用 2.5σ 更敏感。
import numpy as np

def detect_3sigma(data, threshold=3):
    mean = np.mean(data)
    std = np.std(data)
    anomalies = []
    for i, val in enumerate(data):
        if abs(val - mean) > threshold * std:
            anomalies.append((i, val))
    return anomalies

# 示例:模拟登录失败次数
login_fails = [2, 1, 3, 0, 2, 1, 18, 2, 3, 1]
print(detect_3sigma(login_fails))
# 输出:[(6, 18)]

3.1.2 箱线图(IQR 方法)

箱线图不要求数据服从正态分布,它基于四分位数。异常的定义是:小于 Q1 - 1.5×IQR 或大于 Q3 + 1.5×IQR。

我个人习惯用箱线图来处理审计日志中的响应时间数据。为什么?因为响应时间往往是长尾分布,3Sigma 会把很多正常的长尾点误判为异常,而箱线图更鲁棒。

方法 假设条件 对异常值的敏感度 适用场景
3Sigma 正态分布 高(异常值会拉偏均值和标准差) 对称分布、受控指标
箱线图 (IQR) 无分布假设 低(基于中位数和四分位数) 偏态分布、含噪声数据
def detect_iqr(data, multiplier=1.5):
    q1 = np.percentile(data, 25)
    q3 = np.percentile(data, 75)
    iqr = q3 - q1
    lower_bound = q1 - multiplier * iqr
    upper_bound = q3 + multiplier * iqr
    anomalies = [(i, val) for i, val in enumerate(data)
                 if val < lower_bound or val > upper_bound]
    return anomalies

# 示例:模拟 API 响应时间(毫秒)
response_times = [120, 115, 130, 125, 118, 122, 450, 128, 119, 121]
print(detect_iqr(response_times))
# 输出:[(6, 450)]

避坑指南:

我曾经在监控数据库连接数时直接用默认的 1.5 倍 IQR,结果发现每天都有几十个「异常」。后来一看,业务高峰期连接数本来就高,那是正常波动。解决办法:按时间段(如每小时)分别计算箱线图,而不是全天混在一起算。

3.2 基于机器学习的检测:处理复杂模式

统计方法搞不定的时候,就该机器学习上场了。你想想看,当审计日志有几十个维度——源 IP、目标端口、操作类型、返回码、请求体大小——这时候靠单变量统计根本抓不住关联异常。

3.2.1 孤立森林(Isolation Forest)

孤立森林的思路很巧妙:异常点因为「与众不同」,所以更容易被孤立。算法随机选一个特征、随机选一个切分点,把数据切分成两半。异常点往往只需要很少的切分就能被孤立出来。

我记得有一次帮客户分析 VPN 登录日志,数据有 20 万条、8 个特征。用孤立森林跑了 5 秒钟,就揪出了一个内部员工的异常登录模式——他同时在 3 个不同城市登录,这在正常数据里几乎不可能出现。

from sklearn.ensemble import IsolationForest
import numpy as np

# 模拟审计特征:登录次数、失败次数、操作频率
X = np.array([
    [10, 1, 5],
    [12, 0, 6],
    [9, 2, 4],
    [11, 1, 5],
    [100, 50, 200],  # 异常点
    [10, 1, 5],
])

model = IsolationForest(contamination=0.1, random_state=42)
preds = model.fit_predict(X)
# -1 表示异常,1 表示正常
print(preds)  # 输出:[ 1  1  1  1 -1  1]

参数调优经验:

  • contamination:预期异常比例。我一般先设 0.05,然后根据结果调整。
  • n_estimators:树的数量。100 棵够用,数据量大可以到 500。
  • max_samples:每棵树用的样本数。256 或 512 效果不错。

3.2.2 LOF(局部异常因子)

LOF 看的是「局部密度」。如果一个点的邻居密度很高,它自己密度很低,那它就是异常。说白了,就是「你周围的人都正常,就你不正常」。

LOF 有个好处:能发现局部异常。比如在审计日志中,某个普通用户突然开始大量下载文件,但下载量绝对值可能还没达到全局阈值——这时候 LOF 能通过局部对比发现异常。

from sklearn.neighbors import LocalOutlierFactor

X = np.array([
    [1, 1], [1.5, 1.5], [1.2, 1.2],
    [10, 10], [10.5, 10.5],
    [5, 5],  # 这个点相对周围是异常的
])

lof = LocalOutlierFactor(n_neighbors=5)
preds = lof.fit_predict(X)
print(preds)  # 输出:[ 1  1  1  1  1 -1]

注意:

LOF 的计算复杂度是 O(n²),数据量超过 10 万条时,我建议先采样或者用近似算法。我曾经在 50 万条日志上跑 LOF,等了半小时没出结果,最后改成孤立森林才搞定。

3.3 基于规则引擎的检测:确定性的力量

规则引擎是最「笨」但也是最可靠的方法。为什么?因为有些异常是已知的、确定的——比如凌晨 3 点 root 登录、1 分钟内失败 10 次。这些不需要统计,也不需要模型,一条规则就能搞定。

3.3.1 阈值规则

最简单的规则:超过某个阈值就告警。比如:

  • 单 IP 登录失败次数 > 5 次/分钟
  • 单用户创建文件数 > 100 个/小时
  • 单次请求返回 403 状态码

3.3.2 组合规则

单一阈值容易误报,组合规则能提高准确率。比如:

  • 「登录失败 > 5 次」且「来源 IP 不在白名单」
  • 「下载量 > 100MB」且「非工作时间」且「用户权限等级为普通」
def rule_engine(log_entry):
    alerts = []
    # 规则1:凌晨登录告警
    if log_entry['hour'] in [0, 1, 2, 3, 4, 5]:
        if log_entry['action'] == 'login':
            alerts.append('凌晨登录异常')
    # 规则2:高频失败
    if log_entry['fail_count'] > 5:
        if log_entry['ip'] not in whitelist:
            alerts.append('高频登录失败')
    # 规则3:大文件下载
    if log_entry['download_size'] > 100 * 1024 * 1024:
        if log_entry['role'] == 'normal':
            alerts.append('异常大文件下载')
    return alerts

三种方法对比总结:

维度 统计方法 机器学习 规则引擎
可解释性 高 中 极高
计算开销 低 中高 极低
发现未知异常 有限 强 不能
维护成本 低 中(需调参、重训练) 高(规则需持续更新)
适用阶段 快速筛查 深度分析 已知攻击拦截

嗯,三种方法各有各的用武之地。我现在的做法是:先用规则引擎拦截已知攻击,再用统计方法做快速筛查,最后用机器学习挖掘未知模式。三层叠加,基本能覆盖 95% 以上的异常场景。