handlers

package
v0.5.3 Latest Latest
Warning

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

Go to latest
Published: Jun 12, 2026 License: MIT Imports: 38 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	EnvMapTargets = map[string]bool{
		"id":   true,
		"uuid": true,
		"name": true,
	}
)

Define targets to be used to retrieve an environment map

View Source
var ErrAuthUserRejected = errors.New("auth: identity cannot be resolved to an AdminUser")

ErrAuthUserRejected is returned by resolveFederatedUser when the identity cannot be turned into a usable AdminUser. Callers should not surface this to clients verbatim — return a generic "authentication failed" error to avoid leaking which path rejected (timing-oracle and information-disclosure defense).

QueryTargets enumerates the target filters accepted by QueryListHandler. TargetHiddenActive is intentionally excluded: no UI tab references it and GetByEnvTargetPaged has no branch for it (mirrors Gets() which returns nothing).

Functions

func InitOIDC added in v0.5.3

InitOIDC constructs the global OIDC provider for osctrl-api from the YAML/CLI config. Returns a non-nil error on:

  • Config.Validate failure (missing issuer, client id, etc.)
  • OIDC discovery failure (IdP unreachable, malformed metadata)

The caller (cmd/api/main.go) should treat any error as fatal during startup — we want loud failures so misconfigured deployments don't run silently with federated login broken.

Username sanitization defaults to STRICT for cmd/api (no LegacyPermissiveUsername shim). osctrl-api is greenfield; we don't have pre-existing email-format usernames to preserve.

func InitSAML added in v0.5.3

func InitSAML(ctx context.Context, cfg config.YAMLConfigurationSAML, entityID, acsURL string) error

InitSAML constructs the global SAML provider for osctrl-api from the YAML/CLI config. Returns a non-nil error on:

  • Config.Validate failure (missing metadata, required fields, etc.)
  • SAML metadata fetch / parse failure

The caller (cmd/api/main.go) treats any error as fatal during startup — loud failures over silent broken-SSO.

Username sanitization defaults to STRICT for cmd/api (no LegacyPermissiveUsername shim). osctrl-api is greenfield; no pre-existing email-format usernames to preserve.

Types

type APIQueryData

type APIQueryData map[string]string

APIQueryData to hold query result data

type ActivityBucket added in v0.5.2

type ActivityBucket struct {
	BucketStart time.Time `json:"bucket_start"`
	Config      int       `json:"config"`
	Query       int       `json:"query"`
	Carve       int       `json:"carve"`
	Enroll      int       `json:"enroll"`
}

ActivityBucket is one cell of the 24-hour activity heatmap. BucketStart is the start of the 15-minute window (UTC, RFC3339); the four counters are the audit-log entry counts that fell into that window for each category.

Categories (audit log_type → category):

  • config ← Setting (8) + Environment (7)
  • query ← Query (4)
  • carve ← Carve (5)
  • enroll ← Node (3) — covers enroll, archive, deletion

type AuthMethod added in v0.5.3

type AuthMethod struct {
	Type string `json:"type"`
	// LoginURL is the relative URL the SPA should redirect the
	// browser to when this method is chosen. For "password" this
	// is "/api/v1/login/{env}" (env is interpolated client-side
	// from the env switcher). For "oidc" this is the global
	// "/api/v1/auth/oidc/login" — env is irrelevant for federated
	// login because the federated user resolves to a single
	// AdminUser row regardless of which env tab they were viewing.
	LoginURL string `json:"loginUrl"`
}

AuthMethod describes one auth surface advertised to the SPA. `type` is the discriminator; clients render the appropriate UI based on it. We avoid leaking the issuer URL, client id, or any other IdP-specific detail at this layer — those are the IdP's responsibility to reveal once the user is redirected.

type AuthMethodsResponse added in v0.5.3

type AuthMethodsResponse struct {
	Methods []AuthMethod `json:"methods"`
}

AuthMethodsResponse is the JSON shape returned by GET /api/v1/auth/methods. Always returns at least the "password" method; OIDC is added only when --oidc-enabled is true AND the provider validated at startup. The SPA renders one button per method in stable order; we don't promise stable order beyond "password always first."

type ContextKey

type ContextKey string

ContextKey to help with the context key, to pass session data

type ContextValue

type ContextValue map[string]string

ContextValue to hold session data in the context

type EnvStats added in v0.5.2

type EnvStats struct {
	UUID          string `json:"uuid"`
	Name          string `json:"name"`
	Active        int64  `json:"active"`
	Inactive      int64  `json:"inactive"`
	Total         int64  `json:"total"`
	ActiveQueries int    `json:"active_queries"`
	ActiveCarves  int    `json:"active_carves"`
	// PlatformCounts buckets the env's nodes by OS family (linux / darwin /
	// windows / other). Drives the Nodes-table QuickFilters chip row. Counts
	// are total (active + inactive), since the filter chip lists all nodes
	// of that platform regardless of staleness — the Active/Inactive toggle
	// is independent.
	PlatformCounts nodes.PlatformCounts `json:"platform_counts"`
}

EnvStats is one row in the per-env breakdown returned by /api/v1/stats.

type HandlersApi

type HandlersApi struct {
	DB              *gorm.DB
	Users           *users.UserManager
	Tags            *tags.TagManager
	Envs            *environments.EnvManager
	Nodes           *nodes.NodeManager
	Queries         *queries.Queries
	Carves          *carves.Carves
	Settings        *settings.Settings
	RedisCache      *cache.RedisManager
	ServiceVersion  string
	ServiceName     string
	AuditLog        *auditlog.AuditLogManager
	ApiConfig       *config.APIConfiguration
	DebugHTTP       *zerolog.Logger
	DebugHTTPConfig *config.YAMLConfigurationDebug
	OsqueryTables   []types.OsqueryTable
	OsqueryValues   config.YAMLConfigurationOsquery
	// JWTSecret is the HMAC key used by pkg/auth state-cookie
	// helpers. Populated via WithJWTSecret at handler init. Same
	// bytes the Users manager signs user JWTs with; the auth
	// state-cookie code uses audience claims to segregate uses.
	JWTSecret []byte
	// OIDCEnabled is the global toggle for federated login on
	// osctrl-api. When true, /api/v1/auth/methods advertises OIDC
	// and the OIDC login/callback routes initiate the federated
	// flow. When false, only password auth is offered. OIDC is
	// global because osctrl-api is a single-tenant API surface;
	// per-env identity provider config (if ever needed) would
	// belong on the operator layer, not here.
	OIDCEnabled bool
	// SAMLEnabled is the SAML analogue of OIDCEnabled. Same
	// semantics: global, single-tenant, advertised through
	// /api/v1/auth/methods. OIDC and SAML can both be on
	// simultaneously — the SPA renders one button per advertised
	// method.
	SAMLEnabled bool
}

func CreateHandlersApi

func CreateHandlersApi(opts ...HandlersOption) *HandlersApi

CreateHandlersApi to initialize the Admin handlers struct

func (*HandlersApi) ActiveNodesHandler

func (h *HandlersApi) ActiveNodesHandler(w http.ResponseWriter, r *http.Request)

ActiveNodesHandler - GET Handler for active JSON nodes @Summary List active nodes @Description Returns active enrolled nodes for an environment. @Tags nodes @Produce json @Param env path string true "Environment name or UUID" @Success 200 {array} nodes.OsqueryNode @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/nodes/{env}/active [get]

func (*HandlersApi) AllNodesHandler

func (h *HandlersApi) AllNodesHandler(w http.ResponseWriter, r *http.Request)

AllNodesHandler - GET Handler for all JSON nodes @Summary List all nodes @Description Returns all enrolled nodes for an environment. @Tags nodes @Produce json @Param env path string true "Environment name or UUID" @Success 200 {array} nodes.OsqueryNode @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/nodes/{env}/all [get]

func (*HandlersApi) AllQueriesShowHandler

func (h *HandlersApi) AllQueriesShowHandler(w http.ResponseWriter, r *http.Request)

AllQueriesShowHandler - GET Handler to return all queries in JSON @Summary List queries @Description Returns on-demand queries for an environment. @Tags queries @Produce json @Param env path string true "Environment name or UUID" @Success 200 {array} queries.DistributedQuery @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/queries/{env} [get] @Router /api/v1/all-queries/{env} [get]

func (*HandlersApi) AllTagsHandler

func (h *HandlersApi) AllTagsHandler(w http.ResponseWriter, r *http.Request)

AllTagsHandler - GET Handler for all JSON tags @Summary List tags @Description Returns tags across environments. @Tags tags @Produce json @Success 200 {array} tags.AdminTag @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/tags [get]

func (*HandlersApi) AuditLogsHandler added in v0.4.8

func (h *HandlersApi) AuditLogsHandler(w http.ResponseWriter, r *http.Request)

AuditLogsHandler - GET /api/v1/audit-logs

Query params:

?service=...       exact match on service name
?username=...      case-insensitive partial match on username
?type=...          log type integer (1..10), see pkg/auditlog.LogType*
?env_uuid=...      filter to one environment (resolved to internal ID)
?since=RFC3339     created_at >= since
?until=RFC3339     created_at <= until
?page=N            1-indexed page; default 1
?page_size=N       default 50, max 500

