36 KiB
36 KiB
In [ ]:
import numpy as np, pandas as pd, matplotlib.pyplot as plt, re
%matplotlib inline
pd.set_option("display.width", 170); pd.set_option("display.max_columns", None); pd.set_option("display.max_colwidth", 70)
DATA = "/opt/csai/data/web_requests/web_requests.csv" # chỉ có trên máy lớp; đã ẩn danh
req = pd.read_csv(DATA, keep_default_na=False) # keep_default_na: giữ nguyên chuỗi, không biến "" thành NaN
print("📦 req =", req.shape, "| cột:", list(req.columns))
req.head(4)In [ ]:
days = sorted(req.day.unique())
print("Số request :", f"{len(req):,}")
print("Số IP :", f"{req.ip_token.nunique():,}")
print("Khoảng ngày:", days[0], "->", days[-1], f"({len(days)} ngày có log)")
n_ban = int(req.label.sum())
print("Nhãn 1 (IP từng bị fail2ban chặn):", f"{n_ban:,}", "request",
"(%.1f%%)" % (100*req.label.mean()), "từ", int(req[req.label==1].ip_token.nunique()), "IP")In [ ]:
print("10 đường dẫn được các IP-bị-chặn xin nhiều nhất:")
for path, n in req[req.label==1].path.value_counts().head(10).items():
print(" %5d %s" % (n, path[:85]))
# đường dẫn nào xuất hiện ở CẢ hai nhãn (vừa IP sạch vừa IP bị chặn cùng xin)?
ca_hai = req.groupby("path").label.agg(["min","max"])
print("\nSố đường dẫn xuất hiện với CẢ hai nhãn:", int((ca_hai["min"] != ca_hai["max"]).sum()),
"<- ví dụ '/' vừa do người thật xin vừa do máy quét xin")In [ ]:
print("Ô trống (NaN):", int(req.isna().sum().sum()))
print("status là số? ", req.status.dtype, "| ví dụ các mã:", sorted(req.status.unique())[:8], "...")
print("method:", req.method.value_counts().to_dict())
print("query_len: min %d, trung vị %d, max %d" % (req.query_len.min(), int(req.query_len.median()), req.query_len.max()))In [ ]:
# server-side code, bí mật, backup -- KHÔNG tính .txt/.xml/.ico (robots.txt, sitemap.xml, favicon.ico là bình thường)
EXT = re.compile(r"\.(?:php|env|git|asp|aspx|jsp|cgi|sh|sql|bak|zip|tar|gz|ini|conf|yml|yaml|log)(?:$|/|\.)", re.I)
TU_TN = re.compile(r"wp-|admin|login|shell|cgi-bin|phpmyadmin|config|backup|setup|install|xmlrpc|eval|passwd", re.I)
def dac_trung(d):
p = d.path.astype(str)
return pd.DataFrame({
"path_len": p.str.len(), # đường dẫn dài
"depth": p.str.count("/"), # số tầng thư mục
"dot_segment": p.str.contains(r"/\.[^/]").astype(int), # /.env /.git -> đoạn bắt đầu bằng dấu chấm
"risky_ext": p.str.contains(EXT).astype(int), # đuôi file nguy hiểm
"attack_word": p.str.contains(TU_TN).astype(int), # từ hay gặp trong tấn công
"pct_encoded": p.str.count("%"), # số lần mã hoá %xx
"traversal": p.str.contains(r"\.\.|%2e|%%32%65", case=False).astype(int), # leo thư mục ../
"not_a_path": (~p.str.startswith("/")).astype(int), # "host:port" = dò proxy, không phải đường dẫn thật
"query_len": d.query_len,
"method_post": (d.method == "POST").astype(int),
})
X = dac_trung(req) # 📦 X = bảng 41142 × 10 con số
y = req.label # 📦 y = nhãn 0/1 (từ fail2ban)
print("📦 X =", X.shape, "| đặc trưng:", list(X.columns))
X.head(3)In [ ]:
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import GroupShuffleSplit, train_test_split
from sklearn.metrics import precision_score, recall_score, confusion_matrix
# chia theo IP: cùng một IP không nằm ở cả hai bên (groups = ip_token)
tr, te = next(GroupShuffleSplit(n_splits=1, test_size=0.3, random_state=42).split(X, y, groups=req.ip_token))
rf = RandomForestClassifier(n_estimators=200, max_depth=8, class_weight="balanced", random_state=42, n_jobs=2)
rf.fit(X.iloc[tr], y.iloc[tr])
pred = rf.predict(X.iloc[te]); yt = y.iloc[te].to_numpy()
tp = int(((pred==1)&(yt==1)).sum()); fp = int(((pred==1)&(yt==0)).sum()); fn = int(((pred==0)&(yt==1)).sum())
trung = req.iloc[te].ip_token.isin(set(req.iloc[tr].ip_token)).mean() # % request test có IP trùng phần học
print("CHIA THEO IP (test = %d request từ %d IP; %.0f%% request test có IP trùng phần học):"
% (len(te), req.iloc[te].ip_token.nunique(), 100*trung))
print(" precision %.2f recall %.2f | TP=%d FP=%d FN=%d" % (precision_score(yt,pred), recall_score(yt,pred), tp, fp, fn))In [ ]:
# chia NGẪU NHIÊN từng dòng (KHÔNG theo IP) để so sánh
tr2, te2 = train_test_split(np.arange(len(req)), test_size=0.3, stratify=y, random_state=42)
rf2 = RandomForestClassifier(n_estimators=200, max_depth=8, class_weight="balanced", random_state=42, n_jobs=2).fit(X.iloc[tr2], y.iloc[tr2])
pred2 = rf2.predict(X.iloc[te2]); yt2 = y.iloc[te2].to_numpy()
trung2 = req.iloc[te2].ip_token.isin(set(req.iloc[tr2].ip_token)).mean()
print("CHIA NGẪU NHIÊN (%.0f%% request test có IP trùng phần học):" % (100*trung2))
print(" precision %.2f recall %.2f" % (precision_score(yt2,pred2), recall_score(yt2,pred2)))
print("\nĐộ quan trọng đặc trưng (mô hình chia-theo-IP):")
imp = pd.Series(rf.feature_importances_, index=X.columns).sort_values(ascending=False)
print(" " + " ".join("%s %.3f" % (k,v) for k,v in imp.head(5).items()))In [ ]:
imp_full = pd.Series(rf.feature_importances_, index=X.columns).sort_values()
ax = imp_full.plot.barh(figsize=(8, 4), color="tab:red")
ax.set_xlabel("độ quan trọng (tổng = 1)"); ax.set_title("Log thật: đặc trưng nào tố cáo request độc hại")
for i, v in enumerate(imp_full):
ax.text(v + .004, i, "%.3f" % v, va="center", fontsize=9)
plt.tight_layout(); plt.show()In [ ]:
t = req.iloc[te].assign(pred=pred)
ip = t.groupby("ip_token").agg(label=("label","max"), flagged=("pred","mean"))
ip_co = ip.flagged >= 0.5 # gắn cờ một IP nếu >= 50% request của nó bị gắn cờ
print("Theo IP (cờ nếu >=50%% request bị gắn cờ):")
print(" IP bị-chặn bắt được : %d / %d" % (int((ip_co & (ip.label==1)).sum()), int((ip.label==1).sum())))
print(" IP chưa-chặn bị cờ : %d / %d" % (int((ip_co & (ip.label==0)).sum()), int((ip.label==0).sum())))
# các request bị gắn cờ nhưng IP CHƯA từng bị chặn (nhãn 0) -- "lỗi" của mô hình?
fpm = (t.pred==1) & (t.label==0)
hinh_tancong = X.iloc[te][["dot_segment","risky_ext","attack_word","traversal","not_a_path"]].to_numpy().any(axis=1)
print("\nTrong %d request bị cờ nhưng IP chưa bị chặn:" % int(fpm.sum()))
print(" %d trông ĐÚNG LÀ tấn công (dot-file / đuôi nguy hiểm / từ khoá / leo thư mục / dò proxy)" % int((fpm.to_numpy() & hinh_tancong).sum()))
print(" -> nhiều khả năng là tấn công mà fail2ban CHƯA chặn = nhãn sai, không phải mô hình sai")
print(" %d còn lại là đường dẫn thường (favicon.ico, robots.txt...) mà máy quét cũng xin" % int((fpm.to_numpy() & ~hinh_tancong).sum()))