From 632632c4553e251f8ac38bec018b1b8e3c78a4e2 Mon Sep 17 00:00:00 2001 From: TerrifiedBug Date: Thu, 5 Mar 2026 09:18:03 +0000 Subject: [PATCH 1/9] feat(agent): add debug logging to HTTP client --- agent/internal/client/client.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/agent/internal/client/client.go b/agent/internal/client/client.go index 9707588..5e24428 100644 --- a/agent/internal/client/client.go +++ b/agent/internal/client/client.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "io" + "log/slog" "net/http" "time" ) @@ -28,6 +29,14 @@ func (c *Client) SetNodeToken(token string) { c.nodeToken = token } +// tokenPrefix returns the first N chars of a token for safe debug logging. +func tokenPrefix(token string, n int) string { + if len(token) <= n { + return token + } + return token[:n] + "..." +} + // EnrollRequest is sent to POST /api/agent/enroll type EnrollRequest struct { Token string `json:"token"` @@ -57,14 +66,17 @@ func (c *Client) Enroll(req EnrollRequest) (*EnrollResponse, error) { } httpReq.Header.Set("Content-Type", "application/json") + slog.Debug("http request", "method", "POST", "url", c.baseURL+"/api/agent/enroll") resp, err := c.httpClient.Do(httpReq) if err != nil { + slog.Debug("http error", "method", "POST", "url", "/api/agent/enroll", "error", err) return nil, fmt.Errorf("enroll request failed: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { respBody, _ := io.ReadAll(resp.Body) + slog.Debug("http response", "method", "POST", "url", "/api/agent/enroll", "status", resp.StatusCode, "body", string(respBody)) return nil, fmt.Errorf("enroll failed (status %d): %s", resp.StatusCode, string(respBody)) } @@ -72,6 +84,7 @@ func (c *Client) Enroll(req EnrollRequest) (*EnrollResponse, error) { if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, fmt.Errorf("decode enroll response: %w", err) } + slog.Debug("http response", "method", "POST", "url", "/api/agent/enroll", "status", 200) return &result, nil } @@ -118,14 +131,17 @@ func (c *Client) GetConfig() (*ConfigResponse, error) { } httpReq.Header.Set("Authorization", "Bearer "+c.nodeToken) + slog.Debug("http request", "method", "GET", "url", c.baseURL+"/api/agent/config", "tokenPrefix", tokenPrefix(c.nodeToken, 16)) resp, err := c.httpClient.Do(httpReq) if err != nil { + slog.Debug("http error", "method", "GET", "url", "/api/agent/config", "error", err) return nil, fmt.Errorf("config request failed: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { respBody, _ := io.ReadAll(resp.Body) + slog.Debug("http response", "method", "GET", "url", "/api/agent/config", "status", resp.StatusCode, "body", string(respBody)) return nil, fmt.Errorf("config request failed (status %d): %s", resp.StatusCode, string(respBody)) } @@ -133,6 +149,7 @@ func (c *Client) GetConfig() (*ConfigResponse, error) { if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, fmt.Errorf("decode config response: %w", err) } + slog.Debug("http response", "method", "GET", "url", "/api/agent/config", "status", 200, "pipelines", len(result.Pipelines)) return &result, nil } @@ -231,14 +248,17 @@ func (c *Client) SendHeartbeat(req HeartbeatRequest) error { httpReq.Header.Set("Content-Type", "application/json") httpReq.Header.Set("Authorization", "Bearer "+c.nodeToken) + slog.Debug("http request", "method", "POST", "url", c.baseURL+"/api/agent/heartbeat") resp, err := c.httpClient.Do(httpReq) if err != nil { + slog.Debug("http error", "method", "POST", "url", "/api/agent/heartbeat", "error", err) return fmt.Errorf("heartbeat request failed: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { respBody, _ := io.ReadAll(resp.Body) + slog.Debug("http response", "method", "POST", "url", "/api/agent/heartbeat", "status", resp.StatusCode, "body", string(respBody)) return fmt.Errorf("heartbeat failed (status %d): %s", resp.StatusCode, string(respBody)) } return nil From 174c5eaa1c7ce6c38b6c6510a9bbda2ebc55c778 Mon Sep 17 00:00:00 2001 From: TerrifiedBug Date: Thu, 5 Mar 2026 09:19:48 +0000 Subject: [PATCH 2/9] feat(agent): add debug logging to enrollment flow --- agent/internal/agent/enrollment.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/agent/internal/agent/enrollment.go b/agent/internal/agent/enrollment.go index 84364d4..835da40 100644 --- a/agent/internal/agent/enrollment.go +++ b/agent/internal/agent/enrollment.go @@ -2,6 +2,7 @@ package agent import ( "fmt" + "log/slog" "os" "os/exec" "path/filepath" @@ -23,10 +24,13 @@ func loadOrEnroll(cfg *config.Config, c *client.Client) (string, error) { if err == nil { token := strings.TrimSpace(string(data)) if token != "" { + slog.Debug("loaded existing node token", "path", tokenPath) return token, nil } } + slog.Debug("no existing node token", "path", tokenPath) + // Need to enroll if cfg.Token == "" { return "", fmt.Errorf("no node token found at %s and VF_TOKEN is not set — cannot enroll", tokenPath) @@ -35,6 +39,8 @@ func loadOrEnroll(cfg *config.Config, c *client.Client) (string, error) { hostname, _ := os.Hostname() vectorVersion := detectVectorVersion(cfg.VectorBin) + slog.Debug("enrolling", "hostname", hostname, "os", runtime.GOOS+"/"+runtime.GOARCH, "tokenPrefix", cfg.Token[:min(len(cfg.Token), 24)]+"...") + resp, err := c.Enroll(client.EnrollRequest{ Token: cfg.Token, Hostname: hostname, @@ -55,6 +61,7 @@ func loadOrEnroll(cfg *config.Config, c *client.Client) (string, error) { } fmt.Printf("Enrolled as node %s in environment %q\n", resp.NodeID, resp.EnvironmentName) + slog.Info("enrolled successfully", "nodeId", resp.NodeID, "environment", resp.EnvironmentName) return resp.NodeToken, nil } From 0e8872c820e92be486bf8def5971855959ad1337 Mon Sep 17 00:00:00 2001 From: TerrifiedBug Date: Thu, 5 Mar 2026 09:20:58 +0000 Subject: [PATCH 3/9] feat(agent): add debug logging to poll and heartbeat --- agent/internal/agent/agent.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/agent/internal/agent/agent.go b/agent/internal/agent/agent.go index d67d1d4..f79c544 100644 --- a/agent/internal/agent/agent.go +++ b/agent/internal/agent/agent.go @@ -104,6 +104,8 @@ func (a *Agent) pollAndApply() { return } + slog.Debug("poll complete", "actions", len(actions)) + for _, action := range actions { switch action.Action { case ActionStart: @@ -170,6 +172,8 @@ func (a *Agent) sendHeartbeat() { a.sampleResults = append(results, a.sampleResults...) a.mu.Unlock() } + } else { + slog.Debug("heartbeat sent", "pipelines", len(hb.Pipelines), "sampleResults", len(results)) } } From f85defd18514d820b9aa3886f1b2fec85a78ca6b Mon Sep 17 00:00:00 2001 From: TerrifiedBug Date: Thu, 5 Mar 2026 09:24:27 +0000 Subject: [PATCH 4/9] feat(server): add audit logging to enrollment endpoint --- src/app/api/agent/enroll/route.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/app/api/agent/enroll/route.ts b/src/app/api/agent/enroll/route.ts index d7f1e69..52462c0 100644 --- a/src/app/api/agent/enroll/route.ts +++ b/src/app/api/agent/enroll/route.ts @@ -16,6 +16,7 @@ export async function POST(request: Request) { const body = await request.json(); const parsed = enrollSchema.safeParse(body); if (!parsed.success) { + console.error("[enroll] invalid input:", parsed.error.flatten().fieldErrors); return NextResponse.json( { error: "Invalid input", details: parsed.error.flatten().fieldErrors }, { status: 400 }, @@ -23,6 +24,8 @@ export async function POST(request: Request) { } const { token, hostname, os, agentVersion, vectorVersion } = parsed.data; + const tokenPrefix = token.slice(0, 24) + "..."; + console.log(`[enroll] attempt from hostname="${hostname}" tokenPrefix="${tokenPrefix}" agentVersion="${agentVersion ?? "unknown"}"`); // Find all environments that have an enrollment token const environments = await prisma.environment.findMany({ @@ -37,6 +40,7 @@ export async function POST(request: Request) { team: { select: { id: true } }, }, }); + console.log(`[enroll] found ${environments.length} candidate environment(s)`); // Try each environment's enrollment token let matchedEnv: (typeof environments)[0] | null = null; @@ -44,10 +48,13 @@ export async function POST(request: Request) { if (env.enrollmentTokenHash && await verifyEnrollmentToken(token, env.enrollmentTokenHash)) { matchedEnv = env; break; + } else { + console.log(`[enroll] env "${env.name}" -- token mismatch`); } } if (!matchedEnv) { + console.error(`[enroll] REJECTED -- no matching environment (checked ${environments.length})`); return NextResponse.json( { error: "Invalid enrollment token" }, { status: 401 }, @@ -73,6 +80,7 @@ export async function POST(request: Request) { metadata: { enrolledVia: "agent" }, }, }); + console.log(`[enroll] SUCCESS -- node ${node.id} enrolled in "${matchedEnv.name}"`); return NextResponse.json({ nodeId: node.id, @@ -81,7 +89,7 @@ export async function POST(request: Request) { environmentName: matchedEnv.name, }); } catch (error) { - console.error("Agent enrollment error:", error); + console.error("[enroll] unexpected error:", error); return NextResponse.json( { error: "Enrollment failed" }, { status: 500 }, From 756db548f56c8dd81ae21866063d352e6c5c6a8b Mon Sep 17 00:00:00 2001 From: TerrifiedBug Date: Thu, 5 Mar 2026 09:39:08 +0000 Subject: [PATCH 5/9] fix: address greptile review findings - Remove node token prefix from GetConfig debug log (security) - Fix enrollment token prefix guard for short tokens - Remove per-environment name logging on mismatch (info leak) --- agent/internal/agent/enrollment.go | 6 +++++- agent/internal/client/client.go | 2 +- src/app/api/agent/enroll/route.ts | 2 -- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/agent/internal/agent/enrollment.go b/agent/internal/agent/enrollment.go index 835da40..b4b328f 100644 --- a/agent/internal/agent/enrollment.go +++ b/agent/internal/agent/enrollment.go @@ -39,7 +39,11 @@ func loadOrEnroll(cfg *config.Config, c *client.Client) (string, error) { hostname, _ := os.Hostname() vectorVersion := detectVectorVersion(cfg.VectorBin) - slog.Debug("enrolling", "hostname", hostname, "os", runtime.GOOS+"/"+runtime.GOARCH, "tokenPrefix", cfg.Token[:min(len(cfg.Token), 24)]+"...") + prefix := cfg.Token + if len(cfg.Token) > 24 { + prefix = cfg.Token[:24] + "..." + } + slog.Debug("enrolling", "hostname", hostname, "os", runtime.GOOS+"/"+runtime.GOARCH, "tokenPrefix", prefix) resp, err := c.Enroll(client.EnrollRequest{ Token: cfg.Token, diff --git a/agent/internal/client/client.go b/agent/internal/client/client.go index 5e24428..43dde10 100644 --- a/agent/internal/client/client.go +++ b/agent/internal/client/client.go @@ -131,7 +131,7 @@ func (c *Client) GetConfig() (*ConfigResponse, error) { } httpReq.Header.Set("Authorization", "Bearer "+c.nodeToken) - slog.Debug("http request", "method", "GET", "url", c.baseURL+"/api/agent/config", "tokenPrefix", tokenPrefix(c.nodeToken, 16)) + slog.Debug("http request", "method", "GET", "url", c.baseURL+"/api/agent/config") resp, err := c.httpClient.Do(httpReq) if err != nil { slog.Debug("http error", "method", "GET", "url", "/api/agent/config", "error", err) diff --git a/src/app/api/agent/enroll/route.ts b/src/app/api/agent/enroll/route.ts index 52462c0..081e674 100644 --- a/src/app/api/agent/enroll/route.ts +++ b/src/app/api/agent/enroll/route.ts @@ -48,8 +48,6 @@ export async function POST(request: Request) { if (env.enrollmentTokenHash && await verifyEnrollmentToken(token, env.enrollmentTokenHash)) { matchedEnv = env; break; - } else { - console.log(`[enroll] env "${env.name}" -- token mismatch`); } } From dfbb45c1794a9f74b587ad4d6ba9eded44b7cd99 Mon Sep 17 00:00:00 2001 From: TerrifiedBug Date: Thu, 5 Mar 2026 09:50:43 +0000 Subject: [PATCH 6/9] fix: address greptile review findings Remove dead tokenPrefix helper from client.go and strip enrollment token prefix from always-on production log in enrollment endpoint. --- agent/internal/client/client.go | 8 -------- src/app/api/agent/enroll/route.ts | 3 +-- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/agent/internal/client/client.go b/agent/internal/client/client.go index 43dde10..a922bff 100644 --- a/agent/internal/client/client.go +++ b/agent/internal/client/client.go @@ -29,14 +29,6 @@ func (c *Client) SetNodeToken(token string) { c.nodeToken = token } -// tokenPrefix returns the first N chars of a token for safe debug logging. -func tokenPrefix(token string, n int) string { - if len(token) <= n { - return token - } - return token[:n] + "..." -} - // EnrollRequest is sent to POST /api/agent/enroll type EnrollRequest struct { Token string `json:"token"` diff --git a/src/app/api/agent/enroll/route.ts b/src/app/api/agent/enroll/route.ts index 081e674..4edc1fb 100644 --- a/src/app/api/agent/enroll/route.ts +++ b/src/app/api/agent/enroll/route.ts @@ -24,8 +24,7 @@ export async function POST(request: Request) { } const { token, hostname, os, agentVersion, vectorVersion } = parsed.data; - const tokenPrefix = token.slice(0, 24) + "..."; - console.log(`[enroll] attempt from hostname="${hostname}" tokenPrefix="${tokenPrefix}" agentVersion="${agentVersion ?? "unknown"}"`); + console.log(`[enroll] attempt from hostname="${hostname}" agentVersion="${agentVersion ?? "unknown"}"`); // Find all environments that have an enrollment token const environments = await prisma.environment.findMany({ From 620e7aa08483d5ac4f8fcea66f236df2ebc6dba6 Mon Sep 17 00:00:00 2001 From: TerrifiedBug Date: Thu, 5 Mar 2026 09:57:06 +0000 Subject: [PATCH 7/9] fix: sanitize user-controlled fields in enrollment logs Strip newlines and tabs from hostname and agentVersion before interpolating into log messages to prevent log injection attacks on the unauthenticated enrollment endpoint. --- src/app/api/agent/enroll/route.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/api/agent/enroll/route.ts b/src/app/api/agent/enroll/route.ts index 4edc1fb..9dde62f 100644 --- a/src/app/api/agent/enroll/route.ts +++ b/src/app/api/agent/enroll/route.ts @@ -24,7 +24,9 @@ export async function POST(request: Request) { } const { token, hostname, os, agentVersion, vectorVersion } = parsed.data; - console.log(`[enroll] attempt from hostname="${hostname}" agentVersion="${agentVersion ?? "unknown"}"`); + const safeHostname = hostname.replace(/[\r\n\t]/g, " "); + const safeVersion = (agentVersion ?? "unknown").replace(/[\r\n\t]/g, " "); + console.log(`[enroll] attempt from hostname="${safeHostname}" agentVersion="${safeVersion}"`); // Find all environments that have an enrollment token const environments = await prisma.environment.findMany({ From 0bb83a4f2feac5599869dd9f1fd8e1dd668ef831 Mon Sep 17 00:00:00 2001 From: TerrifiedBug Date: Thu, 5 Mar 2026 10:44:21 +0000 Subject: [PATCH 8/9] fix: escape double quotes in enrollment log sanitization Add " to the control character strip regex to prevent intra-line field forgery via quote injection in the unauthenticated enrollment endpoint. --- src/app/api/agent/enroll/route.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/api/agent/enroll/route.ts b/src/app/api/agent/enroll/route.ts index 9dde62f..d2e2ab9 100644 --- a/src/app/api/agent/enroll/route.ts +++ b/src/app/api/agent/enroll/route.ts @@ -24,8 +24,8 @@ export async function POST(request: Request) { } const { token, hostname, os, agentVersion, vectorVersion } = parsed.data; - const safeHostname = hostname.replace(/[\r\n\t]/g, " "); - const safeVersion = (agentVersion ?? "unknown").replace(/[\r\n\t]/g, " "); + const safeHostname = hostname.replace(/[\r\n\t"]/g, " "); + const safeVersion = (agentVersion ?? "unknown").replace(/[\r\n\t"]/g, " "); console.log(`[enroll] attempt from hostname="${safeHostname}" agentVersion="${safeVersion}"`); // Find all environments that have an enrollment token From b61070c2ce0502e3e0efd21c33eb3f95e26e1156 Mon Sep 17 00:00:00 2001 From: TerrifiedBug Date: Thu, 5 Mar 2026 10:53:18 +0000 Subject: [PATCH 9/9] fix: remove duplicate enrollment success output Remove fmt.Printf that duplicated the slog.Info enrollment success message, causing two identical lines on stdout. --- agent/internal/agent/enrollment.go | 1 - 1 file changed, 1 deletion(-) diff --git a/agent/internal/agent/enrollment.go b/agent/internal/agent/enrollment.go index b4b328f..72dfb28 100644 --- a/agent/internal/agent/enrollment.go +++ b/agent/internal/agent/enrollment.go @@ -64,7 +64,6 @@ func loadOrEnroll(cfg *config.Config, c *client.Client) (string, error) { return "", fmt.Errorf("persist node token: %w", err) } - fmt.Printf("Enrolled as node %s in environment %q\n", resp.NodeID, resp.EnvironmentName) slog.Info("enrolled successfully", "nodeId", resp.NodeID, "environment", resp.EnvironmentName) return resp.NodeToken, nil }