Skip to content

Commit 443664b

Browse files
author
Paul C
committed
v23.12.15: abuse-report is MANUAL ONLY — locked in with build-time test
Operator-facing guarantee: WolfStack will NEVER auto-send abuse reports. Send is reachable ONLY from an authenticated operator clicking 'Send report' in the UI. Auditing the v23.12.14 codebase confirmed exactly one caller of send_report exists (the API handler in src/api/mod.rs); this commit locks that property in so it remains true. Three layers of defence: 1) Module-level doc on src/abuse_report/mod.rs now lists the six reasons auto-reporting is forbidden: 1. Mail-reputation damage — auto-mail to abuse desks gets SMTP flagged. Real alerts stop arriving and nobody notices. 2. False positives become permanent the moment they leave the server. Human review catches 'wait, that's our own monitoring' before send. 3. Abuse desks pattern-match auto-mail and deprioritise it. Hand-reviewed reports are dramatically more likely to get the customer suspended. 4. Legal exposure — written accusations against third parties need a human standing behind them. 5. Fleet amplification — one attacker on 12 nodes becomes 12 auto-reports for one incident. 6. Replies need a human to read them. Auto-send + no follow-up = case dies in the queue. 2) Function-level doc on send_report() repeats the prohibition in even sharper terms — explicit list of integration points it must NOT be wired into (limiter hooks, alerting loop, scheduled tasks, log tailers, tokio/thread spawn from event handlers). 3) NEW regression test walks the entire src/ tree at build time and FAILS if a second caller of send_report appears anywhere. Filters out comments + the module's own self-references. Verifies the one remaining caller is the API handler in api/mod.rs. Future PRs that try to slip in an auto-trigger will break CI. No runtime behaviour change — v23.12.14 was already manual-only; this commit makes that property impossible to silently regress.
1 parent d1927f9 commit 443664b

2 files changed

Lines changed: 98 additions & 6 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "wolfstack"
3-
version = "23.12.14"
3+
version = "23.12.15"
44
edition = "2024"
55
authors = ["Wolf Software Systems Ltd"]
66
description = "Server management platform for the Wolf software suite"

src/abuse_report/mod.rs

