Reset query report when platform/min_osquery_version is changed (#17847)

#17018

- [X] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#changes-files)
for more information.
- [X] Added/updated tests
- [X] Manual QA for all new/changed functionality

---------

Co-authored-by: RachelElysia <rachel@fleetdm.com>
Co-authored-by: RachelElysia <71795832+RachelElysia@users.noreply.github.com>
This commit is contained in:
Lucas Manuel Rodriguez 2024-03-29 12:17:52 -03:00 committed by GitHub
parent bb0d031ea8
commit 1833e1fc5b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 247 additions and 4 deletions

View File

@ -0,0 +1 @@
- Query report is reset when there is a change to the selected platform or selected minimum osquery version

View File

@ -638,12 +638,32 @@ const EditQueryForm = ({
"differential_ignore_removals",
].includes(lastEditedQueryLoggingType);
// Note: The backend is not resetting the query reports with equivalent platform strings
// so we are not showing a warning unless the platform combinations differ
const formatPlatformEquivalences = (platforms?: string) => {
// Remove white spaces allowed by API and format into a sorted string converted from a sorted array
return platforms?.replace(/\s/g, "").split(",").sort().toString();
};
const changedPlatforms =
storedQuery &&
formatPlatformEquivalences(lastEditedQueryPlatforms) !==
formatPlatformEquivalences(storedQuery?.platform);
const changedMinOsqueryVersion =
storedQuery &&
lastEditedQueryMinOsqueryVersion !== storedQuery.min_osquery_version;
const enabledDiscardData =
storedQuery && lastEditedQueryDiscardData && !storedQuery.discard_data;
const confirmChanges =
currentlySavingQueryResults &&
(changedSQL || changedLoggingToDifferential || enabledDiscardData);
(changedSQL ||
changedLoggingToDifferential ||
enabledDiscardData ||
changedPlatforms ||
changedMinOsqueryVersion);
const showChangedSQLCopy =
changedSQL && !changedLoggingToDifferential && !enabledDiscardData;
@ -660,6 +680,7 @@ const EditQueryForm = ({
const disableSaveFormErrors =
(lastEditedQueryName === "" && !!lastEditedQueryId) || !!size(errors);
console.log("lastEditedQueryPlatforms", lastEditedQueryPlatforms);
return (
<>
<form className={`${baseClass}`} autoComplete="off">

View File

@ -10017,13 +10017,149 @@ func (s *integrationTestSuite) TestQueryReports() {
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
require.Len(t, gqrr.Results, 2)
// now cause deletions and verify that results are deleted
// now update the query and verify that results are deleted
updatedQuery := "SELECT * FROM some_new_table;"
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/queries/%d", osqueryInfoQuery.ID), modifyQueryRequest{ID: osqueryInfoQuery.ID, QueryPayload: fleet.QueryPayload{Query: &updatedQuery}}, http.StatusOK, &modifyQueryResp)
require.Equal(t, updatedQuery, modifyQueryResp.Query.Query)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
require.Len(t, gqrr.Results, 0)
// Re-add results to our query and check that they're actually there
s.DoJSON("POST", "/api/osquery/log", slreq, http.StatusOK, &slres)
require.NoError(t, slres.Err)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
require.Len(t, gqrr.Results, 1)
// now update the platform and verify that results are deleted
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/queries/%d", osqueryInfoQuery.ID), modifyQueryRequest{
ID: osqueryInfoQuery.ID,
QueryPayload: fleet.QueryPayload{
Platform: ptr.String("linux"),
},
},
http.StatusOK,
&modifyQueryResp,
)
require.Equal(t, "linux", modifyQueryResp.Query.Platform)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
require.Len(t, gqrr.Results, 0)
// Re-add results to our query and check that they're actually there
s.DoJSON("POST", "/api/osquery/log", slreq, http.StatusOK, &slres)
require.NoError(t, slres.Err)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
require.Len(t, gqrr.Results, 1)
// now update the platform to the same value and verify that results are not deleted
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/queries/%d", osqueryInfoQuery.ID), modifyQueryRequest{
ID: osqueryInfoQuery.ID,
QueryPayload: fleet.QueryPayload{
Platform: ptr.String("linux"),
},
},
http.StatusOK,
&modifyQueryResp,
)
require.Equal(t, "linux", modifyQueryResp.Query.Platform)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
require.Len(t, gqrr.Results, 1)
// now update the min_osquery_version and verify that results are deleted
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/queries/%d", osqueryInfoQuery.ID), modifyQueryRequest{
ID: osqueryInfoQuery.ID,
QueryPayload: fleet.QueryPayload{
MinOsqueryVersion: ptr.String("5.9.1"),
},
},
http.StatusOK,
&modifyQueryResp,
)
require.Equal(t, "5.9.1", modifyQueryResp.Query.MinOsqueryVersion)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
require.Len(t, gqrr.Results, 0)
// Re-add results to our query and check that they're actually there
s.DoJSON("POST", "/api/osquery/log", slreq, http.StatusOK, &slres)
require.NoError(t, slres.Err)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
require.Len(t, gqrr.Results, 1)
// now update the min_osquery_version to another value and verify that results are deleted
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/queries/%d", osqueryInfoQuery.ID), modifyQueryRequest{
ID: osqueryInfoQuery.ID,
QueryPayload: fleet.QueryPayload{
MinOsqueryVersion: ptr.String("5.11.0"),
},
},
http.StatusOK,
&modifyQueryResp,
)
require.Equal(t, "5.11.0", modifyQueryResp.Query.MinOsqueryVersion)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
require.Len(t, gqrr.Results, 0)
// Re-add results to our query and check that they're actually there
s.DoJSON("POST", "/api/osquery/log", slreq, http.StatusOK, &slres)
require.NoError(t, slres.Err)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
require.Len(t, gqrr.Results, 1)
// now update the min_osquery_version to the same value and verify that results are not deleted
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/queries/%d", osqueryInfoQuery.ID), modifyQueryRequest{
ID: osqueryInfoQuery.ID,
QueryPayload: fleet.QueryPayload{
MinOsqueryVersion: ptr.String("5.11.0"),
},
},
http.StatusOK,
&modifyQueryResp,
)
require.Equal(t, "5.11.0", modifyQueryResp.Query.MinOsqueryVersion)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
require.Len(t, gqrr.Results, 1)
// now update the query via specs and change the min_osquery_version, results should be deleted.
osqueryInfoQuerySpec := &fleet.QuerySpec{
Name: osqueryInfoQuery.Name,
Description: osqueryInfoQuery.Description,
Query: osqueryInfoQuery.Query,
Interval: osqueryInfoQuery.Interval,
ObserverCanRun: osqueryInfoQuery.ObserverCanRun,
Platform: osqueryInfoQuery.Platform,
MinOsqueryVersion: osqueryInfoQuery.MinOsqueryVersion,
AutomationsEnabled: osqueryInfoQuery.AutomationsEnabled,
Logging: osqueryInfoQuery.Logging,
DiscardData: osqueryInfoQuery.DiscardData,
}
osqueryInfoQuerySpec.MinOsqueryVersion = "5.12.0"
var applyResp applyQuerySpecsResponse
s.DoJSON("POST", "/api/latest/fleet/spec/queries", applyQuerySpecsRequest{
Specs: []*fleet.QuerySpec{osqueryInfoQuerySpec},
}, http.StatusOK, &applyResp)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
require.Len(t, gqrr.Results, 0)
// Re-add results to our query and check that they're actually there
s.DoJSON("POST", "/api/osquery/log", slreq, http.StatusOK, &slres)
require.NoError(t, slres.Err)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
require.Len(t, gqrr.Results, 1)
// don't change platform or min_osquery_version and results should not be deleted
s.DoJSON("POST", "/api/latest/fleet/spec/queries", applyQuerySpecsRequest{
Specs: []*fleet.QuerySpec{osqueryInfoQuerySpec},
}, http.StatusOK, &applyResp)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
require.Len(t, gqrr.Results, 1)
// now update the platform and results should be deleted.
osqueryInfoQuerySpec.Platform = "darwin"
s.DoJSON("POST", "/api/latest/fleet/spec/queries", applyQuerySpecsRequest{
Specs: []*fleet.QuerySpec{osqueryInfoQuerySpec},
}, http.StatusOK, &applyResp)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
require.Len(t, gqrr.Results, 0)
// Update logging type, which should cause results deletion
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/queries/%d", usbDevicesQuery.ID), modifyQueryRequest{ID: usbDevicesQuery.ID, QueryPayload: fleet.QueryPayload{Logging: &fleet.LoggingDifferential}}, http.StatusOK, &modifyQueryResp)
require.Equal(t, fleet.LoggingDifferential, modifyQueryResp.Query.Logging)