Returns the SPA-canonical paginated envelope. The handler audit-logs the visit on success. @Summary List audit logs @Description Returns paginated API audit log entries. @Tags audit @Produce json @Param page query int false "Page number" @Param page_size query int false "Page size" @Param q query string false "Search query" @Param service query string false "Service filter" @Param username query string false "Username filter" @Param env query string false "Environment filter" @Success 200 {object} types.AuditLogsPagedResponse @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/audit-logs [get]

func (*HandlersApi) AuthMethodsHandler added in v0.5.3

func (h *HandlersApi) AuthMethodsHandler(w http.ResponseWriter, r *http.Request)

AuthMethodsHandler — GET /api/v1/auth/methods (no env path).

Unauthenticated by design: the SPA calls this BEFORE the user has logged in to decide which login UI to render. The response leaks only the *list* of auth shapes; no per-user, per-env, or per-IdP detail. The endpoint exists so the SPA never has to ship a "is OIDC compiled in?" build-time flag — operators toggle it server-side without re-deploying the SPA.

Rate-limited at the route layer (same preAuthRateLimit as the env/sample endpoints) to keep this from being a free metadata scrape vector. @Summary List authentication methods @Description Returns the authentication methods enabled for the API login UI. @Tags auth @Produce json @Success 200 {object} AuthMethodsResponse @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Router /api/v1/auth/methods [get]

func (*HandlersApi) CarveArchiveHandler added in v0.5.2

func (h *HandlersApi) CarveArchiveHandler(w http.ResponseWriter, r *http.Request)

CarveArchiveHandler - GET /api/v1/carves/{env}/archive/{name}

