model

package
v0.1.7 Latest Latest
Warning

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

Go to latest
Published: Jul 17, 2026 License: MIT Imports: 21 Imported by: 0

Documentation

Index

Constants

View Source
const (
	RoleAdmin = "admin"
	RoleUser  = "user"
)

Roles. Admin users may manage other users and any API key; regular users manage only their own account and keys.

View Source
const DefaultSessionTTL = 12 * time.Hour

DefaultSessionTTL is how long a login session stays valid.

Variables

View Source
var (
	// ErrInvalidSlug is returned when a caller-supplied slug is empty or
	// contains characters outside the safe set.
	ErrInvalidSlug = errors.New("slug must be 6-64 chars of [a-zA-Z0-9_-]")
	// ErrSlugExists is returned when creating a sink whose slug is taken.
	ErrSlugExists = errors.New("a sink with that slug already exists")
)
View Source
var (
	// ErrInvalidCredentials is returned by Authenticate for any failure so
	// callers can't distinguish "no such user" from "wrong password".
	ErrInvalidCredentials = errors.New("invalid credentials")
	ErrWeakPassword       = errors.New("password must be at least 12 characters")
)
View Source
var ErrNoPurgeConstraint = errors.New("refusing to purge with no filter (specify --remote, --target, or --handler)")

ErrNoPurgeConstraint is returned when a purge is attempted with no filter constraint, which would match (and delete) every interaction.

Functions

func CountAdmins added in v0.1.0

func CountAdmins() int64

CountAdmins returns the number of admin users, used to prevent removing or demoting the last administrator (which would lock everyone out).

func CountInteractions added in v0.1.0

func CountInteractions(f InteractionFilter) int64

CountInteractions returns the total number of interactions matching the filter (ignoring limit/offset), for pagination.

func CountUsers added in v0.1.0

func CountUsers() int64

func DB

func DB() *gorm.DB

func DeleteAPIKey added in v0.1.0

func DeleteAPIKey(id, requesterID uint, requesterIsAdmin bool) error

DeleteAPIKey revokes a key. When requesterIsAdmin is false the key must belong to requesterID.

func DeleteFile added in v0.1.4

func DeleteFile(id uint) error

DeleteFile removes a single uploaded file by ID. Returns gorm.ErrRecordNotFound when the ID does not exist.

func DeleteInteraction added in v0.1.4

func DeleteInteraction(id uint) error

DeleteInteraction removes a single interaction and its associated uploaded files. Returns gorm.ErrRecordNotFound when the ID does not exist.

func DeleteSession added in v0.1.0

func DeleteSession(token string)

DeleteSession revokes a single session by its token (logout).

func DeleteSink added in v0.1.0

func DeleteSink(slug string) error

DeleteSink removes a sink by slug. It hard-deletes (Unscoped) so the slug can be reused; a GORM soft-delete would leave the row occupying the unique index and make the slug permanently un-recreatable. Its interactions are left untouched (a separate table).

func DeleteUser added in v0.1.0

func DeleteUser(id uint) error

DeleteUser removes a user and cascades their sessions and API keys.

func DeleteUserSessions added in v0.1.0

func DeleteUserSessions(userID uint)

DeleteUserSessions revokes every session for a user (e.g. on password change).

func GenerateSlug added in v0.1.0

func GenerateSlug() (string, error)