Lines changed: 97 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,29 @@
3131
//! we can show a "Last reported X days ago" badge and refuse to
3232
//! re-report the same IP within 7 days (configurable).
3333
//!
34-
//! ## What we deliberately DON'T do
34+
//! ## What we deliberately DON'T do — **DO NOT CHANGE**
3535
//!
36-
//! - **Auto-report on every block.** Aggressive abuse reporting is
37-
//! how you get your SMTP server flagged as a spammer. Operator must
38-
//! click "Send" each time. The cool-down further protects against
39-
//! double-sends.
36+
//! - **Auto-report is FORBIDDEN.** This module must ONLY be triggered
37+
//! by an authenticated operator clicking "Send report" in the UI.
38+
//! Reasons (each one alone is sufficient):
39+
//! 1. Mail-reputation damage — auto-sending similar emails to
40+
//! abuse desks gets your SMTP flagged. Your real alerts stop
41+
//! arriving and nobody notices.
42+
//! 2. False positives become public + permanent the moment they
43+
//! leave the SMTP server. A human reading the draft catches
44+
//! "wait, that's our own monitoring" before send.
45+
//! 3. Abuse desks pattern-match auto-mail and deprioritise it.
46+
//! A short hand-reviewed report from a real person is
47+
//! dramatically more likely to get the customer suspended.
48+
//! 4. Legal exposure — making a written accusation against a
49+
//! third party carries some weight. A human must stand behind
50+
//! the claim.
51+
//! 5. Volume amplification across the fleet — one attacker
52+
//! hitting 12 nodes becomes 12 auto-reports for one incident.
53+
//! 6. Replies need a human. Auto-send + no follow-up = case dies
54+
//! in the desk's queue.
55+
//! The regression test `only_api_handler_calls_send_report` enforces
56+
//! this at build time.
4057
//! - **Try to discover NEW abuse contacts beyond whois.** No RIPE
4158
//! API, no AbuseIPDB-as-reporter, no IPinfo. whois is the canonical
4259
//! source and the operator can edit the recipient before sending if
@@ -365,6 +382,22 @@ impl ReportHistory {
365382

366383
/// Send the abuse report and persist a history record.
367384
///
385+
/// # ⚠ MANUAL-ONLY — DO NOT AUTOMATE
386+
///
387+
/// This function MUST ONLY be called from the API handler
388+
/// `abuse_report_send` in `src/api/mod.rs`, which itself is only
389+
/// reachable via an authenticated operator POSTing through the UI
390+
/// Send button. **Never** wire this into:
391+
/// - the LoginRateLimiter's `install_propagation_hooks`
392+
/// - any `tokio::spawn` or `std::thread::spawn` triggered by a
393+
/// block/scan event
394+
/// - the alerting loop in `alerting.rs`
395+
/// - a `cron`-style scheduled task
396+
/// - a tailing loop / log monitor
397+
/// The regression test `only_api_handler_calls_send_report` will fail
398+
/// the build if a second caller appears anywhere in the source tree.
399+
/// See the module-level doc for the six reasons this is forbidden.
400+
///
368401
/// Reuses the AI config's SMTP transport (`ai::send_alert_email`) so
369402
/// the operator doesn't have to configure a separate mail server.
370403
/// The function writes to history regardless of cool-down — the
@@ -415,6 +448,65 @@ pub fn default_cooldown_days() -> u64 { DEFAULT_COOLDOWN_DAYS }
415448
mod tests {
416449
use super::*;
417450

451+
/// Manual-only enforcement: `send_report` may ONLY be called from
452+
/// the `abuse_report_send` API handler in `src/api/mod.rs`. This
453+
/// test walks the source tree and fails the build if a second
454+
/// caller appears anywhere — preventing accidental wiring into a
455+
/// limiter hook, alerting loop, scheduled task, or anything else
456+
/// that would auto-send. See the module-level doc for the six
457+
/// reasons auto-reporting is forbidden.
458+
#[test]
459+
fn only_api_handler_calls_send_report() {
460+
let src_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
461+
let mut callers: Vec<String> = Vec::new();
462+
walk_rs(&src_root, &mut |path, contents| {
463+
// Skip this file itself — the doc comment + definition
464+
// both mention `send_report` and would falsely trigger.
465+
let rel = path.strip_prefix(&src_root).unwrap_or(path);
466+
if rel == std::path::Path::new("abuse_report/mod.rs") { return; }
467+
for (lineno, line) in contents.lines().enumerate() {
468+
let trimmed = line.trim_start();
469+
// Comment lines aren't callers.
470+
if trimmed.starts_with("//") || trimmed.starts_with("/*") || trimmed.starts_with("*") {
471+
continue;
472+
}
473+
if line.contains("abuse_report::send_report")
474+
|| (line.contains("send_report") && line.contains("crate::abuse_report"))
475+
{
476+
callers.push(format!("{}:{}: {}",
477+
rel.display(), lineno + 1, trimmed));
478+
}
479+
}
480+
});
481+
assert_eq!(callers.len(), 1,
482+
"abuse_report::send_report MUST have exactly one caller \
483+
(src/api/mod.rs::abuse_report_send). Found {}:\n {}\n\n\
484+
Auto-reporting is FORBIDDEN — see the module-level doc for \
485+
the six reasons. If you genuinely need a new code path that \
486+
reaches send_report, raise it with the human owner first.",
487+
callers.len(), callers.join("\n "));
488+
// Belt-and-braces: the one caller MUST be in src/api/mod.rs.
489+
assert!(callers[0].starts_with("api/mod.rs"),
490+
"the only caller of send_report must be the API handler in api/mod.rs, \
491+
found: {}", callers[0]);
492+
}
493+
494+
/// Tiny recursive directory walker used by the audit test above.
495+
/// Stays at the test scope so we don't ship it as a public helper.
496+
fn walk_rs<F: FnMut(&std::path::Path, &str)>(dir: &std::path::Path, f: &mut F) {
497+
let entries = match std::fs::read_dir(dir) { Ok(e) => e, Err(_) => return };
498+
for entry in entries.flatten() {
499+
let path = entry.path();
500+
if path.is_dir() {
501+
walk_rs(&path, f);
502+
} else if path.extension().and_then(|s| s.to_str()) == Some("rs") {
503+
if let Ok(c) = std::fs::read_to_string(&path) {
504+
f(&path, &c);
505+
}
506+
}
507+
}
508+
}
509+
418510
#[test]
419511
fn parse_whois_extracts_alibaba_fields() {
420512
// Real whois snippet for 101.200.221.177 (Alibaba) — what we

0 commit comments

Comments
 (0)