From bd413dc1e6ba273fb34ea411d621685991ea5794 Mon Sep 17 00:00:00 2001
From: Javier Marcos <1271349+javuto@users.noreply.github.com>
Date: Tue, 18 Aug 2026 18:53:42 +0200
Subject: [PATCH] Service parameter to enable console independently from
accelerated mode or file explorer
---
cmd/api/handlers/console.go | 9 ++--
cmd/api/handlers/features.go | 4 +-
cmd/api/handlers/features_test.go | 36 ++++++++++++++-
cmd/api/handlers/file_explorer.go | 9 ++--
cmd/api/main.go | 44 ++++++++++---------
cmd/tls/handlers/console_acceleration_test.go | 24 +++++++++-
cmd/tls/handlers/handlers.go | 3 ++
deploy/config/api.yml | 4 +-
deploy/config/tls.yml | 4 +-
docker-compose-dev.yml | 1 +
frontend/src/api/features.ts | 1 +
.../src/features/nodes/NodeConsolePage.tsx | 10 ++---
.../features/nodes/NodeDetailPage.test.tsx | 16 +++----
.../src/features/nodes/NodeDetailPage.tsx | 4 +-
.../features/nodes/NodeFileExplorerTab.tsx | 4 +-
.../service-config/ServiceConfigPage.tsx | 6 ++-
pkg/config/flags.go | 7 +++
pkg/config/flags_test.go | 26 +++++++++++
pkg/config/types.go | 1 +
pkg/console/manager.go | 12 ++---
pkg/fileexplorer/manager.go | 17 +++----
21 files changed, 172 insertions(+), 70 deletions(-)
diff --git a/cmd/api/handlers/console.go b/cmd/api/handlers/console.go
index bd1ef456..5c843b17 100644
--- a/cmd/api/handlers/console.go
+++ b/cmd/api/handlers/console.go
@@ -84,11 +84,10 @@ func (h *HandlersApi) ConsoleSessionCreateHandler(w http.ResponseWriter, r *http
apiErrorResponse(w, "error creating console session", http.StatusInternalServerError, err)
return
}
- // Dispatch a priming metadata query so the node's next QueryRead
- // returns an accelerated interval (fast polling) before the operator
- // types their first command, and live osquery_info metadata can be
- // surfaced in the console header. A priming failure is non-fatal:
- // the session is still usable, acceleration just won't be pre-warmed.
+ // Dispatch a priming metadata query so live osquery_info metadata can
+ // be surfaced in the console header. When acceleration is enabled, the
+ // node's next QueryRead can also switch to fast polling before the
+ // operator types their first command. A priming failure is non-fatal.
var priming *console.Command
if primingCmd, primingErr := h.Console.SubmitPrimingCommand(session.ID, h.consolePrimingTimeout()); primingErr == nil {
priming = &primingCmd
diff --git a/cmd/api/handlers/features.go b/cmd/api/handlers/features.go
index 9c79787e..a6d11e31 100644
--- a/cmd/api/handlers/features.go
+++ b/cmd/api/handlers/features.go
@@ -13,6 +13,7 @@ type FeaturesResponse struct {
// false the /api/v1/service-config routes are not registered at all.
ServiceConfig bool `json:"service_config"`
Accelerated bool `json:"accelerated"`
+ Console bool `json:"console"`
FileExplorer bool `json:"file_explorer"`
}
@@ -25,6 +26,7 @@ func (h *HandlersApi) FeaturesHandler(w http.ResponseWriter, r *http.Request) {
Posture: h.PostureEnabled,
ServiceConfig: h.ServiceConfigEnabled,
Accelerated: h.OsqueryValues.Accelerated,
- FileExplorer: h.OsqueryValues.Query && h.OsqueryValues.Accelerated && h.OsqueryValues.FileExplorer,
+ Console: h.OsqueryValues.Query && h.OsqueryValues.Console,
+ FileExplorer: h.OsqueryValues.Query && h.OsqueryValues.FileExplorer,
})
}
diff --git a/cmd/api/handlers/features_test.go b/cmd/api/handlers/features_test.go
index 06db2bc8..ed0b1077 100644
--- a/cmd/api/handlers/features_test.go
+++ b/cmd/api/handlers/features_test.go
@@ -97,7 +97,39 @@ func TestFeaturesHandlerReportsAcceleratedEnabled(t *testing.T) {
}
}
-func TestFeaturesHandlerReportsFileExplorerOnlyWhenQueryAcceleratedAndEnabled(t *testing.T) {
+func TestFeaturesHandlerReportsConsoleOnlyWhenQueryAndEnabled(t *testing.T) {
+ for _, tt := range []struct {
+ name string
+ cfg config.YAMLConfigurationOsquery
+ want bool
+ }{
+ {name: "disabled by default", cfg: config.YAMLConfigurationOsquery{}, want: false},
+ {name: "requires query", cfg: config.YAMLConfigurationOsquery{Console: true}, want: false},
+ {name: "does not require accelerated", cfg: config.YAMLConfigurationOsquery{Query: true, Console: true}, want: true},
+ } {
+ t.Run(tt.name, func(t *testing.T) {
+ h := &HandlersApi{}
+ WithOsqueryValues(tt.cfg)(h)
+ r := httptest.NewRequest(http.MethodGet, "/api/v1/features", nil)
+ w := httptest.NewRecorder()
+
+ h.FeaturesHandler(w, r)
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("status: got %d want 200", w.Code)
+ }
+ var resp FeaturesResponse
+ if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
+ t.Fatalf("decode: %v", err)
+ }
+ if resp.Console != tt.want {
+ t.Fatalf("console feature: got %t want %t", resp.Console, tt.want)
+ }
+ })
+ }
+}
+
+func TestFeaturesHandlerReportsFileExplorerOnlyWhenQueryAndEnabled(t *testing.T) {
for _, tt := range []struct {
name string
cfg config.YAMLConfigurationOsquery
@@ -105,7 +137,7 @@ func TestFeaturesHandlerReportsFileExplorerOnlyWhenQueryAcceleratedAndEnabled(t
}{
{name: "disabled by default", cfg: config.YAMLConfigurationOsquery{}, want: false},
{name: "requires query", cfg: config.YAMLConfigurationOsquery{Accelerated: true, FileExplorer: true}, want: false},
- {name: "requires accelerated", cfg: config.YAMLConfigurationOsquery{Query: true, FileExplorer: true}, want: false},
+ {name: "does not require accelerated", cfg: config.YAMLConfigurationOsquery{Query: true, FileExplorer: true}, want: true},
{name: "enabled", cfg: config.YAMLConfigurationOsquery{Query: true, Accelerated: true, FileExplorer: true}, want: true},
} {
t.Run(tt.name, func(t *testing.T) {
diff --git a/cmd/api/handlers/file_explorer.go b/cmd/api/handlers/file_explorer.go
index 8398794a..7b340732 100644
--- a/cmd/api/handlers/file_explorer.go
+++ b/cmd/api/handlers/file_explorer.go
@@ -56,10 +56,11 @@ func (h *HandlersApi) FileExplorerSessionCreateHandler(w http.ResponseWriter, r
apiErrorResponse(w, "error creating file explorer session", http.StatusInternalServerError, err)
return
}
- // Dispatch a priming metadata query so the node's next QueryRead
- // returns an accelerated interval (fast polling) before the operator
- // expands the first directory, and live osquery_info metadata can be
- // surfaced in the file explorer header. Non-fatal on failure.
+ // Dispatch a priming metadata query so live osquery_info metadata can
+ // be surfaced in the file explorer header. When acceleration is
+ // enabled, the node's next QueryRead can also switch to fast polling
+ // before the operator expands the first directory. Non-fatal on
+ // failure.
var priming *fileexplorer.Request
if primingReq, primingErr := h.FileExplorer.SubmitPrimingRequest(session.ID, h.fileExplorerRequestTimeout()); primingErr == nil {
priming = &primingReq
diff --git a/cmd/api/main.go b/cmd/api/main.go
index 6b520d64..d6c862d3 100644
--- a/cmd/api/main.go
+++ b/cmd/api/main.go
@@ -741,27 +741,29 @@ func osctrlAPIService() {
muxAPI.Handle(
"POST "+_apiPath(apiQueriesPath)+"/{env}/{action}/{name}",
handlerAuthCheck(http.HandlerFunc(handlersApi.QueriesActionHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
- // API: accelerated per-node console
- muxAPI.Handle(
- "POST "+_apiPath("/console")+"/{env}/nodes/{uuid}/sessions",
- handlerAuthCheck(http.HandlerFunc(handlersApi.ConsoleSessionCreateHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
- muxAPI.Handle(
- "GET "+_apiPath("/console")+"/{env}/sessions/{session_id}",
- handlerAuthCheck(http.HandlerFunc(handlersApi.ConsoleSessionShowHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
- muxAPI.Handle(
- "DELETE "+_apiPath("/console")+"/{env}/sessions/{session_id}",
- handlerAuthCheck(http.HandlerFunc(handlersApi.ConsoleSessionDeleteHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
- muxAPI.Handle(
- "POST "+_apiPath("/console")+"/{env}/sessions/{session_id}/commands",
- handlerAuthCheck(http.HandlerFunc(handlersApi.ConsoleCommandCreateHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
- muxAPI.Handle(
- "GET "+_apiPath("/console")+"/{env}/sessions/{session_id}/commands/{command_id}",
- handlerAuthCheck(http.HandlerFunc(handlersApi.ConsoleCommandShowHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
- muxAPI.Handle(
- "GET "+_apiPath("/console")+"/{env}/sessions/{session_id}/commands/{command_id}/results",
- handlerAuthCheck(http.HandlerFunc(handlersApi.ConsoleCommandResultsHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
- if flagParams.Osquery.Accelerated && flagParams.Osquery.FileExplorer {
- // API: accelerated per-node file explorer
+ if flagParams.Osquery.Console {
+ // API: per-node console
+ muxAPI.Handle(
+ "POST "+_apiPath("/console")+"/{env}/nodes/{uuid}/sessions",
+ handlerAuthCheck(http.HandlerFunc(handlersApi.ConsoleSessionCreateHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
+ muxAPI.Handle(
+ "GET "+_apiPath("/console")+"/{env}/sessions/{session_id}",
+ handlerAuthCheck(http.HandlerFunc(handlersApi.ConsoleSessionShowHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
+ muxAPI.Handle(
+ "DELETE "+_apiPath("/console")+"/{env}/sessions/{session_id}",
+ handlerAuthCheck(http.HandlerFunc(handlersApi.ConsoleSessionDeleteHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
+ muxAPI.Handle(
+ "POST "+_apiPath("/console")+"/{env}/sessions/{session_id}/commands",
+ handlerAuthCheck(http.HandlerFunc(handlersApi.ConsoleCommandCreateHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
+ muxAPI.Handle(
+ "GET "+_apiPath("/console")+"/{env}/sessions/{session_id}/commands/{command_id}",
+ handlerAuthCheck(http.HandlerFunc(handlersApi.ConsoleCommandShowHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
+ muxAPI.Handle(
+ "GET "+_apiPath("/console")+"/{env}/sessions/{session_id}/commands/{command_id}/results",
+ handlerAuthCheck(http.HandlerFunc(handlersApi.ConsoleCommandResultsHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
+ }
+ if flagParams.Osquery.FileExplorer {
+ // API: per-node file explorer
muxAPI.Handle(
"POST "+_apiPath(apiFileExplorerPath)+"/{env}/nodes/{uuid}/sessions",
handlerAuthCheck(http.HandlerFunc(handlersApi.FileExplorerSessionCreateHandler), flagParams.Service.Auth, flagParams.JWT.JWTSecret))
diff --git a/cmd/tls/handlers/console_acceleration_test.go b/cmd/tls/handlers/console_acceleration_test.go
index 5dd2aed5..a249f51d 100644
--- a/cmd/tls/handlers/console_acceleration_test.go
+++ b/cmd/tls/handlers/console_acceleration_test.go
@@ -25,7 +25,7 @@ func TestShouldAccelerateQueryReadForActiveConsoleSession(t *testing.T) {
require.NoError(t, err)
require.NoError(t, db.AutoMigrate(&console.Session{}))
queryManager := queries.CreateQueries(db)
- handler := &HandlersTLS{Queries: queryManager}
+ handler := &HandlersTLS{Queries: queryManager, OsqueryValues: &config.YAMLConfigurationOsquery{Console: true}}
node := nodes.OsqueryNode{ID: 7, UUID: "NODE-UUID", EnvironmentID: 1}
otherNode := nodes.OsqueryNode{ID: 8, UUID: "OTHER-NODE-UUID", EnvironmentID: 1}
@@ -91,6 +91,26 @@ func TestShouldAccelerateQueryReadForActiveConsoleSession(t *testing.T) {
require.True(t, handler.shouldAccelerateQueryRead(node, false))
}
+func TestShouldNotAccelerateQueryReadForConsoleWhenConsoleDisabled(t *testing.T) {
+ db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
+ require.NoError(t, err)
+ require.NoError(t, db.AutoMigrate(&console.Session{}))
+ queryManager := queries.CreateQueries(db)
+ handler := &HandlersTLS{Queries: queryManager, OsqueryValues: &config.YAMLConfigurationOsquery{}}
+ node := nodes.OsqueryNode{ID: 7, UUID: "NODE-UUID", EnvironmentID: 1}
+
+ require.NoError(t, db.Create(&console.Session{
+ EnvironmentID: node.EnvironmentID,
+ NodeID: node.ID,
+ NodeUUID: node.UUID,
+ Creator: "alice",
+ CWD: "/",
+ Platform: "linux",
+ Active: true,
+ }).Error)
+ require.False(t, handler.shouldAccelerateQueryRead(node, false))
+}
+
func TestShouldAccelerateQueryReadForActiveFileExplorerSession(t *testing.T) {
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
require.NoError(t, err)
@@ -160,7 +180,7 @@ func TestQueryReadAcceleratesOnlyConsoleSessionNode(t *testing.T) {
WithQueries(queryManager),
WithSettings(settingsMgr),
WithWriteHandler(NewBatchWriter(100, time.Hour, 10, *nodesMgr)),
- WithOsqueryValues(&config.YAMLConfigurationOsquery{Accelerated: true}),
+ WithOsqueryValues(&config.YAMLConfigurationOsquery{Accelerated: true, Console: true}),
)
consoleResp := queryReadResponse(t, handler, env.UUID, consoleNode.NodeKey)
diff --git a/cmd/tls/handlers/handlers.go b/cmd/tls/handlers/handlers.go
index bcf3c677..4a393225 100644
--- a/cmd/tls/handlers/handlers.go
+++ b/cmd/tls/handlers/handlers.go
@@ -293,6 +293,9 @@ func (h *HandlersTLS) shouldAccelerateQueryRead(node nodes.OsqueryNode, queryAcc
}
func (h *HandlersTLS) hasActiveConsoleSession(node nodes.OsqueryNode) bool {
+ if h.OsqueryValues == nil || !h.OsqueryValues.Console {
+ return false
+ }
if node.ID == 0 || node.UUID == "" || node.EnvironmentID == 0 || h.Queries == nil || h.Queries.DB == nil {
return false
}
diff --git a/deploy/config/api.yml b/deploy/config/api.yml
index dce7106b..a9715ed1 100644
--- a/deploy/config/api.yml
+++ b/deploy/config/api.yml
@@ -185,7 +185,9 @@ osquery:
carve: true
# Whether accelerated query polling features are enabled.
accelerated: false
- # Enables accelerated file explorer routes when query and accelerated are also true.
+ # Whether per-node console routes are enabled when query is also true.
+ console: false
+ # Whether per-node file explorer routes are enabled when query is also true.
fileExplorer: false
# Prevents API-driven osquery configuration changes when true.
readOnly: false
diff --git a/deploy/config/tls.yml b/deploy/config/tls.yml
index 94140712..ea08d3eb 100644
--- a/deploy/config/tls.yml
+++ b/deploy/config/tls.yml
@@ -175,7 +175,9 @@ osquery:
carve: true
# Allows accelerated query polling responses.
accelerated: false
- # Enables file explorer query behavior when accelerated/query are also enabled.
+ # Allows active console sessions to request accelerated query polling when accelerated is also enabled.
+ console: false
+ # Allows active file explorer sessions to request accelerated query polling when accelerated is also enabled.
fileExplorer: false
# Prevents config changes through operator surfaces when true.
readOnly: false
diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml
index f6482c9c..a5687860 100644
--- a/docker-compose-dev.yml
+++ b/docker-compose-dev.yml
@@ -108,6 +108,7 @@ services:
#### osquery settings ####
- OSQUERY_TABLES=/usr/src/app/deploy/osquery/data/${OSQUERY_VERSION}.json
- OSQUERY_ACCELERATED=true
+ - OSQUERY_CONSOLE=true
- OSQUERY_FILE_EXPLORER=true
#### Database settings ####
- DB_HOST=osctrl-postgres
diff --git a/frontend/src/api/features.ts b/frontend/src/api/features.ts
index 9dcced70..f1d249e7 100644
--- a/frontend/src/api/features.ts
+++ b/frontend/src/api/features.ts
@@ -4,6 +4,7 @@ export interface Features {
posture: boolean;
service_config: boolean;
accelerated: boolean;
+ console?: boolean;
file_explorer: boolean;
}
diff --git a/frontend/src/features/nodes/NodeConsolePage.tsx b/frontend/src/features/nodes/NodeConsolePage.tsx
index d4d95007..05ebe8a5 100644
--- a/frontend/src/features/nodes/NodeConsolePage.tsx
+++ b/frontend/src/features/nodes/NodeConsolePage.tsx
@@ -109,10 +109,8 @@ export function NodeConsolePanel({ env, uuid }: { env: string; uuid: string }) {
// Poll the priming metadata command until it reaches a terminal
// status, then fetch its osquery_info results and surface them as
- // live node metadata in the console header. This is what makes the
- // console "already responsive" on first open: the priming query both
- // warms acceleration (so the node polls fast) and gives us fresh
- // osquery version / build / start_time values.
+ // live node metadata in the console header. When accelerated polling
+ // is enabled server-side this also warms the node into fast polling.
const primingCommandId = primingCommand?.id;
const primingQuery = useQuery({
queryKey: ['console-priming', env, session?.id, primingCommandId],
@@ -302,10 +300,10 @@ export function NodeConsolePanel({ env, uuid }: { env: string; uuid: string }) {
{primingCommand && (
- accelerating
+ warming
)}
diff --git a/frontend/src/features/nodes/NodeDetailPage.test.tsx b/frontend/src/features/nodes/NodeDetailPage.test.tsx
index 1d29f12d..ac5468aa 100644
--- a/frontend/src/features/nodes/NodeDetailPage.test.tsx
+++ b/frontend/src/features/nodes/NodeDetailPage.test.tsx
@@ -621,8 +621,8 @@ describe('NodeDetailPage', () => {
expect(screen.queryByText('7d 3h 12m')).not.toBeInTheDocument();
});
- it('shows the console action only when accelerated queries are enabled', async () => {
- mockGetFeatures.mockResolvedValue({ posture: false, service_config: false, accelerated: true, file_explorer: false });
+ it('shows the console action when console is enabled', async () => {
+ mockGetFeatures.mockResolvedValue({ posture: false, service_config: false, accelerated: false, console: true, file_explorer: false });
const router = makeTestRouter();
renderWithProviders(router);
@@ -634,8 +634,8 @@ describe('NodeDetailPage', () => {
expect(screen.getByRole('link', { name: /console/i })).toBeInTheDocument();
});
- it('hides the console action when accelerated queries are disabled', async () => {
- mockGetFeatures.mockResolvedValue({ posture: false, service_config: false, accelerated: false, file_explorer: false });
+ it('hides the console action when console is disabled', async () => {
+ mockGetFeatures.mockResolvedValue({ posture: false, service_config: false, accelerated: true, console: false, file_explorer: false });
renderWithProviders(makeTestRouter());
@@ -646,8 +646,8 @@ describe('NodeDetailPage', () => {
expect(screen.queryByRole('link', { name: /console/i })).not.toBeInTheDocument();
});
- it('hides the console action from non-admin users even when accelerated queries are enabled', async () => {
- mockGetFeatures.mockResolvedValue({ posture: false, service_config: false, accelerated: true, file_explorer: false });
+ it('hides the console action from non-admin users even when console is enabled', async () => {
+ mockGetFeatures.mockResolvedValue({ posture: false, service_config: false, accelerated: false, console: true, file_explorer: false });
mockGetMe.mockResolvedValue({
admin: false,
permissions: {
@@ -664,8 +664,8 @@ describe('NodeDetailPage', () => {
expect(screen.queryByRole('link', { name: /console/i })).not.toBeInTheDocument();
});
- it('shows the file explorer tab when file explorer is enabled', async () => {
- mockGetFeatures.mockResolvedValue({ posture: false, service_config: false, accelerated: true, file_explorer: true });
+ it('shows the file explorer tab when file explorer is enabled without acceleration', async () => {
+ mockGetFeatures.mockResolvedValue({ posture: false, service_config: false, accelerated: false, file_explorer: true });
renderWithProviders(makeTestRouter());
diff --git a/frontend/src/features/nodes/NodeDetailPage.tsx b/frontend/src/features/nodes/NodeDetailPage.tsx
index a28acf78..c13ddad8 100644
--- a/frontend/src/features/nodes/NodeDetailPage.tsx
+++ b/frontend/src/features/nodes/NodeDetailPage.tsx
@@ -609,7 +609,7 @@ export function NodeDetailPage() {
staleTime: 5 * 60_000,
});
const postureEnabled = features?.posture === true;
- const acceleratedEnabled = features?.accelerated === true;
+ const consoleEnabled = features?.console === true;
const fileExplorerEnabled = features?.file_explorer === true;
const visibleTabs = useMemo(
() => TABS.filter((tab) => {
@@ -882,7 +882,7 @@ export function NodeDetailPage() {
Tag
)}
- {acceleratedEnabled && canAdminNode && (
+ {consoleEnabled && canAdminNode && (
- accelerating
+ warming
)}
diff --git a/frontend/src/features/service-config/ServiceConfigPage.tsx b/frontend/src/features/service-config/ServiceConfigPage.tsx
index cfdff328..8e7d15a3 100644
--- a/frontend/src/features/service-config/ServiceConfigPage.tsx
+++ b/frontend/src/features/service-config/ServiceConfigPage.tsx
@@ -89,8 +89,10 @@ const FIELD_HELP: Record = {
'tls:osquery.Carve': 'Enables file carve init/block endpoints.',
'api:osquery.Accelerated': 'Whether accelerated query polling features are enabled.',
'tls:osquery.Accelerated': 'Allows accelerated query polling responses.',
- 'api:osquery.FileExplorer': 'Enables accelerated file explorer routes when query and accelerated are also true.',
- 'tls:osquery.FileExplorer': 'Enables file explorer query behavior when accelerated/query are also enabled.',
+ 'api:osquery.Console': 'Whether per-node console routes are enabled when query is also true.',
+ 'tls:osquery.Console': 'Allows active console sessions to request accelerated query polling when accelerated is also enabled.',
+ 'api:osquery.FileExplorer': 'Whether per-node file explorer routes are enabled when query is also true.',
+ 'tls:osquery.FileExplorer': 'Allows active file explorer sessions to request accelerated query polling when accelerated is also enabled.',
'api:osquery.ReadOnly': 'Prevents API-driven osquery configuration changes when true.',
'tls:osquery.ReadOnly': 'Prevents config changes through operator surfaces when true.',
// --- YAML-annotated: logger / carver / debug
diff --git a/pkg/config/flags.go b/pkg/config/flags.go
index e94e249d..acd2b576 100644
--- a/pkg/config/flags.go
+++ b/pkg/config/flags.go
@@ -1032,6 +1032,13 @@ func initOsqueryFlags(params *ServiceParameters) []cli.Flag {
Sources: cli.EnvVars("OSQUERY_FILE_EXPLORER"),
Destination: ¶ms.Osquery.FileExplorer,
},
+ &cli.BoolFlag{
+ Name: "osquery-console",
+ Value: false,
+ Usage: "Enable on-demand per-node console queries",
+ Sources: cli.EnvVars("OSQUERY_CONSOLE"),
+ Destination: ¶ms.Osquery.Console,
+ },
&cli.BoolFlag{
Name: "read-only-configuration",
Value: false,
diff --git a/pkg/config/flags_test.go b/pkg/config/flags_test.go
index 6cea8692..193df489 100644
--- a/pkg/config/flags_test.go
+++ b/pkg/config/flags_test.go
@@ -111,6 +111,32 @@ func TestOsqueryFileExplorerFlagDefaultsOff(t *testing.T) {
}
}
+func TestOsqueryConsoleFlagDefaultsOff(t *testing.T) {
+ params := &ServiceParameters{Osquery: &YAMLConfigurationOsquery{}}
+ flags := initOsqueryFlags(params)
+
+ if params.Osquery.Console {
+ t.Fatalf("console osquery default: got true want false")
+ }
+
+ var consoleFlag *cli.BoolFlag
+ for _, flag := range flags {
+ if f, ok := flag.(*cli.BoolFlag); ok && f.Name == "osquery-console" {
+ consoleFlag = f
+ break
+ }
+ }
+ if consoleFlag == nil {
+ t.Fatalf("missing osquery-console flag")
+ }
+ if consoleFlag.Value {
+ t.Fatalf("osquery-console flag default: got true want false")
+ }
+ if consoleFlag.Destination != ¶ms.Osquery.Console {
+ t.Fatalf("osquery-console flag destination does not wire Osquery.Console")
+ }
+}
+
func TestRateLimitFlagsWireDefaults(t *testing.T) {
params := &ServiceParameters{}
flags := initRateLimitFlags(params, ServiceAPI)
diff --git a/pkg/config/types.go b/pkg/config/types.go
index 03e687e8..63464e9a 100644
--- a/pkg/config/types.go
+++ b/pkg/config/types.go
@@ -206,6 +206,7 @@ type YAMLConfigurationOsquery struct {
Query bool `yaml:"query"`
Carve bool `yaml:"carve"`
Accelerated bool `yaml:"accelerated"`
+ Console bool `yaml:"console"`
FileExplorer bool `yaml:"fileExplorer"`
ReadOnly bool `yaml:"readOnly"`
}
diff --git a/pkg/console/manager.go b/pkg/console/manager.go
index 080f9ea9..22238afa 100644
--- a/pkg/console/manager.go
+++ b/pkg/console/manager.go
@@ -19,8 +19,9 @@ const defaultCommandTimeout = 10 * time.Second
// PrimingMetadataSQL is the read-only osquery statement dispatched when a
// console session is opened. Its purpose is twofold:
// 1. Be present in the node's pending distributed queue so that the
-// next QueryRead returns an accelerated interval — the node switches
-// to fast polling before the user types their first command.
+// next QueryRead can return an accelerated interval when acceleration
+// is enabled — the node switches to fast polling before the user types
+// their first command.
// 2. Surface live metadata (osquery version, build platform, start time,
// uptime) into the session UI so the operator sees fresh values
// rather than the last-seen DB snapshot.
@@ -185,9 +186,10 @@ func (m *Manager) SubmitCommandWithTimeout(sessionID uint, input string, timeout
// SubmitPrimingCommand dispatches the console priming metadata query for
// the session. The priming query is a hidden ConsoleQueryType distributed
-// query whose presence in the node's pending queue causes the TLS
-// QueryRead handler to return an accelerated interval — so the node
-// switches to fast polling before the operator types their first command.
+// query whose presence in the node's pending queue lets the TLS QueryRead
+// handler return an accelerated interval when acceleration is enabled — so
+// the node switches to fast polling before the operator types their first
+// command.
//
// Unlike SubmitCommand, priming commands are not mutually exclusive with
// each other or with user commands: a fresh session may legitimately have
diff --git a/pkg/fileexplorer/manager.go b/pkg/fileexplorer/manager.go
index 09471f8f..b8347256 100644
--- a/pkg/fileexplorer/manager.go
+++ b/pkg/fileexplorer/manager.go
@@ -20,10 +20,11 @@ const (
// PrimingMetadataSQL is the read-only osquery statement dispatched
// when a file explorer session is opened. Its presence in the node's
- // pending distributed queue causes the TLS QueryRead handler to
- // return an accelerated interval — so the node switches to fast
- // polling before the operator expands the first directory — and it
- // also surfaces live osquery runtime metadata into the session.
+ // pending distributed queue lets the TLS QueryRead handler return an
+ // accelerated interval when acceleration is enabled — so the node
+ // switches to fast polling before the operator expands the first
+ // directory — and it also surfaces live osquery runtime metadata into
+ // the session.
PrimingMetadataSQL = "select version, build_platform, build_distro, start_time, config_valid, optimizations from osquery_info"
)
@@ -201,10 +202,10 @@ func requestSQL(action, target string) (string, error) {
// SubmitPrimingRequest dispatches the file explorer priming metadata
// query for the session. The priming query is a hidden
// FileExplorerQueryType distributed query whose presence in the node's
-// pending queue causes the TLS QueryRead handler to return an
-// accelerated interval — so the node switches to fast polling before
-// the operator expands the first directory — and it also surfaces live
-// osquery runtime metadata into the session UI.
+// pending queue lets the TLS QueryRead handler return an accelerated
+// interval when acceleration is enabled — so the node switches to fast
+// polling before the operator expands the first directory — and it also
+// surfaces live osquery runtime metadata into the session UI.
//
// Priming requests are excluded from the per-session pending cap so a
// still-running priming query never gates the first directory listing.