chore: replace em dashes with hyphens, add no-em-dash rule to CLAUDE.md
This commit is contained in:
@@ -35,15 +35,15 @@ func LogEvent(instanceID, eventType, actor, serverID, keyID, details string) {
|
||||
// AuditFilter narrows a page of the audit log.
|
||||
//
|
||||
// Filtering is done here rather than in the browser because the audit log is
|
||||
// the one collection deliberately kept for months — audit_retention_days is a
|
||||
// licensed entitlement — and it is read to answer questions about the past
|
||||
// the one collection deliberately kept for months - audit_retention_days is a
|
||||
// licensed entitlement - and it is read to answer questions about the past
|
||||
// ("who removed that key in March"). A browser filtering the most recent 200
|
||||
// rows would answer "no results" for an event that exists, which is worse than
|
||||
// having no search at all.
|
||||
type AuditFilter struct {
|
||||
// Search matches actor, details or event type, case-insensitively.
|
||||
Search string
|
||||
// Category matches the segment before the first dot in an event type —
|
||||
// Category matches the segment before the first dot in an event type -
|
||||
// "workflow", "key", "server". Event types are named consistently enough
|
||||
// that the prefix is a real grouping rather than a guess.
|
||||
Category string
|
||||
|
||||
@@ -44,7 +44,7 @@ func StartAuditSweeper(ctx context.Context) {
|
||||
// process restarts.
|
||||
//
|
||||
// It skips an instance whose licence is not valid. A lapsed instance must not
|
||||
// have its history trimmed on the expired term's allowance — expiry degrades to
|
||||
// have its history trimmed on the expired term's allowance - expiry degrades to
|
||||
// read-only, and deleting more of somebody's audit trail is not read-only.
|
||||
//
|
||||
// It skips Unlimited and any non-positive value. A licence that decodes as zero
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
)
|
||||
|
||||
// ErrLockout is returned when a change would leave an instance with neither
|
||||
// local password login nor an enabled provider — nobody could sign in, and no
|
||||
// local password login nor an enabled provider - nobody could sign in, and no
|
||||
// endpoint exists to undo it without database access.
|
||||
var ErrLockout = errors.New("that would leave nobody able to sign in")
|
||||
|
||||
@@ -36,8 +36,8 @@ func authProviderCtx() (context.Context, context.CancelFunc) {
|
||||
return context.WithTimeout(context.Background(), 5*time.Second)
|
||||
}
|
||||
|
||||
// CheckLockout is pure so the two endpoints that can reach this condition —
|
||||
// saving settings and changing a provider — share one answer.
|
||||
// CheckLockout is pure so the two endpoints that can reach this condition -
|
||||
// saving settings and changing a provider - share one answer.
|
||||
func CheckLockout(localEnabled bool, enabledProviders int) error {
|
||||
if localEnabled || enabledProviders > 0 {
|
||||
return nil
|
||||
@@ -244,8 +244,8 @@ func IsLocalLoginEnabled(instanceID string) bool {
|
||||
|
||||
// LocalLoginPermitted answers whether password sign-in must be accepted for
|
||||
// this instance, which is not the same question as whether an administrator
|
||||
// turned it on. An instance whose only providers have become unusable — a
|
||||
// lapsed licence, or every provider disabled — has to keep its password form,
|
||||
// turned it on. An instance whose only providers have become unusable - a
|
||||
// lapsed licence, or every provider disabled - has to keep its password form,
|
||||
// or nobody can sign in and there is no endpoint left to fix it with.
|
||||
func LocalLoginPermitted(instanceID string) bool {
|
||||
if IsLocalLoginEnabled(instanceID) {
|
||||
|
||||
@@ -110,7 +110,7 @@ func UpdateChannel(instanceID, channelID string, upd bson.M) error {
|
||||
// save arrives carrying the sentinel in place of the password. Writing it
|
||||
// through would replace the credential with eight bullet characters and break
|
||||
// delivery on the next alert. A value that is not the sentinel is written
|
||||
// verbatim — including the empty string, which is how a credential is cleared.
|
||||
// verbatim - including the empty string, which is how a credential is cleared.
|
||||
func mergeChannelSecrets(instanceID, channelID string, upd bson.M, cfg map[string]string) (map[string]string, error) {
|
||||
stored, err := GetChannel(instanceID, channelID)
|
||||
if err != nil {
|
||||
|
||||
@@ -30,7 +30,7 @@ var ErrAgentOffline = errors.New("agent is not connected")
|
||||
//
|
||||
// That third line is the one that is easy to get wrong. A ProxyStream is a
|
||||
// separate HTTP/2 request, and an L7 proxy (Traefik, which the chart's gRPC
|
||||
// ingress uses) balances requests rather than connections — so it does not
|
||||
// ingress uses) balances requests rather than connections - so it does not
|
||||
// follow the command stream. Binding the relay listener on the command
|
||||
// stream's pod therefore fails roughly (n-1)/n of the time with "proxy session
|
||||
// not found": the stream arrives at a pod whose registry is empty.
|
||||
@@ -48,7 +48,7 @@ var ErrAgentOffline = errors.New("agent is not connected")
|
||||
//
|
||||
// Teardown needs no message of its own. When the browser goes away guac closes
|
||||
// its connection to the relay, the relay sees the read end, and the session
|
||||
// closes itself — the same path a single-process deployment always took. Only
|
||||
// closes itself - the same path a single-process deployment always took. Only
|
||||
// the *reason* has to cross back, because the pod that writes the audit event
|
||||
// is not the pod that observed the failure.
|
||||
|
||||
|
||||
@@ -19,8 +19,8 @@ import (
|
||||
//
|
||||
// An agent's CommandStream terminates on exactly one process, and with several
|
||||
// replicas that is almost never the process handling the REST request that
|
||||
// wants to talk to it. Publishing unconditionally — rather than checking for a
|
||||
// local stream first and falling back — means one code path, exercised on every
|
||||
// wants to talk to it. Publishing unconditionally - rather than checking for a
|
||||
// local stream first and falling back - means one code path, exercised on every
|
||||
// deployment including the single-replica ones, instead of a rare cross-pod
|
||||
// path that only fails in production.
|
||||
const (
|
||||
@@ -48,7 +48,7 @@ type CommandEnvelope struct {
|
||||
|
||||
// Node names the pod this envelope is for: the presence holder at the time
|
||||
// it was published. The command channel is a fan-out, so during a reconnect
|
||||
// two pods can be subscribed for one agent — the pod with the live stream,
|
||||
// two pods can be subscribed for one agent - the pod with the live stream,
|
||||
// and a pod whose stream is half-open and has not yet noticed. Both would
|
||||
// receive the envelope, and the first to ack wins the request. If that is
|
||||
// the stale one, the command is queued onto a dead stream and acked OK: the
|
||||
@@ -161,7 +161,7 @@ func (d *commandDispatcher) Serve(ctx context.Context, serverID string) (<-chan
|
||||
//
|
||||
// A Redis failure returns true. It is tempting to read an error as loss and
|
||||
// give up, but nothing is known in that moment about who holds the claim, and
|
||||
// the stream this pod is serving is demonstrably alive — the caller is either a
|
||||
// the stream this pod is serving is demonstrably alive - the caller is either a
|
||||
// ticker on that stream or a beat that just succeeded on it. Standing down on a
|
||||
// blip is precisely how an agent ends up connected, beating, and unreachable
|
||||
// until it happens to reconnect.
|
||||
@@ -174,7 +174,7 @@ func renewPresence(ctx context.Context, serverID string) bool {
|
||||
log.Printf("dispatch: presence for %s is held elsewhere, standing down", serverID)
|
||||
return false
|
||||
case bus.RenewedClaim:
|
||||
// Nobody held the key — Redis restarted, failed over, evicted it, or was
|
||||
// Nobody held the key - Redis restarted, failed over, evicted it, or was
|
||||
// unreachable for longer than the TTL. Worth a line: it is the only
|
||||
// evidence that presence was lost and recovered rather than never lost.
|
||||
log.Printf("dispatch: reclaimed presence for %s", serverID)
|
||||
|
||||
@@ -30,7 +30,7 @@ func findingKey(cveID, pkg string) string { return cveID + "\x00" + pkg }
|
||||
// DiffFindings computes the state changes for one server's scan.
|
||||
//
|
||||
// Pure by design: no database, no clock of its own. The ordering below is
|
||||
// load-bearing — see the comment above the second loop.
|
||||
// load-bearing - see the comment above the second loop.
|
||||
func DiffFindings(existing []models.VulnFinding, results []vulndb.Result, now time.Time) FindingDiff {
|
||||
var d FindingDiff
|
||||
|
||||
@@ -104,7 +104,7 @@ func DiffFindings(existing []models.VulnFinding, results []vulndb.Result, now ti
|
||||
}
|
||||
|
||||
// ErrFindingNotFound is returned for a finding that does not exist in this
|
||||
// instance. Callers turn it into a 404 — never a 403, which would confirm the
|
||||
// instance. Callers turn it into a 404 - never a 403, which would confirm the
|
||||
// finding exists in someone else's instance.
|
||||
var ErrFindingNotFound = errors.New("finding not found")
|
||||
|
||||
@@ -116,7 +116,7 @@ type FindingFilter struct {
|
||||
ServerID string
|
||||
Tags map[string]string
|
||||
// HasFix nil is no filter. true is "a vendor fix exists, this is
|
||||
// patchable"; false is the unfixable set — remove the package, disable the
|
||||
// patchable"; false is the unfixable set - remove the package, disable the
|
||||
// service, or accept it, but do not wait for an update.
|
||||
HasFix *bool
|
||||
// TokenScope is the acting credential's tag restriction, nil meaning
|
||||
@@ -172,7 +172,7 @@ func ListInstanceFindings(instanceID string, f FindingFilter) ([]models.VulnFind
|
||||
}
|
||||
|
||||
// The token restriction is applied the same way the Tags selector above
|
||||
// is — by narrowing server_id — rather than by a post-pass, so the two
|
||||
// is - by narrowing server_id - rather than by a post-pass, so the two
|
||||
// cannot disagree and the query keeps one shape. IntersectSelectors is
|
||||
// not used here because Tags has already been resolved to IDs by this
|
||||
// point; intersecting the ID sets is the same operation one level down.
|
||||
@@ -370,7 +370,7 @@ func ListFindings(ctx context.Context, instanceID, serverID string) ([]models.Vu
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ApplyFindingDiff writes a diff. Thin on purpose — the logic worth reading
|
||||
// ApplyFindingDiff writes a diff. Thin on purpose - the logic worth reading
|
||||
// twice is all in DiffFindings.
|
||||
func ApplyFindingDiff(ctx context.Context, instanceID, serverID string, d FindingDiff, now time.Time) error {
|
||||
col := db.Col("vuln_findings")
|
||||
|
||||
@@ -122,7 +122,7 @@ type KeyWithCount struct {
|
||||
// count, narrowed by tokenScope: AssignedCount only counts assignments on
|
||||
// servers ServerInTokenScope admits. Without this, a restricted token reading
|
||||
// the key list would see a nonzero count for a key it cannot see a single
|
||||
// assignment of in its own scope — the same hostname-existence leak
|
||||
// assignment of in its own scope - the same hostname-existence leak
|
||||
// getKey's scope filter closes on the detail route, reachable here through a
|
||||
// count instead of a server object.
|
||||
//
|
||||
@@ -148,7 +148,7 @@ func ListKeys(instanceID string, tokenScope map[string]string) ([]KeyWithCount,
|
||||
|
||||
// Resolve the visible fleet once, outside the per-key loop, so a
|
||||
// restricted token's count costs one extra query total rather than one
|
||||
// per key — the same reasoning ResolveTargetsScoped already applies to
|
||||
// per key - the same reasoning ResolveTargetsScoped already applies to
|
||||
// target resolution.
|
||||
scoped := len(tokenScope) > 0
|
||||
var visibleIDs []string
|
||||
|
||||
@@ -22,7 +22,7 @@ type LicenseState struct {
|
||||
ExpiresAt *time.Time `json:"expires_at,omitempty"`
|
||||
Limits license.Limits `json:"limits"`
|
||||
Features map[string]bool `json:"features"`
|
||||
// Source is "stored", "env" or "none" — useful when a self-hosted operator
|
||||
// Source is "stored", "env" or "none" - useful when a self-hosted operator
|
||||
// asks why the licence they pasted is not the one in effect.
|
||||
Source string `json:"source"`
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ func limitCtx() (context.Context, context.CancelFunc) {
|
||||
// CheckServerLimit refuses a new server when the instance is at its cap.
|
||||
//
|
||||
// Counts live rows only. An instance already over its cap keeps every server it
|
||||
// has — nothing is truncated — it simply cannot add another.
|
||||
// has - nothing is truncated - it simply cannot add another.
|
||||
func CheckServerLimit(instanceID string) error {
|
||||
st := GetLicenseState(instanceID)
|
||||
ctx, cancel := limitCtx()
|
||||
@@ -92,7 +92,7 @@ func CheckChannelLimit(instanceID string) error {
|
||||
// CheckMonitorLimit refuses a new monitor when the instance is at its cap.
|
||||
//
|
||||
// Counts live rows only, like every other check here. An instance already over
|
||||
// its cap keeps every monitor it has and they keep executing — the licence
|
||||
// its cap keeps every monitor it has and they keep executing - the licence
|
||||
// expiry story is that monitoring never stops, so truncating here would
|
||||
// contradict it.
|
||||
func CheckMonitorLimit(instanceID string) error {
|
||||
|
||||
@@ -27,7 +27,7 @@ type legacyInstanceOIDC struct {
|
||||
// migration that needs KEY_ENCRYPTION_KEY fails on an instance that has none
|
||||
// and strands the SSO configuration it was supposed to preserve.
|
||||
//
|
||||
// instance_oidc is left in place and no longer read. Nothing deletes it — a
|
||||
// instance_oidc is left in place and no longer read. Nothing deletes it - a
|
||||
// migration that drops the only copy of a client secret has no undo.
|
||||
func MigrateAuthProviders() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
//
|
||||
// Migration 0004 renames org_id to instance_id in each. A collection missing
|
||||
// from this list keeps the old field name and becomes invisible to every scoped
|
||||
// query — so this list is load-bearing, not documentation.
|
||||
// query - so this list is load-bearing, not documentation.
|
||||
//
|
||||
// AssertNoScopedCollectionMissed checks at boot that nothing outside this list
|
||||
// holds an org_id.
|
||||
@@ -63,7 +63,7 @@ var collectionRenames = []struct{ from, to string }{
|
||||
// bad deploy is recovered by running the inverse rename (cmd/rename-rollback)
|
||||
// rather than by restoring a backup.
|
||||
//
|
||||
// The steps are not atomic across collections — multi-document transactions
|
||||
// The steps are not atomic across collections - multi-document transactions
|
||||
// would require a replica set, which self-hosted installs do not guarantee.
|
||||
// Instead every step is safely repeatable: a collection rename is skipped when
|
||||
// the source is already gone, and $rename matches nothing on a document that
|
||||
|
||||
@@ -55,8 +55,8 @@ func SpecFor(m *models.Monitor) checker.Spec {
|
||||
|
||||
// RedactMonitorRunner replaces m.Runner with models.RunnerRestricted when it
|
||||
// names a server outside the caller's scope, so GET /monitors and GET
|
||||
// /monitors/:id can keep listing the monitor itself — name, type, state,
|
||||
// whether it exists at all — as the first-class object it is, without
|
||||
// /monitors/:id can keep listing the monitor itself - name, type, state,
|
||||
// whether it exists at all - as the first-class object it is, without
|
||||
// disclosing which out-of-scope server it happens to run on. Omitting the
|
||||
// monitor entirely was considered and rejected: a restricted operator has a
|
||||
// legitimate reason to see that a monitor exists and is up or down even when
|
||||
@@ -293,7 +293,7 @@ const MaxMonitorSamples = 6000
|
||||
|
||||
// MonitorSamples returns individual check results since a point in time,
|
||||
// oldest first. Samples older than MonitorSampleTTL have expired, so an early
|
||||
// `since` silently returns a shorter window rather than an error — the caller
|
||||
// `since` silently returns a shorter window rather than an error - the caller
|
||||
// draws the gap.
|
||||
func MonitorSamples(instanceID, monitorID string, since time.Time) ([]models.MonitorSample, error) {
|
||||
ctx, cancel := monCtx()
|
||||
|
||||
@@ -21,7 +21,7 @@ const MonitorSampleTTL = 48 * time.Hour
|
||||
|
||||
// EnsureMonitorSampleIndexes declares the sample range index and its TTL.
|
||||
//
|
||||
// Warn rather than fatal, like the other history indexes — but note the TTL is
|
||||
// Warn rather than fatal, like the other history indexes - but note the TTL is
|
||||
// not an optimisation: without it nothing ever removes a sample, and the
|
||||
// collection grows at the fleet's total check rate forever. A boot that logs
|
||||
// this warning needs following up.
|
||||
|
||||
@@ -87,7 +87,7 @@ type PackageHit struct {
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
// SearchPackages answers "which servers run package X" across the fleet — the
|
||||
// SearchPackages answers "which servers run package X" across the fleet - the
|
||||
// question people actually ask during an incident.
|
||||
//
|
||||
// The Mongo filter narrows to documents containing the name; the second pass is
|
||||
@@ -96,8 +96,8 @@ type PackageHit struct {
|
||||
//
|
||||
// tokenScope is the acting credential's tag restriction, nil meaning
|
||||
// unrestricted; a hit on a server outside it is dropped before it is returned.
|
||||
// The filtering is done with VisibleServerIDs — one membership set resolved
|
||||
// once — rather than by resolving each hit's server individually the way
|
||||
// The filtering is done with VisibleServerIDs - one membership set resolved
|
||||
// once - rather than by resolving each hit's server individually the way
|
||||
// search_fleet does, because a package search can return one hit per host in
|
||||
// the fleet and the query shape must not depend on how many matched. The Mongo
|
||||
// query itself is unchanged: server_packages carries no tags to filter on, so
|
||||
|
||||
@@ -62,7 +62,7 @@ func reapAfter() time.Duration {
|
||||
//
|
||||
// Unexported and unguarded: it trusts its caller completely and performs an
|
||||
// irreversible delete on whatever instance ID it is handed. The tier and expiry
|
||||
// gate — Free tier, an expiry that exists, an expiry past the window — lives in
|
||||
// gate - Free tier, an expiry that exists, an expiry past the window - lives in
|
||||
// ReapFreeInstances, which is the only caller. Do not export this.
|
||||
//
|
||||
// Idempotent: re-running over a half-deleted instance completes it. The instance
|
||||
@@ -93,8 +93,8 @@ func purgeInstance(ctx context.Context, instanceID string) (map[string]int64, er
|
||||
// ago than the configured window.
|
||||
//
|
||||
// Eligibility requires ALL of:
|
||||
// - license_tier == "free" — a paid instance is never eligible
|
||||
// - license_expiry present — an instance that was never licensed, or whose
|
||||
// - license_tier == "free" - a paid instance is never eligible
|
||||
// - license_expiry present - an instance that was never licensed, or whose
|
||||
// issuance failed, has no expiry and is never eligible whatever its age
|
||||
// - license_expiry older than now minus the window
|
||||
//
|
||||
@@ -136,7 +136,7 @@ func ReapFreeInstances(ctx context.Context) (checked, purged int, err error) {
|
||||
// audit entry written here would delete itself moments later. It is
|
||||
// written anyway, because an operator reading audit during the window
|
||||
// should see it coming.
|
||||
log.Printf("REAPING instance %s (%s, slug=%s) — Free licence expired %s, past the %s window",
|
||||
log.Printf("REAPING instance %s (%s, slug=%s) - Free licence expired %s, past the %s window",
|
||||
d.InstanceID, d.Name, d.Slug, d.Expiry.Format(time.RFC3339), window)
|
||||
LogEvent(d.InstanceID, "instance.reaped", "system", "", "",
|
||||
fmt.Sprintf("free licence expired %s, window %s", d.Expiry.Format(time.RFC3339), window))
|
||||
@@ -157,7 +157,7 @@ func ReapFreeInstances(ctx context.Context) (checked, purged int, err error) {
|
||||
//
|
||||
// The pass at boot follows inject.StartReconciler's precedent and earns its keep
|
||||
// the same way: it makes a restart a supported way to force a sweep, which is
|
||||
// the only way this code can be exercised on demand — the ticker is hourly and
|
||||
// the only way this code can be exercised on demand - the ticker is hourly and
|
||||
// deletion is measured in days.
|
||||
func StartReaper(ctx context.Context) {
|
||||
window := reapAfter()
|
||||
@@ -165,7 +165,7 @@ func StartReaper(ctx context.Context) {
|
||||
log.Printf("reaper: DISABLED (FREE_INSTANCE_REAP_AFTER is unset or zero)")
|
||||
return
|
||||
}
|
||||
log.Printf("reaper: ENABLED — Free instances are deleted %s after their licence expires", window)
|
||||
log.Printf("reaper: ENABLED - Free instances are deleted %s after their licence expires", window)
|
||||
|
||||
go func() {
|
||||
reapOnce(ctx)
|
||||
|
||||
@@ -76,7 +76,7 @@ func GetServer(instanceID, serverID string) (*models.Server, error) {
|
||||
// a restricted token must not be able to enumerate the fleet it cannot see by
|
||||
// noticing which IDs answer differently.
|
||||
//
|
||||
// mongo.ErrNoDocuments is GetServer's own not-found identifier — reused here
|
||||
// mongo.ErrNoDocuments is GetServer's own not-found identifier - reused here
|
||||
// rather than introducing a second one, so a caller checking for one keeps
|
||||
// working against a server that exists but is out of the token's scope.
|
||||
func GetServerScoped(instanceID, serverID string, tokenScope map[string]string) (*models.Server, error) {
|
||||
@@ -360,7 +360,7 @@ func markOfflineForFilter(scope bson.M, instanceID string) error {
|
||||
}
|
||||
|
||||
// notifyServerOffline delivers an agent-offline alert over the instance's
|
||||
// chosen notification channels — the same destinations monitors dispatch to,
|
||||
// chosen notification channels - the same destinations monitors dispatch to,
|
||||
// so a webhook or SMTP destination is configured and tested in exactly one
|
||||
// place. No channels selected means the alert is audited but not sent.
|
||||
func notifyServerOffline(instanceID string, channelIDs []string, s models.Server) {
|
||||
@@ -393,7 +393,7 @@ func notifyServerOffline(instanceID string, channelIDs []string, s models.Server
|
||||
}
|
||||
|
||||
// ListServersFiltered is ListServers with an optional tag selector. An empty
|
||||
// selector returns the whole fleet — unlike MatchesTags, where empty means
|
||||
// selector returns the whole fleet - unlike MatchesTags, where empty means
|
||||
// "nothing", because here the caller is a list view whose default is
|
||||
// "everything", not a run about to touch machines.
|
||||
func ListServersFiltered(instanceID string, sel map[string]string) ([]models.Server, error) {
|
||||
|
||||
@@ -74,7 +74,7 @@ func validateIncident(inc *models.StatusIncident) error {
|
||||
// fleet's monitors: publishing "api-gateway is degraded" on a page that never
|
||||
// listed api-gateway names a machine to the public that the page deliberately
|
||||
// does not, which is the same leak assembleSnapshot's redaction boundary exists
|
||||
// to prevent — reached from the authoring side instead of the read side.
|
||||
// to prevent - reached from the authoring side instead of the read side.
|
||||
//
|
||||
// It is a separate pass rather than part of validateIncident because it reads
|
||||
// the database, and validateIncident is a pure function of the document. The
|
||||
@@ -289,7 +289,7 @@ func AppendStatusIncidentUpdate(instanceID, incidentID, status, body, author str
|
||||
set["resolved_at"] = upd.At
|
||||
} else {
|
||||
// Reopening via an appended update must clear a previously-set
|
||||
// resolved_at the same way UpdateStatusIncident does — otherwise a
|
||||
// resolved_at the same way UpdateStatusIncident does - otherwise a
|
||||
// resolved incident reopened to "monitoring" keeps a stale resolved_at
|
||||
// and silently drops off ListStatusIncidentsForPage once started_at
|
||||
// ages past the since cutoff, because none of its $or clauses match.
|
||||
|
||||
@@ -47,7 +47,7 @@ func spCtx() (context.Context, context.CancelFunc) {
|
||||
//
|
||||
// All three are attempted and the failures joined, rather than returning on
|
||||
// the first. The three are independent, and two of them are uniqueness
|
||||
// constraints — bailing out on the status_pages index meant a transient
|
||||
// constraints - bailing out on the status_pages index meant a transient
|
||||
// failure there silently left status_incidents with no unique
|
||||
// (instance_id, incident_id) index at all.
|
||||
func EnsureStatusPageIndexes() error {
|
||||
@@ -86,7 +86,7 @@ var (
|
||||
ErrPageIDTaken = errors.New("that page id is already in use")
|
||||
|
||||
// ErrPageInvalid is the sentinel for validation failures on a page or
|
||||
// incident body — anything the caller can fix by sending a different
|
||||
// incident body - anything the caller can fix by sending a different
|
||||
// request. statusPageError maps it to 400; wrap it rather than returning a
|
||||
// bare error, or a bad request answers 500.
|
||||
ErrPageInvalid = errors.New("status page request invalid")
|
||||
|
||||
@@ -58,7 +58,7 @@ type PublicIncidentUpdate struct {
|
||||
}
|
||||
|
||||
// PublicIncident covers both authored incidents and derived monitor outages.
|
||||
// A derived one carries no updates and no impact — and never a cause, which is
|
||||
// A derived one carries no updates and no impact - and never a cause, which is
|
||||
// where internal hostnames live.
|
||||
type PublicIncident struct {
|
||||
ID string `json:"id"`
|
||||
@@ -138,7 +138,7 @@ func assembleSnapshot(in snapshotInput) StatusSnapshot {
|
||||
names[entry.MonitorID] = name
|
||||
|
||||
// Uptime is computed from the days as reported by rollups, before
|
||||
// any maintenance repaint — a no_data day must never be counted as
|
||||
// any maintenance repaint - a no_data day must never be counted as
|
||||
// zero uptime just because it is later redrawn as "maintenance".
|
||||
days := buildDays(in.Rollups[entry.MonitorID], in.Now)
|
||||
comp := PublicComponent{
|
||||
@@ -294,7 +294,7 @@ func publicFromAuthored(inc models.StatusIncident, names map[string]string) Publ
|
||||
// already read the true no_data/up/down state of each day. Folding the
|
||||
// repaint in here would let a today cell with no rollups yet flip from
|
||||
// no_data to maintenance before its uptime contribution was decided, and
|
||||
// uptimeFromDays skips no_data days by their State — so that day would stop
|
||||
// uptimeFromDays skips no_data days by their State - so that day would stop
|
||||
// being skipped and start counting as a zero.
|
||||
func buildDays(rollups []models.Rollup, now time.Time) []PublicDay {
|
||||
type bucket struct{ checks, up int }
|
||||
@@ -335,7 +335,7 @@ func buildDays(rollups []models.Rollup, now time.Time) []PublicDay {
|
||||
// computation, not before: repainting first would turn a today cell with no
|
||||
// rollups yet from no_data (skipped) into maintenance (a 0% day counted in
|
||||
// the average), and repainting a day that DOES have rollups must still leave
|
||||
// that day's real up/down contribution in the average — maintenance changes
|
||||
// that day's real up/down contribution in the average - maintenance changes
|
||||
// how a day is drawn, never what the numbers say.
|
||||
func applyMaintenanceRepaint(days []PublicDay, inMaintenance bool) []PublicDay {
|
||||
if inMaintenance && len(days) > 0 {
|
||||
|
||||
@@ -165,7 +165,7 @@ func TestAssembleSnapshotMaintenanceDoesNotChangeUptime(t *testing.T) {
|
||||
|
||||
// TestAssembleSnapshotMaintenanceRepaintDoesNotCountNoDataAsZero guards
|
||||
// against the maintenance repaint corrupting Uptime90d for a component whose
|
||||
// today rollup has not landed yet — an in-progress maintenance window on a
|
||||
// today rollup has not landed yet - an in-progress maintenance window on a
|
||||
// young component, or one that simply started before today's hourly rollup
|
||||
// was written. Repainting today's no_data cell to "maintenance" must never
|
||||
// make uptimeFromDays stop skipping it: doing so would turn a component with
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
// process: step output arrives on whichever pod holds the agent's stream, the
|
||||
// run's markers are written by whichever pod started the run, and the browser
|
||||
// asks for the log through whichever pod the load balancer picked. Three pods,
|
||||
// one file, one local disk — two of them see an empty log.
|
||||
// one file, one local disk - two of them see an empty log.
|
||||
//
|
||||
// Mongo makes every pod an equal reader and writer, which is the property that
|
||||
// matters. It costs writes on the hot path, so the writer batches (see
|
||||
@@ -144,7 +144,7 @@ type stepLogRegistry struct {
|
||||
|
||||
// The registry stays process-local, and correctly so: a step's output arrives
|
||||
// on the pod holding that agent's stream, and that is the same pod the
|
||||
// dispatch envelope asked to open the writer. Nothing here crosses pods —
|
||||
// dispatch envelope asked to open the writer. Nothing here crosses pods -
|
||||
// only the lines it produces do, by virtue of landing in Mongo.
|
||||
var StepLogs = &stepLogRegistry{writers: make(map[string]*stepLogWriter)}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ type stepResultRegistry struct{}
|
||||
var StepResults = &stepResultRegistry{}
|
||||
|
||||
// Await subscribes to a command's result channel. The returned cancel function
|
||||
// must be called once the caller is done, whether a result arrived or not —
|
||||
// must be called once the caller is done, whether a result arrived or not -
|
||||
// it is what releases the Redis subscription.
|
||||
func (r *stepResultRegistry) Await(commandID string) (<-chan *pb.StepResult, func()) {
|
||||
out := make(chan *pb.StepResult, 1)
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
)
|
||||
|
||||
// ErrInvalidTag is returned for any tag the rules below reject. Handlers map
|
||||
// it to 400 — a malformed tag is the caller's mistake, not a server fault.
|
||||
// it to 400 - a malformed tag is the caller's mistake, not a server fault.
|
||||
var ErrInvalidTag = errors.New("invalid tag")
|
||||
|
||||
const (
|
||||
@@ -43,8 +43,8 @@ func validTagRunes(s string) bool {
|
||||
}
|
||||
|
||||
// ValidateTags enforces the shape of a whole tag map. It lives in the service
|
||||
// layer rather than a handler so that every write path — the tags endpoint,
|
||||
// server create, anything added later — agrees on what a valid tag is.
|
||||
// layer rather than a handler so that every write path - the tags endpoint,
|
||||
// server create, anything added later - agrees on what a valid tag is.
|
||||
func ValidateTags(tags map[string]string) error {
|
||||
if len(tags) > maxTagsPerHost {
|
||||
return fmt.Errorf("%w: at most %d tags per server", ErrInvalidTag, maxTagsPerHost)
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
// report success over zero servers.
|
||||
var ErrNoTargets = errors.New("workflow has no target servers")
|
||||
|
||||
// MatchesTags reports whether srv carries every pair in sel — AND across keys.
|
||||
// MatchesTags reports whether srv carries every pair in sel - AND across keys.
|
||||
// An empty selector matches nothing. That is deliberate: the alternative,
|
||||
// "matches everything", turns a cleared field in the workflow designer into a
|
||||
// fleet-wide run.
|
||||
|
||||
@@ -129,7 +129,7 @@ func CreateAPIToken(instanceID, userID, name, role string, scopes []string, tagS
|
||||
InstanceID: instanceID,
|
||||
UserID: userID,
|
||||
Name: name,
|
||||
// Hint is "vt_" plus 5 hex characters of the secret (20 bits) — enough
|
||||
// Hint is "vt_" plus 5 hex characters of the secret (20 bits) - enough
|
||||
// for a user to recognise their own token in a list, not enough to be
|
||||
// useful to anyone who only has the hint. Considered and accepted.
|
||||
Hint: plaintext[:8],
|
||||
@@ -197,7 +197,7 @@ var (
|
||||
)
|
||||
|
||||
// ShouldLogExpiredTokenUse reports whether an expired token's use is worth a
|
||||
// fresh audit row, throttled to once per token per minute — the same window
|
||||
// fresh audit row, throttled to once per token per minute - the same window
|
||||
// TouchAPIToken uses for last-used, kept here rather than in the auth package
|
||||
// because the storage concern (what counts as "recent") belongs beside the
|
||||
// token's other storage-backed state, not scattered into the request layer.
|
||||
|
||||
@@ -25,13 +25,13 @@ func ServerInTokenScope(srv models.Server, sel map[string]string) bool {
|
||||
|
||||
// VisibleServerIDs resolves the servers tokenScope admits into a membership
|
||||
// set, for a caller that needs to test many IDs against the caller's scope in
|
||||
// one pass — redacting a monitor's runner, filtering a workflow's target list
|
||||
// — rather than resolving one server at a time the way GetServerScoped does.
|
||||
// one pass - redacting a monitor's runner, filtering a workflow's target list
|
||||
// - rather than resolving one server at a time the way GetServerScoped does.
|
||||
//
|
||||
// restricted is false for an empty tokenScope, matching ServerInTokenScope's
|
||||
// own rule that an empty selector is unrestricted rather than "sees nothing".
|
||||
// ids is then nil, and callers must treat (nil, false) as "everything
|
||||
// visible", never as "nothing visible" — the zero value of a map read is
|
||||
// visible", never as "nothing visible" - the zero value of a map read is
|
||||
// false, which would silently invert the rule for every unrestricted caller
|
||||
// if this contract were not honoured.
|
||||
func VisibleServerIDs(instanceID string, tokenScope map[string]string) (ids map[string]bool, restricted bool, err error) {
|
||||
@@ -55,7 +55,7 @@ func VisibleServerIDs(instanceID string, tokenScope map[string]string) (ids map[
|
||||
// (ids, restricted) pair VisibleServerIDs returns. An unrestricted caller
|
||||
// (restricted false) gets ids back unchanged and hidden is always false.
|
||||
//
|
||||
// hidden reports only whether at least one id was dropped — never how many —
|
||||
// hidden reports only whether at least one id was dropped - never how many -
|
||||
// because the point of surfacing it at all is to let a caller say "some
|
||||
// targets are not visible to you" without the count itself becoming the leak
|
||||
// this exists to close. A workflow that targets both an in-scope and an
|
||||
|
||||
@@ -136,7 +136,7 @@ func TestScopedRunOfOutOfScopeWorkflowReachesNothing(t *testing.T) {
|
||||
t.Errorf("staging-scoped run resolved %v, want nothing", got)
|
||||
}
|
||||
// ResolveTargetsScoped turns that empty set into ErrNoTargets, which is
|
||||
// the same answer a workflow targeting no servers at all gives — so the
|
||||
// the same answer a workflow targeting no servers at all gives - so the
|
||||
// refusal does not tell the caller that production hosts exist.
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ var ErrLastOwner = errors.New("this is the organization's last owner promote ano
|
||||
// An hq-sourced row is projected from a Vantage HQ account: HQ owns its role,
|
||||
// its password and its existence. A role editable in two places is a role with
|
||||
// two answers, and the loser is whichever writer ran first. Refusing here
|
||||
// rather than merely hiding the control in web/ is the point — the API is the
|
||||
// rather than merely hiding the control in web/ is the point - the API is the
|
||||
// boundary, the UI is a courtesy.
|
||||
var ErrHQManaged = errors.New("this member is managed in Vantage HQ; change their role or remove them from the HQ portal")
|
||||
|
||||
@@ -77,7 +77,7 @@ func CreateUser(instanceID, email, password, role, authSource string) (*models.U
|
||||
//
|
||||
// There is deliberately no unscoped lookup by email. users is unique on
|
||||
// (instance_id, email), not on email alone, so an unscoped FindOne would return
|
||||
// an arbitrary one of several matching users — which on the login path means
|
||||
// an arbitrary one of several matching users - which on the login path means
|
||||
// signing someone into a tenant that is not theirs.
|
||||
func GetUserInInstanceByEmail(instanceID, email string) (*models.User, error) {
|
||||
email = strings.ToLower(strings.TrimSpace(email))
|
||||
|
||||
@@ -131,7 +131,7 @@ func validateVulnRule(instanceID string, r *models.VulnAlertRule) error {
|
||||
return validateChannelIDs(instanceID, r.ChannelIDs)
|
||||
}
|
||||
|
||||
// SendVulnDigest delivers one message per rule per tick — never one per
|
||||
// SendVulnDigest delivers one message per rule per tick - never one per
|
||||
// finding. See vulnsched for why the tick is the batch boundary.
|
||||
func SendVulnDigest(instanceID string, newly []models.VulnFinding) {
|
||||
rules, err := ListVulnRules(instanceID)
|
||||
|
||||
@@ -26,7 +26,7 @@ const stepDispatchGrace = 15 * time.Second
|
||||
// trigger would leave the dispatch reaching further than the readout.
|
||||
//
|
||||
// A run whose configured targets fall entirely outside the caller's scope
|
||||
// resolves to nothing and returns ErrNoTargets — the same answer a workflow
|
||||
// resolves to nothing and returns ErrNoTargets - the same answer a workflow
|
||||
// targeting no servers at all gives, so an out-of-scope host stays
|
||||
// indistinguishable from one that does not exist.
|
||||
func TriggerWorkflow(instanceID, workflowID, actor string, tokenScope map[string]string) (string, error) {
|
||||
|
||||
@@ -30,7 +30,7 @@ func TestSaveTargetsRestrictedCallerCannotReachOutsideScope(t *testing.T) {
|
||||
func TestSaveTargetsUnrestrictedCallerUnaffected(t *testing.T) {
|
||||
// validateWorkflowTargetScope short-circuits before ever resolving the
|
||||
// fleet when tokenScope is nil, so an unrestricted caller keeps today's
|
||||
// behaviour exactly — including saving a tag selector matching nothing.
|
||||
// behaviour exactly - including saving a tag selector matching nothing.
|
||||
// listServersForScope is stubbed to fail the test if called at all, so
|
||||
// this proves the short-circuit, not just that the decision would allow
|
||||
// it.
|
||||
@@ -66,7 +66,7 @@ func TestSaveTargetsEqualOrNarrowerSelectorAllowed(t *testing.T) {
|
||||
}
|
||||
|
||||
// The time-of-write/time-of-fire gap: a restricted caller naming IDs or tags
|
||||
// that match no server at all today must be refused, not passed through —
|
||||
// that match no server at all today must be refused, not passed through -
|
||||
// otherwise the caller could save a selector for an environment that does
|
||||
// not exist yet, arm the schedule, and have it fire the moment a server picks
|
||||
// up the tag. This is distinct from "no targets at all" below.
|
||||
@@ -98,7 +98,7 @@ func TestSaveTargetsMatchingNothingIsRefusedForRestrictedCaller(t *testing.T) {
|
||||
// ...
|
||||
// FAIL .../internal/services 0.006s
|
||||
//
|
||||
// rather than a clean assertion failure, which is still a failure — the test
|
||||
// rather than a clean assertion failure, which is still a failure - the test
|
||||
// no longer passes silently once the enforcement is removed.
|
||||
func TestCreateAndUpdateWorkflowBindToTargetScopeCheck(t *testing.T) {
|
||||
restore := listServersForScope
|
||||
@@ -126,7 +126,7 @@ func TestCreateAndUpdateWorkflowBindToTargetScopeCheck(t *testing.T) {
|
||||
}
|
||||
|
||||
// A workflow with no targets at all (no IDs, no tags) must stay creatable
|
||||
// for a restricted caller — there is nothing to escalate through, and this
|
||||
// for a restricted caller - there is nothing to escalate through, and this
|
||||
// must not become collateral damage from the fix above.
|
||||
func TestSaveTargetsNoTargetsAtAllIsUnaffected(t *testing.T) {
|
||||
restore := listServersForScope
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
// ErrDefaultStep is returned when a caller tries to edit or delete a step that
|
||||
// came from the image's default library. Those rows are re-seeded from disk on
|
||||
// every boot, so an edit would be silently reverted and a delete would come
|
||||
// back — refusing is honest about who owns them.
|
||||
// back - refusing is honest about who owns them.
|
||||
var ErrDefaultStep = errors.New("this step ships with Vantage and cannot be edited or deleted; duplicate it to make your own copy")
|
||||
|
||||
func isDefaultStep(ctx context.Context, instanceID, stepID string) (bool, error) {
|
||||
@@ -304,7 +304,7 @@ func UpdateWorkflow(instanceID, id string, w models.Workflow, tokenScope map[str
|
||||
// The first is escalation: without it a token restricted to staging could save
|
||||
// a workflow targeting production and then reach those hosts through the
|
||||
// scheduler, which fires as the system with no restriction of its own. The
|
||||
// second is enumeration — "target server X not found" versus a successful save
|
||||
// second is enumeration - "target server X not found" versus a successful save
|
||||
// is a yes/no oracle over the whole fleet, and the design forbids a restricted
|
||||
// token learning which IDs exist outside its scope.
|
||||
//
|
||||
@@ -347,15 +347,15 @@ var listServersForScope = ListServers
|
||||
// A nil tokenScope is unrestricted and always passes: an unrestricted caller
|
||||
// may save any selector, including one matching nothing today, exactly as
|
||||
// before this fix. A workflow with no targets at all (empty IDs and empty
|
||||
// tags) is also left alone regardless of scope — there is nothing for it to
|
||||
// tags) is also left alone regardless of scope - there is nothing for it to
|
||||
// fire on, and refusing it would break the existing, unrelated ability to
|
||||
// save a workflow before wiring up its targets.
|
||||
//
|
||||
// What IS refused, for a restricted caller only, is a workflow that names IDs
|
||||
// or tags which resolve to no server at all. Without this, a token restricted
|
||||
// to env=staging could save target_tags {env: production} while no server yet
|
||||
// carries that pair — a not-yet-provisioned environment, a tag rollout in
|
||||
// progress, a guessed value — pass validation on an empty set, arm the
|
||||
// carries that pair - a not-yet-provisioned environment, a tag rollout in
|
||||
// progress, a guessed value - pass validation on an empty set, arm the
|
||||
// schedule, and have the scheduler execute on those hosts the moment someone
|
||||
// tags them. That is the same escalation as the out-of-scope case, just
|
||||
// deferred to whenever the fleet catches up to the selector, so it is
|
||||
|
||||
@@ -23,7 +23,7 @@ const workloadResultTimeout = 120 * time.Second
|
||||
|
||||
// MaxWorkloadLogLines mirrors the agent's own cap. It is declared again here
|
||||
// rather than imported: agent/ is a separate module with an internal/ tree, so
|
||||
// the two cannot share a constant. Change one, change the other — the same
|
||||
// the two cannot share a constant. Change one, change the other - the same
|
||||
// shape of hazard as the mirrored token blocks in the web apps.
|
||||
const MaxWorkloadLogLines = 500
|
||||
|
||||
@@ -33,7 +33,7 @@ const MaxWorkloadLogLines = 500
|
||||
const workloadProtectedMarker = "workload is protected"
|
||||
|
||||
// IsWorkloadProtected reports whether an agent refused because the target is
|
||||
// protected — the agent's own guard, which is the boundary. Nothing failed, so
|
||||
// protected - the agent's own guard, which is the boundary. Nothing failed, so
|
||||
// the API answers 409 rather than an error status.
|
||||
func IsWorkloadProtected(err error) bool {
|
||||
return err != nil && strings.Contains(err.Error(), workloadProtectedMarker)
|
||||
@@ -91,14 +91,14 @@ type WorkloadHit struct {
|
||||
Workload models.Workload `json:"workload"`
|
||||
}
|
||||
|
||||
// SearchWorkloads answers "which servers run image X" — the reason the snapshot
|
||||
// SearchWorkloads answers "which servers run image X" - the reason the snapshot
|
||||
// is stored rather than fetched on demand and discarded.
|
||||
//
|
||||
// tokenScope is the acting credential's tag restriction, nil meaning
|
||||
// unrestricted. A WorkloadHit names a server ID, so an unfiltered fleet-wide
|
||||
// search enumerates hosts a restricted token must not see. server_workloads
|
||||
// carries no tags of its own, so the narrowing is a membership test against
|
||||
// VisibleServerIDs resolved once — the same shape SearchPackages uses.
|
||||
// VisibleServerIDs resolved once - the same shape SearchPackages uses.
|
||||
func SearchWorkloads(instanceID, image, stack, state string, tokenScope map[string]string) ([]WorkloadHit, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user