(The literal `archive` lives in segment 2 — not as a `/{name}/archive` suffix — because Go's ServeMux refuses to register patterns that ambiguously overlap with `/{env}/queries/{target}` registered on the same prefix.)

Streams (or redirects to) the reassembled carve archive blob.

Resolution rules:

  • The carve query identified by {name} must exist and be type=carve.
  • If exactly one CarvedFile exists for the query, it is served.
  • If multiple exist, an explicit ?session=<session-id> must select one. A missing/ambiguous session selector returns 409 Conflict.
  • If the underlying file is not yet archived, it is archived on demand (local or DB carver: written to a temp dir, then served; S3: a presigned download URL is returned via 302 redirect).

Content-Disposition is set to attachment with the carve archive filename. @Summary Download carve archive @Description Downloads the archive for a completed file carve. @Tags carves @Produce application/octet-stream @Param env path string true "Environment name or UUID" @Param name path string true "Carve query name" @Success 200 {file} file @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/carves/{env}/archive/{name} [get]

func (*HandlersApi) CarveListHandler

func (h *HandlersApi) CarveListHandler(w http.ResponseWriter, r *http.Request)

CarveListHandler - GET /api/v1/carves/{env}

Paginated, sorted, searchable list of carve queries (DistributedQuery rows with type=carve). Query params: page, page_size, q, sort, dir, target. Empty result → HTTP 200 with items: []. @Summary List file carves @Description Returns paginated file carves for an environment. @Tags carves @Produce json @Param env path string true "Environment name or UUID" @Param page query int false "Page number" @Param page_size query int false "Page size" @Param q query string false "Search query" @Success 200 {object} types.CarvesPagedResponse @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/carves/{env} [get] @Router /api/v1/carves/{env}/list [get]

func (*HandlersApi) CarveQueriesHandler

func (h *HandlersApi) CarveQueriesHandler(w http.ResponseWriter, r *http.Request)

CarveQueriesHandler - GET /api/v1/carves/{env}/queries/{target}

Returns carve queries by target. Retained from the legacy contract; the canonical list endpoint is now CarveListHandler at /api/v1/carves/{env}. @Summary List carve queries @Description Returns file carve queries by target and environment. @Tags carves @Produce json @Param env path string true "Environment name or UUID" @Param target path string true "Carve target filter" @Success 200 {array} queries.DistributedQuery @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/carves/{env}/queries/{target} [get]

func (*HandlersApi) CarveSamplesHandler added in v0.5.2

func (h *HandlersApi) CarveSamplesHandler(w http.ResponseWriter, r *http.Request)

CarveSamplesHandler - GET /api/v1/carves/samples

Returns the static starter library of common carve-target file paths (e.g., /etc/passwd, C:\Windows\System32\config\SAM). Same auth posture as QuerySamplesHandler. The path list is the set of high-value exfiltration locations osctrl is provisioned to carve; surfacing it to anonymous callers was a free recon gift to attackers. @Summary List carve samples @Description Returns sample carve path templates. @Tags carves @Produce json @Success 200 {array} carves.CarveSample @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/carves/samples [get]

func (*HandlersApi) CarveShowHandler

func (h *HandlersApi) CarveShowHandler(w http.ResponseWriter, r *http.Request)

CarveShowHandler - GET /api/v1/carves/{env}/{name}

Returns the carve query metadata plus the array of per-node CarvedFile rows produced by the carve. Returns 404 when the carve query name does not exist in the environment. @Summary Get file carve @Description Returns a file carve and the files produced by it. @Tags carves @Produce json @Param env path string true "Environment name or UUID" @Param name path string true "Carve query name" @Success 200 {object} types.CarveDetailResponse @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/carves/{env}/{name} [get]

func (*HandlersApi) CarvesActionHandler

func (h *HandlersApi) CarvesActionHandler(w http.ResponseWriter, r *http.Request)

CarvesActionHandler - POST /api/v1/carves/{env}/{action}/{name} @Summary Execute carve action @Description Deletes, expires, or otherwise acts on a file carve. @Tags carves @Accept json @Produce json @Param env path string true "Environment name or UUID" @Param action path string true "Carve action" @Param name path string true "Carve query name" @Success 200 {object} types.ApiGenericResponse @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/carves/{env}/{action}/{name} [post]

func (*HandlersApi) CarvesRunHandler

func (h *HandlersApi) CarvesRunHandler(w http.ResponseWriter, r *http.Request)

CarvesRunHandler - POST /api/v1/carves/{env} @Summary Run file carve @Description Starts a new file carve. @Tags carves @Accept json @Produce json @Param env path string true "Environment name or UUID" @Param request body types.ApiDistributedQueryRequest true "Request body" @Success 200 {object} types.ApiQueriesResponse @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/carves/{env} [post]

func (*HandlersApi) CheckHandlerAuth added in v0.4.6

func (h *HandlersApi) CheckHandlerAuth(w http.ResponseWriter, r *http.Request)

CheckHandlerAuth - Handle authenticated check requests @Summary Authenticated API check @Description Returns API availability for an authenticated user. @Tags checks @Produce json @Success 200 {string} string @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/checks-auth [get]

func (*HandlersApi) CheckHandlerNoAuth added in v0.4.6

func (h *HandlersApi) CheckHandlerNoAuth(w http.ResponseWriter, r *http.Request)

CheckHandlerNoAuth - Handle unauthenticated check requests @Summary Unauthenticated API check @Description Returns API availability without requiring authentication. @Tags checks @Produce json @Success 200 {string} string @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Router /api/v1/checks-no-auth [get]

func (*HandlersApi) DeleteNodeHandler

func (h *HandlersApi) DeleteNodeHandler(w http.ResponseWriter, r *http.Request)

DeleteNodeHandler - POST Handler to delete single node @Summary Delete node @Description Deletes a node from an environment. @Tags nodes @Accept json @Produce json @Param env path string true "Environment name or UUID" @Param request body types.ApiNodeGenericRequest true "Request body" @Success 200 {object} types.ApiGenericResponse @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/nodes/{env}/delete [post]

func (*HandlersApi) DeleteUserTokenHandler added in v0.5.2

func (h *HandlersApi) DeleteUserTokenHandler(w http.ResponseWriter, r *http.Request)

DeleteUserTokenHandler - DELETE /api/v1/users/{username}/token

Clears the user's APIToken so any existing JWT for them stops working. Requires super-admin OR the user themselves. @Summary Delete user token @Description Deletes an API token for a user. @Tags users @Produce json @Param username path string true "Username" @Success 200 {object} types.ApiGenericResponse @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/users/{username}/token [delete]

func (*HandlersApi) EnvActionsHandler added in v0.5.2

func (h *HandlersApi) EnvActionsHandler(w http.ResponseWriter, r *http.Request)

EnvActionsHandler - POST Handler to perform actions (create, delete, edit) on environments @Summary Execute environment action @Description Creates or modifies an environment using the legacy action endpoint. @Tags environments @Accept json @Produce json @Param request body types.ApiEnvRequest true "Request body" @Success 200 {object} types.ApiGenericResponse @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/environments/actions [post]

func (*HandlersApi) EnvActivityHandler added in v0.5.2

func (h *HandlersApi) EnvActivityHandler(w http.ResponseWriter, r *http.Request)

EnvActivityHandler — GET /api/v1/stats/activity/{env}?interval=KEY

Returns audit-log activity for one env over the requested interval, bucketed at a fixed size per interval (see activityIntervalPresets). `interval` accepts 3h / 6h / 12h / 1d / 2d / 3d / 7d (default 1d, falls back to 1d on any unknown value rather than 400ing — the SPA picker is the only allowed source).

Buckets are emitted contiguously — empty windows return zero rows for that bucket — so the SPA can render the grid without densifying client-side. @Summary Get environment activity @Description Returns activity buckets for an environment. @Tags stats @Produce json @Param env path string true "Environment name or UUID" @Param hours query int false "Number of hours to include" @Success 200 {object} ActivityBucket @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/stats/activity/{env} [get]

func (*HandlersApi) EnvCertUploadHandler added in v0.5.3

func (h *HandlersApi) EnvCertUploadHandler(w http.ResponseWriter, r *http.Request)

EnvCertUploadHandler - POST handler to upload the enrollment certificate for an environment. Body: { "certificate_b64": "<base64 PEM>" }. The PEM must parse as one or more CERTIFICATE blocks and the leaf must be a real x509 cert — we don't accept "looks like base64 of something." Legacy admin's equivalent path skipped this validation; the SPA target gets it so a typo'd paste fails fast instead of breaking enrollment downloads.

func (*HandlersApi) EnvConfigurationHandler added in v0.5.3

func (h *HandlersApi) EnvConfigurationHandler(w http.ResponseWriter, r *http.Request)

EnvConfigurationHandler - GET handler returning the assembled osquery configuration JSON for an environment. Returns the stored composed blob (options + schedule + packs + decorators + ATC). The composition is kept up to date by RefreshConfiguration, which fires from every parts mutation (UpdateOptions / UpdateSchedule / UpdatePacks / etc. in pkg/environments/osqueryconf.go), so reading the cached value is safe — the agents see the exact same blob.

SECURITY: deliberately a pure read. The first cut of this handler called RefreshConfiguration on every GET, which turned the endpoint into a CSRF-via-GET shape and a hot-loop DB-write hazard when React-Query's stale refetch path hit it. The mutation path on the parts is the canonical place for the recompose.

func (*HandlersApi) EnvEnrollActionsHandler

func (h *HandlersApi) EnvEnrollActionsHandler(w http.ResponseWriter, r *http.Request)

EnvEnrollActionsHandler - POST Handler to perform actions (extend, expire) in enroll values @Summary Execute enrollment action @Description Extends, expires, rotates, or updates enrollment values for an environment. @Tags environments @Accept json @Produce json @Param env path string true "Environment name or UUID" @Param action path string true "Enrollment action" @Param request body types.ApiActionsRequest true "Request body" @Success 200 {object} types.ApiGenericResponse @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/environments/{env}/enroll/{action} [post]

func (*HandlersApi) EnvEnrollHandler

func (h *HandlersApi) EnvEnrollHandler(w http.ResponseWriter, r *http.Request)

EnvEnrollHandler - GET Handler to return node enrollment values (secret, certificate, one-liner) for an environment as JSON @Summary Get enrollment values @Description Returns enrollment helper values for an environment. @Tags environments @Produce json @Param env path string true "Environment name or UUID" @Param target path string true "Enrollment target" @Success 200 {object} types.ApiDataResponse @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/environments/{env}/enroll/{target} [get]

func (*HandlersApi) EnvRemoveActionsHandler

func (h *HandlersApi) EnvRemoveActionsHandler(w http.ResponseWriter, r *http.Request)

EnvRemoveActionsHandler - POST Handler to perform actions (extend, expire) in remove values @Summary Execute removal action @Description Extends, expires, rotates, or updates removal values for an environment. @Tags environments @Accept json @Produce json @Param env path string true "Environment name or UUID" @Param action path string true "Removal action" @Param request body types.ApiActionsRequest true "Request body" @Success 200 {object} types.ApiGenericResponse @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/environments/{env}/remove/{action} [post]

func (*HandlersApi) EnvRemoveHandler

func (h *HandlersApi) EnvRemoveHandler(w http.ResponseWriter, r *http.Request)

EnvRemoveHandler - GET Handler to return node removal values for an environment as JSON @Summary Get removal values @Description Returns removal helper values for an environment. @Tags environments @Produce json @Param env path string true "Environment name or UUID" @Param target path string true "Removal target" @Success 200 {object} types.ApiDataResponse @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/environments/{env}/remove/{target} [get]

func (*HandlersApi) EnvironmentConfigHandler added in v0.5.2

func (h *HandlersApi) EnvironmentConfigHandler(w http.ResponseWriter, r *http.Request)

EnvironmentConfigHandler - GET /api/v1/environments/config/{env}

Returns the env's JSON-shaped config sections (options/schedule/packs/ decorators/atc/flags) so the SPA's Monaco editor can render each section. @Summary Get environment config @Description Returns raw osquery config sections for an environment. @Tags environments @Produce json @Param env path string true "Environment name or UUID" @Success 200 {object} types.EnvConfigResponse @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/environments/config/{env} [get]

func (*HandlersApi) EnvironmentConfigPatchHandler added in v0.5.2

func (h *HandlersApi) EnvironmentConfigPatchHandler(w http.ResponseWriter, r *http.Request)

EnvironmentConfigPatchHandler - PATCH /api/v1/environments/config/{env}

Body: optional options/schedule/packs/decorators/atc/flags string fields. Each non-nil field is validated as JSON before persisting; an invalid payload is rejected with 400 (no partial writes). @Summary Update environment config @Description Updates raw osquery config sections for an environment. @Tags environments @Accept json @Produce json @Param env path string true "Environment name or UUID" @Param request body types.EnvConfigPatchRequest true "Request body" @Success 200 {object} types.EnvConfigResponse @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/environments/config/{env} [patch]

func (*HandlersApi) EnvironmentCreateHandler added in v0.5.2

func (h *HandlersApi) EnvironmentCreateHandler(w http.ResponseWriter, r *http.Request)

EnvironmentCreateHandler - POST /api/v1/environments

Body: { name, hostname, type? }. Generates a UUID, defaults config / schedule / packs / decorators / ATC to "{}", and persists the env. Returns 201 with the created TLSEnvironment. Super-admin only. @Summary Create environment @Description Creates an environment. @Tags environments @Accept json @Produce json @Param request body types.EnvCreateRequest true "Request body" @Success 200 {object} environments.TLSEnvironment @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/environments [post]

func (*HandlersApi) EnvironmentDeleteHandler added in v0.5.2

func (h *HandlersApi) EnvironmentDeleteHandler(w http.ResponseWriter, r *http.Request)

EnvironmentDeleteHandler - DELETE /api/v1/environments/{env}

Removes the environment. Super-admin only. Returns 200 with a message. @Summary Delete environment @Description Deletes an environment. @Tags environments @Produce json @Param env path string true "Environment name or UUID" @Success 200 {object} types.ApiGenericResponse @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/environments/{env} [delete]

func (*HandlersApi) EnvironmentExpirationPatchHandler added in v0.5.2

func (h *HandlersApi) EnvironmentExpirationPatchHandler(w http.ResponseWriter, r *http.Request)

EnvironmentExpirationPatchHandler - PATCH /api/v1/environments/expiration/{env}

Convenience wrapper around the existing enrollment lifecycle actions (extend / expire / rotate / not-expire), accepting one of those actions via JSON body instead of as a path segment. Mirrors the legacy EnvEnrollActionsHandler semantics for both enroll and remove paths. @Summary Update environment expiration @Description Updates enrollment expiration state for an environment. @Tags environments @Accept json @Produce json @Param env path string true "Environment name or UUID" @Param request body types.EnvExpirationPatchRequest true "Request body" @Success 200 {object} environments.TLSEnvironment @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/environments/expiration/{env} [patch]

func (*HandlersApi) EnvironmentHandler

func (h *HandlersApi) EnvironmentHandler(w http.ResponseWriter, r *http.Request)

EnvironmentHandler - GET Handler to return one environment by UUID as JSON @Summary Get environment @Description Returns one environment by name or UUID. @Tags environments @Produce json @Param env path string true "Environment name or UUID" @Success 200 {object} types.TLSEnvironmentView @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/environments/{env} [get]

func (*HandlersApi) EnvironmentIntervalsPatchHandler added in v0.5.2

func (h *HandlersApi) EnvironmentIntervalsPatchHandler(w http.ResponseWriter, r *http.Request)

EnvironmentIntervalsPatchHandler - PATCH /api/v1/environments/intervals/{env}

Body: { config_interval?, log_interval?, query_interval? }. Updates the three node-pull intervals atomically. Unsupplied fields are kept. @Summary Update environment intervals @Description Updates osquery interval settings for an environment. @Tags environments @Accept json @Produce json @Param env path string true "Environment name or UUID" @Param request body types.EnvIntervalsPatchRequest true "Request body" @Success 200 {object} environments.TLSEnvironment @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/environments/intervals/{env} [patch]

func (*HandlersApi) EnvironmentMapHandler

func (h *HandlersApi) EnvironmentMapHandler(w http.ResponseWriter, r *http.Request)

EnvironmentMapHandler - GET Handler to return one environment as JSON @Summary Map environments @Description Returns an environment lookup map by target key. @Tags environments @Produce json @Param target path string true "Map target: id, name, or uuid" @Success 200 {object} map[string]environments.NameUUID @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/environments/map/{target} [get]

func (*HandlersApi) EnvironmentUpdateHandler added in v0.5.2

func (h *HandlersApi) EnvironmentUpdateHandler(w http.ResponseWriter, r *http.Request)

EnvironmentUpdateHandler - PATCH /api/v1/environments/{env}

Updates name / hostname / type / icon / debug_http / accept_enrolls. Other env fields go through the per-section endpoints. Super-admin only. @Summary Update environment @Description Updates an environment. @Tags environments @Accept json @Produce json @Param env path string true "Environment name or UUID" @Param request body types.EnvUpdateRequest true "Request body" @Success 200 {object} environments.TLSEnvironment @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/environments/{env} [patch]

func (*HandlersApi) EnvironmentsHandler

func (h *HandlersApi) EnvironmentsHandler(w http.ResponseWriter, r *http.Request)

EnvironmentsHandler - GET Handler to return all environments as JSON.

Super-admins see every env; non-super-admins get the subset where their EnvAccess.User OR EnvAccess.Admin is true (the read-surface gate elsewhere in the API). The previous super-admin-only gate meant a non-super-admin user with valid env permissions couldn't even populate the SPA's env switcher — their nav read "No environments configured" even though they had access to envs. @Summary List environments @Description Returns environments visible to the authenticated user. @Tags environments @Produce json @Success 200 {array} types.TLSEnvironmentView @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/environments [get]

func (*HandlersApi) ErrorHandler

func (h *HandlersApi) ErrorHandler(w http.ResponseWriter, r *http.Request)

ErrorHandler - Handle error requests @Summary API error response @Description Returns a generic API error response. @Tags system @Produce json @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {string} string "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Router /error [get]

func (*HandlersApi) ForbiddenHandler

func (h *HandlersApi) ForbiddenHandler(w http.ResponseWriter, r *http.Request)

ForbiddenHandler - Handle forbidden error requests @Summary API forbidden response @Description Returns a generic forbidden response. @Tags system @Produce json @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {string} string "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Router /forbidden [get]

func (*HandlersApi) GetUserPermissionsHandler added in v0.5.3

func (h *HandlersApi) GetUserPermissionsHandler(w http.ResponseWriter, r *http.Request)

GetUserPermissionsHandler - GET /api/v1/users/{username}/permissions

Returns the target user's current permission map: env UUID → {user, query, carve, admin}. Envs with no permission rows are omitted (treated as "no access" by the SPA). Requires super-admin (AdminLevel, NoEnvironment).

Used by the Permissions modal to prefill checkboxes with the user's existing access for the selected env, so the operator sees current state before making changes — no more accidentally overwriting (user:true, query:true) by re-saving the modal's default of (user:true, query:false). @Summary Get user permissions @Description Returns per-environment permissions for a user. @Tags users @Produce json @Param username path string true "Username" @Success 200 {object} types.GetPermissionsResponse @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/users/{username}/permissions [get]

func (*HandlersApi) HealthHandler

func (h *HandlersApi) HealthHandler(w http.ResponseWriter, r *http.Request)

HealthHandler - Handle health requests @Summary API health check @Description Returns the API health response. @Tags system @Produce json @Success 200 {string} string @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Router /health [get]

func (*HandlersApi) InactiveNodesHandler

func (h *HandlersApi) InactiveNodesHandler(w http.ResponseWriter, r *http.Request)

InactiveNodesHandler - GET Handler for inactive JSON nodes @Summary List inactive nodes @Description Returns inactive enrolled nodes for an environment. @Tags nodes @Produce json @Param env path string true "Environment name or UUID" @Success 200 {array} nodes.OsqueryNode @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/nodes/{env}/inactive [get]

func (*HandlersApi) LoginEnvironmentsHandler added in v0.5.2

func (h *HandlersApi) LoginEnvironmentsHandler(w http.ResponseWriter, r *http.Request)

LoginEnvironmentsHandler - GET /api/v1/login/environments

Pre-auth endpoint that returns the list of environments the user may attempt to log into. Surface is intentionally minimal: only the env UUID and name. No enroll secrets, no certificates, no settings, no hostnames — those all stay behind auth on /api/v1/environments and its CRUD siblings.

Rationale: forcing the user to type the env name on the login screen is bad UX (you don't know it until you've logged in once, and single-env installs only ever have one option). The legacy admin shows env names pre-auth in its login form, so we're not changing the security posture — just exposing the same identifiers that the URL space already commits to using post-auth.

Like POST /login/{env}, this lives behind the per-IP rate limit registered in main.go so the endpoint can't be turned into an env-enumeration oracle for brute-force prep beyond the limit. @Summary List login environments @Description Returns pre-auth environment choices for the login UI. @Tags auth @Produce json @Success 200 {array} types.LoginEnvironment @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Router /api/v1/login/environments [get]

func (*HandlersApi) LoginHandler

func (h *HandlersApi) LoginHandler(w http.ResponseWriter, r *http.Request)

LoginHandler - POST Handler for API login request

Registered on both POST /api/v1/login and POST /api/v1/login/{env}. When {env} is present, the handler additionally verifies that the user has AdminLevel access to that specific environment before issuing a token. When {env} is absent (the SPA's default), the handler authenticates the user and issues a token without an env-scoped permission check — per-request authorization on every subsequent endpoint enforces env access anyway. @Summary Log in @Description Authenticates an API user and returns a JWT token. @Tags auth @Accept json @Produce json @Param env path string false "Environment name or UUID" @Param request body types.ApiLoginRequest true "Request body" @Success 200 {object} types.ApiLoginResponse @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Router /api/v1/login [post] @Router /api/v1/login/{env} [post]

func (*HandlersApi) LogoutHandler added in v0.5.3

func (h *HandlersApi) LogoutHandler(w http.ResponseWriter, r *http.Request)

LogoutHandler — POST /api/v1/logout.

Three-tier teardown:

  1. Clear osctrl_token + osctrl_csrf cookies on the response (Max-Age=0 expires them immediately client-side).

  2. Clear the user's APIToken in the DB so any cached copy of the JWT — including the same JWT in another browser tab — fails the handlerAuthCheck APIToken-match guard on its next request. This is the server-side revocation half: cookie expiry alone is client-honor-system; APIToken=="" turns the token into a no-op even if the bytes are reused.

  3. Return the IdP's end_session_endpoint URL so the SPA can navigate to it. Keycloak's logout page in turn redirects the browser to post_logout_redirect_uri (here, the SPA's /login route). Without this step, the next "Continue with SSO" would silently re-auth against the still-valid IdP session cookie and the user would never see a credential prompt.

Idempotent: a request with no auth context (already-logged-out user) still emits cookie-clearing headers and an end-session URL, returns 200. We do not gate the endpoint behind handlerAuthCheck for this reason — logging out from an expired session should not require a fresh login first.

Username derivation: pulled from the in-memory CSRF cookie pair rather than re-parsing the JWT. The osctrl_token cookie + the stored APIToken match each other in handlerAuthCheck; here we trust the token cookie at face value because the worst case is "someone forged a logout request" — the only consequence is invalidating the legitimate user's token, which is exactly what logout is supposed to do. No privilege gain. @Summary Log out @Description Clears API session cookies and revokes the active token when present. @Tags auth @Accept json @Produce json @Success 200 {object} LogoutResponse @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Router /api/v1/logout [post]

func (*HandlersApi) LookupNodeHandler added in v0.4.5

func (h *HandlersApi) LookupNodeHandler(w http.ResponseWriter, r *http.Request)

LookupNodeHandler - POST Handler to lookup a node by identifier @Summary Lookup node @Description Looks up a node by UUID, hostname, or local name. @Tags nodes @Accept json @Produce json @Param request body types.ApiLookupRequest true "Request body" @Success 200 {object} nodes.OsqueryNode @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/nodes/lookup [post]

func (*HandlersApi) MeHandler added in v0.5.2

func (h *HandlersApi) MeHandler(w http.ResponseWriter, r *http.Request)

MeHandler - GET /api/v1/users/me

Returns the currently authenticated user's profile (sans password hash and API token). Useful for the SPA's Profile page. @Summary Get current user @Description Returns the currently authenticated user profile. @Tags users @Produce json @Success 200 {object} types.UserMeResponse @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/users/me [get]

func (*HandlersApi) MePasswordHandler added in v0.5.2

func (h *HandlersApi) MePasswordHandler(w http.ResponseWriter, r *http.Request)

MePasswordHandler - POST /api/v1/users/me/password

Changes the currently authenticated user's password. Verifies the current password (bcrypt) before persisting the new hash. @Summary Change current user password @Description Changes the current user's password. @Tags users @Accept json @Produce json @Param request body types.PasswordChangeRequest true "Request body" @Success 200 {object} types.ApiGenericResponse @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/users/me/password [post]

func (*HandlersApi) MePatchHandler added in v0.5.2

func (h *HandlersApi) MePatchHandler(w http.ResponseWriter, r *http.Request)

MePatchHandler - PATCH /api/v1/users/me

Updates email and/or fullname for the currently authenticated user. Sends each empty field through unchanged. Returns the updated profile. @Summary Update current user @Description Updates the current user's profile fields. @Tags users @Accept json @Produce json @Param request body types.UserMePatchRequest true "Request body" @Success 200 {object} types.UserMeResponse @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/users/me [patch]

func (*HandlersApi) NodeActivityBatchHandler added in v0.5.2

func (h *HandlersApi) NodeActivityBatchHandler(w http.ResponseWriter, r *http.Request)

NodeActivityBatchHandler — GET /api/v1/stats/activity/node-batch/{env}?uuids=A,B,C&interval=KEY

Returns activity buckets for up to 100 nodes in one call. The response is a map keyed by node UUID so the SPA can render a sparkline per row in the Nodes table without firing N parallel requests.

Cap is 100 to bound the per-request DB load — each node still requires 4 timestamp queries. The SPA's pagination is already <=500 page size; for pages above 100 nodes the SPA fans out 2-3 batch requests instead.

Unknown / unauthorized UUIDs are silently omitted from the response (they're treated as "no data"), not 404'd — that lets a single bad UUID in the list not break the whole page render. @Summary Get node activity batch @Description Returns activity buckets for multiple nodes in an environment. @Tags stats @Produce json @Param env path string true "Environment name or UUID" @Param uuids query string false "Comma-separated node UUIDs" @Param hours query int false "Number of hours to include" @Success 200 {object} map[string][]NodeActivityBucket @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/stats/activity/node-batch/{env} [get]

func (*HandlersApi) NodeActivityHandler added in v0.5.2

func (h *HandlersApi) NodeActivityHandler(w http.ResponseWriter, r *http.Request)

NodeActivityHandler — GET /api/v1/stats/activity/node/{env}/{uuid}?interval=KEY

Per-node version of EnvActivityHandler. Same bucketing rules (see activityIntervalPresets). The four categories partition different DB tables (see NodeActivityBucket) keyed by the node's UUID — except node_queries which keys by numeric NodeID, looked up once from the resolved node. @Summary Get node activity @Description Returns activity buckets for a node. @Tags stats @Produce json @Param env path string true "Environment name or UUID" @Param uuid path string true "Node UUID" @Param hours query int false "Number of hours to include" @Success 200 {object} NodeActivityBucket @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/stats/activity/node/{env}/{uuid} [get]

func (*HandlersApi) NodeHandler

func (h *HandlersApi) NodeHandler(w http.ResponseWriter, r *http.Request)

NodeHandler - GET Handler for single JSON nodes @Summary Get node @Description Returns a single enrolled node in an environment. @Tags nodes @Produce json @Param env path string true "Environment name or UUID" @Param node path string true "Node UUID, hostname, or local name" @Success 200 {object} types.NodeView @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/nodes/{env}/node/{node} [get]

func (*HandlersApi) NodeLogsHandler added in v0.5.2

func (h *HandlersApi) NodeLogsHandler(w http.ResponseWriter, r *http.Request)

NodeLogsHandler returns recent log entries for a node.

Path: /api/v1/logs/{type}/{env}/{uuid}

type:  "status" | "result"
env:   UUID or name
uuid:  node UUID

Query params:

since:  RFC3339 timestamp; entries strictly after this point only
limit:  1..1000 (default 100)

@Summary Get node logs @Description Returns recent status or result logs for a node. @Tags logs @Produce json @Param env path string true "Environment name or UUID" @Param type path string true "Log type: status or result" @Param uuid path string true "Node UUID" @Param limit query int false "Maximum number of log rows" @Param since query string false "RFC3339 lower bound" @Success 200 {object} NodeLogsResponse @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/logs/{type}/{env}/{uuid} [get]

func (*HandlersApi) NodesPagedHandler added in v0.5.2

func (h *HandlersApi) NodesPagedHandler(w http.ResponseWriter, r *http.Request)

NodesPagedHandler returns paginated, sorted, searchable nodes for an env. This is the canonical endpoint consumed by the React admin SPA.

Query params:

status:    "all" | "active" | "inactive" (default "all")
q:         free-text search (case-insensitive partial match on uuid,
           hostname, localname, ip, username, osquery_user, platform, version)
sort:      one of nodes.SortableColumns keys (default "lastseen")
dir:       "asc" | "desc" (default "desc" for lastseen, "asc" otherwise)
page:      1-indexed page number (default 1)
page_size: 1..500 (default 50)

@Summary List paginated nodes @Description Returns paginated, filtered, and sorted nodes for an environment. @Tags nodes @Produce json @Param env path string true "Environment name or UUID" @Param page query int false "Page number" @Param page_size query int false "Page size" @Param q query string false "Search query" @Param status query string false "Node status filter" @Param platform query string false "Platform filter" @Param sort query string false "Sort field" @Param order query string false "Sort order" @Success 200 {object} types.NodesPagedResponse @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/nodes/{env} [get]

func (*HandlersApi) OIDCCallbackHandler added in v0.5.3

func (h *HandlersApi) OIDCCallbackHandler(w http.ResponseWriter, r *http.Request)

OIDCCallbackHandler — GET /api/v1/auth/oidc/callback.

Consumes the IdP's callback URL:

  1. Parse + verify the state JWT cookie (audience, expiry, signature).
  2. Clear the state cookie immediately — single-use semantics.
  3. Run Provider.HandleCallback (the full 10-step verification chain including signature, iss, aud, exp, nonce, group gate, username sanitization).
  4. Resolve the identity to an AdminUser (existing row or JIT provision per oidcJITProvision).
  5. Mint user JWT + CSRF cookies via userJWTSessionTokens.
  6. 302-redirect to "/" — the SPA root takes over from there.

All failure paths redirect to "/" too (no error param leak). The server-side log records WHY; the client gets a generic outcome. This is the timing-oracle defense (threat T31): every failure mode produces an indistinguishable client-visible response. @Summary Complete OIDC login @Description Handles the OIDC authorization callback and creates an API session. @Tags auth @Produce json @Param code query string false "OIDC authorization code" @Param state query string false "OIDC state" @Success 200 {string} string @Failure 302 {string} string @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Router /api/v1/auth/oidc/callback [get]

func (*HandlersApi) OIDCLoginHandler added in v0.5.3

func (h *HandlersApi) OIDCLoginHandler(w http.ResponseWriter, r *http.Request)

OIDCLoginHandler — GET /api/v1/auth/oidc/login.

Starts the Authorization Code flow:

  1. Mint a fresh 256-bit nonce (and, if PKCE is enabled, a verifier).
  2. Issue the state JWT cookie (Path=/api/v1/auth/, HttpOnly, Secure, SameSite=Lax, 10-minute TTL).
  3. Build the IdP authorize URL with state=<nonce>, nonce=<nonce>, and (when PKCE on) code_challenge=S256(verifier).
  4. 302-redirect the browser to the IdP.

Unauthenticated: the user is logging IN, so they cannot have a session yet. The state cookie is the only thing tying the eventual callback back to this request.

EnvUUID on the State is a fixed sentinel ("api") because osctrl-api has no per-env IdP concept — see auth-providers spec § "OIDC is global." The legacy admin uses "admin" for the same reason. @Summary Start OIDC login @Description Redirects the browser to the configured OIDC identity provider. @Tags auth @Produce json @Success 200 {string} string @Failure 302 {string} string @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Router /api/v1/auth/oidc/login [get]

func (*HandlersApi) OsqueryTablesHandler added in v0.5.2

func (h *HandlersApi) OsqueryTablesHandler(w http.ResponseWriter, r *http.Request)

OsqueryTablesHandler - GET Handler to return the osquery schema tables

Path: /api/v1/osquery/tables The schema is global (not env-scoped). Requires any authenticated user. Responses are cache-able for one hour since the schema rarely changes. @Summary List osquery tables @Description Returns the osquery schema table metadata known to the API. @Tags osquery @Produce json @Success 200 {array} types.OsqueryTable @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/osquery/tables [get]

func (*HandlersApi) OsqueryVersionsHandler added in v0.5.2

func (h *HandlersApi) OsqueryVersionsHandler(w http.ResponseWriter, r *http.Request)

OsqueryVersionsHandler — GET /api/v1/stats/osquery-versions.

Returns fleet-wide osquery agent version breakdown for the dashboard's "fleet hygiene" panel. Operators use this to spot stale agents that need upgrading. Cross-env (no env filter); the dashboard already surfaces the per-env breakdown in its env tiles.

Counts include both active and inactive nodes — a node sitting at an old osquery version is still "stale" even if it's offline today, because once it comes back online it'll come back stale. @Summary Get osquery version stats @Description Returns fleet-wide osquery version counts. @Tags stats @Produce json @Success 200 {object} nodes.OsqueryVersionCount @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/stats/osquery-versions [get]

func (*HandlersApi) PlatformsEnvHandler

func (h *HandlersApi) PlatformsEnvHandler(w http.ResponseWriter, r *http.Request)

PlatformsEnvHandler - GET Handler to return platforms for one environment as JSON @Summary List platforms @Description Returns platform counts for an environment. @Tags platforms @Produce json @Param env path string true "Environment name or UUID" @Success 200 {array} nodes.PlatformCounts @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/platforms/{env} [get]

func (*HandlersApi) PlatformsHandler

func (h *HandlersApi) PlatformsHandler(w http.ResponseWriter, r *http.Request)

PlatformsHandler - GET Handler for multiple JSON platforms

func (*HandlersApi) QueriesActionHandler

func (h *HandlersApi) QueriesActionHandler(w http.ResponseWriter, r *http.Request)

QueriesActionHandler - POST Handler to delete/expire a query @Summary Execute query action @Description Deletes, expires, or otherwise acts on an on-demand query. @Tags queries @Accept json @Produce json @Param env path string true "Environment name or UUID" @Param action path string true "Query action" @Param name path string true "Query name" @Success 200 {object} types.ApiGenericResponse @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/queries/{env}/{action}/{name} [post]

func (*HandlersApi) QueriesRunHandler

func (h *HandlersApi) QueriesRunHandler(w http.ResponseWriter, r *http.Request)

QueriesRunHandler - POST Handler to run a query @Summary Run query @Description Starts a new distributed query. @Tags queries @Accept json @Produce json @Param env path string true "Environment name or UUID" @Param request body types.ApiDistributedQueryRequest true "Request body" @Success 200 {object} types.ApiQueriesResponse @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/queries/{env} [post]

func (*HandlersApi) QueryListHandler

func (h *HandlersApi) QueryListHandler(w http.ResponseWriter, r *http.Request)

QueryListHandler - GET Handler to return queries in JSON by target and environment (paginated)

Query params: page, page_size, q (free-text search), sort (column key), dir (asc|desc) @Summary List paginated queries @Description Returns paginated on-demand queries by target and environment. @Tags queries @Produce json @Param env path string true "Environment name or UUID" @Param target path string true "Query target filter" @Param page query int false "Page number" @Param page_size query int false "Page size" @Param q query string false "Search query" @Param sort query string false "Sort field" @Param order query string false "Sort order" @Success 200 {object} types.QueriesPagedResponse @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/queries/{env}/list/{target} [get]

func (*HandlersApi) QueryResultsCSVHandler added in v0.5.2

func (h *HandlersApi) QueryResultsCSVHandler(w http.ResponseWriter, r *http.Request)

QueryResultsCSVHandler - GET Handler to stream query results as CSV

Path: /api/v1/queries/{env}/results/csv/{name}

(The `.csv` lives as a literal path segment before `{name}` because Go's ServeMux grammar requires wildcards to end at `/` or end-of-pattern, so `{name}.csv` is a parse error at registration time.) @Summary Export query results CSV @Description Streams query results as CSV. @Tags queries @Produce text/csv @Param env path string true "Environment name or UUID" @Param name path string true "Query name" @Success 200 {string} string @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/queries/{env}/results/csv/{name} [get]

func (*HandlersApi) QueryResultsHandler

func (h *HandlersApi) QueryResultsHandler(w http.ResponseWriter, r *http.Request)

QueryResultsHandler - GET Handler to return paginated query results in JSON

Path: /api/v1/queries/{env}/results/{name} Params: page, page_size, since (RFC3339 timestamp; unparseable → ignored)

Empty results are a valid state and return HTTP 200 with items: []. @Summary Get query results @Description Returns paginated results for an on-demand query. @Tags queries @Produce json @Param env path string true "Environment name or UUID" @Param name path string true "Query name" @Param page query int false "Page number" @Param page_size query int false "Page size" @Param since query string false "RFC3339 lower bound" @Success 200 {object} types.QueryResultsResponse @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/queries/{env}/results/{name} [get]

func (*HandlersApi) QuerySamplesHandler added in v0.5.2

func (h *HandlersApi) QuerySamplesHandler(w http.ResponseWriter, r *http.Request)

QuerySamplesHandler - GET /api/v1/queries/samples

Returns the static starter library of osquery SQL templates so the SPA's queries/new form can populate its QuickTemplates row. Authenticated.

History: an earlier revision exposed this pre-auth on the rationale that the data is static and ships with the binary. Even read-only data fingerprints the deployment as osctrl and reveals the SQL-template starter pack to anonymous callers — neither of which the only consumer (the post-login queries/new form) requires at pre-auth time. Moved behind handlerAuthCheck in cmd/api/main.go. @Summary List query samples @Description Returns sample query templates. @Tags queries @Produce json @Success 200 {array} queries.QuerySample @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/queries/samples [get]

func (*HandlersApi) QueryShowHandler

func (h *HandlersApi) QueryShowHandler(w http.ResponseWriter, r *http.Request)

QueryShowHandler - GET Handler to return a single query in JSON @Summary Get query @Description Returns a single on-demand query. @Tags queries @Produce json @Param env path string true "Environment name or UUID" @Param name path string true "Query name" @Success 200 {object} queries.DistributedQuery @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/queries/{env}/{name} [get]

func (*HandlersApi) RefreshUserTokenHandler added in v0.5.2

func (h *HandlersApi) RefreshUserTokenHandler(w http.ResponseWriter, r *http.Request)

RefreshUserTokenHandler - POST /api/v1/users/{username}/token/refresh

Generates a new JWT for the target user, persists it as the user's APIToken (invalidating the previous token), and returns the new token + expiry. Requires super-admin OR the request author asking for their own token. Audit-logged on success. @Summary Refresh user token @Description Refreshes an API token for a user. @Tags users @Accept json @Produce json @Param username path string true "Username" @Success 200 {object} types.TokenResponse @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/users/{username}/token/refresh [post]

func (*HandlersApi) RootHandler

func (h *HandlersApi) RootHandler(w http.ResponseWriter, r *http.Request)

RootHandler - Handle root requests.

`GET /` is registered as the api's liveness check (returns "✅" with 200). Go's ServeMux uses `/` as a wildcard match for any GET request the mux doesn't have a more-specific pattern for, which means typos like `GET /api/v1/totally-fake-endpoint` would land here and return 200 — confusing for clients debugging an integration. We tighten the contract: respond 200 ONLY when the request actually targets `/`. Other GETs that fall through fall out as 404.

Returning 404 here doesn't leak endpoint structure beyond what's already in the public OpenAPI; it just stops the api from silently claiming success on misrouted requests. @Summary API root liveness check @Description Returns the API root liveness response. @Tags system @Produce json @Success 200 {string} string @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Router / [get]

func (*HandlersApi) SAMLACSHandler added in v0.5.3

func (h *HandlersApi) SAMLACSHandler(w http.ResponseWriter, r *http.Request)

SAMLACSHandler — POST /api/v1/auth/saml/acs.

Consumes the IdP's signed SAMLResponse POST:

  1. Parse + verify the state JWT cookie (audience, expiry, signature).
  2. Clear the state cookie immediately — single-use semantics.
  3. Run Provider.HandleCallback (RelayState match, signature, audience, NotBefore/NotOnOrAfter, recipient, InResponseTo, replay cache, groups gate, username sanitization).
  4. Resolve the identity to an AdminUser (existing row or JIT provision per samlJITProvision).
  5. Mint user JWT + CSRF cookies via userJWTSessionTokens.
  6. 302-redirect to "/" — the SPA root takes over.

Failure paths redirect to "/" too (no error param leak). Server-side log records WHY; client sees a generic outcome. Timing-oracle defense matches the OIDC handler. @Summary Complete SAML login @Description Handles the SAML assertion consumer service callback and creates an API session. @Tags auth @Accept json @Produce json @Param SAMLResponse formData string false "SAML response assertion" @Param RelayState formData string false "SAML relay state" @Success 200 {string} string @Failure 302 {string} string @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Router /api/v1/auth/saml/acs [post]

func (*HandlersApi) SAMLLoginHandler added in v0.5.3

func (h *HandlersApi) SAMLLoginHandler(w http.ResponseWriter, r *http.Request)

SAMLLoginHandler — GET /api/v1/auth/saml/login.

Starts the SAML SP-initiated SSO flow:

  1. Mint a fresh 256-bit nonce + OAuthState (defense-in-depth split, same shape as the OIDC handler).
  2. Issue the state JWT cookie (Path=/api/v1/auth/, HttpOnly, Secure, SameSite=Lax, 10-minute TTL).
  3. Build the IdP SSO URL with RelayState=<OAuthState>. The IdP echoes RelayState verbatim on the ACS POST; SAMLACSHandler validates the echo against the state cookie.
  4. 302-redirect the browser to the IdP.

Unauthenticated by design: the user is logging IN. The state cookie is the only thing tying the eventual ACS POST back to this request.

EnvUUID on the State is a fixed sentinel ("api") because osctrl-api is single-tenant for federated login. Matches the OIDC handler's posture. @Summary Start SAML login @Description Redirects the browser to the configured SAML identity provider. @Tags auth @Produce json @Success 200 {string} string @Failure 302 {string} string @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Router /api/v1/auth/saml/login [get]

func (*HandlersApi) SAMLMetadataHandler added in v0.5.3

func (h *HandlersApi) SAMLMetadataHandler(w http.ResponseWriter, r *http.Request)

SAMLMetadataHandler — GET /api/v1/auth/saml/metadata.

Serves the SP metadata XML for IdP-side registration. The IdP administrator points their config at this URL (or downloads the XML and uploads it) so the IdP knows our EntityID + ACS URL + supported NameID formats.

Public by design — SP metadata is meant to be machine-readable by anyone who wants to federate with us. The information it carries is the same information the IdP would learn via the AuthnRequest URL during the first login attempt.

Rate-limited at the route layer like the other unauth endpoints. @Summary Get SAML metadata @Description Returns service provider metadata for SAML identity provider registration. @Tags auth @Produce application/xml @Success 200 {string} string @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Router /api/v1/auth/saml/metadata [get]

func (*HandlersApi) SavedQueriesListHandler added in v0.5.2

func (h *HandlersApi) SavedQueriesListHandler(w http.ResponseWriter, r *http.Request)

SavedQueriesListHandler - GET /api/v1/saved-queries/{env}

Paginated, sorted, searchable list of saved queries for an environment. Query params: page, page_size, q (free-text), sort (column key), dir (asc|desc). @Summary List saved queries @Description Returns paginated saved queries for an environment. @Tags saved-queries @Produce json @Param env path string true "Environment name or UUID" @Param page query int false "Page number" @Param page_size query int false "Page size" @Param q query string false "Search query" @Success 200 {object} types.SavedQueriesPagedResponse @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/saved-queries/{env} [get]

func (*HandlersApi) SavedQueryCreateHandler added in v0.5.2

func (h *HandlersApi) SavedQueryCreateHandler(w http.ResponseWriter, r *http.Request)

SavedQueryCreateHandler - POST /api/v1/saved-queries/{env}

Body: { "name": string, "query": string }. Returns 201 with the created view, 409 if a saved query with that name already exists in the environment. @Summary Create saved query @Description Creates a saved query in an environment. @Tags saved-queries @Accept json @Produce json @Param env path string true "Environment name or UUID" @Param request body types.SavedQueryCreateRequest true "Request body" @Success 200 {object} types.SavedQueryView @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/saved-queries/{env} [post]

func (*HandlersApi) SavedQueryDeleteHandler added in v0.5.2

func (h *HandlersApi) SavedQueryDeleteHandler(w http.ResponseWriter, r *http.Request)

SavedQueryDeleteHandler - DELETE /api/v1/saved-queries/{env}/{name} @Summary Delete saved query @Description Deletes a saved query in an environment. @Tags saved-queries @Produce json @Param env path string true "Environment name or UUID" @Param name path string true "Saved query name" @Success 200 {object} types.ApiGenericResponse @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/saved-queries/{env}/{name} [delete]

func (*HandlersApi) SavedQueryUpdateHandler added in v0.5.2

func (h *HandlersApi) SavedQueryUpdateHandler(w http.ResponseWriter, r *http.Request)

SavedQueryUpdateHandler - PATCH /api/v1/saved-queries/{env}/{name}

Body: { "query": string }. Updates the SQL body only; the original creator is preserved. Returns the updated view. @Summary Update saved query @Description Updates a saved query in an environment. @Tags saved-queries @Accept json @Produce json @Param env path string true "Environment name or UUID" @Param name path string true "Saved query name" @Param request body types.SavedQueryUpdateRequest true "Request body" @Success 200 {object} types.SavedQueryView @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/saved-queries/{env}/{name} [patch]

func (*HandlersApi) SetUserPermissionsAllHandler added in v0.5.3

func (h *HandlersApi) SetUserPermissionsAllHandler(w http.ResponseWriter, r *http.Request)

SetUserPermissionsAllHandler - POST /api/v1/users/{username}/permissions/all

Bulk variant of SetUserPermissionsHandler: applies the same EnvAccess shape to every environment that exists at request time. Body: { access: { user, query, carve, admin } }. Returns { updated, total, access } on success.

Same authn/authz posture as the per-env handler: requires super-admin (AdminLevel, NoEnvironment). Same lockout guards:

  • Super-admins cannot self-demote via this endpoint.
  • The last super-admin cannot be demoted under any path.

"All current envs" semantics: enumeration happens server-side at request time. Envs created LATER do not inherit; the operator re-applies as needed. See SetPermissionsAllRequest godoc. @Summary Set all user permissions @Description Sets permissions across all environments for a user. @Tags users @Accept json @Produce json @Param username path string true "Username" @Param request body types.SetPermissionsAllRequest true "Request body" @Success 200 {object} types.SetPermissionsAllResponse @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/users/{username}/permissions/all [post]

func (*HandlersApi) SetUserPermissionsHandler added in v0.5.2

func (h *HandlersApi) SetUserPermissionsHandler(w http.ResponseWriter, r *http.Request)

SetUserPermissionsHandler - POST /api/v1/users/{username}/permissions

Body: { env_uuid, access: { user, query, carve, admin } }. Replaces the target user's per-env access rows. Returns 200 with the new EnvAccess. Requires super-admin (AdminLevel, NoEnvironment) — env-scoped admins can not grant permissions for their environment from this endpoint. @Summary Set user permissions @Description Sets per-environment permissions for a user. @Tags users @Accept json @Produce json @Param username path string true "Username" @Param request body types.SetPermissionsRequest true "Request body" @Success 200 {object} types.EnvAccessView @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/users/{username}/permissions [post]

func (*HandlersApi) SettingPatchHandler added in v0.5.2

func (h *HandlersApi) SettingPatchHandler(w http.ResponseWriter, r *http.Request)

SettingPatchHandler — PATCH /api/v1/settings/{service}/{name}

Body shape (one of String, Boolean, Integer):

{ "string": "value" }
{ "boolean": true }
{ "integer": 42 }

The handler reads the existing setting first to determine its type, then applies the matching typed setter. Mismatched payloads return 400. The setting must already exist (creation is the legacy admin's job); a missing setting → 404. Audit-log on success only. @Summary Update setting @Description Updates a mutable setting value. @Tags settings @Accept json @Produce json @Param service path string true "Service name" @Param name path string true "Setting name" @Param request body types.SettingPatchRequest true "Request body" @Success 200 {object} settings.SettingValue @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/settings/{service}/{name} [patch]

func (*HandlersApi) SettingsHandler

func (h *HandlersApi) SettingsHandler(w http.ResponseWriter, r *http.Request)

SettingsHandler - GET Handler for all settings including JSON @Summary List settings @Description Returns settings for all services. @Tags settings @Produce json @Success 200 {array} settings.SettingValue @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/settings [get]

func (*HandlersApi) SettingsServiceEnvHandler

func (h *HandlersApi) SettingsServiceEnvHandler(w http.ResponseWriter, r *http.Request)

SettingsServiceEnvHandler - GET Handler for service and environment specific settings excluding JSON @Summary List service environment settings @Description Returns settings for a service and environment. @Tags settings @Produce json @Param service path string true "Service name" @Param env path string true "Environment name or UUID" @Success 200 {array} settings.SettingValue @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/settings/{service}/{env} [get]

func (*HandlersApi) SettingsServiceEnvJSONHandler

func (h *HandlersApi) SettingsServiceEnvJSONHandler(w http.ResponseWriter, r *http.Request)

GET Handler for service and environment specific settings including JSON @Summary List service environment JSON settings @Description Returns JSON settings for a service and environment. @Tags settings @Produce json @Param service path string true "Service name" @Param env path string true "Environment name or UUID" @Success 200 {array} settings.SettingValue @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/settings/{service}/json/{env} [get]

func (*HandlersApi) SettingsServiceHandler

func (h *HandlersApi) SettingsServiceHandler(w http.ResponseWriter, r *http.Request)

SettingsServiceHandler - GET Handler for service specific settings excluding JSON @Summary List service settings @Description Returns settings for a service. @Tags settings @Produce json @Param service path string true "Service name" @Success 200 {array} settings.SettingValue @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/settings/{service} [get]

func (*HandlersApi) SettingsServiceJSONHandler

func (h *HandlersApi) SettingsServiceJSONHandler(w http.ResponseWriter, r *http.Request)

SettingsServiceJSONHandler - GET Handler for service specific settings including JSON @Summary List service JSON settings @Description Returns JSON settings for a service. @Tags settings @Produce json @Param service path string true "Service name" @Success 200 {array} settings.SettingValue @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/settings/{service}/json [get]

func (*HandlersApi) StatsHandler added in v0.5.2

func (h *HandlersApi) StatsHandler(w http.ResponseWriter, r *http.Request)

StatsHandler returns cross-env totals + per-env counts, filtered to the envs the calling user has UserLevel access to. Used by the SPA dashboard.

No query params. The response is small (one entry per accessible env) and cacheable for 30s on the client (Cache-Control: private, max-age=30).

NOTE on query/carve counting:

  • GetActive(envID) returns ALL active rows regardless of type (union).
  • To avoid double-counting we call GetQueries("active", envID) for standard queries and GetCarves("active", envID) for carves separately.
  • Unit test for this handler is deferred: the underlying pkg/queries functions are exercised by existing tests in pkg/queries; a full integration test would require DB fixture setup that is out of scope for Track 2.

@Summary Get dashboard stats @Description Returns cross-environment dashboard statistics. @Tags stats @Produce json @Success 200 {object} StatsResponse @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/stats [get]

func (*HandlersApi) TagEnvHandler

func (h *HandlersApi) TagEnvHandler(w http.ResponseWriter, r *http.Request)

TagEnvHandler - GET Handler to return one tag for one environment as JSON. Permission is scoped to env.UUID admin so non-super operators with admin rights on this specific environment can view its tags. @Summary Get environment tag @Description Returns one tag by name for an environment. @Tags tags @Produce json @Param env path string true "Environment name or UUID" @Param name path string true "Tag name" @Success 200 {object} tags.AdminTag @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/tags/{env}/{name} [get]

func (*HandlersApi) TagNodeHandler added in v0.4.6

func (h *HandlersApi) TagNodeHandler(w http.ResponseWriter, r *http.Request)

TagNodeHandler - POST Handler to tag a node @Summary Tag node @Description Adds or updates a tag on a node. @Tags nodes @Accept json @Produce json @Param env path string true "Environment name or UUID" @Param request body types.ApiNodeTagRequest true "Request body" @Success 200 {object} types.ApiGenericResponse @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/nodes/{env}/tag [post]

func (*HandlersApi) TagsActionHandler

func (h *HandlersApi) TagsActionHandler(w http.ResponseWriter, r *http.Request)

TagsActionHandler - POST Handler to create / update / delete tags. The action arrives as a URL path segment (legacy contract retained because Track 6 doesn't introduce new tag routes); body validation surfaces 400 on parse error and 409 on duplicate-name conflicts. @Summary Execute tag action @Description Creates, updates, deletes, or applies tags in an environment. @Tags tags @Accept json @Produce json @Param env path string true "Environment name or UUID" @Param action path string true "Tag action" @Param request body types.ApiTagsRequest true "Request body" @Success 200 {object} types.ApiDataResponse @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/tags/{env}/{action} [post]

func (*HandlersApi) TagsEnvHandler

func (h *HandlersApi) TagsEnvHandler(w http.ResponseWriter, r *http.Request)

TagsEnvHandler - GET Handler to return tags for one environment as JSON. Permission is scoped to env.UUID admin (see TagEnvHandler note). @Summary List environment tags @Description Returns tags for an environment. @Tags tags @Produce json @Param env path string true "Environment name or UUID" @Success 200 {array} tags.AdminTag @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/tags/{env} [get]

func (*HandlersApi) UserActionHandler added in v0.4.6

func (h *HandlersApi) UserActionHandler(w http.ResponseWriter, r *http.Request)

UserActionHandler - POST Handler to take actions on a user by username and environment @Summary Execute user action @Description Creates, updates, deletes, or changes flags on a user. @Tags users @Accept json @Produce json @Param username path string true "Username" @Param action path string true "User action" @Param request body types.ApiUserRequest true "Request body" @Success 200 {object} types.ApiDataResponse @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/users/{username}/{action} [post]

func (*HandlersApi) UserHandler

func (h *HandlersApi) UserHandler(w http.ResponseWriter, r *http.Request)

UserHandler - GET Handler for environment users @Summary Get user @Description Returns an API user by username. @Tags users @Produce json @Param username path string true "Username" @Success 200 {object} types.AdminUserView @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/users/{username} [get]

func (*HandlersApi) UsersHandler

func (h *HandlersApi) UsersHandler(w http.ResponseWriter, r *http.Request)

UsersHandler - GET Handler for multiple JSON nodes @Summary List users @Description Returns API users. @Tags users @Produce json @Success 200 {array} types.AdminUserView @Failure 400 {object} types.ApiErrorResponse "Bad request" @Failure 401 {object} types.ApiErrorResponse "Unauthorized" @Failure 403 {object} types.ApiErrorResponse "Forbidden" @Failure 404 {object} types.ApiErrorResponse "Not found" @Failure 409 {object} types.ApiErrorResponse "Conflict" @Failure 429 {object} types.ApiErrorResponse "Too many requests" @Failure 500 {object} types.ApiErrorResponse "Internal server error" @Failure 503 {object} types.ApiErrorResponse "Service unavailable" @Security ApiKeyAuth @Router /api/v1/users [get]

type HandlersOption

type HandlersOption func(*HandlersApi)

func WithAuditLog added in v0.4.8

func WithAuditLog(auditLog *auditlog.AuditLogManager) HandlersOption

func WithCache

func WithCache(rds *cache.RedisManager) HandlersOption

func WithCarves

func WithCarves(carves *carves.Carves) HandlersOption

func WithDB

func WithDB(db *gorm.DB) HandlersOption

func WithDebugHTTP added in v0.4.5

func WithDebugHTTP(cfg *config.YAMLConfigurationDebug) HandlersOption

func WithEnvs

func WithEnvs(envs *environments.EnvManager) HandlersOption

func WithJWTSecret added in v0.5.3

func WithJWTSecret(secret []byte) HandlersOption

WithJWTSecret attaches the HMAC key for auth state-cookie signing. MUST be the same secret pkg/users uses for user JWTs; the audience claim ("osctrl-auth-state" vs "osctrl-api") segregates the two purposes so sharing the underlying secret is safe.

func WithName

func WithName(name string) HandlersOption

func WithNodes

func WithNodes(nodes *nodes.NodeManager) HandlersOption

func WithOIDC added in v0.5.3

func WithOIDC(enabled bool) HandlersOption

WithOIDC toggles the global OIDC routes and the /api/v1/auth/methods response. Pass true at startup when the operator has configured an OIDC provider. When false, /api/v1/auth/methods returns password-only so the SPA renders the password form alone.

func WithOsqueryTables added in v0.5.2

func WithOsqueryTables(tables []types.OsqueryTable) HandlersOption

func WithOsqueryValues added in v0.5.2

func WithOsqueryValues(values config.YAMLConfigurationOsquery) HandlersOption

func WithQueries

func WithQueries(queries *queries.Queries) HandlersOption

func WithSAML added in v0.5.3

func WithSAML(enabled bool) HandlersOption

WithSAML toggles the global SAML routes and the /api/v1/auth/methods response. SAML analogue of WithOIDC; the two can be enabled simultaneously.

func WithSettings

func WithSettings(settings *settings.Settings) HandlersOption

func WithTags

func WithTags(tags *tags.TagManager) HandlersOption

func WithUsers

func WithUsers(users *users.UserManager) HandlersOption

func WithVersion

func WithVersion(version string) HandlersOption

type LogoutResponse added in v0.5.3

type LogoutResponse struct {
	AuthSource     string `json:"auth_source,omitempty"`
	IdPLogoutURL   string `json:"idp_logout_url,omitempty"`
	IdPClientID    string `json:"idp_client_id,omitempty"`
	IdPIDTokenHint string `json:"idp_id_token_hint,omitempty"`
}

LogoutResponse is the JSON payload returned by POST /api/v1/logout.

AuthSource carries which provider issued the active session ("oidc" / "saml" / ""). The SPA uses it to decide which IdP-logout flow to run — sending a SAML user to the OIDC end-session URL with a stale id_token_hint just produces a confusing Keycloak error page.

IdPLogoutURL is non-empty when AuthSource=="oidc" AND the OIDC provider advertised an end_session_endpoint in its discovery document — the SPA navigates the browser there to terminate the IdP session. Empty for SAML users (SLO deferred to v2 per spec) and for password users.

IdPClientID is the OIDC client_id registered with the IdP. Some IdPs (Keycloak) accept it as an alternative to id_token_hint when the operator chains post_logout_redirect_uri. The SPA appends it as ?client_id=...

IdPIDTokenHint is the raw id_token from the user's most recent federated login. Okta REQUIRES this on /v1/logout when chaining a post-logout redirect; without it Okta returns "Missing parameter: id_token_hint". Non-empty when the osctrl_id_token cookie was present and valid; empty for password-only operators or after a fresh login from a cookie-stripped browser. The SPA appends it as ?id_token_hint=... — the IdP verifies the token's signature before terminating the session, so this is safe to expose to the browser (we set it back into the URL the browser navigates to).

type NodeActivityBucket added in v0.5.2

type NodeActivityBucket struct {
	BucketStart time.Time `json:"bucket_start"`
	Status      int       `json:"status"`
	Result      int       `json:"result"`
	Query       int       `json:"query"`
	Carve       int       `json:"carve"`
}

NodeActivityBucket is one cell of the per-node 24h activity heatmap. Categories pivot from the env-scoped variant — node-scoped activity is about what THIS device has been doing, not what operators have done to the env. So:

  • status ← osquery_status_data row count (status logs received from this node)
  • result ← osquery_result_data row count (query results returned by this node)
  • query ← node_queries row count (distributed queries scheduled against this node)
  • carve ← carved_files row count (carves this node has produced)

All four are joinable by node uuid (or numeric node id for node_queries).

type NodeLogsResponse added in v0.5.2

type NodeLogsResponse struct {
	Items []map[string]any `json:"items"`
	Type  string           `json:"type"`
	UUID  string           `json:"uuid"`
	Env   string           `json:"env"`
	Since string           `json:"since,omitempty"`
	Limit int              `json:"limit"`
}

NodeLogsResponse is the SPA-canonical response for GET /api/v1/logs/{type}/{env}/{uuid}.

type StatsResponse added in v0.5.2

type StatsResponse struct {
	// Cross-env totals (the user's allowed envs only).
	TotalNodes    int64 `json:"total_nodes"`
	ActiveNodes   int64 `json:"active_nodes"`
	InactiveNodes int64 `json:"inactive_nodes"`
	// TotalActiveQueries counts standard query-type active queries (excludes carves).
	TotalActiveQueries int `json:"total_active_queries"`
	// TotalActiveCarves counts active carve-type queries.
	TotalActiveCarves int `json:"total_active_carves"`
	// Cross-env platform breakdown — sum of every accessible env's PlatformCounts.
	PlatformCounts nodes.PlatformCounts `json:"platform_counts"`

	// Per-env breakdown, in stable alphabetical order by name.
	Environments []EnvStats `json:"environments"`
}

StatsResponse is the canonical /api/v1/stats shape consumed by the dashboard.

Jump to

Keyboard shortcuts

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