mirror of
https://github.com/fleetdm/fleet
synced 2026-05-23 00:49:03 +00:00
Fix JSON marshal for software entries (#35011)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #35005 # Checklist for submitter ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually For unreleased bug fixes in a release candidate, one of: - [x] Confirmed that the fix is not expected to adversely impact load test results <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Software API responses now properly include installed paths and path signature information when populated. * **Tests** * Added test coverage for software data serialization and installed paths functionality in API responses. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Victor Lyuboslavsky <2685025+getvictor@users.noreply.github.com>
This commit is contained in:
parent
6c9cd40513
commit
fdfd050d87
3 changed files with 214 additions and 0 deletions
|
|
@ -397,6 +397,22 @@ type HostSoftwareEntry struct {
|
|||
PathSignatureInformation []PathSignatureInformation `json:"signature_information"`
|
||||
}
|
||||
|
||||
// MarshalJSON implements custom JSON marshaling for HostSoftwareEntry to ensure
|
||||
// all fields (both from embedded Software and the additional fields) are marshaled
|
||||
func (hse *HostSoftwareEntry) MarshalJSON() ([]byte, error) {
|
||||
hse.populateBrowserField()
|
||||
type Alias Software
|
||||
return json.Marshal(&struct {
|
||||
*Alias
|
||||
InstalledPaths []string `json:"installed_paths"`
|
||||
PathSignatureInformation []PathSignatureInformation `json:"signature_information"`
|
||||
}{
|
||||
Alias: (*Alias)(&hse.Software),
|
||||
InstalledPaths: hse.InstalledPaths,
|
||||
PathSignatureInformation: hse.PathSignatureInformation,
|
||||
})
|
||||
}
|
||||
|
||||
type PathSignatureInformation struct {
|
||||
InstalledPath string `json:"installed_path"`
|
||||
TeamIdentifier string `json:"team_identifier"`
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
|
@ -194,3 +195,69 @@ func TestEnhanceOutputDetails(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostSoftwareEntryMarshalJSON(t *testing.T) {
|
||||
// Test that HostSoftwareEntry properly marshals all fields including
|
||||
// InstalledPaths and PathSignatureInformation from the embedded Software struct
|
||||
hashValue := "abc123"
|
||||
entry := HostSoftwareEntry{
|
||||
Software: Software{
|
||||
ID: 1,
|
||||
Name: "Test Software",
|
||||
Version: "1.0.0",
|
||||
Source: "chrome_extensions",
|
||||
BundleIdentifier: "com.test.software",
|
||||
ExtensionID: "test-extension-id",
|
||||
ExtensionFor: "chrome",
|
||||
Browser: "",
|
||||
Release: "1",
|
||||
Vendor: "Test Vendor",
|
||||
Arch: "x86_64",
|
||||
GenerateCPE: "cpe:2.3:a:test:software:1.0.0:*:*:*:*:*:*:*",
|
||||
Vulnerabilities: Vulnerabilities{},
|
||||
HostsCount: 5,
|
||||
ApplicationID: ptr.String("com.test.app"),
|
||||
},
|
||||
InstalledPaths: []string{"/usr/local/bin/test", "/opt/test"},
|
||||
PathSignatureInformation: []PathSignatureInformation{
|
||||
{
|
||||
InstalledPath: "/usr/local/bin/test",
|
||||
TeamIdentifier: "ABCDE12345",
|
||||
HashSha256: &hashValue,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Marshal to JSON
|
||||
data, err := entry.MarshalJSON()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Expected JSON with all fields including browser and extension_for
|
||||
expectedJSON := `{
|
||||
"id": 1,
|
||||
"name": "Test Software",
|
||||
"version": "1.0.0",
|
||||
"bundle_identifier": "com.test.software",
|
||||
"source": "chrome_extensions",
|
||||
"extension_id": "test-extension-id",
|
||||
"extension_for": "chrome",
|
||||
"browser": "chrome",
|
||||
"release": "1",
|
||||
"vendor": "Test Vendor",
|
||||
"arch": "x86_64",
|
||||
"generated_cpe": "cpe:2.3:a:test:software:1.0.0:*:*:*:*:*:*:*",
|
||||
"vulnerabilities": [],
|
||||
"hosts_count": 5,
|
||||
"application_id": "com.test.app",
|
||||
"installed_paths": ["/usr/local/bin/test", "/opt/test"],
|
||||
"signature_information": [
|
||||
{
|
||||
"installed_path": "/usr/local/bin/test",
|
||||
"team_identifier": "ABCDE12345",
|
||||
"hash_sha256": "abc123"
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
assert.JSONEq(t, expectedJSON, string(data))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2031,6 +2031,137 @@ func (s *integrationTestSuite) TestListHosts() {
|
|||
require.Equal(t, label2.Name, resp.Hosts[0].Labels[1].Name)
|
||||
}
|
||||
|
||||
func (s *integrationTestSuite) TestListHostsPopulateSoftwareWithInstalledPaths() {
|
||||
t := s.T()
|
||||
ctx := context.Background()
|
||||
|
||||
// Create a host for this test
|
||||
host, err := s.ds.NewHost(ctx, &fleet.Host{
|
||||
DetailUpdatedAt: time.Now(),
|
||||
LabelUpdatedAt: time.Now(),
|
||||
PolicyUpdatedAt: time.Now(),
|
||||
SeenTime: time.Now(),
|
||||
NodeKey: ptr.String(t.Name() + "1"),
|
||||
OsqueryHostID: ptr.String(t.Name() + "1"),
|
||||
UUID: t.Name() + "1",
|
||||
Hostname: t.Name() + "foo.local",
|
||||
PrimaryIP: "192.168.1.10",
|
||||
PrimaryMac: "30-65-EC-6F-C4-58",
|
||||
Platform: "darwin",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, host)
|
||||
|
||||
// Create software with installed paths and signature information
|
||||
software := []fleet.Software{
|
||||
{
|
||||
Name: "Google Chrome.app",
|
||||
Version: "121.0.6167.160",
|
||||
Source: "chrome_extensions",
|
||||
ExtensionID: "test-extension-id",
|
||||
ExtensionFor: "chrome",
|
||||
BundleIdentifier: "com.google.Chrome",
|
||||
},
|
||||
}
|
||||
hostSoftware, err := s.ds.UpdateHostSoftware(ctx, host.ID, software)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, hostSoftware.CurrInstalled(), 1)
|
||||
|
||||
// Add installed paths and signature information
|
||||
swPaths := map[string]struct{}{}
|
||||
for _, s := range software {
|
||||
pathItems := [][3]string{
|
||||
{"/Applications/Google Chrome.app", "EQHXZ8M8AV", "abc123hash"},
|
||||
{"/Users/test/Applications/Google Chrome.app", "", ""},
|
||||
}
|
||||
for _, pathItem := range pathItems {
|
||||
path := pathItem[0]
|
||||
teamIdentifier := pathItem[1]
|
||||
cdHash := pathItem[2]
|
||||
key := fmt.Sprintf(
|
||||
"%s%s%s%s%s%s%s",
|
||||
path, fleet.SoftwareFieldSeparator, teamIdentifier, fleet.SoftwareFieldSeparator, cdHash, fleet.SoftwareFieldSeparator, s.ToUniqueStr(),
|
||||
)
|
||||
swPaths[key] = struct{}{}
|
||||
}
|
||||
}
|
||||
err = s.ds.UpdateHostSoftwareInstalledPaths(ctx, host.ID, swPaths, hostSoftware)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Sync to ensure counts are updated
|
||||
err = s.ds.SyncHostsSoftware(ctx, time.Now().UTC())
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test: GET /api/latest/fleet/hosts with populate_software=true
|
||||
var listResp listHostsResponse
|
||||
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listResp, "populate_software", "true")
|
||||
|
||||
// Find our test host in the response
|
||||
var testHost *fleet.HostResponse
|
||||
for i, h := range listResp.Hosts {
|
||||
if h.ID == host.ID {
|
||||
testHost = &listResp.Hosts[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
require.NotNil(t, testHost, "test host not found in response")
|
||||
|
||||
// Verify software is populated
|
||||
require.NotEmpty(t, testHost.Software, "software should be populated")
|
||||
require.Len(t, testHost.Software, 1, "expected 1 software entry")
|
||||
|
||||
// Verify the software entry has the expected fields
|
||||
sw := testHost.Software[0]
|
||||
assert.Equal(t, "Google Chrome.app", sw.Name)
|
||||
assert.Equal(t, "121.0.6167.160", sw.Version)
|
||||
assert.Equal(t, "chrome_extensions", sw.Source)
|
||||
assert.Equal(t, "test-extension-id", sw.ExtensionID)
|
||||
assert.Equal(t, "chrome", sw.ExtensionFor)
|
||||
assert.Equal(t, "chrome", sw.Browser) // backward compatibility field
|
||||
assert.Equal(t, "com.google.Chrome", sw.BundleIdentifier)
|
||||
|
||||
// Verify installed_paths is populated and not empty
|
||||
assert.NotEmpty(t, sw.InstalledPaths, "installed_paths should be populated")
|
||||
assert.Len(t, sw.InstalledPaths, 2, "expected 2 installed paths")
|
||||
assert.Contains(t, sw.InstalledPaths, "/Applications/Google Chrome.app")
|
||||
assert.Contains(t, sw.InstalledPaths, "/Users/test/Applications/Google Chrome.app")
|
||||
|
||||
// Verify signature_information is populated
|
||||
assert.NotEmpty(t, sw.PathSignatureInformation, "signature_information should be populated")
|
||||
assert.Len(t, sw.PathSignatureInformation, 2, "expected 2 signature information entries")
|
||||
|
||||
// Sort by installed path for consistent ordering
|
||||
sort.Slice(sw.PathSignatureInformation, func(i, j int) bool {
|
||||
return sw.PathSignatureInformation[i].InstalledPath < sw.PathSignatureInformation[j].InstalledPath
|
||||
})
|
||||
|
||||
// Verify first signature information (system-level with team identifier)
|
||||
sigInfo0 := sw.PathSignatureInformation[0]
|
||||
assert.Equal(t, "/Applications/Google Chrome.app", sigInfo0.InstalledPath)
|
||||
assert.Equal(t, "EQHXZ8M8AV", sigInfo0.TeamIdentifier)
|
||||
assert.NotNil(t, sigInfo0.HashSha256)
|
||||
assert.Equal(t, "abc123hash", *sigInfo0.HashSha256)
|
||||
|
||||
// Verify second signature information (user-level without team identifier)
|
||||
sigInfo1 := sw.PathSignatureInformation[1]
|
||||
assert.Equal(t, "/Users/test/Applications/Google Chrome.app", sigInfo1.InstalledPath)
|
||||
assert.Equal(t, "", sigInfo1.TeamIdentifier)
|
||||
assert.Nil(t, sigInfo1.HashSha256)
|
||||
|
||||
// Also verify the JSON marshaling by checking the raw JSON response
|
||||
rawResp := s.Do("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, "populate_software", "true")
|
||||
defer rawResp.Body.Close()
|
||||
body, err := io.ReadAll(rawResp.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the JSON contains our expected fields
|
||||
assert.Contains(t, string(body), "installed_paths", "JSON should contain installed_paths field")
|
||||
assert.Contains(t, string(body), "signature_information", "JSON should contain signature_information field")
|
||||
assert.Contains(t, string(body), "/Applications/Google Chrome.app", "JSON should contain the installed path")
|
||||
assert.Contains(t, string(body), "EQHXZ8M8AV", "JSON should contain the team identifier")
|
||||
assert.Contains(t, string(body), "abc123hash", "JSON should contain the hash")
|
||||
}
|
||||
|
||||
func (s *integrationTestSuite) TestInvites() {
|
||||
t := s.T()
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue