report

package
v0.0.9 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 23, 2026 License: AGPL-3.0 Imports: 15 Imported by: 0

Documentation

Overview

Package report submits offenders to abuse providers.

The split is the one SPEC §5.5 draws: the provider's API is compiled in — its endpoint, its authentication, its category vocabulary, how it answers — and everything a report *says* is data the operator owns. This file is the vocabulary half for AbuseIPDB.

Package report is abuse reporting: the provider client, the allowance arithmetic, the templates and the sender's state machine. Public because the controller runs the very same machine on its agents' behalf (SPEC §5.5, "who does the reporting"); it depends on nothing but the standard library, and its Source is an interface the store implements on either side.

Index

Constants

View Source
const DefaultEndpoint = "https://api.abuseipdb.com/api/v2/report"

DefaultEndpoint is theirs. https://docs.abuseipdb.com/

View Source
const DefaultProbeInterval = 5 * time.Minute

DefaultProbeInterval bounds what an exhausted agent costs the provider: twelve requests an hour, per machine, and each one might be the one that discovers the allowance is back.

Variables

View Source
var Categories = []Category{
	{1, "dns-compromise", "altering DNS records to redirect improperly"},
	{2, "dns-poisoning", "falsifying domain server cache"},
	{3, "fraud-orders", "fraudulent orders"},
	{4, "ddos", "participating in a distributed denial of service, usually as part of a botnet"},
	{5, "ftp-brute-force", "brute-force attempts against an FTP service"},
	{6, "ping-of-death", "oversized IP packet"},
	{7, "phishing", "phishing websites or email"},
	{8, "fraud-voip", "VoIP fraud"},
	{9, "open-proxy", "open proxy, open relay, or Tor exit node"},
	{10, "web-spam", "comment, forum, referer or other CMS spam"},
	{11, "email-spam", "spam content, infected attachments, phishing email"},
	{12, "blog-spam", "CMS blog comment spam"},
	{13, "vpn-ip", "VPN-related activity"},
	{14, "port-scan", "scanning for open ports and vulnerable services"},
	{15, "hacking", "hacking attempts"},
	{16, "sql-injection", "attempts at SQL injection"},
	{17, "spoofing", "email sender spoofing"},
	{18, "brute-force", "credential brute-force against logins and services — SSH, FTP, SIP, SMTP, RDP"},
	{19, "bad-web-bot", "scraping, crawlers ignoring robots.txt, user-agent spoofing, excessive requests"},
	{20, "exploited-host", "host likely infected and used for other attacks; combine with what it did"},
	{21, "web-app-attack", "probing or exploiting web applications — a CMS, a store, phpMyAdmin, plugins"},
	{22, "ssh", "SSH abuse; combine with a more specific category"},
	{23, "iot-targeted", "abuse aimed at an IoT device"},
}

Categories, in the provider's order. Two of them say in their own description that they are meant to be combined with a more specific one — SSH and Exploited Host — which is why a report carries a list and not a single value.

Functions

func CategoryNames

func CategoryNames() string

CategoryNames lists what may be written, for an error message that answers the question it raises.

func IDs

func IDs(cats []Category) string

IDs renders a set of categories the way the provider wants them: its own numbers, comma-separated.

func NextReset

func NextReset(now time.Time) time.Time

NextReset is when the provider's daily count goes back to zero: midnight UTC.

Expressed in UTC and never in local time, which is the trap this function exists to avoid. AbuseIPDB's reset falls at 2 a.m. French SUMMER time — and at 1 a.m. French winter time, being the same instant. An agent that stored "2 a.m. locally" would be an hour wrong for half the year and would discover it on a Sunday in October.

Types

type AbuseIPDB

type AbuseIPDB struct {
	Key      string
	Endpoint string // empty = the real one
	Client   *http.Client
}

AbuseIPDB submits offenders to https://www.abuseipdb.com.

This is the compiled half of SPEC §5.5: the endpoint, the authentication, the category vocabulary (category.go) and how the provider answers. What a report SAYS is data the operator owns and arrives here already composed.

func (*AbuseIPDB) Report

func (a *AbuseIPDB) Report(ctx context.Context, s Submission) (Result, error)

Report submits one offender.

It returns an error only when the request could not be made at all. Everything the provider says — including every refusal — is a Result, because a refusal is information to record, not a failure to bubble.

func (*AbuseIPDB) Withdraw

func (a *AbuseIPDB) Withdraw(ctx context.Context, ip netip.Addr) (Withdrawal, error)

Withdraw asks the provider to remove what THIS ACCOUNT reported about an address — every report the account ever made about it, theirs being per account, not per report. https://docs.abuseipdb.com/#clear-address-endpoint It is what a lift with "withdraw the report" means (SPEC §5.4): the operator found the ban wrong, so the report should not stand either.

type Allowance

