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
63 changes: 63 additions & 0 deletions cmd/tls/handlers/debug_http_filter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package handlers

import (
"testing"

"github.com/jmpsec/osctrl/pkg/config"
"github.com/stretchr/testify/assert"
)

// debugCfg is a shorthand builder for the debug-HTTP config used by the
// filter tests.
func debugCfg(enable bool, target string) *config.YAMLConfigurationDebug {
return &config.YAMLConfigurationDebug{
EnableHTTP: enable,
TargetHostIdentifier: target,
}
}

func TestShouldDebugHTTPDisabled(t *testing.T) {
h := &HandlersTLS{DebugHTTPConfig: debugCfg(false, "")}
assert.False(t, h.shouldDebugHTTP("ABC-123"))
assert.False(t, h.shouldDebugHTTP(""))
// The "dump everything" path is also off when debug is disabled.
assert.False(t, h.debugHTTPAll())
}

func TestShouldDebugHTTPNoFilterDumpsAll(t *testing.T) {
// Legacy behavior: empty target + EnableHTTP → dump every request,
// regardless of the node UUID (including unknown/failed lookups).
h := &HandlersTLS{DebugHTTPConfig: debugCfg(true, "")}
// shouldDebugHTTP intentionally returns false here; the legacy path is
// driven by debugHTTPAll() so that malformed/pre-lookup requests are
// still dumped at the top of the handler.
assert.False(t, h.shouldDebugHTTP("ABC-123"))
assert.True(t, h.debugHTTPAll())
}

func TestShouldDebugHTTPFiltered(t *testing.T) {
h := &HandlersTLS{DebugHTTPConfig: debugCfg(true, "DEADBEEF-1234")}

// Matching UUID (case-insensitive on both sides).
assert.True(t, h.shouldDebugHTTP("DEADBEEF-1234"))
assert.True(t, h.shouldDebugHTTP("deadbeef-1234"))
assert.True(t, h.shouldDebugHTTP("DeadBeef-1234"))

// Non-matching UUID never dumps.
assert.False(t, h.shouldDebugHTTP("CAFE-9999"))

// Unknown node (empty UUID) never matches a set filter — this is what
// excludes invalid node_key / anonymous traffic from the filtered
// dump on a busy server.
assert.False(t, h.shouldDebugHTTP(""))

// Filter set ⇒ the legacy "dump everything" path is suppressed.
assert.False(t, h.debugHTTPAll())
}

