-
Notifications
You must be signed in to change notification settings - Fork 1.4k
SW inventory for MacOS #45533
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
SW inventory for MacOS #45533
Changes from all commits
a4415c5
0291f92
2a05318
7b47710
a4c884a
fe8b555
6fb15f4
f1b955f
38f6fa8
04475d7
b6e04b2
3f22692
a95bf2a
703da49
bc14af8
0f681cb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| // Unless explicitly stated otherwise all files in this repository are licensed | ||
| // under the Apache License Version 2.0. | ||
| // This product includes software developed at Datadog (https://www.datadoghq.com/). | ||
| // Copyright 2016-present Datadog, Inc. | ||
|
|
||
| //go:build darwin || windows | ||
|
|
||
| package modules | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "net/http" | ||
|
|
||
| "github.com/DataDog/datadog-agent/pkg/inventory/software" | ||
| "github.com/DataDog/datadog-agent/pkg/system-probe/api/module" | ||
| "github.com/DataDog/datadog-agent/pkg/system-probe/config" | ||
| sysconfigtypes "github.com/DataDog/datadog-agent/pkg/system-probe/config/types" | ||
| "github.com/DataDog/datadog-agent/pkg/system-probe/utils" | ||
| "github.com/DataDog/datadog-agent/pkg/util/log" | ||
| ) | ||
|
|
||
| // systemProbeEntry is a wrapper around software.Entry that includes InstallPath | ||
| // in JSON for system-probe internal communication. This ensures InstallPath | ||
| // is preserved when data is serialized/deserialized between system-probe and agent. | ||
| type systemProbeEntry struct { | ||
| software.Entry | ||
| // InstallPath is included in JSON for system-probe communication | ||
| // This is needed for proper deduplication (GetID uses InstallPath) | ||
| InstallPathInternal string `json:"install_path,omitempty"` | ||
| } | ||
|
|
||
| // MarshalJSON customizes JSON marshaling to include InstallPath | ||
| func (e *systemProbeEntry) MarshalJSON() ([]byte, error) { | ||
| // Create a type alias to avoid infinite recursion | ||
| type Alias software.Entry | ||
| aux := &struct { | ||
| *Alias | ||
| InstallPathInternal string `json:"install_path,omitempty"` | ||
| }{ | ||
| Alias: (*Alias)(&e.Entry), | ||
| InstallPathInternal: e.Entry.InstallPath, | ||
| } | ||
| return json.Marshal(aux) | ||
| } | ||
|
|
||
| // UnmarshalJSON customizes JSON unmarshaling to restore InstallPath | ||
| func (e *systemProbeEntry) UnmarshalJSON(data []byte) error { | ||
| type Alias software.Entry | ||
| aux := &struct { | ||
| *Alias | ||
| InstallPathInternal string `json:"install_path,omitempty"` | ||
| }{ | ||
| Alias: (*Alias)(&e.Entry), | ||
| } | ||
| if err := json.Unmarshal(data, aux); err != nil { | ||
| return err | ||
| } | ||
| // Restore InstallPath from the JSON field | ||
| e.Entry.InstallPath = aux.InstallPathInternal | ||
| return nil | ||
| } | ||
|
|
||
| // toSystemProbeEntries converts []*software.Entry to []*systemProbeEntry | ||
| func toSystemProbeEntries(entries []*software.Entry) []*systemProbeEntry { | ||
| result := make([]*systemProbeEntry, len(entries)) | ||
| for i, entry := range entries { | ||
| result[i] = &systemProbeEntry{Entry: *entry} | ||
| } | ||
| return result | ||
| } | ||
|
|
||
| func init() { registerModule(SoftwareInventory) } | ||
|
|
||
| // SoftwareInventory Factory | ||
| var SoftwareInventory = &module.Factory{ | ||
| Name: config.SoftwareInventoryModule, | ||
| ConfigNamespaces: []string{"software_inventory"}, | ||
| Fn: func(_ *sysconfigtypes.Config, _ module.FactoryDependencies) (module.Module, error) { | ||
| return &softwareInventoryModule{}, nil | ||
| }, | ||
| } | ||
|
|
||
| var _ module.Module = &softwareInventoryModule{} | ||
|
|
||
| type softwareInventoryModule struct{} | ||
|
|
||
| func (sim *softwareInventoryModule) Register(httpMux *module.Router) error { | ||
| httpMux.HandleFunc("/check", utils.WithConcurrencyLimit(1, func(w http.ResponseWriter, _ *http.Request) { | ||
| log.Infof("Got check request in software inventory") | ||
| inventory, warnings, err := software.GetSoftwareInventory() | ||
| if err != nil { | ||
| log.Errorf("Error getting software inventory: %v", err) | ||
| w.WriteHeader(500) | ||
| return | ||
| } | ||
| for _, warning := range warnings { | ||
| _ = log.Warnf("warning: %s", warning) | ||
| } | ||
| // Convert to systemProbeEntry to include InstallPath in JSON | ||
| // This ensures InstallPath is preserved for proper deduplication | ||
| sysProbeInventory := toSystemProbeEntries(inventory) | ||
| utils.WriteAsJSON(w, sysProbeInventory, utils.CompactOutput) | ||
| })) | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| func (sim *softwareInventoryModule) GetStats() map[string]interface{} { | ||
| return map[string]interface{}{} | ||
| } | ||
|
|
||
| func (sim *softwareInventoryModule) Close() {} |
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,9 +8,12 @@ package softwareinventoryimpl | |
| import ( | ||
| "embed" | ||
| "io" | ||
| "sort" | ||
| "strings" | ||
| "time" | ||
|
|
||
| "github.com/DataDog/datadog-agent/comp/core/status" | ||
| "github.com/DataDog/datadog-agent/pkg/inventory/software" | ||
| ) | ||
|
|
||
| //go:embed status_templates | ||
|
|
@@ -66,20 +69,56 @@ func formatYYYYMMDD(ts string) (string, error) { | |
|
|
||
| // populateStatus populates the status map with software inventory data. | ||
| // This method processes the cached inventory data and formats it for display | ||
| // in the status output. It handles date formatting and organizes the data | ||
| // by software ID for easy lookup. | ||
| // in the status output. It handles date formatting, computes statistics by | ||
| // software type, and organizes the data by software ID for easy lookup. | ||
| // Entries are sorted by Source (software type) and then by DisplayName for | ||
| // easier navigation in the GUI and status output. | ||
| // Note: Stats are computed from deduplicated entries to ensure consistency | ||
| // between the total count and the breakdown by type. | ||
| func (is *softwareInventory) populateStatus(status map[string]interface{}) { | ||
| data := map[string]interface{}{} | ||
|
|
||
| is.cachedInventoryMu.RLock() | ||
| cachedInventory := is.cachedInventory | ||
| is.cachedInventoryMu.RUnlock() | ||
|
|
||
| // First pass: deduplicate entries by ID and format dates | ||
| data := map[string]interface{}{} | ||
|
Comment on lines
+83
to
+84
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why there is duplication?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We have multiple inventory collectors which could report the same software from different sources, and the same software can appear in multiple locations. For example, Microsoft Word could appear in both application category and package category. The implemented duplication removal is the best effort and might not catch all cases. |
||
| for _, inventory := range cachedInventory { | ||
| inventory.InstallDate, _ = formatYYYYMMDD(inventory.InstallDate) | ||
| data[inventory.GetID()] = inventory | ||
| } | ||
| status["software_inventory_metadata"] = data | ||
|
|
||
| // Convert to slice and sort by Source (category), then DisplayName | ||
| sortedEntries := make([]software.Entry, 0, len(data)) | ||
| for _, v := range data { | ||
| sortedEntries = append(sortedEntries, v.(software.Entry)) | ||
| } | ||
| sort.Slice(sortedEntries, func(i, j int) bool { | ||
| // First sort by Source (software type) | ||
| if sortedEntries[i].Source != sortedEntries[j].Source { | ||
| return sortedEntries[i].Source < sortedEntries[j].Source | ||
| } | ||
| // Then sort by DisplayName within each category | ||
| return sortedEntries[i].DisplayName < sortedEntries[j].DisplayName | ||
| }) | ||
|
|
||
| // Second pass: compute stats from deduplicated entries | ||
| // This ensures stats sum matches the total count | ||
| stats := map[string]int{} | ||
| brokenCount := 0 | ||
| for _, inventory := range sortedEntries { | ||
| stats[inventory.Source]++ | ||
| if strings.Contains(inventory.Status, "broken") { | ||
| brokenCount++ | ||
| } | ||
| } | ||
|
|
||
| status["software_inventory_metadata"] = sortedEntries | ||
| status["software_inventory_stats"] = stats | ||
| status["software_inventory_total"] = len(sortedEntries) | ||
| // Only include broken count if there are broken entries | ||
| if brokenCount > 0 { | ||
| status["software_inventory_broken"] = brokenCount | ||
| } | ||
| } | ||
|
|
||
| // getStatusInfo returns the status information map for the software inventory. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,11 @@ | ||
| {{- $swMap := index . "software_inventory_metadata" }} | ||
| {{- if index $swMap "error" }} | ||
| Error refreshing software inventory: {{ index $swMap "error" }} | ||
| {{- else }} | ||
| {{- $count := 0 }} | ||
| {{- range $productCode, $meta := $swMap }} | ||
| {{- $count = add $count 1 }} | ||
| {{- $total := index . "software_inventory_total" }} | ||
| {{- $stats := index . "software_inventory_stats" }} | ||
| {{- $broken := index . "software_inventory_broken" }} | ||
| Detected {{ $total }} installed software entries{{- if $broken }} ({{ $broken }} broken){{- end }}. | ||
|
|
||
| By type: | ||
| {{- range $type, $count := $stats }} | ||
| {{ $type }}: {{ $count }} | ||
| {{- end }} | ||
| Detected {{ $count }} installed software entries. | ||
|
|
||
| The full list is not displayed in this status output, but is available in a flare, the Agent status page in the GUI, or in the Datadog Application. | ||
| {{- end }} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure about this file name, should not it have darwin in the name? Does it mean it will be executed on Linux?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was in response to #45533 (comment)
See: fe8b555