sanction

package
v0.0.3 Latest Latest
Warning

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

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

Documentation

Overview

Package sanction decides what a trigger becomes (SPEC §5.4): the local checks — exemptions first, trust bounds next — then the policy ladder, escalation and port scope. The trust threshold multiplier acts at detection, not here: CountScale plugs into the evaluator so that a trusted origin's rules simply need more evidence before they fire.

What this agent cannot resolve locally is skipped and reported, never silently applied wrong: trust and exemption entries for countries and ASNs need the enrichment databases and are listed in Skipped until those arrive.

Index

Constants

View Source
const ReasonAlreadyRecorded = "an active record already answers this trigger"

ReasonAlreadyRecorded marks the deduplicated detect-only decision, so the caller can log it quietly: a flood of them is one fact, not one line each.

Variables

This section is empty.

Functions

This section is empty.

Types

type Decider

type Decider struct {

	// Skipped lists config entries this agent cannot resolve without the
	// enrichment databases (countries, ASNs). Surface them; never let
	// them look enforced.
	Skipped []string
	// contains filtered or unexported fields
}

Decider turns triggers into decisions. Compile once per config load.

func NewDecider

func NewDecider(cfg *config.Config, history History) (*Decider, error)

NewDecider compiles the decision side of the configuration.

func (*Decider) Active

func (d *Decider) Active() int

Active is how many enforced sanctions the index currently holds.

func (*Decider) ActiveFor

func (d *Decider) ActiveFor(value string, now time.Time) (store.Sanction, bool)

ActiveFor returns a copy of the sanction the index holds in force against a value — what the restore's finish re-asserts to the data plane, so the freshest deadline is the one that lands last.

func (*Decider) Commit

func (d *Decider) Commit(dec Decision)

Commit records the result of a decision that was successfully enforced, so the next trigger on the same value is answered from memory. The caller invokes it after the data plane accepted the change.

func (*Decider) CommitRecord added in v0.0.3

func (d *Decider) CommitRecord(key string, id int64)

CommitRecord teaches a fresh detect-only dedup entry its store row id, so later returns can be counted against the row. A key the map no longer holds (cleared under flood) is simply gone: the next trigger makes a new row.

func (*Decider) CountScale

func (d *Decider) CountScale(group string, count int) int

CountScale is the evaluator hook (eval.Options.CountScale): a trusted origin's threshold is multiplied before any rule fires, so no evidence is wasted on triggers that trust would refuse.

func (*Decider) Decide

func (d *Decider) Decide(trg eval.Trigger, now time.Time) (Decision, error)

Decide runs the local checks in order (SPEC §5.4): exemptions, trust bounds, then the policy — ladder rung from escalation, duration start, port scope.

func (*Decider) Exempt

func (d *Decider) Exempt(prefix netip.Prefix, until time.Time, note string)

Exempt adds an exemption while the agent runs, so that an operator who has just locked themselves out is back in now rather than after an edit and a restart they may no longer be able to perform. The durable copy is a file the CLI writes; this is the same thing in memory, applied at once.

func (*Decider) ExemptSelf added in v0.0.3

func (d *Decider) ExemptSelf(addrs []netip.Addr)

ExemptSelf marks the machine's own addresses as never sanctionable. Each address counts as itself (/32 or /128), never its subnet: the machine is exempt, the neighbourhood is the operator's decision.

func (*Decider) Forget

func (d *Decider) Forget(value string)

Forget drops a value from the index — used when a sanction is lifted. It must also leave the recency list, or its element lingers there as an orphan: trimActive pops from the front, and popping an orphan would evict whatever fresher entry now answers to the same value.

func (*Decider) Knows

func (d *Decider) Knows(value netip.Prefix) bool

Knows reports whether the index already holds a sanction for a value — how a boot-time restore tells its snapshot from a decision made since.

func (*Decider) Misses

func (d *Decider) Misses() int64

Misses is how often a decision had to read a sanction back from the store because the index no longer held it — the price of the cap, and the number that says whether it is set too low.

func (*Decider) Note

func (d *Decider) Note(sn store.Sanction)

Note puts one sanction into the index. The boot pass calls it for every active sanction as it streams them into the data plane, so the millions of rows a fleet comes back to are read once and not twice.

func (*Decider) SetMaxActive

func (d *Decider) SetMaxActive(n int)

SetMaxActive caps the index. Zero leaves it unbounded.

type Decision

type Decision struct {
	Outcome  Outcome
	Reason   string
	Sanction *store.Sanction // set for enforce, record, expired — and the updated state for absorbed/expanded
	Previous *store.Sanction // absorbed/expanded: the state being replaced
	// Changed reports whether Sanction differs from Previous — an
	// absorption that extends nothing needs no data-plane touch.
	Changed bool
	// Span is the rung duration the sanction was (re)armed at, carried so
	// the caller can Commit it to the in-memory index without a re-query.
	Span time.Duration
	// RecordKey is set on a fresh detect-only record: once the caller has
	// persisted the row, CommitRecord(key, id) teaches the dedup entry
	// its row id. RecordID is that id coming back on every deduplicated
	// return, so the caller can count the return against the row.
	RecordKey string
	RecordID  int64
}

Decision is the outcome of one trigger.

type History

type History interface {
	SanctionCountSince(value string, since time.Time) (int, error)
	ActiveSanctionFor(value string, now time.Time) (*store.Sanction, error)
}

History is the durable store behind the decision layer. The active sanction of any value is answered from an in-memory index (below), not from here — a flood must never mean a database round-trip per line. The store is consulted only on the cold paths: the escalation count when a new episode begins, and a value the capped index no longer holds.

type Outcome

type Outcome string

Outcome is what a trigger became.

const (
	// OutcomeEnforce: sanction to persist and enforce.
	OutcomeEnforce Outcome = "enforce"
	// OutcomeRecord: a detect-only rule — the sanction is computed and
	// recorded so observed-versus-enforced comparison works, but the data
	// plane is never touched.
	OutcomeRecord Outcome = "record"
	// OutcomeExempt: an exemption removed the sanction. The trigger and
	// this decision remain observable — never the observation is removed.
	OutcomeExempt Outcome = "exempt"
	// OutcomeTrustRefused: a trust bound blocked the sanction (a range
	// ban inside an origin whose trust forbids it).
	OutcomeTrustRefused Outcome = "trust-refused"
	// OutcomeExpired: an evidence-started duration is already over —
	// history and escalation, no enforcement.
	OutcomeExpired Outcome = "expired"
	// OutcomeAbsorbed: an active sanction already answers this trigger
	// (same service, SPEC §5.4 check 5) — at most its end is extended,
	// never a new episode, never a rung climbed.
	OutcomeAbsorbed Outcome = "absorbed"
	// OutcomeExpanded: the trigger proves the offender attacks a service
	// the active sanction does not cover — the sanction re-arms at its
	// current rung and widens its ports. Escalated or expanded, never
	// duplicated.
	OutcomeExpanded Outcome = "expanded"
)

Jump to

Keyboard shortcuts

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