func TestShouldDebugHTTPNilConfig(t *testing.T) {
// Defensive: a handler with no debug config wired must never dump.
h := &HandlersTLS{}
assert.False(t, h.shouldDebugHTTP("DEADBEEF-1234"))
assert.False(t, h.debugHTTPAll())
}
38 changes: 38 additions & 0 deletions cmd/tls/handlers/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package handlers
import (
"net/http"
"strconv"
"strings"
"time"

"github.com/jmpsec/osctrl/pkg/auditlog"
Expand Down Expand Up @@ -215,6 +216,43 @@ func CreateHandlersTLS(opts ...Option) *HandlersTLS {
return h
}

// shouldDebugHTTP reports whether a request whose originating node UUID is
// `uuid` should be dumped to the debug HTTP log. It encodes the per-host
// filter introduced to make debug HTTP usable on busy servers:
//
// - EnableHTTP off → never dump.
// - TargetHostIdentifier empty → this returns false; the legacy "dump
// everything" path is handled inline at the top of each handler
// (gated on `TargetHostIdentifier == ""`), so non-empty-filter mode
// stays byte-for-byte identical to the previous behavior.
// - TargetHostIdentifier set → dump only when uuid matches, case-
// insensitively. A zero uuid (failed node lookup / invalid key) never
// matches, so anonymous or malformed traffic is excluded from the
// filtered dump — which is the intent.
//
// Node UUIDs are stored uppercase and the osquery host_identifier may
// arrive in any case, so EqualFold is used on both sides.
func (h *HandlersTLS) shouldDebugHTTP(uuid string) bool {
if h.DebugHTTPConfig == nil || !h.DebugHTTPConfig.EnableHTTP {
return false
}
if h.DebugHTTPConfig.TargetHostIdentifier == "" {
return false
}
return uuid != "" && strings.EqualFold(uuid, h.DebugHTTPConfig.TargetHostIdentifier)
}

// debugHTTPAll reports whether the legacy "dump every request" path
// should run at the top of a handler. It is true only when HTTP debug is
// enabled and no per-host filter is configured; in that mode behavior is
// identical to the previous implementation (full dump before the body is
// parsed, including malformed bodies). When a filter is set this returns
// false and the handler instead dumps only matching requests after the
// node has been identified, via shouldDebugHTTP + DebugHTTPDumpWithBody.
func (h *HandlersTLS) debugHTTPAll() bool {
return h.DebugHTTPConfig != nil && h.DebugHTTPConfig.EnableHTTP && h.DebugHTTPConfig.TargetHostIdentifier == ""
}

func (h *HandlersTLS) PrometheusMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
Expand Down
99 changes: 78 additions & 21 deletions cmd/tls/handlers/post.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ func (h *HandlersTLS) EnrollHandler(w http.ResponseWriter, r *http.Request) {
return
}
// Debug HTTP for environment
if h.DebugHTTPConfig.EnableHTTP {
if h.debugHTTPAll() {
utils.DebugHTTPDump(h.DebugHTTP, r, h.DebugHTTPConfig.ShowBody)
}
// Decode read POST body
Expand All @@ -121,6 +121,13 @@ func (h *HandlersTLS) EnrollHandler(w http.ResponseWriter, r *http.Request) {
utils.HTTPResponse(w, "", http.StatusInternalServerError, []byte(""))
return
}
// Per-host filtered HTTP debug dump. In legacy (no-filter) mode the
// top-of-handler dump already covered this request. For enroll the
// host identifier is the osquery host_identifier; it becomes the
// node UUID (uppercased) once the node is created.
if h.shouldDebugHTTP(t.HostIdentifier) {
utils.DebugHTTPDumpWithBody(h.DebugHTTP, r, body, h.DebugHTTPConfig.ShowBody)
}
// Check if received secret is valid
var nodeKey string
var newNode nodes.OsqueryNode
Expand Down Expand Up @@ -190,7 +197,7 @@ func (h *HandlersTLS) ConfigHandler(w http.ResponseWriter, r *http.Request) {
return
}
// Debug HTTP for environment
if h.DebugHTTPConfig.EnableHTTP {
if h.debugHTTPAll() {
utils.DebugHTTPDump(h.DebugHTTP, r, h.DebugHTTPConfig.ShowBody)
}
// Decode read POST body
Expand All @@ -209,7 +216,17 @@ func (h *HandlersTLS) ConfigHandler(w http.ResponseWriter, r *http.Request) {
return
}
// We need to update the node info in another go routine
if node, err := h.Nodes.GetByKey(t.NodeKey); err == nil {
node, nodeErr := h.Nodes.GetByKey(t.NodeKey)
matchedUUID := ""
if nodeErr == nil {
matchedUUID = node.UUID
}
// Per-host filtered HTTP debug dump. Legacy (no-filter) mode already
// dumped this request at the top of the handler.
if h.shouldDebugHTTP(matchedUUID) {
utils.DebugHTTPDumpWithBody(h.DebugHTTP, r, body, h.DebugHTTPConfig.ShowBody)
}
if nodeErr == nil {
// Check if node belongs to the environment
if node.EnvironmentID != env.ID {
log.Warn().Msgf("node UUID: %s in %s environment does not belong to the environment", node.UUID, env.Name)
Expand Down Expand Up @@ -282,7 +299,7 @@ func (h *HandlersTLS) LogHandler(w http.ResponseWriter, r *http.Request) {
}()
}
// Debug HTTP here so the body will be uncompressed
if h.DebugHTTPConfig.EnableHTTP {
if h.debugHTTPAll() {
utils.DebugHTTPDump(h.DebugHTTP, r, h.DebugHTTPConfig.ShowBody)
}
// Extract POST body and decode JSON
Expand Down Expand Up @@ -310,7 +327,17 @@ func (h *HandlersTLS) LogHandler(w http.ResponseWriter, r *http.Request) {
var nodeInvalid bool
var response types.LogResponse
// Check if provided node_key is valid and if so, update node
if node, err := h.Nodes.GetByKey(t.NodeKey); err == nil {
node, nodeErr := h.Nodes.GetByKey(t.NodeKey)
matchedUUID := ""
if nodeErr == nil {
matchedUUID = node.UUID
}
// Per-host filtered HTTP debug dump. Legacy (no-filter) mode already
// dumped this request at the top of the handler.
if h.shouldDebugHTTP(matchedUUID) {
utils.DebugHTTPDumpWithBody(h.DebugHTTP, r, body, h.DebugHTTPConfig.ShowBody)
}
if nodeErr == nil {
// Check if node belongs to the environment
if node.EnvironmentID != env.ID {
log.Warn().Msgf("node UUID: %s in %s environment does not belong to the environment", node.UUID, env.Name)
Expand Down Expand Up @@ -367,7 +394,7 @@ func (h *HandlersTLS) QueryReadHandler(w http.ResponseWriter, r *http.Request) {
return
}
// Debug HTTP
if h.DebugHTTPConfig.EnableHTTP {
if h.debugHTTPAll() {
utils.DebugHTTPDump(h.DebugHTTP, r, h.DebugHTTPConfig.ShowBody)
}
// Decode read POST body
Expand All @@ -389,7 +416,17 @@ func (h *HandlersTLS) QueryReadHandler(w http.ResponseWriter, r *http.Request) {
var response interface{}
qs := make(queries.QueryReadQueries)
// Check if provided node_key is valid and if so, update node
if node, err := h.Nodes.GetByKey(t.NodeKey); err == nil {
node, nodeErr := h.Nodes.GetByKey(t.NodeKey)
matchedUUID := ""
if nodeErr == nil {
matchedUUID = node.UUID
}
// Per-host filtered HTTP debug dump. Legacy (no-filter) mode already
// dumped this request at the top of the handler.
if h.shouldDebugHTTP(matchedUUID) {
utils.DebugHTTPDumpWithBody(h.DebugHTTP, r, body, h.DebugHTTPConfig.ShowBody)
}
if nodeErr == nil {
// Check if node belongs to the environment
if node.EnvironmentID != env.ID {
log.Warn().Msgf("node UUID: %s in %s environment does not belong to the environment", node.UUID, env.Name)
Expand All @@ -416,7 +453,7 @@ func (h *HandlersTLS) QueryReadHandler(w http.ResponseWriter, r *http.Request) {
log.Debug().Msgf("node-uuid: %s with nodeid %d added to batch writer for query read update", node.UUID, node.ID)
h.recordActivity(env.UUID, node.UUID, activity.EventQueryRead)
} else {
log.Err(err).Msg("GetByKey")
log.Err(nodeErr).Msg("GetByKey")
nodeInvalid = true
accelerate = false
}
Expand Down Expand Up @@ -456,7 +493,7 @@ func (h *HandlersTLS) QueryWriteHandler(w http.ResponseWriter, r *http.Request)
return
}
// Debug HTTP
if h.DebugHTTPConfig.EnableHTTP {
if h.debugHTTPAll() {
utils.DebugHTTPDump(h.DebugHTTP, r, h.DebugHTTPConfig.ShowBody)
}
// Decode read POST body
Expand All @@ -477,7 +514,17 @@ func (h *HandlersTLS) QueryWriteHandler(w http.ResponseWriter, r *http.Request)
var nodeInvalid bool
var response types.QueryWriteResponse
// Check if provided node_key is valid and if so, update node
if node, err := h.Nodes.GetByKey(t.NodeKey); err == nil {
node, nodeErr := h.Nodes.GetByKey(t.NodeKey)
matchedUUID := ""
if nodeErr == nil {
matchedUUID = node.UUID
}
// Per-host filtered HTTP debug dump. Legacy (no-filter) mode already
// dumped this request at the top of the handler.
if h.shouldDebugHTTP(matchedUUID) {
utils.DebugHTTPDumpWithBody(h.DebugHTTP, r, body, h.DebugHTTPConfig.ShowBody)
}
if nodeErr == nil {
// Check if node belongs to the environment
if node.EnvironmentID != env.ID {
log.Warn().Msgf("node UUID: %s in %s environment does not belong to the environment", node.UUID, env.Name)
Expand Down Expand Up @@ -551,7 +598,7 @@ func (h *HandlersTLS) QuickEnrollHandler(w http.ResponseWriter, r *http.Request)
return
}
// Debug HTTP
if h.DebugHTTPConfig.EnableHTTP {
if h.debugHTTPAll() {
utils.DebugHTTPDump(h.DebugHTTP, r, h.DebugHTTPConfig.ShowBody)
}
// Retrieve type of script
Expand Down Expand Up @@ -624,7 +671,7 @@ func (h *HandlersTLS) QuickRemoveHandler(w http.ResponseWriter, r *http.Request)
return
}
// Debug HTTP
if h.DebugHTTPConfig.EnableHTTP {
if h.debugHTTPAll() {
utils.DebugHTTPDump(h.DebugHTTP, r, h.DebugHTTPConfig.ShowBody)
}
// Retrieve type of script
Expand Down Expand Up @@ -699,7 +746,7 @@ func (h *HandlersTLS) CarveInitHandler(w http.ResponseWriter, r *http.Request) {
return
}
// Debug HTTP
if h.DebugHTTPConfig.EnableHTTP {
if h.debugHTTPAll() {
utils.DebugHTTPDump(h.DebugHTTP, r, h.DebugHTTPConfig.ShowBody)
}
// Decode read POST body
Expand All @@ -721,7 +768,17 @@ func (h *HandlersTLS) CarveInitHandler(w http.ResponseWriter, r *http.Request) {
var carveSessionID string
var response types.CarveInitResponse
// Check if provided node_key is valid and if so, update node
if node, err := h.Nodes.GetByKey(t.NodeKey); err == nil {
node, nodeErr := h.Nodes.GetByKey(t.NodeKey)
matchedUUID := ""
if nodeErr == nil {
matchedUUID = node.UUID
}
// Per-host filtered HTTP debug dump. Legacy (no-filter) mode already
// dumped this request at the top of the handler.
if h.shouldDebugHTTP(matchedUUID) {
utils.DebugHTTPDumpWithBody(h.DebugHTTP, r, body, h.DebugHTTPConfig.ShowBody)
}
if nodeErr == nil {
// Check if node belongs to the environment
if node.EnvironmentID != env.ID {
log.Warn().Msgf("node UUID: %s in %s environment does not belong to the environment", node.UUID, env.Name)
Expand Down Expand Up @@ -779,7 +836,7 @@ func (h *HandlersTLS) CarveBlockHandler(w http.ResponseWriter, r *http.Request)
return
}
// Debug HTTP
if h.DebugHTTPConfig.EnableHTTP {
if h.debugHTTPAll() {
utils.DebugHTTPDump(h.DebugHTTP, r, h.DebugHTTPConfig.ShowBody)
}
// Decode read POST body
Expand Down Expand Up @@ -846,7 +903,7 @@ func (h *HandlersTLS) FlagsHandler(w http.ResponseWriter, r *http.Request) {
return
}
// Debug HTTP if enabled
if h.DebugHTTPConfig.EnableHTTP {
if h.debugHTTPAll() {
utils.DebugHTTPDump(h.DebugHTTP, r, h.DebugHTTPConfig.ShowBody)
}
// Decode read POST body
Expand Down Expand Up @@ -907,7 +964,7 @@ func (h *HandlersTLS) CertHandler(w http.ResponseWriter, r *http.Request) {
return
}
// Debug HTTP if enabled
if h.DebugHTTPConfig.EnableHTTP {
if h.debugHTTPAll() {
utils.DebugHTTPDump(h.DebugHTTP, r, h.DebugHTTPConfig.ShowBody)
}
// Decode read POST body
Expand Down Expand Up @@ -962,7 +1019,7 @@ func (h *HandlersTLS) VerifyHandler(w http.ResponseWriter, r *http.Request) {
return
}
// Debug HTTP if enabled
if h.DebugHTTPConfig.EnableHTTP {
if h.debugHTTPAll() {
utils.DebugHTTPDump(h.DebugHTTP, r, h.DebugHTTPConfig.ShowBody)
}
// Decode read POST body
Expand Down Expand Up @@ -1054,7 +1111,7 @@ func (h *HandlersTLS) ScriptHandler(w http.ResponseWriter, r *http.Request) {
actionVar += environments.PowershellTarget
}
// Debug HTTP if enabled
if h.DebugHTTPConfig.EnableHTTP {
if h.debugHTTPAll() {
utils.DebugHTTPDump(h.DebugHTTP, r, h.DebugHTTPConfig.ShowBody)
}
// Decode read POST body
Expand Down Expand Up @@ -1114,7 +1171,7 @@ func (h *HandlersTLS) EnrollPackageHandler(w http.ResponseWriter, r *http.Reques
return
}
// Debug HTTP if enabled
if h.DebugHTTPConfig.EnableHTTP {
if h.debugHTTPAll() {
utils.DebugHTTPDump(h.DebugHTTP, r, h.DebugHTTPConfig.ShowBody)
}
// Retrieve package
Expand Down Expand Up @@ -1245,7 +1302,7 @@ func (h *HandlersTLS) OsqueryConfigEndpointHandler(w http.ResponseWriter, r *htt
return
}
// Debug HTTP
if h.DebugHTTPConfig.EnableHTTP {
if h.debugHTTPAll() {
utils.DebugHTTPDump(h.DebugHTTP, r, h.DebugHTTPConfig.ShowBody)
}
// Decode read POST body. Even though we cap the *decompressed*
Expand Down
5 changes: 5 additions & 0 deletions deploy/config/tls.yml
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,8 @@ debug:
enableHttp: false
httpFile: ./debug-http-tls.log
showBody: false
# When non-empty, only dump requests from the osquery node whose UUID
# (or enroll host_identifier) matches this value (case-insensitive).
# Empty dumps every request when enableHttp is true. Useful to isolate
# one host's traffic on a busy server.
hostIdentifier: ""
7 changes: 7 additions & 0 deletions pkg/config/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -959,6 +959,13 @@ func initDebugFlags(params *ServiceParameters, service string) []cli.Flag {
Sources: cli.EnvVars("HTTP_DEBUG_SHOW_BODY"),
Destination: &params.Debug.ShowBody,
},
&cli.StringFlag{
Name: "http-debug-host",
Value: "",
Usage: "Only dump HTTP requests from the osquery node with this UUID / host_identifier (case-insensitive). Empty dumps everything when --enable-http-debug is set.",
Sources: cli.EnvVars("HTTP_DEBUG_HOST"),
Destination: &params.Debug.TargetHostIdentifier,
},
}
}

Expand Down
Loading
Loading