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
+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: