Files
warmbly/internal/models/audit.go
T
Matthew Meszaros b019174910 feat: give an organization one fused abuse posture (#233)
* feat: give an organization one fused abuse posture, because every existing control watches a single subject and an actor slightly wrong on several axes sits under all of them: organizations gains risk_state, risk_score, risk_reason and an append-only risk_signals evidence blob, modelled on the warmup participant health machine that already works rather than a second vocabulary for the same idea; restricted cuts per-mailbox cold volume to a quarter and forces the free warmup pool so a risky tenant cannot spend the paid pool's shared reputation, suspended stops sending at the send gate, and watch deliberately changes nothing a customer can feel so evidence accumulates before anything is taken away; an operator's suspension outranks the derived band so a detector clearing cannot release a workspace a human suspended, transitions ride the audit spine to every teammate's dashboard, a banner says which limit is active and why rather than letting volume drop silently, and the posture never travels in a workspace archive since it is one platform's verdict reached from evidence the destination never saw

* feat: make the suspension actually stop sending, and emit the audit transitions the PR claimed: emailsend.SendEmail is only the manual and API path, so campaign and warmup sends went nowhere near the gate and a suspended workspace kept sending on its schedule, while the restricted multiplier floored every mailbox at one a day which quietly turned suspension into a trickle rather than a stop; the campaign scheduler now defers the whole campaign with a reason and the warmup task skips as org-suspended, since warmup is outbound mail from the same domains; separately the band change emitted no audit entry at all despite the entity type and the frontend spine entry both existing, so no banner moved for a teammate and there was no trail of who was restricted when, and only a real transition is logged so a detector re-recording the same finding cannot fill the feed; one of my own live tests also asserted how far out a slot lands, which depends on the hour the suite runs, and now asserts the property it was about

* chore: drop a test that belongs to the signup-risk change and reached this branch by mistake, where the type it exercises does not exist
2026-08-28 11:20:19 -07:00

186 lines
7.1 KiB
Go

package models
import (
"time"
"github.com/google/uuid"
)
type AuditAction string
const (
AuditActionCreate AuditAction = "create"
AuditActionUpdate AuditAction = "update"
AuditActionDelete AuditAction = "delete"
AuditActionAPICall AuditAction = "api_call"
AuditActionExport AuditAction = "export"
AuditActionImport AuditAction = "import"
AuditActionRevoke AuditAction = "revoke"
AuditActionConnect AuditAction = "connect"
// Lifecycle / workflow actions
AuditActionStart AuditAction = "start"
AuditActionStop AuditAction = "stop"
AuditActionPause AuditAction = "pause"
AuditActionResume AuditAction = "resume"
AuditActionSend AuditAction = "send"
AuditActionDuplicate AuditAction = "duplicate"
AuditActionDisconnect AuditAction = "disconnect"
AuditActionRotate AuditAction = "rotate"
// Membership / governance actions
AuditActionInvite AuditAction = "invite"
AuditActionRemove AuditAction = "remove"
AuditActionTransfer AuditAction = "transfer"
// Worker / fleet operations
AuditActionTest AuditAction = "test"
AuditActionInstall AuditAction = "install"
AuditActionRestart AuditAction = "restart"
AuditActionUpgrade AuditAction = "upgrade"
AuditActionUninstall AuditAction = "uninstall"
AuditActionRotateKeys AuditAction = "rotate_keys"
AuditActionApply AuditAction = "apply"
AuditActionAssign AuditAction = "assign"
AuditActionSystemUpdate AuditAction = "system_update"
AuditActionReboot AuditAction = "reboot"
AuditActionCheckReleases AuditAction = "check_releases"
)
type AuditEntityType string
const (
AuditEntityCampaign AuditEntityType = "campaign"
AuditEntityContact AuditEntityType = "contact"
AuditEntityEmailAccount AuditEntityType = "email_account"
AuditEntityAPIKey AuditEntityType = "api_key"
AuditEntitySequence AuditEntityType = "step"
AuditEntityUser AuditEntityType = "user"
AuditEntityOrganization AuditEntityType = "organization"
// AuditEntityOrgRisk carries a risk-posture transition. It rides the audit
// spine like every other entity, so a change reaches every teammate's
// dashboard without a bespoke emit site.
AuditEntityOrgRisk AuditEntityType = "org_risk"
AuditEntityWorker AuditEntityType = "worker"
AuditEntityAWSCredentials AuditEntityType = "aws_credentials"
AuditEntityWorkerProfile AuditEntityType = "worker_profile"
AuditEntityRelease AuditEntityType = "release"
// Org-scoped configuration & governance entities
AuditEntityOrganizationMember AuditEntityType = "organization_member"
AuditEntityInvitation AuditEntityType = "invitation"
AuditEntityTemplate AuditEntityType = "template"
AuditEntityWebhook AuditEntityType = "webhook"
AuditEntityIntegration AuditEntityType = "integration"
AuditEntityWarmupRoutingRule AuditEntityType = "warmup_routing_rule"
AuditEntityFolder AuditEntityType = "folder"
AuditEntityTag AuditEntityType = "tag"
AuditEntityCategory AuditEntityType = "category"
AuditEntitySubscription AuditEntityType = "subscription"
AuditEntitySettings AuditEntityType = "settings"
// CRM entities
AuditEntityCRMPipeline AuditEntityType = "crm_pipeline"
AuditEntityCRMStage AuditEntityType = "crm_stage"
AuditEntityCRMDeal AuditEntityType = "crm_deal"
AuditEntityCRMTask AuditEntityType = "crm_task"
AuditEntityCRMNote AuditEntityType = "crm_note"
// Inbox
AuditEntityUnibox AuditEntityType = "unibox"
// Collaboration / automation surfaces
AuditEntityTeam AuditEntityType = "team"
AuditEntityAutomation AuditEntityType = "automation"
AuditEntityLeadSyncSource AuditEntityType = "lead_sync_source"
AuditEntityMeeting AuditEntityType = "meeting"
// Workspace archives (export/import for moving between instances).
AuditEntityOrgArchive AuditEntityType = "org_archive"
AuditEntityRole AuditEntityType = "role"
// Referral program. referral = the share code / attribution; referral_credit
// = a reward or clawback on the referrer's earnings ledger.
AuditEntityReferral AuditEntityType = "referral"
AuditEntityReferralCredit AuditEntityType = "referral_credit"
// AI credits. credit_purchase = a top-up pack was fulfilled; credit_grant =
// the monthly plan allowance was reset/granted. Both change the org's
// billing/credits view, so the spine refreshes teammates on either.
AuditEntityCreditPurchase AuditEntityType = "credit_purchase"
AuditEntityCreditGrant AuditEntityType = "credit_grant"
// AI assistant session (per-user dashboard agent conversation).
AuditEntityAISession AuditEntityType = "ai_session"
// AI skill (org playbook) create/update/delete.
AuditEntityAISkill AuditEntityType = "ai_skill"
// Connected MCP server (external tools) connect/update/disconnect.
AuditEntityMCPServer AuditEntityType = "mcp_server"
// Advisor recommendation: applied, undone, snoozed, dismissed, or a
// background evaluation that opened or resolved findings. Rides the audit
// spine so every teammate's advisor strips and nav badges stay live.
AuditEntityAdvisorFinding AuditEntityType = "advisor_finding"
)
// AuditActor is the minimal identity of the member who performed an action,
// resolved by joining the users table so the dashboard can render "who"
// instead of a bare UUID. Nil when the acting user has since been deleted.
type AuditActor struct {
ID uuid.UUID `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email string `json:"email"`
}
type AuditLog struct {
ID uuid.UUID `json:"id"`
OrgID uuid.UUID `json:"org_id"`
UserID uuid.UUID `json:"user_id"` // actor id; kept for backwards-compatible JSON
Actor *AuditActor `json:"actor,omitempty"`
ActionDate time.Time `json:"action_date"`
Action AuditAction `json:"action"`
EntityType AuditEntityType `json:"entity_type"`
EntityID *uuid.UUID `json:"entity_id,omitempty"`
IPAddress string `json:"ip_address"`
UserAgent string `json:"user_agent"`
Changes map[string]string `json:"changes,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
Timestamp time.Time `json:"timestamp"`
}
// AuditLogSearch filters an organization's audit trail. OrgID is required and
// is always set server-side from the caller's session — never from a
// client-supplied value — so one organization can never read another's trail.
type AuditLogSearch struct {
OrgID *uuid.UUID
ActorID *uuid.UUID
EntityType *AuditEntityType
EntityID *uuid.UUID
Action *AuditAction
Since *time.Time
Until *time.Time
Limit int
Cursor string
}
type AuditLogsResult struct {
Data []AuditLog `json:"data"`
Pagination CPagination `json:"pagination"`
}
// CreateAuditLog is a helper struct for creating audit logs.
type CreateAuditLog struct {
OrgID uuid.UUID
UserID uuid.UUID // actor id
Action AuditAction
EntityType AuditEntityType
EntityID *uuid.UUID
IPAddress string
UserAgent string
Changes map[string]string
Metadata map[string]string
}