type Allowance struct {
	// Limit is the operator's subscription — or deliberately less than
	// it. Zero with Auto means "learn the plan": the first request of
	// the day goes out and the provider's answer says what it is. Zero
	// WITHOUT Auto means no reporting — an allowance nobody set is not a
	// licence to send, it is an operator who has not decided.
	Limit int
	Auto  bool

	// ProbeInterval is how often one request is allowed through while
	// the PROVIDER says the account is spent. Not a contradiction of
	// "stop attempting": one request every few minutes is not hammering,
	// and it is the only way to notice a plan upgraded mid-day, another
	// machine's traffic freeing room, or a reset that came early. The
	// probe never crosses the operator's own daily_limit — that ceiling
	// is absolute and has nothing to discover.
	ProbeInterval time.Duration
	// contains filtered or unexported fields
}

Allowance is what keeps the agent from becoming the thing it defends against. It answers one question — may another report be sent right now — and it answers it BEFORE a request is built, so that an exhausted allowance costs nothing and reaches nobody.

Two ceilings apply at once and the LOWER one always wins. The operator's daily_limit is absolute: a machine given fifty reports a day sends fifty, whatever the account still holds — that is how one account is shared between machines without any one of them draining it. The provider's figure is the truth about the account: the same account may be reporting from several places, so what we have sent is not what has been sent, and every response carries the real number. We read it every time.

func (*Allowance) Allow

func (a *Allowance) Allow(now time.Time) bool

Allow reports whether one more may be sent now, and is the only thing that decides it.

func (*Allowance) Configured

func (a *Allowance) Configured() bool

Configured reports whether reporting may happen at all.

func (*Allowance) Exhausted

func (a *Allowance) Exhausted(now, resetAt time.Time)

Exhausted records the provider saying no more, and when to come back. A zero instant means it did not say, and the daily reset is assumed.

func (*Allowance) Observe

func (a *Allowance) Observe(now time.Time, remaining, limit int, resetAt time.Time)

Observe takes what the provider said about the allowance, from any response — accepted or refused. This is where a plan upgrade, another machine's spending and an early reset all arrive, and it is why the local count is only a guard.

remaining < 0 means the response carried no such header.

func (*Allowance) Remaining

func (a *Allowance) Remaining(now time.Time) int

Remaining is what is left as far as anyone knows: the lower of the operator's ceiling and the provider's figure where it has given one.

func (*Allowance) ResetsAt

func (a *Allowance) ResetsAt(now time.Time) time.Time

ResetsAt is when this window ends.

func (*Allowance) Sent

func (a *Allowance) Sent(now time.Time)

Sent records one submission that actually left. Called BEFORE Observe reads the response: the response's own figure already counts the request it answers, and decrementing after reading it would count it twice.

func (*Allowance) Today

func (a *Allowance) Today(now time.Time) (sent, providerRemaining int, told bool)

Today answers the two counts a person needs apart, never blended: what THIS sender has sent in the window, and what the PROVIDER last said was left on the account (told false: it never said). One account is often shared — other machines, other tools — so the two disagree by exactly what the others sent.

type Category

type Category struct {
	ID   int
	Name string // what a preset writes
	What string // the provider's own description, shortened
}

Category is one of AbuseIPDB's report categories.

The list is theirs and is reproduced from https://www.abuseipdb.com/categories. It is compiled in rather than configured because it is not ours to choose: a number this program invents is a report the provider rejects, and the whole point of naming them here is that a preset can be checked before it is sent rather than after.

func ParseCategory

func ParseCategory(s string) (Category, error)

ParseCategory accepts a name ("brute-force") or the provider's number ("18"). Names are what a preset should use — a configuration file full of bare integers cannot be reviewed, and reviewing what leaves the machine is the point — but the numbers are accepted because they are what the provider's own documentation shows.

type Fact

type Fact struct {
	Name  string
	Value string
}

Fact is one named value a template allowed out. Nothing reaches here that the preset did not name: the caller filters, this only renders.

type Outcome

type Outcome string

Outcome is what became of one submission. They are distinguished because they demand opposite answers, and the distinction is the whole reason this type exists rather than an error.

const (
	// Accepted: the provider took it.
	Accepted Outcome = "accepted"
	// Duplicate: this address was already reported within the
	// provider's window. Not a failure and not worth retrying — it is
	// reported, by us or by somebody else on the same account.
	Duplicate Outcome = "duplicate"
	// Malformed: the provider refused the request itself. It will never
	// succeed, so it must never be retried; it is a bug in a template
	// or in this code, and it has to be visible as one.
	Malformed Outcome = "malformed"
	// Exhausted: the allowance is spent. Nothing is wrong with the
	// report; come back at ResetAt.
	Exhausted Outcome = "exhausted"
	// Unauthorized: the key is wrong, missing or revoked. Retrying
	// cannot help and every attempt is noise on someone else's service.
	Unauthorized Outcome = "unauthorized"
	// Unavailable: the provider could not be reached or failed. This is
	// the only outcome that deserves a patient retry.
	Unavailable Outcome = "unavailable"
)

type Owed

type Owed struct {
	ID         int64
	IP         netip.Addr
	Rule       string
	Categories []Category
	Comment    string
	At         time.Time
	RenewedAt  time.Time // set for a renewed report
}

Owed is one offender waiting, as the store hands it over. Rule is the rule that decided the sanction, carried so the sender's log lines read like the decision lines they follow.

