chore: replace em dashes with hyphens, add no-em-dash rule to CLAUDE.md
Chart Release / chart (push) Successful in 20s
Server Deploy / deploy (push) Failing after 1m52s

This commit is contained in:
2026-09-10 09:18:55 +00:00
parent b36a696d0e
commit 6ee203f5e9
172 changed files with 860 additions and 856 deletions
+1 -1
View File
@@ -25,7 +25,7 @@ RUN --mount=type=secret,id=netrc,target=/root/.netrc \
-ldflags="-s -w -X main.Version=${VERSION}" -o /vantage-server ./cmd
# Staged so the scratch image below can have a /tmp. It cannot mkdir one
# itself scratch has no shell and os.MkdirTemp fails outright without it.
# itself - scratch has no shell - and os.MkdirTemp fails outright without it.
RUN mkdir -p /staging/tmp && chmod 1777 /staging/tmp
# Runtime stage
+8 -8
View File
@@ -39,7 +39,7 @@ import (
// comment group, separated by a real blank line rather than a bare "//": Go's
// parser only splits ast.CommentGroups on an actual blank line, and
// swag v2.0.0-rc5's parseSecAttributesV3 resolves a scheme's map key by
// scanning from the start of whatever comment group it was handed so three
// scanning from the start of whatever comment group it was handed - so three
// stacked blocks sharing one group all collapse onto the first block's name.
// Three groups means three independent scans, each finding its own name.
@@ -63,7 +63,7 @@ func main() {
//
// Under Docker Compose neither is set and nothing changes: one process
// migrates and then serves. Under Kubernetes with more than one replica
// that is unsafe every pod would run MigrateOrgToInstance at once, and
// that is unsafe - every pod would run MigrateOrgToInstance at once, and
// renaming collections while a sibling reads them is not a race anyone
// wins. The chart therefore runs a pre-upgrade Job with MIGRATE_ONLY and
// starts the Deployment with SKIP_MIGRATIONS.
@@ -198,7 +198,7 @@ func runSchemaSetup() {
}
// apiVersion mirrors the @version annotation on the swagger block above,
// which is the only version string this server already establishes there is
// which is the only version string this server already establishes - there is
// no separate runtime build-version constant to reuse instead. Nothing ties
// the two together mechanically, so change them in the same commit: this is
// the value mcp.SetVersion reports to MCP clients, and it must keep agreeing
@@ -225,8 +225,8 @@ func serve() {
}
log.Printf("message bus ready as node %s", bus.NodeID())
// Cancelled on SIGTERM/SIGINT. Everything below that takes a context the
// housekeeping jobs, the leader lock stops when the pod is asked to.
// Cancelled on SIGTERM/SIGINT. Everything below that takes a context - the
// housekeeping jobs, the leader lock - stops when the pod is asked to.
ctx, shutdown := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer shutdown()
@@ -241,7 +241,7 @@ func serve() {
// every monitor check firing N times, every incident notification delivered
// to the customer N times, every retention sweep deleting concurrently, and
// N reapers racing to purge the same instance. They share one lock rather
// than holding four, because they are one role housekeeping and
// than holding four, because they are one role - housekeeping - and
// splitting them would only spread that role across pods for no benefit.
bus.RunAsLeader(ctx, "housekeeping", func(jobCtx context.Context) {
services.StartLogSweeper(jobCtx)
@@ -278,7 +278,7 @@ func serve() {
// caller wrote in X-Forwarded-For. That was survivable while ClientIP()
// only produced audit strings; the public status limiter makes it load
// bearing. Empty means trust nobody, which is correct for a direct
// exposure and wrong behind a proxy hence the explicit setting.
// exposure and wrong behind a proxy - hence the explicit setting.
if err := r.SetTrustedProxies(api.TrustedProxies()); err != nil {
log.Fatalf("trusted proxies: %v", err)
}
@@ -365,7 +365,7 @@ func boolEnv(key string) bool {
// It replaces a substring filter that fed in only routes whose path contained
// "server", ":serverId", "console" or "assign". That filter could only ever
// catch a route whose *path* named a server, and a route can act on one named
// in its body, in a query parameter, or derived by the handler it caught one
// in its body, in a query parameter, or derived by the handler - it caught one
// of the leaks found in the final review of the MCP feature, and none of the
// eleven found during implementation. Declaring every route is more typing
// once and no maintenance after: a new route fails boot until somebody answers
+4 -4
View File
@@ -19,7 +19,7 @@ import (
// consoleConnect godoc
//
// @Summary Open a browser console session
// @Description Mints a one-time session token for the /console/tunnel websocket. Requires a live agent answers 409 agent_offline otherwise.
// @Description Mints a one-time session token for the /console/tunnel websocket. Requires a live agent - answers 409 agent_offline otherwise.
// @Tags console
// @Accept json
// @Produce json
@@ -109,7 +109,7 @@ func queryIntDefault(r *http.Request, key string, def int) int {
// Every branch here logs. That is deliberate and worth keeping: this handler
// spans four hops (session store, agent dispatch, relay announcement, guacd),
// any of which can fail, and the client is told the same near-useless thing by
// most of them a 500 that guacamole then reports as an *upstream* error,
// most of them - a 500 that guacamole then reports as an *upstream* error,
// naming the wrong hop entirely. Without a line per branch the only evidence a
// failure leaves is a GIN status code, and with several replicas you cannot
// even tell which process produced it.
@@ -215,7 +215,7 @@ func consoleTunnel(c *gin.Context) {
return
}
// The client is deliberately told nothing specific, so this is the only
// place the real reason exists a failed dispatch and a relay that was
// place the real reason exists - a failed dispatch and a relay that was
// never announced are the same generic 500 to the browser.
tlog("reject: open relay: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "could not open relay"})
@@ -286,7 +286,7 @@ func consoleTunnel(c *gin.Context) {
}
// The handshake is where guacd connects onward to the relay, so a
// failure here is guacd reporting it could not reach %s:%d the hop
// failure here is guacd reporting it could not reach %s:%d - the hop
// that has been hardest to see from either end.
stream := guac.NewStream(conn, guac.SocketTimeout)
if err := stream.Handshake(config); err != nil {
+8 -8
View File
@@ -1631,14 +1631,14 @@
"$ref": "#/components/schemas/models.AlertSettings"
},
"api_token_max_days": {
"description": "APITokenMaxDays caps how long a newly created API token may live.\n\nA pointer for the same reason the retention fields are: absent must mean\nthe default, and the default here is no cap at all never-expire tokens\nare allowed until an instance decides otherwise, so an upgrade changes\nnothing. Nil or 0 is no cap. A positive value refuses both a longer\nexpiry and a token with no expiry.\n\nIt is a policy on issuance, not on use: raising or lowering it never\ninvalidates a token that already exists.",
"description": "APITokenMaxDays caps how long a newly created API token may live.\n\nA pointer for the same reason the retention fields are: absent must mean\nthe default, and the default here is no cap at all - never-expire tokens\nare allowed until an instance decides otherwise, so an upgrade changes\nnothing. Nil or 0 is no cap. A positive value refuses both a longer\nexpiry and a token with no expiry.\n\nIt is a policy on issuance, not on use: raising or lowering it never\ninvalidates a token that already exists.",
"type": "integer"
},
"instance_id": {
"type": "string"
},
"local_login_enabled": {
"description": "LocalLoginEnabled is a pointer because it is absent on every settings\ndocument written before this feature existed, and a plain bool would read\nabsent as disabled turning off password login for the entire fleet at\nupgrade. Nil means enabled.",
"description": "LocalLoginEnabled is a pointer because it is absent on every settings\ndocument written before this feature existed, and a plain bool would read\nabsent as disabled - turning off password login for the entire fleet at\nupgrade. Nil means enabled.",
"type": "boolean"
},
"secrets": {
@@ -1970,7 +1970,7 @@
"type": "string"
},
"fixed_in": {
"description": "FixedIn empty means no vendor fix has been published. That is a real and\ncommon state and must never be conflated with \"not vulnerable\" it is\nthe finding most in need of acceptance, since there is nothing to patch.",
"description": "FixedIn empty means no vendor fix has been published. That is a real and\ncommon state and must never be conflated with \"not vulnerable\" - it is\nthe finding most in need of acceptance, since there is nothing to patch.",
"type": "string"
},
"id": {
@@ -3319,7 +3319,7 @@
},
"/console/connect": {
"post": {
"description": "Mints a one-time session token for the /console/tunnel websocket. Requires a live agent answers 409 agent_offline otherwise.",
"description": "Mints a one-time session token for the /console/tunnel websocket. Requires a live agent - answers 409 agent_offline otherwise.",
"requestBody": {
"content": {
"application/json": {
@@ -5645,7 +5645,7 @@
},
"/secrets/{group}/values": {
"get": {
"description": "Consumed by Kubernetes External Secrets Operator. Authenticated with a bearer token whose SHA-256 hash is stored in settings a different credential from an API token, never substitutable for one.",
"description": "Consumed by Kubernetes External Secrets Operator. Authenticated with a bearer token whose SHA-256 hash is stored in settings - a different credential from an API token, never substitutable for one.",
"parameters": [
{
"description": "Secret group name",
@@ -6278,7 +6278,7 @@
},
"/servers/{id}/packages": {
"get": {
"description": "A server that has not reported yet answers reported=false rather than 404 that is the normal state for the first hour after install.",
"description": "A server that has not reported yet answers reported=false rather than 404 - that is the normal state for the first hour after install.",
"parameters": [
{
"description": "Server ID",
@@ -8627,7 +8627,7 @@
},
"/vulnerabilities": {
"get": {
"description": "Groups findings by CVE, most severe first the same CVE on forty servers is one decision, not forty rows.",
"description": "Groups findings by CVE, most severe first - the same CVE on forty servers is one decision, not forty rows.",
"parameters": [
{
"description": "Filter by severity",
@@ -8848,7 +8848,7 @@
]
},
"post": {
"description": "Requires a reason and a future expiry. Reopens automatically at expiry permanent dismissal is never allowed.",
"description": "Requires a reason and a future expiry. Reopens automatically at expiry - permanent dismissal is never allowed.",
"parameters": [
{
"description": "Finding ID",
File diff suppressed because one or more lines are too long
+5 -5
View File
@@ -70,7 +70,7 @@ func RegisterRoutes(r *gin.Engine) {
apiGroup.GET("/servers", listServers)
// Static segment, registered alongside /servers/:id exactly as
// /servers/new already is gin resolves statics ahead of wildcards.
// /servers/new already is - gin resolves statics ahead of wildcards.
apiGroup.GET("/servers/tags", listKnownTags)
apiGroup.POST("/servers", createServer)
apiGroup.GET("/servers/new", newServer)
@@ -132,7 +132,7 @@ func RegisterRoutes(r *gin.Engine) {
// handler answers every GET with a hardcoded 405, because a stateless
// server has no session to open the server-to-client SSE stream against.
// That 405 is the protocol-correct response for an MCP server that offers
// no SSE leg an unregistered GET would 404 instead, which a client reads
// no SSE leg - an unregistered GET would 404 instead, which a client reads
// as "no MCP endpoint here at all" rather than "this one is POST-only".
// This route is not a working GET; it exists solely to produce that 405.
mcpGroup := apiGroup.Group("/mcp", RequireFeature(license.FeatureMCP))
@@ -576,11 +576,11 @@ func getKey(c *gin.Context) {
all, _ := services.GetAssignmentsWithServers(auth.InstanceID(c), id)
// A tag-restricted token may legitimately hold a key that is also
// assigned to a server outside its restriction the key itself is
// assigned to a server outside its restriction - the key itself is
// still returned above. Only the assignment list is filtered, and
// silently: an assignment whose Server is nil or out of scope is
// dropped rather than kept with the hostname redacted, so the response
// gives no signal not even a count of what was removed.
// gives no signal - not even a count - of what was removed.
scope := auth.ServerScope(c)
assignments := make([]services.AssignmentWithServer, 0, len(all))
for _, a := range all {
@@ -778,7 +778,7 @@ func applyUpdates(c *gin.Context) {
// downloads and installs the latest agent. Deliberately not in the generated
// OpenAPI document: it is registered on the bare engine, not under the /api
// group the document's BasePath assumes, so a @Router annotation here would
// publish /api/update a path that 404s rather than the real top-level
// publish /api/update - a path that 404s - rather than the real top-level
// /update. It serves a shell script, not JSON, so there is nothing lost by
// leaving it out of a JSON API reference.
func handleUpdateScript(c *gin.Context) {
+1 -1
View File
@@ -14,7 +14,7 @@ import (
//
// /healthz is liveness: the process is up and serving. It touches nothing
// external, because a Mongo outage must not make Kubernetes restart every
// server pod a restart loop cannot fix someone else's database, and it
// server pod - a restart loop cannot fix someone else's database, and it
// destroys every open command stream and console session on the way.
//
// /readyz is readiness: this pod can serve a request end to end, which needs
+1 -1
View File
@@ -48,7 +48,7 @@ func licenceExempt(c *gin.Context) bool {
// RequireActiveLicense blocks mutating requests when the licence is not valid.
//
// Mounted on the /api group, so a route added tomorrow is gated because of where
// it lives rather than because someone remembered. GET and HEAD always pass
// it lives rather than because someone remembered. GET and HEAD always pass -
// reading is never blocked.
func RequireActiveLicense() gin.HandlerFunc {
return func(c *gin.Context) {
+4 -4
View File
@@ -20,7 +20,7 @@ import (
const publicStatusRateLimit = 120
// RateLimitPublicStatus counts requests per client address in a one-minute
// fixed window, exactly as RateLimitTokens does including the part that
// fixed window, exactly as RateLimitTokens does - including the part that
// matters most: when Redis is unavailable it allows rather than denies. A
// status page must survive the outage it exists to report.
func RateLimitPublicStatus() gin.HandlerFunc {
@@ -65,7 +65,7 @@ func RateLimitPublicStatus() gin.HandlerFunc {
//
// It carries no @Router annotation deliberately. openapi.json declares a
// single server of "/api", so a @Router of /public/status/{pageId} would be
// published as /api/public/status/{pageId} a path that does not exist, and
// published as /api/public/status/{pageId} - a path that does not exist, and
// which would sit behind auth.Middleware if it did. The real address is:
//
// GET {scheme}://{instance-host}/public/status/{pageId}
@@ -116,8 +116,8 @@ func getPublicStatusPage(c *gin.Context) {
// so it is honoured only when the machine that opened the connection is one of
// the configured trusted proxies.
//
// When the resulting host names no slug at all vantage.acme.com,
// status.acme.com, a bare IP and the deployment is not cloud, the single
// When the resulting host names no slug at all - vantage.acme.com,
// status.acme.com, a bare IP - and the deployment is not cloud, the single
// instance of that install is used. A self-hosted install has exactly one, and
// without this every self-hosted status page 404s forever. More than one is a
// refusal rather than a guess.
+2 -2
View File
@@ -18,7 +18,7 @@ func runFixture() *models.WorkflowRun {
// A run document names every host it touched, hostname included. A restricted
// caller must see only its own, and must be told some entries are missing
// without being told how many the targets_restricted precedent.
// without being told how many - the targets_restricted precedent.
func TestScopeRunHidesOutOfScopeServerRuns(t *testing.T) {
got := scopeRun(runFixture(), map[string]bool{"stg-1": true}, true)
if len(got.ServerRuns) != 1 || got.ServerRuns[0].ServerID != "stg-1" {
@@ -45,7 +45,7 @@ func TestScopeRunLeavesUnrestrictedCallerWhole(t *testing.T) {
}
// A restricted caller whose scope happens to cover the whole run must not be
// told anything was hidden the flag is about disclosure, not about being
// told anything was hidden - the flag is about disclosure, not about being
// restricted in general.
func TestScopeRunNoFlagWhenNothingDropped(t *testing.T) {
got := scopeRun(runFixture(), map[string]bool{"stg-1": true, "prod-1": true}, true)
+1 -1
View File
@@ -11,7 +11,7 @@ import (
"github.com/gin-gonic/gin"
)
// routeScopes maps a registered gin route "<METHOD> <full path pattern>" to
// routeScopes maps a registered gin route - "<METHOD> <full path pattern>" - to
// the scope an API token must hold to reach it.
//
// It is keyed on the route pattern rather than declared per route with a
+1 -1
View File
@@ -40,7 +40,7 @@ func secretsReadAuth() gin.HandlerFunc {
// esoGetGroup godoc
//
// @Summary Read a secret group's values (ESO)
// @Description Consumed by Kubernetes External Secrets Operator. Authenticated with a bearer token whose SHA-256 hash is stored in settings a different credential from an API token, never substitutable for one.
// @Description Consumed by Kubernetes External Secrets Operator. Authenticated with a bearer token whose SHA-256 hash is stored in settings - a different credential from an API token, never substitutable for one.
// @Tags secrets
// @Produce json
// @Param group path string true "Secret group name"
+29 -29
View File
@@ -10,7 +10,7 @@ const (
scoped scopeDecl = iota
// fleetWide: the route deliberately reaches the whole fleet. Every
// fleetWide entry carries a comment giving the reason. It must never mean
// "not scoped yet" an unresolved gap belongs on a fix list, not here,
// "not scoped yet" - an unresolved gap belongs on a fix list, not here,
// because this value is read as a considered decision.
fleetWide
// exempt: the route touches no server-derived data at all. Every exempt
@@ -38,14 +38,14 @@ const (
// 1. It can only ever check that a DECLARATION EXISTS, never that the handler
// honours it. "POST /api/workflows/:id/run" was declared scoped here while
// services.TriggerWorkflow resolved its targets through the unscoped
// ResolveTargets a true entry that lied, boot-enforced, for the whole
// ResolveTargets - a true entry that lied, boot-enforced, for the whole
// life of the feature. A declaration is a claim a reviewer must verify,
// not a property this file establishes.
//
// 2. /api/mcp is exempt at route level, and that is the honest answer rather
// than an omission. One route serves roughly twenty tools of very
// different shapes some read no server data at all, some resolve one
// host, some enumerate the fleet so no single route-level value could
// different shapes - some read no server data at all, some resolve one
// host, some enumerate the fleet - so no single route-level value could
// be true of all of them. The decision genuinely lives per tool, where
// each tool that touches server data applies auth.ServerScope's selector
// itself, and the registry's own tests are where that is enforced.
@@ -68,8 +68,8 @@ var serverScopedRoutes = map[string]scopeDecl{
"GET /api/servers/new": fleetWide,
"POST /api/servers/new": fleetWide,
// KnownTags aggregates the tag *vocabulary* in use across the fleet keys
// and the values seen for them never a server identifier or any other
// KnownTags aggregates the tag *vocabulary* in use across the fleet - keys
// and the values seen for them - never a server identifier or any other
// server attribute, so it does not let a restricted token enumerate which
// hosts exist. Filtering it would mean plumbing a selector through an
// aggregation query for a leak that carries no server identity; ruled
@@ -124,7 +124,7 @@ var serverScopedRoutes = map[string]scopeDecl{
"POST /api/vulnerabilities/rescan": fleetWide,
// Accepting or reopening a finding names the finding, not a server, but a
// finding does belong to one so a restricted token can accept a finding
// finding does belong to one - so a restricted token can accept a finding
// on a host outside its scope if it learns the finding ID. It cannot learn
// one through this API any more (every listing is now scoped), so this is
// left fleet-wide rather than given a lookup of its own. Owner|admin only.
@@ -143,7 +143,7 @@ var serverScopedRoutes = map[string]scopeDecl{
// assignments whose server passes services.ServerInTokenScope before
// returning it, so a restricted token cannot learn the hostname of an
// out-of-scope server through a key it happens to also hold there. The
// key document itself is still returned unfiltered a token restricted
// key document itself is still returned unfiltered - a token restricted
// to staging may legitimately hold a key that is also assigned in prod,
// and only the assignment list, not the key's existence, is the leak
// this closes.
@@ -152,7 +152,7 @@ var serverScopedRoutes = map[string]scopeDecl{
// listKeys' services.ListKeys narrows each key's AssignedCount to
// assignments on servers ServerInTokenScope admits, for the same reason
// as getKey above: a nonzero count on a key a restricted token sees
// nothing assigned to in its own scope is itself the leak it tells the
// nothing assigned to in its own scope is itself the leak - it tells the
// token an assignment exists on a host it must not know about, without
// naming the host.
"GET /api/keys": scoped,
@@ -169,7 +169,7 @@ var serverScopedRoutes = map[string]scopeDecl{
"GET /api/keys/:id/private-key": exempt,
// Deleting a key removes it everywhere it is assigned, including on hosts
// outside a restricted token's scope the delete is of the key, not of a
// outside a restricted token's scope - the delete is of the key, not of a
// server, and there is no partial delete that leaves a key half-revoked.
// Nothing about which hosts held it is disclosed by the call.
"DELETE /api/keys/:id": fleetWide,
@@ -179,7 +179,7 @@ var serverScopedRoutes = map[string]scopeDecl{
// listWorkflows/getWorkflow narrow Workflow.TargetServerIDs to what the
// caller's scope admits via services.VisibleServerIDs +
// FilterVisibleServerIDs, wrapped in WorkflowResponse so the JSON field
// name is unchanged. TargetTags is left untouched the tag vocabulary
// name is unchanged. TargetTags is left untouched - the tag vocabulary
// itself is ruled acceptable to expose, unlike a resolved server ID.
// TargetsRestricted is set (with no count) whenever at least one target
// was dropped.
@@ -191,7 +191,7 @@ var serverScopedRoutes = map[string]scopeDecl{
// validate the ID-union-tags target set as a whole through
// services.validateWorkflowTargetScope, which resolves the workflow's
// targets both unscoped and scoped and refuses to save unless they match
// the same all-or-nothing rule the MCP create_workflow tool applies.
// - the same all-or-nothing rule the MCP create_workflow tool applies.
// Together these mean a restricted token can neither save a workflow
// targeting a host or tag outside its scope (which the scheduler, firing
// as the system, would otherwise run there) nor learn which IDs or tags
@@ -203,12 +203,12 @@ var serverScopedRoutes = map[string]scopeDecl{
// runWorkflow passes auth.ServerScope into services.TriggerWorkflow, which
// resolves through ResolveTargetsScoped. Note the history: this entry read
// scoped for the whole life of the feature while TriggerWorkflow called
// the UNSCOPED ResolveTargets see this file's header on what this
// the UNSCOPED ResolveTargets - see this file's header on what this
// assertion can and cannot prove.
"POST /api/workflows/:id/run": scoped,
// getRun and listWorkflowRuns narrow WorkflowRun.ServerRuns each entry
// of which carries a ServerID and a Hostname to what the caller's scope
// getRun and listWorkflowRuns narrow WorkflowRun.ServerRuns - each entry
// of which carries a ServerID and a Hostname - to what the caller's scope
// admits, setting servers_restricted (a boolean, never a count) when any
// entry was dropped.
"GET /api/runs/:runId": scoped,
@@ -224,7 +224,7 @@ var serverScopedRoutes = map[string]scopeDecl{
// Deleting a workflow and cancelling a run both act on a definition rather
// than on a server, and neither returns server data. Each can
// nevertheless reach a definition whose targets a restricted token cannot
// see a cancel stops work on out-of-scope hosts. That reach is real but
// see - a cancel stops work on out-of-scope hosts. That reach is real but
// bounded: the caller learns nothing about which hosts are involved (both
// /workflows listings are scoped), and a scope-narrowed variant of
// "cancel this run" would have to either half-cancel a run or refuse one
@@ -236,17 +236,17 @@ var serverScopedRoutes = map[string]scopeDecl{
// Arming a schedule applies no scope check of its own, and that is safe
// only because it has nothing left to check: CreateWorkflow and
// UpdateWorkflow (internal/services/workflows.go) already refuse to save
// a workflow whose resolved targets TargetServerIDs union TargetTags
// a workflow whose resolved targets - TargetServerIDs union TargetTags -
// reach outside the acting credential's scope, the same all-or-nothing
// rule the MCP create_workflow tool applies. So a workflow written after
// this check existed had its targets constrained to whichever scope wrote
// it, and the scheduler firing it later with a nil token scope acting
// as the system, not as any caller reaches nothing that write didn't
// it, and the scheduler firing it later with a nil token scope - acting
// as the system, not as any caller - reaches nothing that write didn't
// already allow.
//
// This holds only for workflows written after the check was added. Rows
// already in the database were saved under the old, unvalidated rule and
// are never re-validated neither this route nor the writers re-check an
// are never re-validated - neither this route nor the writers re-check an
// existing row's targets after the fact. A workflow saved before this fix
// with an out-of-scope tag selector still schedules and fires exactly as
// it did before.
@@ -270,17 +270,17 @@ var serverScopedRoutes = map[string]scopeDecl{
// listMonitors/getMonitor redact models.Monitor.Runner to
// models.RunnerRestricted via services.RedactMonitorRunner when it names
// a server outside the caller's scope Runner is literally a server ID
// a server outside the caller's scope - Runner is literally a server ID
// for an agent-pushed monitor, so left unfiltered it discloses one
// directly. The monitor itself is still returned: a restricted operator
// may legitimately need to see that it exists and is up or down, so only
// the runner field goes neutral. Runner "server" (control-plane-run) is
// never touched it names no server.
// never touched - it names no server.
"GET /api/monitors": scoped,
"GET /api/monitors/:id": scoped,
// createMonitor/updateMonitor validate the runner which is a server ID
// for an agent-pushed monitor through services.validateRunner, resolving
// createMonitor/updateMonitor validate the runner - which is a server ID
// for an agent-pushed monitor - through services.validateRunner, resolving
// with GetServerScoped so a restricted token can neither point a check at
// an out-of-scope agent nor use the not-found answer as an oracle.
"POST /api/monitors": scoped,
@@ -293,7 +293,7 @@ var serverScopedRoutes = map[string]scopeDecl{
"DELETE /api/monitors/:id": fleetWide,
// A monitor's incidents, uptime rollups and recent samples are all about
// the monitored endpoint status, latency, timestamps and carry no
// the monitored endpoint - status, latency, timestamps - and carry no
// server identifier at all; the runner is a field of the monitor
// document, which these do not return.
"GET /api/monitors/:id/incidents": exempt,
@@ -302,7 +302,7 @@ var serverScopedRoutes = map[string]scopeDecl{
// ---- notification channels ----
// A channel is an outbound destination a webhook URL, an SMTP account.
// A channel is an outbound destination - a webhook URL, an SMTP account.
// Nothing about a server reaches these routes.
"GET /api/channels": exempt,
"POST /api/channels": exempt,
@@ -328,7 +328,7 @@ var serverScopedRoutes = map[string]scopeDecl{
// A status page pairs monitor IDs with per-page display names, and every
// public read goes through services.assembleSnapshot, which is the
// redaction boundary its PublicComponent vocabulary has no field for a
// redaction boundary - its PublicComponent vocabulary has no field for a
// host, URL or runner. These authoring routes handle the page document
// itself and never a server.
"GET /api/status-pages": exempt,
@@ -347,7 +347,7 @@ var serverScopedRoutes = map[string]scopeDecl{
// Audit rows are a record of what people and tokens did, and a row's free
// text detail can name a host in passing ("run <id> triggered", "key
// assigned to web-01"). Filtering the log by tag would mean parsing those
// strings, or dropping every row whose target this token cannot resolve
// strings, or dropping every row whose target this token cannot resolve -
// which would hide a restricted token's own actions from itself the
// moment a server is renamed or deleted. The log is left whole and
// deliberately so: an audit trail with holes in it is worth less than the
@@ -402,7 +402,7 @@ var serverScopedRoutes = map[string]scopeDecl{
// AssertServerScopeMapComplete refuses to boot when any registered /api route
// is missing from serverScopedRoutes. routes is every /api route the engine
// registered not a filtered subset which is the whole point of the
// registered - not a filtered subset - which is the whole point of the
// inversion: a new route is checked by default rather than only when its path
// happens to match a pattern somebody remembered to add.
func AssertServerScopeMapComplete(routes []string) error {
+1 -1
View File
@@ -32,7 +32,7 @@ func registerStatusPageRoutes(g *gin.RouterGroup) {
// statusPageError maps the service errors onto codes once, so ten handlers do
// not each invent their own. services.ErrPageInvalid covers every validation
// failure in the status page and incident services a missing title or an
// failure in the status page and incident services - a missing title or an
// invalid incident status is a 400, not a 500.
func statusPageError(c *gin.Context, err error) {
switch {
+1 -1
View File
@@ -87,7 +87,7 @@ func createToken(c *gin.Context) {
// A token-authenticated request may only mint a token whose scopes are a
// subset of its own. Role is capped against the creating *user* below (in
// services.CreateAPIToken), but a role cap alone does not confine scopes
// services.CreateAPIToken), but a role cap alone does not confine scopes -
// without this, a CI token holding only settings:write could mint a token
// holding keys:write and secrets:write, since minting only ever required
// settings:write and never checked what the caller itself could reach. A
+3 -3
View File
@@ -24,7 +24,7 @@ type LimitExceededResponse struct {
}
// LicenceErrorResponse pairs an error with a machine-readable reason rather
// than a code used only on the two licence rejection paths that predate the
// than a code - used only on the two licence rejection paths that predate the
// error/code convention used everywhere else.
type LicenceErrorResponse struct {
Error string `json:"error"`
@@ -115,7 +115,7 @@ type AgentVersionResponse struct {
}
// WorkflowResponse is a workflow with its TargetServerIDs narrowed to what
// the acting token's scope admits the explicit field shadows the embedded
// the acting token's scope admits - the explicit field shadows the embedded
// one for JSON marshalling, matching the pattern KeyDetailResponse already
// uses. TargetTags is not filtered: the tag vocabulary itself is ruled
// acceptable to expose, and only the resolved ID list can name a specific
@@ -134,7 +134,7 @@ type WorkflowResponse struct {
// RunResponse is a workflow run with its ServerRuns narrowed to the servers
// the acting token's scope admits. Each models.ServerRun carries both a
// ServerID and a Hostname, so an unfiltered run document names every host it
// touched the same disclosure WorkflowResponse.TargetServerIDs closes one
// touched - the same disclosure WorkflowResponse.TargetServerIDs closes one
// level up, and the parent of the per-server log routes that were already
// scoped.
//
+5 -5
View File
@@ -29,7 +29,7 @@ type vulnGroup struct {
// listVulnerabilities godoc
//
// @Summary List vulnerabilities
// @Description Groups findings by CVE, most severe first the same CVE on forty servers is one decision, not forty rows.
// @Description Groups findings by CVE, most severe first - the same CVE on forty servers is one decision, not forty rows.
// @Tags vulnerabilities
// @Produce json
// @Param severity query string false "Filter by severity"
@@ -102,7 +102,7 @@ func groupByCVE(findings []models.VulnFinding) []vulnGroup {
}
// hasFixFromQuery reads ?has_fix=true|false. Anything else, including an empty
// or malformed value, is no filter a filter nobody asked for must never hide
// or malformed value, is no filter - a filter nobody asked for must never hide
// findings, and the wrong direction here hides the unfixable ones.
func hasFixFromQuery(c *gin.Context) *bool {
switch c.Query("has_fix") {
@@ -199,7 +199,7 @@ type acceptFindingRequest struct {
// acceptFinding godoc
//
// @Summary Accept a finding
// @Description Requires a reason and a future expiry. Reopens automatically at expiry permanent dismissal is never allowed.
// @Description Requires a reason and a future expiry. Reopens automatically at expiry - permanent dismissal is never allowed.
// @Tags vulnerabilities
// @Accept json
// @Produce json
@@ -221,7 +221,7 @@ func acceptFinding(c *gin.Context) {
// Both rejected deliberately. An acceptance with no reason is a dismissal
// nobody can audit, and one already expired is a permanent dismissal
// wearing an expiry the graveyard the expiry exists to prevent.
// wearing an expiry - the graveyard the expiry exists to prevent.
if strings.TrimSpace(req.Reason) == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "a reason is required"})
return
@@ -313,7 +313,7 @@ func listServerVulnerabilities(c *gin.Context) {
// getServerPackages godoc
//
// @Summary Get a server's package inventory
// @Description A server that has not reported yet answers reported=false rather than 404 that is the normal state for the first hour after install.
// @Description A server that has not reported yet answers reported=false rather than 404 - that is the normal state for the first hour after install.
// @Tags vulnerabilities
// @Produce json
// @Param id path string true "Server ID"
+1 -1
View File
@@ -151,7 +151,7 @@ func controlWorkload(c *gin.Context) {
case errors.Is(err, services.ErrAgentNotConnected):
c.JSON(http.StatusServiceUnavailable, gin.H{"error": err.Error()})
case services.IsWorkloadProtected(err):
// Nothing failed the agent refused, which is the design. 409, not
// Nothing failed - the agent refused, which is the design. 409, not
// 500, and the reason is carried through.
c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
default:
+1 -1
View File
@@ -44,7 +44,7 @@ func githubOAuthConfig(p *models.AuthProvider, secret, redirectURL string) *oaut
//
// Verified alone is not enough: a non-primary address is one the person happens
// to have proved, not the one they present as themselves. Primary alone is far
// worse an unverified address is not proof of control at all, and accepting
// worse - an unverified address is not proof of control at all, and accepting
// one would let anyone with a GitHub account claim any address in the instance.
func selectGitHubEmail(emails []githubEmail) (string, error) {
for _, e := range emails {
+4 -4
View File
@@ -61,8 +61,8 @@ func hostSlug(host string) string {
func HostSlug(host string) string { return hostSlug(host) }
// InstanceFromHost resolves the instance named by the request's own Host
// header. Callers that must resolve a host from somewhere else the public
// status page reads a trusted X-Forwarded-Host use InstanceForHost so the
// header. Callers that must resolve a host from somewhere else - the public
// status page reads a trusted X-Forwarded-Host - use InstanceForHost so the
// slug rules and the 60s cache stay single-implementation.
func InstanceFromHost(c *gin.Context) (*models.Instance, bool) {
return InstanceForHost(c.Request.Host)
@@ -82,7 +82,7 @@ func InstanceForHost(host string) (*models.Instance, bool) {
if err != nil || inst == nil {
// Negative entries are cached too. Without them an unknown but
// well-formed host costs a Mongo query per anonymous request, which
// the public status page exposes to the open internet and the
// the public status page exposes to the open internet - and the
// round trip is itself a timing oracle separating "no such instance"
// from "instance exists, page does not".
storeInstance(slug, nil)
@@ -93,7 +93,7 @@ func InstanceForHost(host string) (*models.Instance, bool) {
}
// SoleInstance resolves the one instance of a deployment that has exactly one.
// It is how a self-hosted install serves a host that names no slug at all
// It is how a self-hosted install serves a host that names no slug at all -
// vantage.acme.com, status.acme.com, or a bare address. It reuses the same
// count-then-read that bootstrap uses, and refuses rather than guessing when
// more than one instance exists.
+1 -1
View File
@@ -89,7 +89,7 @@ func HandleLocalLogin(c *gin.Context) {
}
// HandleListPublicProviders is unauthenticated: it is what the login page reads
// to decide what to draw. It carries no issuer, no client ID and no secret
// to decide what to draw. It carries no issuer, no client ID and no secret -
// only what a button needs, because anyone who can reach the login page can
// read this.
func HandleListPublicProviders(c *gin.Context) {
+2 -2
View File
@@ -104,7 +104,7 @@ func sessionFromCookie(c *gin.Context) (*Session, bool) {
}
sess, err := GetSession(c.Request.Context(), cookie.Value)
if err != nil {
// A stale cookie plus a valid bearer token is a real combination
// A stale cookie plus a valid bearer token is a real combination -
// a browser tab left open beside a curl. Fall through rather than
// refusing a credential that would have worked.
if bearerToken(c) != "" {
@@ -203,7 +203,7 @@ func IsToken(c *gin.Context) bool { return TokenID(c) != "" }
// ServerScope is the tag restriction the acting credential carries, or nil for
// an unrestricted token and for every cookie session. Callers pass it to
// services.ServerInTokenScope or services.IntersectSelectors nil means the
// services.ServerInTokenScope or services.IntersectSelectors - nil means the
// whole fleet, never nothing.
func ServerScope(c *gin.Context) map[string]string {
if s := GetSessionFromContext(c); s != nil {
+1 -1
View File
@@ -44,7 +44,7 @@ var presets = []Preset{
Kind: models.KindOIDC,
IssuerFormat: "https://%s/oauth2/default",
InputLabel: "Okta org domain",
InputHint: "e.g. acme.okta.com no scheme, no trailing slash.",
InputHint: "e.g. acme.okta.com - no scheme, no trailing slash.",
Scopes: []string{"openid", "profile", "email"},
},
{
+2 -2
View File
@@ -24,7 +24,7 @@ type Session struct {
Name string `json:"name"`
// The four fields below are set only when the request authenticated with
// an API token. They are never persisted to Redis a token authenticates
// an API token. They are never persisted to Redis - a token authenticates
// per request and mints no session, so a revoked token stops working
// immediately rather than at the end of a session TTL.
TokenID string `json:"-"`
@@ -38,7 +38,7 @@ var rdb *redis.Client
// InitRedis connects the session store.
//
// Username and password may both be empty for an unauthenticated instance. For
// a legacy `requirepass` Redis, pass the password with an empty username
// a legacy `requirepass` Redis, pass the password with an empty username -
// go-redis then sends AUTH with one argument instead of two.
func InitRedis(addr, username, password string) error {
rdb = redis.NewClient(&redis.Options{
+4 -4
View File
@@ -15,8 +15,8 @@
//
// Everything here is deliberately best-effort delivery with an explicit ack
// rather than a queue. A command whose owner pod died between the presence
// check and the publish must fail loudly and immediately the caller answers
// 503 and the operator retries not sit in a queue waiting for a stream that
// check and the publish must fail loudly and immediately - the caller answers
// 503 and the operator retries - not sit in a queue waiting for a stream that
// no longer exists.
package bus
@@ -97,7 +97,7 @@ const (
//
// The listener cannot be bound in advance on any particular pod. An agent's
// ProxyStream is a separate HTTP/2 request from its CommandStream, and an
// L7 proxy (Traefik) balances requests, not connections so it may land on
// L7 proxy (Traefik) balances requests, not connections - so it may land on
// any replica, not the one holding the command stream. The pod it does land
// on binds the listener and announces it here.
ProxyAddrChannel = prefix + "proxyaddr:"
@@ -295,7 +295,7 @@ func SetPendingProxy(ctx context.Context, proxyID, instanceID, serverID string,
// round trips: single use is the whole security property, and two agents
// racing the same proxy_id must not both be served.
//
// A missing record is reported as "", "" rather than an error an unknown
// A missing record is reported as "", "" rather than an error - an unknown
// proxy_id, an expired one and a second claim are all the same refusal.
func ClaimPendingProxy(ctx context.Context, proxyID string) (instanceID, serverID string) {
v, err := claimPending.Run(ctx, rdb, []string{ProxyPendingKey + proxyID}).Text()
+1 -1
View File
@@ -23,7 +23,7 @@ const (
// a race anyone wins.
//
// Redis rather than a Kubernetes Lease so that Docker Compose, which has no
// API server, takes the identical code path one implementation to reason
// API server, takes the identical code path - one implementation to reason
// about, not two.
//
// job is given a context cancelled the moment leadership is lost, and must
+1 -1
View File
@@ -45,7 +45,7 @@ func (s *vantageServer) ProxyStream(stream pb.Vantage_ProxyStreamServer) error {
// testable without a real stream.
//
// The listener is bound here, on whichever replica the stream reached, rather
// than in advance on the pod holding the agent's command stream those are not
// than in advance on the pod holding the agent's command stream - those are not
// the same pod, because an L7 proxy balances HTTP/2 requests independently.
func serveProxy(open *pb.ProxyOpen, instanceID string, stream proxy.AgentStream) error {
sess, err := services.ClaimProxyStream(instanceID, open.ServerId, open.ProxyId)
+4 -4
View File
@@ -168,7 +168,7 @@ func (s *vantageServer) ReportPackages(ctx context.Context, req *pb.ReportPackag
//
// It is not gated by licence: the workload registry reads as core fleet
// management rather than a premium add-on. If that ever changes, the check
// belongs here gating collection, not display for the same reason it does
// belongs here - gating collection, not display - for the same reason it does
// in ReportPackages.
func (s *vantageServer) ReportWorkloads(ctx context.Context, req *pb.ReportWorkloadsRequest) (*pb.ReportWorkloadsResponse, error) {
srv, err := services.ValidateAgentToken(req.ServerId, req.AgentToken)
@@ -304,7 +304,7 @@ func (s *vantageServer) CommandStream(stream pb.Vantage_CommandStreamServer) err
// Serve claims this agent's presence on the bus and subscribes this pod to
// its command channel, so a dispatch issued by any other replica arrives
// here. The teardown releases both an agent that reconnects to a
// here. The teardown releases both - an agent that reconnects to a
// different pod must not leave this one advertising a stream it no longer
// has.
ch, release := services.Dispatcher.Serve(stream.Context(), srv.ServerID)
@@ -422,7 +422,7 @@ const pingSummaryInterval = 5 * time.Minute
// presence claim, released by a deferred call that a killed process never runs.
// The claim then outlives its owner for the remainder of its 30s TTL, during
// which dispatch believes the agent is reachable, publishes to a channel with
// no subscriber, and fails as "agent offline" a pod that has already exited
// no subscriber, and fails as "agent offline" - a pod that has already exited
// still answering for an agent it can no longer reach.
func StartGRPC(port int) (stop func(), err error) {
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
@@ -458,7 +458,7 @@ func StartGRPC(port int) (stop func(), err error) {
//
// It is bounded: an idle CommandStream returns as soon as its context is
// cancelled, but a console relay mid-transfer would otherwise hold the
// process past the pod's grace period and earn a SIGKILL which is the
// process past the pod's grace period and earn a SIGKILL - which is the
// abrupt exit this exists to avoid.
return func() {
done := make(chan struct{})
+3 -3
View File
@@ -35,7 +35,7 @@ var ErrConfirmRequired = errors.New("confirmation required")
var ErrOutOfScope = errors.New("targets outside token scope")
// logEvent is services.LogEvent behind a package variable so tests can
// observe what would have been audited without a live database connection
// observe what would have been audited without a live database connection -
// services.LogEvent talks straight to Mongo via db.Col, which panics on a nil
// client outside a real boot.
var logEvent = services.LogEvent
@@ -117,7 +117,7 @@ func LogDenied(c Caller, toolName, gate string) {
}
// LogFailure records a write tool call that reached a service and that
// service returned an error as opposed to LogDenied, which records a
// service returned an error - as opposed to LogDenied, which records a
// policy refusal that never reached one. Distinguishing the two in
// audit_logs is what lets a human reading it tell "the agent was stopped"
// from "the agent tried and the machine failed".
@@ -130,7 +130,7 @@ func LogFailure(c Caller, t Tool, args map[string]any, err error) {
//
// It is a distinct event type rather than another mcp.tool_call row because of
// the question a human will actually ask, which is "what has this agent added
// to my instance" an answer buried among hundreds of read rows is not an
// to my instance" - an answer buried among hundreds of read rows is not an
// answer.
func LogCreated(c Caller, kind, id, name string) {
logEvent(c.InstanceID, "mcp.created", c.TokenName, "", "",
+5 -5
View File
@@ -3,8 +3,8 @@
// It is a presentation layer over the service layer and introduces no authority
// of its own: every tool calls the same service functions the REST handlers
// call, and every decision about who may do what is made by machinery that
// already exists. Three gates apply to every call the licence feature, the
// mcp:* scope, and the tool's own resource scope and all three must pass.
// already exists. Three gates apply to every call - the licence feature, the
// mcp:* scope, and the tool's own resource scope - and all three must pass.
package mcp
import (
@@ -50,7 +50,7 @@ const (
// what tagArg decodes.
ArgTagMap ArgType = "tag_map"
// ArgObject is a free-form object whose inner shape the tool documents in
// the argument description create_monitor's target, whose fields differ
// the argument description - create_monitor's target, whose fields differ
// per monitor type.
ArgObject ArgType = "object"
)
@@ -83,7 +83,7 @@ type Tool struct {
Write bool
// Args declares every argument the handler reads, in the order a client
// should see them. A tool taking none declares an empty slice, which is
// distinct from "nobody has written the schema yet" see the registry
// distinct from "nobody has written the schema yet" - see the registry
// tests, which require the declaration to be deliberate.
Args []ToolArg
// TouchesServers marks a tool that returns or acts on server-derived data:
@@ -92,7 +92,7 @@ type Tool struct {
// ResolveTargetsScoped, ListServersFiltered or VisibleServerIDs.
//
// Like serverScopedRoutes in the api package, this can only ever assert
// that a declaration exists, never that the handler honours it get_run_logs
// that a declaration exists, never that the handler honours it - get_run_logs
// proved the run's instance and the server's membership in the run and then
// read production stdout for a staging token. What it does buy is that
// adding a tool forces an answer to "does this touch server data?", and the
+2 -2
View File
@@ -72,8 +72,8 @@ func TestInputSchemaMarshals(t *testing.T) {
// serverTouchingTools names every tool that returns or acts on server-derived
// data. The test below pins the registry against it, so a tool added that
// reads a hostname, a server ID, a package list or a run's per-server output
// fails until somebody declares TouchesServers and the point of the exercise
// decides how it applies Caller.TokenScope.
// fails until somebody declares TouchesServers and - the point of the exercise
// - decides how it applies Caller.TokenScope.
//
// This is the assertion that would have caught get_run_logs, which proved the
// run's instance and the named server's membership in the run and then read
+2 -2
View File
@@ -141,7 +141,7 @@ func buildMonitor(args map[string]any) (models.Monitor, error) {
// named agent, and a silently ignored argument would leave a model
// believing it had. services.CreateMonitor now validates a runner
// through GetServerScoped as well, so this is a second line rather than
// the only one but the clearer answer belongs here.
// the only one - but the clearer answer belongs here.
if _, present := args["runner"]; present {
return models.Monitor{}, fmt.Errorf("runner cannot be set from here; monitors created this way always run on the control plane")
}
@@ -176,7 +176,7 @@ func init() {
Write: true,
Scope: "workflows:write",
Description: "Create a reusable workflow step: a named script with an interpreter. " +
"The step is SAVED to this Vantage instance but is not run by creating it " +
"The step is SAVED to this Vantage instance but is not run by creating it - " +
"add it to a workflow with create_workflow, then run that with run_workflow. " +
"Steps created this way cannot reference secrets.",
Handler: func(ctx context.Context, c Caller, args map[string]any) (any, error) {
+1 -1
View File
@@ -16,7 +16,7 @@ import (
// Runner in particular is not merely omitted as noise: for an agent-pushed
// monitor it is literally a server ID, and REST's listMonitors/getMonitor
// redact it to models.RunnerRestricted when that server is outside the
// caller's scope. This projection never had a runner field to redact the
// caller's scope. This projection never had a runner field to redact - the
// same outcome, reached by never including it rather than by filtering it
// out, so this tool and get_monitor_status cannot disagree with the REST
// surface about what a restricted token learns.
+1 -1
View File
@@ -60,7 +60,7 @@ func TestServerSummaryStaysSmall(t *testing.T) {
}
// The real status vocabulary is "pending" / "active" / "offline" (see
// internal/services/servers.go) "online" is never assigned anywhere. A
// internal/services/servers.go) - "online" is never assigned anywhere. A
// server carrying the live status ("active") must project as Online: true,
// or list_servers/get_server misreport the entire fleet as down.
func TestSummariseServerReportsActiveAsOnline(t *testing.T) {
+6 -6
View File
@@ -39,7 +39,7 @@ type workflowDetail struct {
Tags map[string]string `json:"target_tags,omitempty"`
Schedule string `json:"schedule,omitempty"`
// TargetsRestricted is set, with no count, when Targets omits at least
// one server ID outside this token's scope mirroring
// one server ID outside this token's scope - mirroring
// WorkflowResponse's REST field, so a model reading this alongside a
// run_workflow refusal for the same workflow is not left to conclude the
// refusal invented a problem this tool never mentioned.
@@ -341,7 +341,7 @@ func init() {
//
// The refusal reuses the membership message verbatim so that
// "in the run but out of your scope" and "not in the run at all"
// are indistinguishable otherwise the difference between the
// are indistinguishable - otherwise the difference between the
// two answers enumerates hosts the token cannot see.
if _, err := services.GetServerScoped(c.InstanceID, serverID, c.TokenScope); err != nil {
return nil, fmt.Errorf("server %q is not part of run %q", serverID, runID)
@@ -461,7 +461,7 @@ func init() {
}
// Go randomises map iteration order, so truncating a ranged map
// to a page made two identical calls return different CVEs a
// to a page made two identical calls return different CVEs - a
// model comparing its own two answers would see the fleet change
// under it. Sorting by CVE ID (then package, since the key is a
// pair) makes the page deterministic.
@@ -541,7 +541,7 @@ func init() {
},
TouchesServers: true,
Scope: "vulns:read",
Description: "Search every server's installed packages by name across the whole fleet " +
Description: "Search every server's installed packages by name across the whole fleet - " +
"answers questions like \"which hosts still run OpenSSL 1.1\". version_below is not " +
"currently supported: filtering package versions correctly requires knowing each " +
"distribution's own version-ordering scheme (dpkg/rpm/apk), which this tool cannot " +
@@ -555,7 +555,7 @@ func init() {
if stringArg(args, "version_below") != "" {
return nil, fmt.Errorf("version_below is not supported: correct version ordering " +
"depends on each host's distribution (dpkg/rpm/apk each order differently), " +
"which this tool cannot resolve here omit version_below and every matching " +
"which this tool cannot resolve here - omit version_below and every matching " +
"install is returned instead")
}
@@ -616,7 +616,7 @@ func init() {
// This tool reads no arguments at all.
Args: []ToolArg{},
Scope: "secrets:read",
Description: "List secret group and key names on this instance. Metadata only no " +
Description: "List secret group and key names on this instance. Metadata only - no " +
"tool ever returns a secret's plaintext value to a model.",
Handler: func(ctx context.Context, c Caller, args map[string]any) (any, error) {
groups, err := services.ListSecretGroups(c.InstanceID)
+9 -9
View File
@@ -43,8 +43,8 @@ type runStartedResult struct {
// workflow's own configured target_server_ids/target_tags via
// services.ResolveTargets (unscoped) and runs against exactly that set. There
// is no per-call server_ids/tags override to plumb through, so this tool takes
// only workflow_id. To keep the token's scope meaningful TriggerWorkflow
// itself does not consult it this handler first loads the workflow and
// only workflow_id. To keep the token's scope meaningful - TriggerWorkflow
// itself does not consult it - this handler first loads the workflow and
// resolves its configured targets through ResolveTargetsScoped with the
// caller's TokenScope, and refuses the run outright if that scoped view does
// not cover every server the unscoped resolution would touch. That is the
@@ -61,7 +61,7 @@ func init() {
Write: true,
Scope: "workflows:write",
Description: "Run a workflow against the servers it is already configured to target " +
"(its saved server list and tags this call does not let you pick different " +
"(its saved server list and tags - this call does not let you pick different " +
"targets). This EXECUTES COMMANDS on real machines and cannot be undone from " +
"here. Returns a run ID immediately; poll get_run for progress and get_run_logs " +
"for output. Refused if the workflow's targets reach outside this token's own " +
@@ -121,7 +121,7 @@ type cancelledResult struct {
// cancel_run. The REST cancel route (workflows.go's cancelRun) calls
// services.CancelRun(instanceID, runID) directly; that call is already scoped
// to the caller's instance by instanceID, which is what "verifies the run
// belongs to the caller's instance" reduces to here there is no separate
// belongs to the caller's instance" reduces to here - there is no separate
// per-server scope to check, since cancelling touches the run record, not a
// server.
func init() {
@@ -164,7 +164,7 @@ type updateBatchResult struct {
// There is no fleet-wide variant of that service call to invoke once, so this
// tool resolves the requested targets through ResolveTargetsScoped exactly as
// the brief describes, then calls the same DispatchApplyUpdates the REST route
// calls, once per resolved server the identical dispatch, just looped
// calls, once per resolved server - the identical dispatch, just looped
// instead of hardcoded to one server_id from the URL.
func init() {
All().Register(Tool{
@@ -275,12 +275,12 @@ type assignKeyResult struct {
// assign_key. The REST route (handlers.go's assignKey) takes one server_id in
// the body and calls services.AssignKey(instanceID, keyID, serverID) directly
// AssignKey itself resolves the server with the unscoped services.GetServer,
// - AssignKey itself resolves the server with the unscoped services.GetServer,
// not GetServerScoped, so the REST route carries no token-scope check of its
// own (session auth has no server-scope restriction; only API tokens do). For
// the MCP surface, this tool resolves every named target through
// ResolveTargetsScoped first the same chokepoint every other target-
// resolving write tool goes through so a token whose scope excludes a server
// ResolveTargetsScoped first - the same chokepoint every other target-
// resolving write tool goes through - so a token whose scope excludes a server
// cannot reach it here even though the REST handler's own server lookup would
// not have stopped it. Then it calls the identical AssignKey once per resolved
// server.
@@ -298,7 +298,7 @@ func init() {
Scope: "keys:write",
Description: "Assign an SSH key to real servers, selected by server_ids and/or " +
"tags. The agent rewrites /root/.ssh/authorized_keys on each targeted machine " +
"and this cannot be undone from here use revoke to remove it afterward.",
"and this cannot be undone from here - use revoke to remove it afterward.",
Handler: func(ctx context.Context, c Caller, args map[string]any) (any, error) {
keyID := stringArg(args, "key_id")
if keyID == "" {
+6 -6
View File
@@ -31,7 +31,7 @@ func callerFromContext(c *gin.Context) Caller {
// because a stateless server has no session to open the server-to-client SSE
// stream against. The GET route is still registered deliberately (see
// handlers.go) so a client probing for the endpoint sees a protocol-correct
// 405 rather than gin's 404 the MCP spec expects exactly that response from
// 405 rather than gin's 404 - the MCP spec expects exactly that response from
// a server that does not offer the GET/SSE leg. Nothing here should route GET
// requests differently or try to make them do anything else.
func Handler() gin.HandlerFunc {
@@ -69,7 +69,7 @@ func Handler() gin.HandlerFunc {
func registerSDKTool(srv *sdk.Server, tool Tool, caller Caller) {
// InputSchema is set explicitly rather than inferred from the handler's
// argument type. The SDK can infer one from a typed In parameter, which is
// cleaner where it fits but every ToolFunc here takes map[string]any, and
// cleaner where it fits - but every ToolFunc here takes map[string]any, and
// inference over that yields a bare open object saying nothing. Giving each
// tool its own Go argument struct would mean twenty-odd structs and a
// generic registry that could no longer hold them in one map, losing the
@@ -86,8 +86,8 @@ func registerSDKTool(srv *sdk.Server, tool Tool, caller Caller) {
}
// callTool is the gate check, dispatch and audit write registerSDKTool wraps
// onto the SDK's call signature. It is a separate function rather than the
// closure body inline so it can be exercised directly in tests without
// onto the SDK's call signature. It is a separate function - rather than the
// closure body inline - so it can be exercised directly in tests without
// standing up an sdk.Server and driving a real MCP request through it.
func callTool(ctx context.Context, tool Tool, caller Caller, args map[string]any) (*sdk.CallToolResult, any, error) {
if ok, gate := Allowed(tool, caller); !ok {
@@ -98,14 +98,14 @@ func callTool(ctx context.Context, tool Tool, caller Caller, args map[string]any
out, err := tool.Handler(ctx, caller, args)
if err != nil {
// A write tool's own handler never gets a chance to audit its own
// refusal or failure it returns before reaching its LogCall, and
// refusal or failure - it returns before reaching its LogCall, and
// unlike a successful write, this layer does not know a resolved
// server count to pass along anyway. So every write failure is
// audited here instead: a policy refusal (fan-out or tag scope) as
// mcp.tool_denied naming the gate, everything else as
// mcp.tool_failed, so a human reading audit_logs can tell "the agent
// was stopped" from "the agent tried and the machine failed". Read
// tools are unaffected a failed read was never going to change
// tools are unaffected - a failed read was never going to change
// anything and carries no gate to name.
if tool.Write {
switch {
+3 -3
View File
@@ -7,7 +7,7 @@ import (
// TestRefusedWriteIsAudited exercises the real dispatch path (callTool, which
// registerSDKTool wraps) for a write tool whose handler refuses the call
// before it ever reaches its own LogCall a fan-out refusal, in this case,
// before it ever reaches its own LogCall - a fan-out refusal, in this case,
// which run_workflow, apply_updates, update_agent and assign_key all reach
// the same way via CheckFanOut. The refusal must still produce an audit row:
// a blocked mutation attempt is the single most audit-worthy event a write
@@ -77,8 +77,8 @@ func TestOutOfScopeWriteIsAudited(t *testing.T) {
}
// TestServiceFailureIsAuditedDistinctly makes sure a write tool failing for a
// reason that is not a policy refusal the underlying service call itself
// erroring is still audited, but as mcp.tool_failed rather than
// reason that is not a policy refusal - the underlying service call itself
// erroring - is still audited, but as mcp.tool_failed rather than
// mcp.tool_denied, so a human reading audit_logs can tell the two apart.
func TestServiceFailureIsAuditedDistinctly(t *testing.T) {
var events []string
+1 -1
View File
@@ -10,7 +10,7 @@ import (
//
// The plaintext is shown once at creation and never stored: only TokenHash,
// which is sha256 hex of the value, exactly as servers.agent_token_hash and the
// ESO read token already are. bcrypt is deliberately not used the value is
// ESO read token already are. bcrypt is deliberately not used - the value is
// full-entropy random rather than a chosen password, and a per-token salt would
// force a collection scan where an indexed lookup is wanted.
//
+1 -1
View File
@@ -22,7 +22,7 @@ const RedactedSecret = "••••••••"
// channelSecretKeys names, per channel type, the config entries that are
// credentials rather than settings. A Slack or Discord webhook URL is on this
// list because possession of the URL *is* the authorisation to post to that
// channel there is nothing else to steal.
// channel - there is nothing else to steal.
var channelSecretKeys = map[string][]string{
ChannelWebhook: {"url"},
ChannelSlack: {"url"},
+2 -2
View File
@@ -23,8 +23,8 @@ const RunnerServer = "server"
// RunnerRestricted replaces a monitor's runner in an API response when the
// real value is a server ID the acting token's scope does not admit. The
// monitor itself is still returned a restricted operator may legitimately
// need to see its name and state only where it runs is hidden, the same
// monitor itself is still returned - a restricted operator may legitimately
// need to see its name and state - only where it runs is hidden, the same
// way a workflow's target list can omit an ID without the whole workflow
// disappearing from a list.
const RunnerRestricted = "restricted"
+1 -1
View File
@@ -63,7 +63,7 @@ type VulnFinding struct {
PackageName string `bson:"package_name" json:"package_name"`
Installed string `bson:"installed_version" json:"installed_version"`
// FixedIn empty means no vendor fix has been published. That is a real and
// common state and must never be conflated with "not vulnerable" it is
// common state and must never be conflated with "not vulnerable" - it is
// the finding most in need of acceptance, since there is nothing to patch.
FixedIn string `bson:"fixed_in,omitempty" json:"fixed_in,omitempty"`
Severity string `bson:"severity" json:"severity"`
+1 -1
View File
@@ -10,7 +10,7 @@ import (
// dispatchSMTP delivers a state change over one channel's own SMTP settings.
//
// The transport, the envelope and the look of the message all live in
// shared/mail, which admin and sitesvc use too a Vantage alert and a Vantage
// shared/mail, which admin and sitesvc use too - a Vantage alert and a Vantage
// licence email should not look like they came from different products. This
// function only turns a channel document into a Sender.
func dispatchSMTP(ch models.NotificationChannel, ev Event) error {
+2 -2
View File
@@ -16,7 +16,7 @@ const TypeVuln = "vulnerability"
//
// One per rule per scan, never one per finding: a database refresh can open
// several hundred at once, and a message each would rate-limit the webhook or
// get the channel muted either way the alerts stop being read.
// get the channel muted - either way the alerts stop being read.
type VulnDigest struct {
InstanceName string
RuleName string
@@ -74,7 +74,7 @@ func vulnLines(d VulnDigest) string {
if r.FixedIn != "" {
fix = "fixed in " + r.FixedIn
}
s += fmt.Sprintf("\n• %s (%s) %s on %s, %s", r.CVEID, r.Severity, r.PackageName, r.ServerName, fix)
s += fmt.Sprintf("\n• %s (%s) - %s on %s, %s", r.CVEID, r.Severity, r.PackageName, r.ServerName, fix)
}
if d.More > 0 {
s += fmt.Sprintf("\n…and %d more.", d.More)
+1 -1
View File
@@ -34,4 +34,4 @@ func NewID() (string, error) {
//
// The pending record lives in Redis instead (bus.SetPendingProxy /
// ClaimPendingProxy), and the listener is bound by whichever pod the stream
// actually reaches see services.ClaimProxyStream.
// actually reaches - see services.ClaimProxyStream.
+2 -2
View File
@@ -90,7 +90,7 @@ func (s *Session) Reason() string {
// a no-op once a deliberate teardown (Close) has begun: a local Close closing
// the conn out from under the relay goroutines produces exactly the kind of
// error (net.ErrClosed, a broken pipe on write, ...) that looks like a remote
// failure but is not one, and must not overwrite or race to set the real
// failure but is not one, and must not overwrite - or race to set - the real
// reason, or invent one where a clean local close has none.
func (s *Session) setReason(r string) {
s.mu.Lock()
@@ -103,7 +103,7 @@ func (s *Session) setReason(r string) {
// Close tears the session down once. A non-empty reason is recorded only if no
// reason has been recorded already, and only before teardown begins. It closes
// both the listener and, if a connection has already been accepted, that
// connection too an unconditional kill for the whole relay chain regardless
// connection too - an unconditional kill for the whole relay chain regardless
// of which stage it is in.
func (s *Session) Close(reason string) {
if reason != "" {
+3 -3
View File
@@ -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
+1 -1
View File
@@ -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
+5 -5
View File
@@ -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) {
+1 -1
View File
@@ -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 {
+2 -2
View File
@@ -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.
+5 -5
View File
@@ -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)
+5 -5
View File
@@ -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")
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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"`
}
+2 -2
View File
@@ -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)
+2 -2
View File
@@ -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
+3 -3
View File
@@ -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.
+3 -3
View File
@@ -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
+6 -6
View File
@@ -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)
+3 -3
View File
@@ -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) {
+2 -2
View File
@@ -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.
+2 -2
View File
@@ -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")
+4 -4
View File
@@ -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
+2 -2
View File
@@ -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)}
+1 -1
View File
@@ -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)
+3 -3
View File
@@ -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)
+1 -1
View File
@@ -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.
+2 -2
View File
@@ -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.
+4 -4
View File
@@ -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
+1 -1
View File
@@ -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.
}
+2 -2
View File
@@ -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))
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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
+5 -5
View File
@@ -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
+4 -4
View File
@@ -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()
+1 -1
View File
@@ -15,7 +15,7 @@ type Advisory struct {
// state, not an absence of data, and callers must treat it as vulnerable.
FixedVersion string
Severity string
// Status is filled by trivy-db ONLY when FixedVersion is empty when there
// Status is filled by trivy-db ONLY when FixedVersion is empty - when there
// is a fix, "fixed" is the obvious state and the field is left zero. It is
// what separates "the vendor confirms this package is affected and has not
// fixed it" from "nobody has looked yet".
+2 -2
View File
@@ -8,7 +8,7 @@ type VulnSource interface {
// MetaCache enriches match results with the CVE's own metadata.
//
// This is not an optimisation, it is where severity comes from. Debian, Ubuntu
// and Alpine advisories carry no severity of their own trivy-db leaves
// and Alpine advisories carry no severity of their own - trivy-db leaves
// Advisory.Severity zero for those buckets and keeps the rating in the
// vulnerability bucket's VendorSeverity map instead. Taking the advisory's
// value alone reported an entire fleet as "unknown".
@@ -27,7 +27,7 @@ func NewMetaCache(src VulnSource) *MetaCache {
// Enrich fills severity, title, score and references in place.
//
// The advisory's severity is kept when the vulnerability bucket has nothing
// better to say RHEL does publish it per advisory so this can only raise
// better to say - RHEL does publish it per advisory - so this can only raise
// the quality of the answer, never lower it.
func (m *MetaCache) Enrich(results []Result) {
if m == nil {
+10 -10
View File
@@ -16,7 +16,7 @@ type AdvisorySource interface {
// Result is one vulnerable package on one server, before it becomes a finding.
//
// Severity, Title, CVSSScore and References are only as good as the advisory
// until MetaCache.Enrich has run over them for the Debian-family buckets the
// until MetaCache.Enrich has run over them - for the Debian-family buckets the
// advisory carries no severity at all, so an unenriched Result reads "unknown".
type Result struct {
CVEID string
@@ -33,7 +33,7 @@ type Result struct {
//
// Vulnerable means: no fix has been published, or the installed version sorts
// strictly before the fixed version under the distribution's own ordering.
// Equal is NOT vulnerable that is the backported-fix case, where a
// Equal is NOT vulnerable - that is the backported-fix case, where a
// distribution patches in place without changing the upstream version, and
// treating it as vulnerable reports a patched fleet as exposed.
func Match(src AdvisorySource, os models.OSRelease, pkgs []models.InstalledPackage) ([]Result, error) {
@@ -47,7 +47,7 @@ func Match(src AdvisorySource, os models.OSRelease, pkgs []models.InstalledPacka
var advisoryCount, skipped, unactionable, noFix int
// Advisories are keyed on the SOURCE package, and several hundred binary
// packages on a host resolve to the same few hundred sources linux-modules,
// packages on a host resolve to the same few hundred sources - linux-modules,
// linux-image and linux-headers all ask about "linux", whose advisory list is
// thousands long. Without this the same bolt read is repeated once per binary
// package, which is most of what made a single Ubuntu host take minutes.
@@ -78,7 +78,7 @@ func Match(src AdvisorySource, os models.OSRelease, pkgs []models.InstalledPacka
for _, a := range advs {
// No published fix. Whether that is a finding depends entirely on the
// status the vendor attached to it see actionable().
// status the vendor attached to it - see actionable().
if a.FixedVersion == "" {
if !actionable(a.Status) {
unactionable++
@@ -97,7 +97,7 @@ func Match(src AdvisorySource, os models.OSRelease, pkgs []models.InstalledPacka
if err != nil {
// Skip this one advisory rather than failing the whole server:
// one unparseable version must not blind us to every other CVE
// on the host. Log it a silent skip is a silent false
// on the host. Log it - a silent skip is a silent false
// negative, which is the direction that hurts.
log.Printf("vulndb: compare %s %s vs %s: %v", p.Name, p.Version, a.FixedVersion, err)
skipped++
@@ -127,8 +127,8 @@ func Match(src AdvisorySource, os models.OSRelease, pkgs []models.InstalledPacka
// necessary: Ubuntu encodes the ABI in the binary name, so an upgrade INSTALLS
// linux-headers-6.8.0-137 beside linux-headers-6.8.0-124 rather than replacing
// it, and the old one lingers until an autoremove. Matched per binary package,
// a fully patched host reports every superseded ABI package as vulnerable
// which is the noise this exists to stop and reports it twice over, once for
// a fully patched host reports every superseded ABI package as vulnerable -
// which is the noise this exists to stop - and reports it twice over, once for
// linux-headers-6.8.0-124 and again for its -generic sibling.
//
// The version, not the name, decides. There is no kernel special case here: a
@@ -176,7 +176,7 @@ func newestPerSource(family string, pkgs []models.InstalledPackage) []models.Ins
// actionable decides whether an advisory with no fixed version is a finding.
//
// trivy-db fills Status only when FixedVersion is empty, and Ubuntu publishes a
// status for every CVE against every source package it ships the vast
// status for every CVE against every source package it ships - the vast
// majority being "under_investigation" (the tracker's needs-triage), meaning
// nobody has yet established that the package is affected at all. Reporting
// those produced ~24,000 findings for a single 797-package host, which is not a
@@ -185,11 +185,11 @@ func newestPerSource(family string, pkgs []models.InstalledPackage) []models.Ins
//
// What survives is what the vendor has confirmed: affected, will_not_fix,
// fix_deferred, end_of_life. Those are exactly the findings the CLAUDE.md rule
// is about an empty fixed_in that means "no fix exists", the one most in need
// is about - an empty fixed_in that means "no fix exists", the one most in need
// of acceptance rather than patching.
// Only the two statuses that positively say "this is not a finding" are
// dropped. "unknown" is kept: a feed that sets no status at all must not become
// a silent false negative, and it is not what generates the noise Ubuntu
// a silent false negative, and it is not what generates the noise - Ubuntu
// states under_investigation explicitly.
func actionable(status string) bool {
switch status {
+2 -2
View File
@@ -25,7 +25,7 @@ const DefaultRef = "ghcr.io/aquasecurity/trivy-db:2"
// SupportedSchema is the trivy-db schema version this code understands.
//
// A different version is refused rather than parsed on the assumption it is
// close enough. Mis-reading the schema would not fail loudly it would return
// close enough. Mis-reading the schema would not fail loudly - it would return
// no advisories, which is indistinguishable from a clean fleet.
const SupportedSchema = 2
@@ -54,7 +54,7 @@ func Disabled() bool {
// DebugEnabled turns on per-package and per-advisory tracing.
//
// It is a switch rather than always-on because a single scan asks the store one
// question per installed package ~2000 lines per server, per tick which
// question per installed package - ~2000 lines per server, per tick - which
// would bury every other subsystem's logs on a fleet of any size. The lifecycle
// logs (pull, tick, per-server totals) are unconditional; only the inner loop
// is gated.
+1 -1
View File
@@ -6,7 +6,7 @@
// epochs and sorts "~" before the empty string, rpmvercmp has its own segment
// rules and treats "~" and "^" differently again, and any ordering that falls
// back on string comparison puts 1.10 before 1.9. Every one of those mistakes
// produces a false negative a vulnerable host reported clean which is the
// produces a false negative - a vulnerable host reported clean - which is the
// failure nobody notices.
package vulndb
+3 -3
View File
@@ -101,7 +101,7 @@ func (s *scheduler) tick(ctx context.Context) {
}
// ensureDB pulls a fresh database when the local copy is stale, and marks the
// whole fleet for rescanning when the version changes which is what makes a
// whole fleet for rescanning when the version changes - which is what makes a
// newly published CVE flag existing servers within a minute rather than at the
// next agent report.
func (s *scheduler) ensureDB(ctx context.Context) error {
@@ -188,7 +188,7 @@ func (s *scheduler) scanPending(ctx context.Context) {
for _, sp := range pending {
if ctx.Err() != nil {
// Leadership lost. scan_pending is still set, so the next leader
// picks these up which is why it lives on the document.
// picks these up - which is why it lives on the document.
return
}
opened := s.scanOne(ctx, sp, meta)
@@ -215,7 +215,7 @@ func (s *scheduler) scanOne(ctx context.Context, sp models.ServerPackages, meta
results, err := vulndb.Match(s.store, sp.OS, sp.Packages)
if err != nil {
// We hold no feed for this distribution, so we cannot answer whether it
// is vulnerable. Say "unsupported" reporting zero findings here would
// is vulnerable. Say "unsupported" - reporting zero findings here would
// be indistinguishable from reporting a clean host, and one of those is
// a lie.
status := models.ScanStatusUnsupported
+3 -3
View File
@@ -1,6 +1,6 @@
// Package workflowsched fires workflow runs on a cron schedule.
//
// Only robfig/cron's parser is used Parse and Next. Its own scheduler is
// Only robfig/cron's parser is used - Parse and Next. Its own scheduler is
// not, because this work runs under the housekeeping leader lock and has to
// stop the moment leadership is lost.
package workflowsched
@@ -14,12 +14,12 @@ import (
)
// ErrBadSchedule covers both a malformed expression and an unknown timezone.
// Handlers map it to 400 both are the caller's mistake, and both are much
// Handlers map it to 400 - both are the caller's mistake, and both are much
// cheaper to find at save time than at 2am.
var ErrBadSchedule = errors.New("invalid schedule")
// Standard 5-field cron: minute hour dom month dow. Deliberately no seconds
// field and no descriptors a schedule a person cannot read back is a
// field and no descriptors - a schedule a person cannot read back is a
// schedule nobody can audit.
var cronParser = cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow)
+2 -2
View File
@@ -85,7 +85,7 @@ func process(ctx context.Context, deps Deps, wf models.Workflow, now time.Time)
// The claim. Matching on the current next_run_at as well as the id means a
// second process reaching this document after another has claimed it
// matches nothing and does nothing. This not the leader lock is what
// matches nothing and does nothing. This - not the leader lock - is what
// makes a double fire impossible; the lock only keeps it cheap.
res, err := db.Col("workflows").UpdateOne(ctx,
bson.M{"workflow_id": wf.WorkflowID, "next_run_at": dueAt},
@@ -107,7 +107,7 @@ func process(ctx context.Context, deps Deps, wf models.Workflow, now time.Time)
case Fire:
// nil tokenScope: the scheduler acts as the system, not as any user.
// A schedule fires the workflow's own saved targets, and there is no
// acting credential whose tag restriction could narrow them the
// acting credential whose tag restriction could narrow them - the
// person who armed the schedule is not present at fire time, and
// inheriting a restriction from whoever last saved the workflow would
// make a run's reach depend on an editor's credential.