Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions cmd/api/handlers/console.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion cmd/api/handlers/features.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}

Expand All @@ -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,
})
}
36 changes: 34 additions & 2 deletions cmd/api/handlers/features_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,15 +97,47 @@ 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
want bool
}{
{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) {
Expand Down
9 changes: 5 additions & 4 deletions cmd/api/handlers/file_explorer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
44 changes: 23 additions & 21 deletions cmd/api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
24 changes: 22 additions & 2 deletions cmd/tls/handlers/console_acceleration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions cmd/tls/handlers/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
4 changes: 3 additions & 1 deletion deploy/config/api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion deploy/config/tls.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions docker-compose-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions frontend/src/api/features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export interface Features {
posture: boolean;
service_config: boolean;
accelerated: boolean;
console?: boolean;
file_explorer: boolean;
}

Expand Down
10 changes: 4 additions & 6 deletions frontend/src/features/nodes/NodeConsolePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down Expand Up @@ -302,10 +300,10 @@ export function NodeConsolePanel({ env, uuid }: { env: string; uuid: string }) {
{primingCommand && (
<span
className="inline-flex shrink-0 items-center gap-1 rounded border border-[color:var(--border)] bg-[color:var(--bg-2)] px-2 py-1 text-[10px] leading-none text-[color:var(--text-3)]"
title="Warming accelerated query polling"
title="Warming console metadata"
>
<Loader2 className="h-3 w-3 animate-spin" aria-hidden="true" />
accelerating
warming
</span>
)}
</div>
Expand Down
16 changes: 8 additions & 8 deletions frontend/src/features/nodes/NodeDetailPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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());

Expand All @@ -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: {
Expand All @@ -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());

Expand Down
4 changes: 2 additions & 2 deletions frontend/src/features/nodes/NodeDetailPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -882,7 +882,7 @@ export function NodeDetailPage() {
Tag
</button>
)}
{acceleratedEnabled && canAdminNode && (
{consoleEnabled && canAdminNode && (
<Link
to="/_app/env/$env/nodes/$uuid/console"
params={{ env, uuid }}
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/features/nodes/NodeFileExplorerTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -220,10 +220,10 @@ export function NodeFileExplorerTab({ env, uuid }: { env: string; uuid: string }
{primingRequest && (
<span
className="inline-flex items-center gap-1 rounded border border-[color:var(--border)] bg-[color:var(--bg-2)] px-1.5 py-0.5 text-[10px] leading-none text-[color:var(--text-3)]"
title="Warming accelerated query polling"
title="Warming file explorer metadata"
>
<Loader2 className="h-3 w-3 animate-spin" aria-hidden="true" />
accelerating
warming
</span>
)}
</div>
Expand Down
6 changes: 4 additions & 2 deletions frontend/src/features/service-config/ServiceConfigPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,10 @@ const FIELD_HELP: Record<string, string> = {
'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
Expand Down
7 changes: 7 additions & 0 deletions pkg/config/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -1032,6 +1032,13 @@ func initOsqueryFlags(params *ServiceParameters) []cli.Flag {
Sources: cli.EnvVars("OSQUERY_FILE_EXPLORER"),
Destination: &params.Osquery.FileExplorer,
},
&cli.BoolFlag{
Name: "osquery-console",
Value: false,
Usage: "Enable on-demand per-node console queries",
Sources: cli.EnvVars("OSQUERY_CONSOLE"),
Destination: &params.Osquery.Console,
},
&cli.BoolFlag{
Name: "read-only-configuration",
Value: false,
Expand Down
Loading
Loading