type Provider

type Provider interface {
	Report(ctx context.Context, s Submission) (Result, error)
}

Provider is one abuse provider. AbuseIPDB implements it; the queue and the state machine are shared.

type Result

type Result struct {
	Outcome Outcome
	// Score is the address's abuse confidence after our report, which
	// the provider returns and which is worth keeping: it is the only
	// feedback we get that a report meant something.
	Score int
	// Detail is the provider's own words, stored verbatim so an
	// operator reads what was said rather than our paraphrase.
	Detail string
	// Remaining, Limit and ResetAt come from the rate-limit headers
	// when the provider sends them. Limit is the plan itself, which is
	// how `daily_limit: auto` learns what the operator is paying for
	// without anyone typing it into a file.
	Remaining int
	Limit     int
	ResetAt   time.Time
}

Result is what the provider answered.

type Sender

type Sender struct {
	Provider  Provider
	Allowance *Allowance
	Source    Source
	Log       *slog.Logger
	// Batch is how many are read from the store per query.
	Batch int
	// Every is how often a pass runs.
	Every time.Duration
	// Concurrency is how many submissions may be in flight at once. One
	// (the default) is a machine reporting its own bans; a controller
	// reporting for a fleet under a flood needs more than one request's
	// round trip per offender, or it falls behind for good.
	Concurrency int

	// Paused is the controller's word that somebody else reports this
	// machine's sanctions — the controller itself, or nobody (SPEC §5.5:
	// who reports is set per agent). While paused the sender sends
	// nothing and lets its cursor pass what appears: those sanctions had
	// their one chance where the controller said, not here.
	Paused atomic.Bool
	// contains filtered or unexported fields
}

Sender reports new offenders as they are sanctioned, and that is all it does.

It runs on a goroutine of its own and touches nothing the decision path touches. That is a requirement, not an optimisation (SPEC §5.5): a report is a courtesy to the internet, stopping the attack is the job, and a provider that is slow, rate limiting or down must have no way to slow down a ban.

A report gets ONE chance, at the moment of the detection it describes. Whatever stops it — the allowance spent, the provider down, the key refused — it is not sent, the reason is logged on its own line, and the sanction stays on record still owed. Nothing is retried on a cadence and nothing drains a backlog by itself: sending the past is a deliberate act, `shield report`, where the operator names the window and confirms the count. The one exception is a pass cut short by an unreachable provider: what was never attempted keeps its chance for the next pass.

func (*Sender) Run

func (s *Sender) Run(ctx context.Context)

Run reports until the context ends.

func (*Sender) Wake

func (s *Sender) Wake()

Wake asks for a pass now: something was just decided. Non-blocking and coalescing — a burst of bans is one nudge, and a slow provider can never make the caller wait.

type Source

type Source interface {
	// LastID is the newest sanction id right now — where the sender's
	// first pass starts. Everything already on record belongs to the
	// operator's deliberate replay, not to this loop.
	LastID() (int64, error)
	// Renewed returns offenders whose report was reopened after the
	// given instant — they kept attacking while banned — oldest first,
	// bounded. Their At is the latest hit, not the ban's start.
	Renewed(after time.Time, limit int) ([]Owed, error)
	// Owed returns offenders past afterID still owed a report, oldest
	// of them first, bounded.
	Owed(afterID int64, limit int) ([]Owed, error)
	// Settle records a terminal outcome: never offered again. The
	// outcome is passed as this package's own word for it, so the
	// mapping to whatever the store calls its states lives with the
	// store and neither side repeats the other's strings.
	Settle(id int64, outcome Outcome, detail string, score int, now time.Time) error
}

Source is where the unsent reports come from and where their fate goes back. The store implements it; this package does not know what SQL is.

type Submission

type Submission struct {
	IP         netip.Addr
	Categories []Category
	Comment    string
	At         time.Time // when the abuse happened, not when we send
}

Submission is one composed report, ready to leave. The comment has already been through its template: nothing here decides what a report says.

type Template

type Template struct {
	Name       string
	Categories []Category
	Says       string
	Include    []string
}

Template is a preset resolved against the provider's vocabulary: what a report says, which categories it carries, and which parser fields may leave with it.

func (Template) Compose

func (t Template) Compose(facts []Fact) string

Compose builds the comment that will be sent, in the shape Robin's CrowdSec reports used: the description, then one sample per included field with a count of how many distinct values were seen.

Web application probing | path: /wp-admin/css/ (+11 more) | ua: Mozilla/5.0 …

WHAT IT NEVER SAYS: how many hits crossed which threshold in which window. A report is public and attackers read their own — telling them "20 hits in 1m" hands over the exact rate to stay under (Robin, 2026-08-15). The evidence stays on the machine; the report carries what the offender already knows they sent.

type Withdrawal

type Withdrawal struct {
	Outcome Outcome // Accepted (deleted, possibly zero), Unauthorized, Malformed, Unavailable
	Deleted int     // reports the provider removed
	Detail  string
}

Withdrawal is what became of asking the provider to forget an address.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL