exception

package
v0.8.4 Latest Latest
Warning

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

Go to latest
Published: Oct 9, 2026 License: Apache-2.0 Imports: 13 Imported by: 0

Documentation

Overview

Background expiry sweep, wired in serve. Mirrors posture.Run: an immediate pass at boot plus an hourly tick.

Spec: api-compliance-exceptions v1.0.0 (C-06).

Package exception implements compliance exception governance: operator-approved rule waivers with a request -> approve/reject -> revoke/expire lifecycle.

DB-backed (scan plan decision 2026-06-13): an exception is approval-workflow data with a lifecycle, queries, and an audit trail, not a signed static policy file.

OVERLAY MODEL: an exception NEVER mutates host_rule_state. A failing rule with an active exception stays 'fail' in the raw scan results (Kensa's verdict is authoritative); the exception is a governance annotation the lens/UI reads to mark a failure as accepted risk.

Separation of duties: the requester cannot approve their own request (enforced here, on top of the distinct exception:request vs exception:approve RBAC permissions).

Spec: api-compliance-exceptions v1.0.0.

Index

Constants

View Source
const ExpiringSoonWindow = 72 * time.Hour

ExpiringSoonWindow is how far ahead the expiring-soon sweep looks: approvers are warned this long before an approved exception lapses.

View Source
const ExpirySweepInterval = time.Hour

ExpirySweepInterval is the production cadence of the expiry sweep. Hourly is fine: an exception only needs to flip to expired soon after its expires_at, and the count/annotation queries already guard on expires_at so suppression stops at the deadline regardless.

Variables

View Source
var (
	// ErrNotFound is returned when an exception id does not exist.
	ErrNotFound = errors.New("exception: not found")
	// ErrDuplicateOpen is returned when a requested/approved exception
	// already exists for the same host+rule (partial-unique violation).
	ErrDuplicateOpen = errors.New("exception: an open exception already exists for this host and rule")
	// ErrWrongState is returned when a transition does not apply to the
	// current status (e.g. approving a rejected exception).
	ErrWrongState = errors.New("exception: action not valid for the current state")
	// ErrSelfReview is returned when the reviewer is the requester:
	// separation of duties forbids approving your own request.
	ErrSelfReview = errors.New("exception: requester cannot review their own request")
	// ErrInvalidInput is returned for empty rule_id/reason etc.
	ErrInvalidInput = errors.New("exception: invalid input")
	// ErrExpired is returned when Approve is called on a requested row whose
	// expires_at has already passed: the requested waiver end is in the past,
	// so approving it would create an immediately-dead exception. The
	// requester must submit a fresh request with a future expiry.
	ErrExpired = errors.New("exception: the request's expiry has already passed")
)

Functions

This section is empty.

Types

type EmitFunc

type EmitFunc func(ctx context.Context, code audit.Code, ev audit.Event)

EmitFunc is the audit-emission shape (matches audit.Emit). Tests pass a fake.

type Exception

type Exception struct {
	ID     uuid.UUID
	HostID uuid.UUID
	// HostName is populated by the list queries (ListForHost,
	// ListFleet) via a join; the single-row lifecycle ops leave it
	// empty (the UI re-fetches the list after a mutation).
	HostName    string
	RuleID      string
	Reason      string
	Status      Status
	RequestedBy uuid.UUID
	ReviewedBy  *uuid.UUID
	ReviewNote  string
	ExpiresAt   *time.Time
	RequestedAt time.Time
	ReviewedAt  *time.Time
}

Exception is one compliance_exceptions row.

func (Exception) Active

func (e Exception) Active(now time.Time) bool

Active reports whether the exception is currently suppressing: an approved row that has not passed its expiry.

type Notifier

type Notifier interface {
	ExceptionRequested(ctx context.Context, exceptionID, hostID uuid.UUID, ruleID string) error
	ExceptionDecided(ctx context.Context, exceptionID, requestedBy uuid.UUID, ruleID string, approved bool) error
	ExceptionExpiringSoon(ctx context.Context, exceptionID, hostID uuid.UUID, ruleID string) error
	ExceptionExpired(ctx context.Context, exceptionID, hostID uuid.UUID, ruleID string) error
}

Notifier receives exception lifecycle signals for the in-app notification feed. notifyfeed.GovernanceProjector implements it; the service holds the interface so it does not import the feed package. Methods are best-effort: the service logs (never fails the transition on) a notifier error. nil disables in-app exception notifications.

type Service

type Service struct {
	// contains filtered or unexported fields
}

Service is the exception governance service.

func NewService

func NewService(pool *pgxpool.Pool, emit EmitFunc) *Service

NewService wires the service. emit is audit.Emit in production.

func (*Service) ActiveCountForHost

func (s *Service) ActiveCountForHost(ctx context.Context, hostID uuid.UUID) (int, error)

ActiveCountForHost counts the host's currently-suppressing exceptions (approved, not past expiry). Backs the host-detail Watchlist Exceptions row.

func (*Service) ActiveRuleIDsForHost

func (s *Service) ActiveRuleIDsForHost(ctx context.Context, hostID uuid.UUID) (map[string]bool, error)

ActiveRuleIDsForHost returns the set of rule ids the host has an active (suppressing) exception for. Backs the Compliance-tab waived-rule annotation - read alongside host_rule_state, never mutating it.

func (*Service) Approve

func (s *Service) Approve(ctx context.Context, id, reviewedBy uuid.UUID, note string) (Exception, error)

Approve transitions a 'requested' exception to 'approved'. The reviewer must differ from the requester (separation of duties). Emits compliance.exception.approved.

func (*Service) ExpireSweep

func (s *Service) ExpireSweep(ctx context.Context) (int, error)

ExpireSweep flips any OPEN exception (approved OR requested) whose expires_at has passed to 'expired' and emits compliance.exception.expired for each. Returns the count expired. Idempotent: a second run finds nothing.

A requested row is expired too: its expires_at is the requested waiver end, so once that has passed there is nothing left to approve — leaving it pending forever would strand it and block a fresh request for the same host+rule (the open-uniqueness rule, C-02). Approve independently rejects a lapsed request (ErrExpired), so the two guards agree.

func (*Service) ExpiringSoonSweep

func (s *Service) ExpiringSoonSweep(ctx context.Context) (int, error)

ExpiringSoonSweep warns approvers about approved exceptions whose expires_at falls within ExpiringSoonWindow. Read-only (no state change); the projector's quiet fan-out makes repeated hourly sweeps notify each recipient at most once. A no-op without a notifier wired. Returns the count warned.

func (*Service) ListFleet

func (s *Service) ListFleet(ctx context.Context, status Status, limit int) ([]Exception, error)

ListFleet returns fleet-wide exceptions, optionally filtered by status. Empty status returns all. Newest first.

func (*Service) ListForHost

func (s *Service) ListForHost(ctx context.Context, hostID uuid.UUID, includeHistory bool) ([]Exception, error)

ListForHost returns a host's exceptions. When includeHistory is false, only open rows (requested + approved) are returned; true returns every row, newest first.

func (*Service) Reject

func (s *Service) Reject(ctx context.Context, id, reviewedBy uuid.UUID, note string) (Exception, error)

Reject transitions a 'requested' exception to 'rejected'. Like Approve, the reviewer must differ from the requester. Emits compliance.exception.rejected.

func (*Service) Request

func (s *Service) Request(ctx context.Context, hostID uuid.UUID, ruleID, reason string,
	requestedBy uuid.UUID, expiresAt *time.Time) (Exception, error)

Request submits a new exception (status 'requested'). Returns ErrDuplicateOpen when a requested/approved exception already exists for the same host+rule. Emits compliance.exception.requested.

func (*Service) Revoke

func (s *Service) Revoke(ctx context.Context, id, reviewedBy uuid.UUID, note string) (Exception, error)

Revoke transitions an 'approved' exception to 'revoked' before its expiry. The revoker may be anyone with the permission (revoking is not a self-review concern). Emits compliance.exception.revoked.

func (*Service) Run

func (s *Service) Run(ctx context.Context, interval time.Duration) *cron.Scheduler

Run starts the expiry sweep on a cron tick with one immediate pass.

func (*Service) WithNotifier

func (s *Service) WithNotifier(n Notifier) *Service

WithNotifier attaches the in-app notification projector and returns the service for chaining at boot. nil leaves in-app notifications disabled.

type Status

type Status string

Status is the exception lifecycle state.

const (
	StatusRequested Status = "requested"
	StatusApproved  Status = "approved"
	StatusRejected  Status = "rejected"
	StatusRevoked   Status = "revoked"
	StatusExpired   Status = "expired"
)

Jump to

Keyboard shortcuts

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