use security_rust::{
Decision, MemoryStore, MemoryThrottleStore, RequestContext, RiskAssessment, RiskLevel, Scanner,
SessionConfig, SessionGuard, SessionVerdict, Throttle, ThrottleConfig, ThrottleDecision,
ThrottleOutcome,
};
const T0: u64 = 1_700_000_000;
const FP_ALICE: &str = "ip=10.0.0.1|ua=Firefox/128";
#[derive(Debug, Clone, PartialEq, Eq)]
enum Action {
Allow,
StepUp,
Reject(String),
}
struct Request<'a> {
title: &'a str,
ip: &'a str,
account: &'a str,
payload: &'a str,
password_ok: bool,
ctx: RequestContext<'a>,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let scanner = Scanner::new();
let throttle = Throttle::new(
MemoryThrottleStore::new(),
ThrottleConfig {
threshold: 5,
window_secs: 60,
ban_secs: 900,
},
);
let guard = SessionGuard::new(MemoryStore::new(), SessionConfig::default());
println!("{}", security_rust::pet::ASCII);
println!("\nsecurity-rust · 端到端 WAF 链路示例");
println!("固定时钟 T0={T0};限流 5 次失败 / 60s 窗口 → 封禁 900s");
let login = RequestContext {
token: "tok-alice-1",
subject: "alice",
fingerprint: FP_ALICE,
location: Some("CN-BJ"),
coords: Some((39.9042, 116.4074)),
signature: None,
at: Some(T0),
};
let verdict = guard.bind(&login, T0)?;
println!(
"\nlogin : alice @ CN-BJ · token=tok-alice-1\n {}{}",
describe(&verdict),
if verdict.is_allowed() {
"(异地只影响 verdict,不阻断登录)"
} else {
""
}
);
let mut allowed = 0usize;
let mut step_ups = 0usize;
let mut rejected = 0usize;
for req in requests().iter() {
match handle(req, &scanner, &throttle, &guard, req.ctx.at.unwrap_or(T0)) {
Action::Allow => {
allowed += 1;
println!(" verdict : ✅ 放行");
}
Action::StepUp => {
step_ups += 1;
println!(" verdict : ⚠️ 二次验证(step-up),带外确认后再放行");
}
Action::Reject(reason) => {
rejected += 1;
println!(" verdict : ⛔ 拒绝 —— {reason}");
}
}
}
let after_ban = T0 + 1_000;
println!("\n── 封禁到期观察 ── t={after_ban}");
match throttle.check("ip:203.0.113.7", after_ban) {
ThrottleDecision::Allow { remaining } => println!(
" throttle : 封禁已自动到期(now >= until),剩余额度 {remaining} —— 无需人工解封"
),
other => println!(" throttle : {other:?}"),
}
let purged = throttle.purge_expired(after_ban)?;
println!(" throttle : purge_expired 清掉 {purged} 条已过期状态");
println!("\n汇总:放行 {allowed} · 二次验证 {step_ups} · 拒绝 {rejected}");
Ok(())
}
fn handle(
req: &Request<'_>,
scanner: &Scanner,
throttle: &Throttle<MemoryThrottleStore>,
guard: &SessionGuard<MemoryStore>,
now: u64,
) -> Action {
println!("\n── {} ── t={now}", req.title);
let hits = scanner.scan(req.payload);
if hits.is_empty() {
println!(" scan : 无命中");
} else {
for h in &hits {
println!(
" scan : [{}] {} · {} · offset={} · 命中 {:?}",
h.category, h.attack_type, h.severity, h.offset, h.matched_pattern
);
}
}
let risk = scanner.assess(req.payload);
println!(" risk : {}", describe_risk(&risk));
if risk.level >= RiskLevel::High {
return Action::Reject(format!("payload risk {}", risk.level));
}
let ip_key = format!("ip:{}", req.ip);
let acct_key = format!("acct:{}", req.account);
match throttle.check_any(&[ip_key.as_str(), acct_key.as_str()], now) {
ThrottleDecision::Banned { until } => {
println!(
" throttle : BANNED · {ip_key} / {acct_key} · 解封于 {until}(还剩 {}s)",
until.saturating_sub(now)
);
return Action::Reject("throttle: banned".into());
}
ThrottleDecision::Allow { remaining } => {
println!(" throttle : allow · {ip_key} / {acct_key} · 剩余额度 {remaining}");
}
ThrottleDecision::Unavailable => {
println!(" throttle : store 不可用 → 放行 + 告警(本模块有意不 fail-closed)");
}
}
if req.password_ok {
for key in [&ip_key, &acct_key] {
if let Err(e) = throttle.record_success(key) {
println!(" auth : record_success({key}) 失败: {e}");
}
}
println!(" auth : 密码正确 → 清空失败计数(注意:不清封禁)");
} else {
for key in [&ip_key, &acct_key] {
match throttle.record_failure(key, now) {
Ok(ThrottleOutcome::Banned { until }) => {
println!(" auth : 密码错误 · {key} 达到阈值 → 封禁至 {until}");
}
Ok(ThrottleOutcome::Allow { remaining }) => {
println!(" auth : 密码错误 · {key} · 剩余额度 {remaining}");
}
Err(e) => println!(" auth : 密码错误 · {key} · 记账失败: {e}"),
}
}
}
let verdict = guard.verify(&req.ctx, now);
println!(" session : {}", describe(&verdict));
match verdict.decision {
Decision::Allow => Action::Allow,
Decision::Challenge => Action::StepUp,
Decision::Block => Action::Reject(format!("session: {}", threat_list(&verdict))),
}
}
fn requests() -> Vec<Request<'static>> {
let alice =
|token: &'static str, fp: &'static str, loc: &'static str, at: u64| RequestContext {
token,
subject: "alice",
fingerprint: fp,
location: Some(loc),
coords: None,
signature: None,
at: Some(at),
};
vec![
Request {
title: "REQ-1 正常请求",
ip: "10.0.0.1",
account: "alice",
payload: "/products/running-shoes",
password_ok: true,
ctx: alice("tok-alice-1", FP_ALICE, "CN-BJ", T0 + 1),
},
Request {
title: "REQ-2 SQL 注入",
ip: "198.51.100.4",
account: "alice",
payload: "/search?q=' OR 1=1 --",
password_ok: true,
ctx: alice("tok-alice-1", FP_ALICE, "CN-BJ", T0 + 2),
},
Request {
title: "REQ-3 XSS",
ip: "198.51.100.5",
account: "alice",
payload: "/comment?body=<script>alert(document.cookie)</script>",
password_ok: true,
ctx: alice("tok-alice-1", FP_ALICE, "CN-BJ", T0 + 3),
},
Request {
title: "REQ-4 异地登录(同指纹、同 token,位置变了)",
ip: "10.0.0.1",
account: "alice",
payload: "/account",
password_ok: true,
ctx: alice("tok-alice-1", FP_ALICE, "US-NY", T0 + 4),
},
Request {
title: "REQ-5 会话劫持(token 被盗,指纹不符)",
ip: "203.0.113.9",
account: "alice",
payload: "/account",
password_ok: true,
ctx: alice("tok-alice-1", "ip=203.0.113.9|ua=curl/8", "CN-BJ", T0 + 5),
},
Request {
title: "REQ-6a 暴力破解 · 第 1 次失败",
ip: "203.0.113.7",
account: "mallory",
payload: "/login",
password_ok: false,
ctx: alice("tok-forged", FP_ALICE, "CN-BJ", T0 + 10),
},
Request {
title: "REQ-6b 暴力破解 · 第 2 次失败",
ip: "203.0.113.7",
account: "mallory",
payload: "/login",
password_ok: false,
ctx: alice("tok-forged", FP_ALICE, "CN-BJ", T0 + 11),
},
Request {
title: "REQ-6c 暴力破解 · 第 3 次失败",
ip: "203.0.113.7",
account: "mallory",
payload: "/login",
password_ok: false,
ctx: alice("tok-forged", FP_ALICE, "CN-BJ", T0 + 12),
},
Request {
title: "REQ-6d 暴力破解 · 第 4 次失败",
ip: "203.0.113.7",
account: "mallory",
payload: "/login",
password_ok: false,
ctx: alice("tok-forged", FP_ALICE, "CN-BJ", T0 + 13),
},
Request {
title: "REQ-6e 暴力破解 · 第 5 次失败(达到阈值)",
ip: "203.0.113.7",
account: "mallory",
payload: "/login",
password_ok: false,
ctx: alice("tok-forged", FP_ALICE, "CN-BJ", T0 + 14),
},
Request {
title: "REQ-7 拿到正确密码也进不来(封禁未到期)",
ip: "203.0.113.7",
account: "mallory",
payload: "/login",
password_ok: true,
ctx: alice("tok-forged", FP_ALICE, "CN-BJ", T0 + 15),
},
]
}
fn threat_list(v: &SessionVerdict) -> String {
if v.threats.is_empty() {
return "无威胁".into();
}
v.threats
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", ")
}
fn describe(v: &SessionVerdict) -> String {
match &v.severity {
None => format!("{} · threats=[{}]", v.decision, threat_list(v)),
Some(severity) => format!(
"{} · severity={severity} · threats=[{}]",
v.decision,
threat_list(v)
),
}
}
fn describe_risk(r: &RiskAssessment) -> String {
format!("level={} score={} results={}", r.level, r.score, r.results)
}