View File

@ -3,6 +3,8 @@ package service
import (
"context"
"fmt"
"slices"
"strings"
"github.com/fleetdm/fleet/v4/server/authz"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
@ -325,7 +327,6 @@ func modifyQueryEndpoint(ctx context.Context, request interface{}, svc fleet.Ser
func (svc *Service) ModifyQuery(ctx context.Context, id uint, p fleet.QueryPayload) (*fleet.Query, error) {
// Load query first to determine if the user can modify it.
query, err := svc.ds.Query(ctx, id)
shouldDiscardQueryResults, shouldDeleteStats := false, false
if err != nil {
setAuthCheckedOnPreAuthErr(ctx)
return nil, err
@ -344,6 +345,8 @@ func (svc *Service) ModifyQuery(ctx context.Context, id uint, p fleet.QueryPaylo
})
}
shouldDiscardQueryResults, shouldDeleteStats := false, false
if p.Name != nil {
query.Name = *p.Name
}
@ -361,9 +364,15 @@ func (svc *Service) ModifyQuery(ctx context.Context, id uint, p fleet.QueryPaylo
query.Interval = *p.Interval
}
if p.Platform != nil {
if !comparePlatforms(query.Platform, *p.Platform) {
shouldDiscardQueryResults = true
}
query.Platform = *p.Platform
}
if p.MinOsqueryVersion != nil {
if query.MinOsqueryVersion != *p.MinOsqueryVersion {
shouldDiscardQueryResults = true
}
query.MinOsqueryVersion = *p.MinOsqueryVersion
}
if p.AutomationsEnabled != nil {
@ -405,6 +414,17 @@ func (svc *Service) ModifyQuery(ctx context.Context, id uint, p fleet.QueryPaylo
return query, nil
}
func comparePlatforms(platform1, platform2 string) bool {
if platform1 == platform2 {
return true
}
p1s := strings.Split(platform1, ",")
slices.Sort(p1s)
p2s := strings.Split(platform2, ",")
slices.Sort(p2s)
return slices.Compare(p1s, p2s) == 0
}
////////////////////////////////////////////////////////////////////////////////
// Delete Query
////////////////////////////////////////////////////////////////////////////////
@ -627,7 +647,9 @@ func (svc *Service) ApplyQuerySpecs(ctx context.Context, specs []*fleet.QuerySpe
if (query.DiscardData && query.DiscardData != dbQuery.DiscardData) ||
(query.Logging != dbQuery.Logging && query.Logging != fleet.LoggingSnapshot) ||
query.Query != dbQuery.Query {
query.Query != dbQuery.Query ||
query.MinOsqueryVersion != dbQuery.MinOsqueryVersion ||
!comparePlatforms(query.Platform, dbQuery.Platform) {
queriesToDiscardResults[dbQuery.ID] = struct{}{}
}
}

View File

@ -714,3 +714,66 @@ func TestQueryReportReturnsNilIfDiscardDataIsTrue(t *testing.T) {
require.NoError(t, err)
require.Nil(t, results)
}
func TestComparePlatforms(t *testing.T) {
for _, tc := range []struct {
name string
p1 string
p2 string
expected bool
}{
{
name: "equal single value",
p1: "linux",
p2: "linux",
expected: true,
},
{
name: "different single value",
p1: "macos",
p2: "linux",
expected: false,
},
{
name: "equal multiple values",
p1: "linux,windows",
p2: "linux,windows",
expected: true,
},
{
name: "equal multiple values out of order",
p1: "linux,windows",
p2: "windows,linux",
expected: true,
},
{
name: "different multiple values",
p1: "linux,windows",
p2: "linux,windows,darwin",
expected: false,
},
{
name: "no values set",
p1: "",
p2: "",
expected: true,
},
{
name: "no values set",
p1: "",
p2: "linux",
expected: false,
},
{
name: "single and multiple values",
p1: "linux",
p2: "windows,linux",
expected: false,
},
} {
t.Run(tc.name, func(t *testing.T) {
actual := comparePlatforms(tc.p1, tc.p2)
require.Equal(t, tc.expected, actual)
})
}
}