Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions cmd/agent/subcommands/run/command_notwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,14 @@

package run

import "go.uber.org/fx"
import (
"go.uber.org/fx"

softwareinventoryfx "github.com/DataDog/datadog-agent/comp/softwareinventory/fx"
)

func getPlatformModules() fx.Option {
return fx.Options()
return fx.Options(
Copy link
Contributor

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?

Copy link
Contributor

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

softwareinventoryfx.Module(),
)
}
112 changes: 112 additions & 0 deletions cmd/system-probe/modules/software_inventory.go
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() {}
59 changes: 0 additions & 59 deletions cmd/system-probe/modules/software_inventory_windows.go

This file was deleted.

19 changes: 19 additions & 0 deletions comp/core/gui/guiimpl/views/private/js/javascript.js
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,25 @@ $(document).on('click', '.load_more', function (e) {
loadMore();
});

// Delegate change handler for software inventory filter (works after DOMPurify sanitization)
$(document).on('change', '#sw-type-filter', function () {
var type = this.value;
var entries = document.querySelectorAll('.sw-entry');
var visibleCount = 0;
entries.forEach(function(entry) {
if (type === '' || entry.getAttribute('data-sw-type') === type) {
entry.style.display = '';
visibleCount++;
} else {
entry.style.display = 'none';
}
});
var countSpan = document.getElementById('sw-filter-count');
if (countSpan) {
countSpan.textContent = type !== '' ? 'Showing ' + visibleCount + ' entries' : '';
}
});

// Handler for loading more lines of the currently displayed log file
function loadMore() {
var data = $(".log_data").html();
Expand Down
43 changes: 40 additions & 3 deletions comp/softwareinventory/impl/inventorysoftware.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ package softwareinventoryimpl

import (
"context"
"encoding/json"
"errors"
"fmt"
"math/rand"
Expand Down Expand Up @@ -51,6 +52,32 @@ type sysProbeClient interface {
GetCheck(module types.ModuleName) ([]software.Entry, error)
}

// sysProbeEntryResponse is used to unmarshal system-probe responses that include InstallPath.
// This type matches the systemProbeEntry type used in system-probe modules to ensure
// InstallPath is preserved through JSON serialization/deserialization.
type sysProbeEntryResponse struct {
software.Entry
// InstallPathInternal is the JSON field name used by system-probe
InstallPathInternal string `json:"install_path,omitempty"`
}

// UnmarshalJSON customizes JSON unmarshaling to restore InstallPath from the JSON field
func (e *sysProbeEntryResponse) 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
}

// sysProbeClientWrapper wraps the real sysprobeclient.CheckClient to implement mockSysProbeClient.
// This wrapper provides a clean interface to the System Probe client while maintaining
// compatibility with the existing client implementation.
Expand All @@ -62,13 +89,23 @@ type sysProbeClientWrapper struct {
}

// GetCheck implements mockSysProbeClient.GetCheck by delegating to the wrapped client.
// This method uses the generic GetCheck function to retrieve software inventory data
// from the System Probe with proper type safety.
// This method uses an intermediate type to preserve InstallPath through JSON serialization,
// then converts back to []software.Entry.
func (w *sysProbeClientWrapper) GetCheck(module types.ModuleName) ([]software.Entry, error) {
if w.client == nil {
w.client = w.clientFn()
}
return sysprobeclient.GetCheck[[]software.Entry](w.client, module)
// Unmarshal into sysProbeEntryResponse to preserve InstallPath
responses, err := sysprobeclient.GetCheck[[]sysProbeEntryResponse](w.client, module)
if err != nil {
return nil, err
}
// Convert back to []software.Entry
entries := make([]software.Entry, len(responses))
for i, resp := range responses {
entries[i] = resp.Entry
}
return entries, nil
}

// softwareInventory is the implementation of the Component interface.
Expand Down
49 changes: 44 additions & 5 deletions comp/softwareinventory/impl/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why there is duplication?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.
Expand Down
18 changes: 9 additions & 9 deletions comp/softwareinventory/impl/status_templates/inventory.tmpl
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 }}
Loading