Add support for custom pack_delimiter to query reports (#16162)

#15490

- [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
This commit is contained in:
Lucas Manuel Rodriguez
2024-01-18 12:41:06 -03:00
committed by GitHub
parent 80c574298e
commit 330088aeba
3 changed files with 80 additions and 17 deletions
@@ -0,0 +1 @@
* Query reports feature now supports a custom `pack_delimiter` in the agent settings.
+50 -17
View File
@@ -1703,35 +1703,68 @@ func getMostRecentResults(results []*fleet.ScheduledQueryResult) []*fleet.Schedu
return filteredResults
}
// Query names recieved from osqueryd are prefixed by teamID so we need
// to pull them out to match the query name and team ID in the database
// findPackDelimiterString attempts to find the `pack_delimiter` string in the scheduled
// query name reported by osquery (note that `pack_delimiter` can contain multiple characters).
//
// The expected format for s is "pack<pack_delimiter>{Global|team-<team_id>}<pack_delimiter><query_name>"
//
// Returns "" if it failed to parse the pack_delimiter.
func findPackDelimiterString(scheduledQueryName string) string {
// Go's regexp doesn't support backreferences so we have to perform some manual work.
scheduledQueryName = scheduledQueryName[4:] // always starts with "pack"
for l := 1; l < len(scheduledQueryName); l++ {
sep := scheduledQueryName[:l]
rest := scheduledQueryName[l:]
pattern := fmt.Sprintf(`^(?:(Global)|(team-\d+))%s.+`, regexp.QuoteMeta(sep))
matched, _ := regexp.MatchString(pattern, rest)
if matched {
return sep
}
}
return ""
}
// getQueryNameAndTeamIDFromResult attempts to parse the scheduled query name reported by osquery.
//
// The expected format of query names managed by Fleet is:
// "pack<pack_delimiter>{Global|team-<team_id>}<pack_delimiter><query_name>"
func getQueryNameAndTeamIDFromResult(path string) (*uint, string, error) {
if !strings.HasPrefix(path, "pack") || len(path) <= 4 {
return nil, "", fmt.Errorf("unknown format: %q", path)
}
sep := findPackDelimiterString(path)
if sep == "" {
// If a pack_delimiter could not be parsed we return an error.
//
// 2017/legacy packs with the format "pack/<Pack name>/<Query name> are
// considered unknown format (they are not considered global or team
// scheduled queries).
return nil, "", fmt.Errorf("unknown format: %q", path)
}
// For pattern: pack/Global/Name
if strings.HasPrefix(path, "pack/Global/") {
return nil, strings.TrimPrefix(path, "pack/Global/"), nil
globalPattern := "pack" + sep + "Global" + sep
if strings.HasPrefix(path, globalPattern) {
return nil, strings.TrimPrefix(path, globalPattern), nil
}
// For pattern: pack/team-<ID>/Name
if strings.HasPrefix(path, "pack/team-") {
parts := strings.SplitN(path, "/", 3)
if len(parts) != 3 {
return nil, "", fmt.Errorf("unknown format: %q", path)
teamPattern := "pack" + sep + "team-"
if strings.HasPrefix(path, teamPattern) {
teamIDAndRest := strings.TrimPrefix(path, teamPattern)
teamIDAndQueryNameParts := strings.SplitN(teamIDAndRest, sep, 2)
if len(teamIDAndQueryNameParts) != 2 {
return nil, "", fmt.Errorf("parsing team number part: %s", path)
}
teamNumberStr := strings.TrimPrefix(parts[1], "team-")
teamNumberUint, err := strconv.ParseUint(teamNumberStr, 10, 32)
teamNumberUint, err := strconv.ParseUint(teamIDAndQueryNameParts[0], 10, 32)
if err != nil {
return nil, "", fmt.Errorf("parsing team number: %w", err)
}
teamNumber := uint(teamNumberUint)
return &teamNumber, parts[2], nil
return &teamNumber, teamIDAndQueryNameParts[1], nil
}
// 2017/legacy packs with the format "pack/<Pack name>/<Query name> are
// considered unknown format (they are not considered global or team
// scheduled queries).
// If none of the above patterns match, return error
return nil, "", fmt.Errorf("unknown format: %q", path)
}
+29
View File
@@ -564,6 +564,13 @@ func TestSubmitResultLogsToLogDestination(t *testing.T) {
AutomationsEnabled: true,
Logging: fleet.LoggingSnapshot,
}, nil
case teamID == nil && name == "query_should_be_saved_and_submitted_with_custom_pack_delimiter":
return &fleet.Query{
ID: 1234,
Name: name,
AutomationsEnabled: true,
Logging: fleet.LoggingSnapshot,
}, nil
case teamID == nil && name == "query_should_be_saved_but_not_submitted":
return &fleet.Query{
ID: 444,
@@ -628,6 +635,7 @@ func TestSubmitResultLogsToLogDestination(t *testing.T) {
`{"diffResults":{"removed":[{"address":"127.0.0.1","hostnames":"kl.groob.io"}],"added":""},"name":"pack\/team-1/hosts","hostIdentifier":"FA01680E-98CA-5557-8F59-7716ECFEE964","calendarTime":"Sun Nov 19 00:02:08 2017 UTC","unixTime":1511049728,"epoch":"0","counter":"10","decorations":{"host_uuid":"FA01680E-98CA-5557-8F59-7716ECFEE964","hostname":"kl.groob.io"}}`,
`{"snapshot":[{"hour":"20","minutes":"8"}],"action":"snapshot","name":"pack/Global/query_should_be_saved_and_submitted","hostIdentifier":"1379f59d98f4","calendarTime":"Tue Jan 10 20:08:51 2017 UTC","unixTime":1484078931,"decorations":{"host_uuid":"EB714C9D-C1F8-A436-B6DA-3F853C5502EA"}}`,
`{"snapshot":[{"hour":"20","minutes":"8"}],"action":"snapshot","name":"pack_Global_query_should_be_saved_and_submitted_with_custom_pack_delimiter","hostIdentifier":"1379f59d98f4","calendarTime":"Tue Jan 10 20:08:52 2017 UTC","unixTime":1484078932,"decorations":{"host_uuid":"EB714C9D-C1F8-A436-B6DA-3F853C5502EA"}}`,
// Fleet doesn't know of this query, so this result should be streamed as is (This is to support streaming results for osquery nodes that are configured outside of Fleet, e.g. `--config_plugin=filesystem`).
`{"snapshot":[{"hour":"20","minutes":"8"}],"action":"snapshot","name":"pack/Global/doesntexist","hostIdentifier":"1379f59d98f4","calendarTime":"Tue Jan 10 20:08:51 2017 UTC","unixTime":1484078931,"decorations":{"host_uuid":"EB714C9D-C1F8-A436-B6DA-3F853C5502EA"}}`,
@@ -844,19 +852,40 @@ func TestGetQueryNameAndTeamIDFromResult(t *testing.T) {
{"pack/team-12345/Another Query", ptr.Uint(12345), "Another Query", false},
{"pack/team-foo/Query", nil, "", true},
{"pack/Global/QueryWith/Slash", nil, "QueryWith/Slash", false},
{"packGlobalGlobalGlobalGlobal", nil, "Global", false}, // pack_delimiter=Global
{"packXGlobalGlobalXGlobalQueryWith/Slash", nil, "QueryWith/Slash", false}, // pack_delimiter=XGlobal
{"pack//Global//QueryWith/Slash", nil, "QueryWith/Slash", false}, // pack_delimiter=//
{"pack/team-1/QueryWith/Slash", ptr.Uint(1), "QueryWith/Slash", false},
{"pack_team-1_QueryWith/Slash", ptr.Uint(1), "QueryWith/Slash", false},
{"packFOOBARteam-1FOOBARQueryWith/Slash", ptr.Uint(1), "QueryWith/Slash", false}, // pack_delimiter=FOOBAR
{"pack123😁123team-1123😁123QueryWith/Slash", ptr.Uint(1), "QueryWith/Slash", false}, // pack_delimiter=123😁123
{"pack(foo)team-1(foo)fo(o)bar", ptr.Uint(1), "fo(o)bar", false}, // pack_delimiter=(foo)
{"packteam-1team-1team-1team-1", ptr.Uint(1), "team-1", false}, // pack_delimiter=team-1
{"InvalidString", nil, "", true},
{"Invalid/Query", nil, "", true},
{"pac", nil, "", true},
{"pack", nil, "", true},
{"pack/", nil, "", true},
{"pack/Global", nil, "", true},
{"pack/Global/", nil, "", true},
{"pack/team/foo", nil, "", true},
{"pack/team-123", nil, "", true},
{"pack/team-/foo", nil, "", true},
{"pack/team-123/", nil, "", true},
// Legacy 2017 packs should fail the parsing as they are separate
// from global or team queries.
{"pack/PackName/Query", nil, "", true},
{"pack/PackName/QueryWith/Slash", nil, "", true},
{"packFOOBARPackNameFOOBARQueryWith/Slash", nil, "", true}, // pack_delimiter=FOOBAR
}
for _, tt := range tests {
tt := tt
t.Run(tt.input, func(t *testing.T) {
t.Parallel()
id, str, err := getQueryNameAndTeamIDFromResult(tt.input)
assert.Equal(t, tt.expectedID, id)
assert.Equal(t, tt.expectedName, str)