GenerateSlug returns a short, random slug for embedding in payloads. It uses lowercase base32 (not model.randomToken's base64url) so the slug is a valid DNS label — no '_' or uppercase — since slugs are often used as DNS subdomains.

func IsBot added in v0.0.19

func IsBot(remoteAddr string) bool

func LoadDBWithOptions

func LoadDBWithOptions(options DBOptions)

func NewSession added in v0.1.0

func NewSession(userID uint, ttl time.Duration, userAgent, remoteIP string) (string, error)

NewSession creates a session for userID and returns the plaintext token (shown to the client once, stored only hashed).

func PublishInteraction added in v0.1.0

func PublishInteraction(i *Interaction)

PublishInteraction fans out i to every current subscriber. Sends are non-blocking: a subscriber whose buffer is full misses the event rather than stalling the event loop (the client can reload to catch up). It is a cheap no-op when nobody is subscribed.

func PurgeExpiredSessions added in v0.1.0

func PurgeExpiredSessions()

PurgeExpiredSessions removes all sessions past their expiry.

func PurgeInteractions added in v0.1.2

func PurgeInteractions(f InteractionPurgeFilter) (int64, error)

PurgeInteractions deletes the interactions the filter selects and returns the number removed. See MatchingInteractions for filter semantics and errors.

func PurgeInteractionsOlderThan added in v0.1.3

func PurgeInteractionsOlderThan(days int) (int64, error)

PurgeInteractionsOlderThan deletes interactions whose CreatedAt is older than the given number of days, including their associated uploaded files. Returns the number of interaction rows deleted. Returns an error if days < 1 to prevent accidentally nuking everything.

func SinkEventCount added in v0.1.0

func SinkEventCount(slug string) int64

SinkEventCount returns the number of interactions attributed to the slug.

func SubscribeInteractions added in v0.1.0

func SubscribeInteractions() (<-chan *Interaction, func())

SubscribeInteractions registers a subscriber and returns its channel plus an unsubscribe function. The caller must call unsubscribe when done; it removes and closes the channel.

func ValidSlug added in v0.1.0

func ValidSlug(s string) bool

ValidSlug reports whether s is an acceptable caller-supplied slug.

Types

type APIKey added in v0.1.0

type APIKey struct {
	gorm.Model
	UserID     uint       `json:"user_id" gorm:"index"`
	Name       string     `json:"name"`
	Prefix     string     `json:"prefix" gorm:"index"`
	Hash       string     `json:"-"`
	LastUsedAt *time.Time `json:"last_used_at"`
	ExpiresAt  *time.Time `json:"expires_at"`
}

APIKey is a bearer credential for programmatic access. Only the SHA-256 of the full key is stored; Prefix is a non-secret display/lookup handle.

func ListAPIKeys added in v0.1.0

func ListAPIKeys(userID uint) []APIKey

func NewAPIKey added in v0.1.0

func NewAPIKey(userID uint, name string, expiresAt *time.Time) (string, *APIKey, error)

NewAPIKey issues a key for userID and returns the plaintext key (shown once) plus the stored record. expiresAt is optional (nil = never expires).

type DBOptions

type DBOptions struct {
	Reset bool
	Path  string
}

func (*DBOptions) DBPath

func (o *DBOptions) DBPath() string

func (*DBOptions) ShouldReset

func (o *DBOptions) ShouldReset() bool

type Interaction

type Interaction struct {
	gorm.Model

	PayloadID uint    `json:"payload_id"`
	Payload   Payload `json:"-"`

	ProjectID uint    `json:"project_id"`
	Project   Project `json:"-"`

	RemoteAddr    string `json:"remote_addr" gorm:"index:idx_remote_client"`
	RemotePort    string `json:"remote_port"`
	Handler       string `json:"handler"`
	RequestType   string `json:"request_type"`
	RequestTarget string `json:"request_target"`
	Protocol      string `json:"protocol"`
	UserAgent     string `json:"user_agent" gorm:"index:idx_remote_client"`
	Headers       string `json:"headers"`

	Data  []byte         `json:"data"`
	Files []UploadedFile `json:"-" gorm:"foreignKey:InteractionID"`
}

func InteractionByID added in v0.1.0

func InteractionByID(id uint) (*Interaction, error)

InteractionByID fetches a single interaction.

func MatchingInteractions added in v0.1.2

func MatchingInteractions(f InteractionPurgeFilter) ([]Interaction, error)

MatchingInteractions returns the interactions the filter selects, applying the SQL-expressible constraints (handler, target substring) in the query and the CIDR match in Go (SQLite can't do CIDR containment). Returns an error if the filter has no constraint, or if any Remotes entry is an invalid CIDR/IP.

func QueryInteractions added in v0.1.0

func QueryInteractions(f InteractionFilter) []Interaction

QueryInteractions returns interactions matching the filter, newest first.

func SinkEvents added in v0.1.0

func SinkEvents(slug string, limit, offset int) []Interaction

SinkEvents returns the interactions attributed to the slug, newest first.

func SortedInteractions added in v0.0.19

func SortedInteractions(limit int) []Interaction

type InteractionFilter added in v0.1.0

type InteractionFilter struct {
	Handler       string
	RemoteAddr    string
	RequestTarget string
	Limit         int
	Offset        int
}

InteractionFilter narrows and paginates an interaction query for the admin UI. Zero-value fields are ignored; Limit <= 0 means no limit. RequestTarget backs the "all hits to a path" (webhook-style) view.

type InteractionPurgeFilter added in v0.1.2

type InteractionPurgeFilter struct {
	// Remotes is a list of source IPs/CIDRs; an interaction whose RemoteAddr
	// falls in any of them matches. Empty means "any source".
	Remotes []string
	// Target is a case-sensitive substring matched against RequestTarget
	// (the HTTP path / DNS qname). Empty means "any target".
	Target string
	// Handler restricts to a single handler name (e.g. "httpx"). Empty means
	// "any handler".
	Handler string
}

InteractionPurgeFilter selects interactions to delete. Fields are ANDed together; a zero-value filter matches everything, so callers must supply at least one constraint (enforced by Matches returning false for an empty filter) to avoid nuking the whole table by accident.

type OIDCProfile added in v0.1.2

type OIDCProfile struct {
	Subject           string
	Email             string
	PreferredUsername string
	Role              string
}

OIDCProfile is the subset of ID-token claims used to provision and update an OIDC-backed account. Subject is the stable identity key; the rest are used for display and role assignment.

type Payload

type Payload struct {
	Name             string `json,yaml:"name" gorm:"unique"`
	Description      string `json,yaml:"description"`
	Type             string `json,yaml:"type"`
	IsFinal          bool   `json,yaml:"is_final"`
	SortOrder        int    `yaml:"sort_order"`
	Pattern          string `json,yaml:"pattern"`
	InternalFunction string `json,yaml:"internal_function"`
	Data             string `json,yaml:"data"`

	ProjectID uint     `json,yaml:"project_id"`
	Project   *Project `yaml:"-"`

	gorm.Model
	// contains filtered or unexported fields
}

func SortedPayloads

func SortedPayloads() []Payload

func (*Payload) PatternRegexp

func (p *Payload) PatternRegexp() *regexp.Regexp

type Project

type Project struct {
	gorm.Model

	Name    string `gorm:"unique"`
	Code    string `gorm:"unique"`
	Default bool   `gorm:"default:false"`
}

func DefaultProject

func DefaultProject() *Project

type Result added in v0.0.19

type Result struct {
	RemoteAddr  string `json:"remote_addr"`
	Total       int64  `json:"total"`
	MinuteGroup int64  `json:"minute_group"`
}

func Bots added in v0.0.19

func Bots() []Result

type Session added in v0.1.0

type Session struct {
	gorm.Model
	UserID    uint      `gorm:"index"`
	TokenHash string    `gorm:"uniqueIndex"`
	ExpiresAt time.Time `gorm:"index"`
	UserAgent string
	RemoteIP  string
}

Session is a server-side browser session. Only the SHA-256 of the token is stored; the plaintext lives solely in the client's HttpOnly cookie.

type Sink added in v0.1.0

type Sink struct {
	gorm.Model
	Slug        string `json:"slug" gorm:"uniqueIndex"`
	Description string `json:"description"`
	Notify      bool   `json:"notify"`
}

Sink is a named, described slug an operator uses to correlate out-of-band interactions. The slug is embedded in a payload (a URL path, a DNS label, a query value, …); any interaction whose target or raw request contains the slug is attributed to the sink. Sinks are a saved, described view over interactions — creating one does not change what the honeypot captures (every path/name is already recorded), it just labels and groups the hits.

func CreateSink added in v0.1.0

func CreateSink(slug, description string, notify bool) (*Sink, error)

CreateSink stores a new sink. If slug is empty a random one is generated.

func ListSinks added in v0.1.0

func ListSinks() []Sink

ListSinks returns all sinks, newest first.

func NotifySinks added in v0.1.5

func NotifySinks(i *Interaction) []Sink

NotifySinks returns sinks with Notify=true whose slug matches the given interaction (appears in request_target or headers). Returns nil when no notify-enabled sink matches.

func SinkBySlug added in v0.1.0

func SinkBySlug(slug string) (*Sink, error)

SinkBySlug fetches a single sink by its slug.

func UpdateSinkDescription added in v0.1.0

func UpdateSinkDescription(slug, description string) (*Sink, error)

UpdateSinkDescription sets a sink's description (the slug is immutable) and returns the updated record.

func UpdateSinkNotify added in v0.1.5

func UpdateSinkNotify(slug string, notify bool) (*Sink, error)

UpdateSinkNotify sets a sink's notify flag and returns the updated record.

type UploadedFile added in v0.1.3

type UploadedFile struct {
	gorm.Model
	InteractionID uint   `json:"interaction_id" gorm:"index"`
	FileName      string `json:"file_name"`
	ContentType   string `json:"content_type"`
	Size          int64  `json:"size"`
	ContentHash   string `json:"content_hash" gorm:"index"`
	Data          []byte `json:"-"`
}

UploadedFile holds a single file part extracted from a multipart/form-data HTTP request. Files are associated with the Interaction that received them and stored as raw BLOBs in SQLite. ContentHash (SHA-256 hex) is used for deduplication: when a file with the same hash already exists, Data is left nil and the download handler resolves the bytes via FindFileByHash.

func FilesForInteraction added in v0.1.3

func FilesForInteraction(interactionID uint) []UploadedFile

FilesForInteraction returns all uploaded files for the given interaction ID, without the raw Data blob (use UploadedFileByID to fetch with data).

func FindFileByHash added in v0.1.3

func FindFileByHash(hash string) (*UploadedFile, error)

FindFileByHash returns the first uploaded file with the given SHA-256 content hash that has a non-empty Data blob (the canonical copy). Returns nil when no match is found.

func SinkFiles added in v0.1.3

func SinkFiles(slug string, limit, offset int) ([]UploadedFile, int64)

SinkFiles returns uploaded files attributed to interactions matching the given sink slug, newest first (by file creation time). The returned files do not include raw Data; use UploadedFileByID for that.

func UploadedFileByID added in v0.1.3

func UploadedFileByID(id uint) (*UploadedFile, error)

UploadedFileByID fetches a single uploaded file including its raw Data.

type User added in v0.1.0

type User struct {
	gorm.Model
	Username     string `json:"username" gorm:"uniqueIndex"`
	PasswordHash string `json:"-"`
	Role         string `json:"role"`
	// Subject links this account to an external OIDC identity. Empty for local
	// (password) accounts. Indexed (not unique — many local accounts share the
	// empty value); uniqueness of non-empty subjects is enforced in code.
	Subject string `json:"-" gorm:"index"`
}

User is an admin-console account. PasswordHash is a bcrypt hash and is never serialized. Subject is set for accounts provisioned via OIDC (the stable "iss#sub" identity from the ID token); it is empty for local accounts.

func Authenticate added in v0.1.0

func Authenticate(username, password string) (*User, error)

Authenticate verifies a username/password and returns the user. It always runs a bcrypt comparison (against a dummy hash when the user is missing) so response timing does not reveal whether the username exists.

func CreateUser added in v0.1.0

func CreateUser(username, password, role string) (*User, error)

CreateUser creates a user with a bcrypt-hashed password. Usernames are normalized (trimmed, lower-cased) and passwords have a minimum length.

func ListUsers added in v0.1.0

func ListUsers() []User

func UpsertOIDCUser added in v0.1.2

func UpsertOIDCUser(p OIDCProfile) (*User, error)

UpsertOIDCUser provisions or refreshes an account for an OIDC identity. On first login it is created (with no password, so it can never authenticate by password); on subsequent logins its role is re-synced from the current claims so IdP group changes take effect. The account is matched solely by Subject — never by email/username — so a colliding email can't silently take over an existing local account.

func UserByID added in v0.1.0

func UserByID(id uint) (*User, error)

func UserByUsername added in v0.1.0

func UserByUsername(username string) (*User, error)

func UserForAPIKey added in v0.1.0

func UserForAPIKey(full string) *User

UserForAPIKey verifies a bearer key and returns its user, or nil. It looks the row up by the non-secret prefix, then compares the stored hash to the presented key's hash in constant time. LastUsedAt is updated on success.

func UserForSession added in v0.1.0

func UserForSession(token string) *User

UserForSession resolves a session token to its user, or nil if the token is unknown or expired. Expired sessions are pruned on access.

func UserForSubject added in v0.1.2

func UserForSubject(subject string) *User

UserForSubject resolves an OIDC subject to its user, or nil if none is linked. The empty subject never matches (that is the local-account value).

func (*User) IsAdmin added in v0.1.0

func (u *User) IsAdmin() bool

IsAdmin reports whether the user has the admin role.

func (*User) IsOIDC added in v0.1.2

func (u *User) IsOIDC() bool

IsOIDC reports whether the account was provisioned via OIDC (has no password and is bound to an external identity).

func (*User) SetPassword added in v0.1.0

func (u *User) SetPassword(password string) error

SetPassword updates the user's password (bcrypt).

Jump to

Keyboard shortcuts

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