[fix-no-Query-field] fleet-mcp: expose policy SQL in get_policies response (#45513)

## Summary

The `get_policies` MCP tool wasn't returning each policy's osquery SQL,
making it impossible to diagnose policy behavior via MCP — you had to
fall back to `curl` against `GET /api/v1/fleet/global/policies/{id}` to
read the `query` field.

Root cause: the `Policy` struct in
`tools/fleet-mcp/fleet_integration.go` didn't declare a `Query` field,
so Go's JSON decoder silently dropped the key from the Fleet API
response. One-line fix on the struct; the same struct backs both the
global path and the per-team fan-out, so both are covered.

  Purely additive — no existing field changes shape, name, or type.

  # Checklist for submitter
  ## Testing
  - [x] Added/updated automated tests
  - [x] QA'd all new/changed functionality manually


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Fleet policies now include a query field so the actual policy
query/SQL content is available with policy metadata.

* **Tests**
* Added integration-style test coverage to confirm the query field is
parsed and returned (including empty queries) and that policy responses
contain the expected entries.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/fleetdm/fleet/pull/45513)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: nulmete <nicoulmete1@gmail.com>
This commit is contained in:
Joaquín
2026-07-03 09:55:37 -03:00
committed by GitHub
co-authored by nulmete
parent dbc14b248b
commit 8c544ea826
2 changed files with 34 additions and 0 deletions
+1
View File
@@ -216,6 +216,7 @@ type Policy struct {
ID uint `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Query string `json:"query"`
Platform string `json:"platform"`
PassingHostCount int `json:"passing_host_count"`
FailingHostCount int `json:"failing_host_count"`
+33
View File
@@ -1070,3 +1070,36 @@ func TestGetHostSoftware_SourceFilterAndPerPage(t *testing.T) {
}
}
}
func TestGetPolicies_IncludesQueryField(t *testing.T) {
const wantSQL = "SELECT 1;"
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/v1/fleet/global/policies":
_, _ = w.Write([]byte(`{"policies":[
{"id":1,"name":"with sql","query":"` + wantSQL + `"},
{"id":2,"name":"empty sql","query":""}
]}`))
case "/api/v1/fleet/teams":
_, _ = w.Write([]byte(`{"teams":[]}`))
default:
http.Error(w, "unexpected path "+r.URL.Path, http.StatusNotFound)
}
}))
defer srv.Close()
fc := newTestClient(srv.URL)
policies, err := fc.GetPolicies(context.Background())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(policies) != 2 {
t.Fatalf("expected 2 policies, got %d", len(policies))
}
if policies[0].Query != wantSQL {
t.Errorf("policy 1 Query = %q, want %q", policies[0].Query, wantSQL)
}
if policies[1].Query != "" {
t.Errorf("policy 2 Query = %q, want empty string", policies[1].Query)
}
}