Report failed IDENTIFY against a password-less account to the auth feed
All checks were successful
CI / check (push) Successful in 4m10s

This commit is contained in:
Jean Chevronnet 2026-07-19 05:00:12 +00:00
parent 876bc55e2b
commit 1ecb76affb
No known key found for this signature in database
3 changed files with 38 additions and 0 deletions

View file

@ -485,6 +485,21 @@ pub struct ServiceCtx {
// engine drains these to the log channel, prefixing the sender's nick/host —
// e.g. a blocked look-alike registration attempt.
pub alerts: Vec<(String, String)>,
// Login-attempt outcomes for the auth audit feed. The engine drains these
// through its own auth_report (which resolves the client's source), so a
// module can report a reject the engine's verify path never sees — e.g. a
// failed IDENTIFY against a cert-only account with no password verifier.
pub auth_reports: Vec<AuthReport>,
}
// A login-attempt outcome a module hands back for the auth audit feed.
#[derive(Default)]
pub struct AuthReport {
pub ok: bool,
pub account: Option<String>,
pub mech: String,
pub client: String,
pub reason: Option<String>,
}
impl ServiceCtx {
@ -536,6 +551,19 @@ impl ServiceCtx {
self.alerts.push((category.into(), text.into()));
}
// Report a login-attempt outcome to the auth audit feed. For a reject the
// engine's own verify path won't produce (a failed IDENTIFY against an account
// with no password verifier), so operators see it like any other failed login.
pub fn auth_report(&mut self, ok: bool, account: Option<&str>, mech: &str, client: &str, reason: Option<&str>) {
self.auth_reports.push(AuthReport {
ok,
account: account.map(String::from),
mech: mech.to_string(),
client: client.to_string(),
reason: reason.map(String::from),
});
}
// A channel/target message sourced from one of our pseudo-clients (e.g. a
// bot answering a fantasy command, or BotServ SAY).
pub fn privmsg(&mut self, from: &str, to: &str, text: impl Into<String>) {

View file

@ -49,8 +49,11 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
match db.scram_verifier(account_name) {
None => {
// Exists but has no verifier (e.g. cert-only) — a password can't match.
// Report it to the auth feed too, or probing a cert-only account is a
// blind spot the deferred wrong-password path doesn't have.
db.note_auth(account_name, false);
ctx.count("nickserv.identify_fail");
ctx.auth_report(false, Some(account_name), "NickServ IDENTIFY", from.uid, Some("no password set"));
ctx.fail(me, from.uid, "IDENTIFY", "INVALID_CREDENTIALS", "Invalid password. Please try again.");
}
Some((account, verifier)) => {

View file

@ -113,6 +113,13 @@ impl Engine {
out.push(line);
}
}
// Login-attempt outcomes a command reported (e.g. a failed IDENTIFY against
// a cert-only account), routed through the same auth feed as the verify path.
for r in std::mem::take(&mut ctx.auth_reports) {
if let Some(line) = self.auth_report(r.ok, r.account.as_deref(), &r.mech, &r.client, r.reason.as_deref()) {
out.push(line);
}
}
self.apply_dict_limit(&mut out);
self.apply_msg_style(&mut out);
out