fleet/orbit/pkg/table/windowsupdatetable/windows_update.go
Jordan Montgomery c97a0e2363
Fix Windows lint issues and enable linting on Windows (#28704)
For https://github.com/fleetdm/fleet/issues/9943

This will help us avoid issues like this where the log message never
worked right:
https://github.com/fleetdm/fleet/pull/28296#discussion_r2047505191

Most of the changes are no-op type changes like removing unneeded
typecast or disabling gosec on reviewed lines of code

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

<!-- Note that API documentation changes are now addressed by the
product design team. -->

- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
- [x] Added/updated automated tests
- [x] A detailed QA plan exists on the associated ticket (if it isn't
there, work with the product group's QA engineer to add it)
- [x] Manual QA for all new/changed functionality
- For Orbit and Fleet Desktop changes:
- [x] Make sure fleetd is compatible with the latest released version of
Fleet (see [Must
rule](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/fleetd-development-and-release-strategy.md)).
- [x] Orbit runs on macOS, Linux and Windows. Check if the orbit
feature/bugfix should only apply to one platform (`runtime.GOOS`).
- [x] Manual QA must be performed in the three main OSs, macOS, Windows
and Linux.
- [x] Auto-update manual QA, from released version of component to new
version (see [tools/tuf/test](../tools/tuf/test/README.md)).
2025-05-02 16:11:26 -04:00

188 lines
5.2 KiB
Go

//go:build windows
// +build windows
// based on github.com/kolide/launcher/pkg/osquery/tables
package windowsupdatetable
import (
"context"
"encoding/json"
"fmt"
"strconv"
"strings"
"github.com/fleetdm/fleet/v4/orbit/pkg/dataflatten"
"github.com/fleetdm/fleet/v4/orbit/pkg/table/dataflattentable"
"github.com/fleetdm/fleet/v4/orbit/pkg/table/tablehelpers"
"github.com/fleetdm/fleet/v4/orbit/pkg/windows/windowsupdate"
"github.com/osquery/osquery-go/plugin/table"
"github.com/rs/zerolog"
"github.com/scjalliance/comshim"
)
type tableMode int
const (
UpdatesTable tableMode = iota
HistoryTable
)
type windowsUpdatesSearcher interface {
QueryHistoryAll() ([]*windowsupdate.IUpdateHistoryEntry, error)
Search(criteria string) (*windowsupdate.ISearchResult, error)
}
type queryFuncType func(searcher windowsUpdatesSearcher) (interface{}, error)
type Table struct {
logger zerolog.Logger
queryFunc queryFuncType
name string
}
func TablePlugin(mode tableMode, logger zerolog.Logger) *table.Plugin {
columns := dataflattentable.Columns(
table.TextColumn("locale"),
table.IntegerColumn("is_default"),
)
t := &Table{}
switch mode {
case UpdatesTable:
t.queryFunc = queryUpdates
t.name = "windows_updates"
case HistoryTable:
t.queryFunc = queryHistory
t.name = "windows_update_history"
}
t.logger = logger.With().Str("table", t.name).Logger()
return table.NewPlugin(t.name, columns, t.generate)
}
func queryUpdates(searcher windowsUpdatesSearcher) (interface{}, error) {
searchResult, err := searcher.Search("Type='Software' AND IsInstalled=0")
if err != nil {
return nil, err
}
// We only care about the results iff we got some updates
if searchResult != nil && len(searchResult.Updates) == 0 {
return nil, nil
}
return searchResult, nil
}
func queryHistory(searcher windowsUpdatesSearcher) (interface{}, error) {
return searcher.QueryHistoryAll()
}
func (t *Table) generate(ctx context.Context, queryContext table.QueryContext) ([]map[string]string, error) {
var results []map[string]string
for _, locale := range tablehelpers.GetConstraints(queryContext, "locale", tablehelpers.WithDefaults("_default")) {
result, err := t.searchLocale(locale, queryContext)
if err != nil {
t.logger.Info().Err(err).Str("locale", locale).Msg("got error searching")
continue
}
results = append(results, result...)
}
return results, nil
}
func (t *Table) searchLocale(locale string, queryContext table.QueryContext) ([]map[string]string, error) {
comshim.Add(1)
defer comshim.Done()
var results []map[string]string
searcher, setLocale, isDefaultLocale, err := getSearcher(locale)
if err != nil {
return nil, fmt.Errorf("new searcher: %w", err)
}
searchResults, err := t.queryFunc(searcher)
if err != nil {
return nil, fmt.Errorf("search: %w", err)
}
for _, dataQuery := range tablehelpers.GetConstraints(queryContext, "query", tablehelpers.WithDefaults("*")) {
flatData, err := t.flattenOutput(dataQuery, searchResults)
if err != nil {
t.logger.Err(err).Msg("flatten failed")
continue
}
rowData := map[string]string{
"locale": setLocale,
"is_default": strconv.Itoa(isDefaultLocale),
}
results = append(results, dataflattentable.ToMap(flatData, dataQuery, rowData)...)
}
return results, nil
}
func (t *Table) flattenOutput(dataQuery string, searchResults interface{}) ([]dataflatten.Row, error) {
flattenOpts := []dataflatten.FlattenOpts{
dataflatten.WithLogger(t.logger),
dataflatten.WithQuery(strings.Split(dataQuery, "/")),
}
// dataflatten won't parse the raw searchResults. As a workaround,
// we marshal to json. This is a deficiency in dataflatten.
jsonBytes, err := json.Marshal(searchResults)
if err != nil {
return nil, fmt.Errorf("json: %w", err)
}
return dataflatten.Json(jsonBytes, flattenOpts...)
}
func getSearcher(locale string) (*windowsupdate.IUpdateSearcher, string, int, error) {
isDefaultLocale := 0
session, err := windowsupdate.NewUpdateSession()
if err != nil {
return nil, locale, isDefaultLocale, fmt.Errorf("NewUpdateSession: %w", err)
}
// If a specific locale is requested, set it.
if locale == "_default" {
isDefaultLocale = 1
} else {
requestedLocale, err := strconv.ParseUint(locale, 10, 32)
if err != nil {
return nil, locale, isDefaultLocale, fmt.Errorf("Parse locale %s: %w", locale, err)
}
if err := session.SetLocal(uint32(requestedLocale)); err != nil {
return nil, locale, isDefaultLocale, fmt.Errorf("setting local to %d: %w", uint32(requestedLocale), err)
}
}
// What local is this data for? If it doesn't match the
// requested one, throw an error, since sqlite is going to
// block it.
getLocale, err := session.GetLocal()
if err != nil {
return nil, locale, isDefaultLocale, fmt.Errorf("getlocale: %w", err)
}
if strconv.FormatUint(uint64(getLocale), 10) != locale && isDefaultLocale == 0 {
return nil, locale, isDefaultLocale, fmt.Errorf("set locale(%s) doesn't match returned locale(%d) sqlite will filter: %w", locale, getLocale, err)
}
locale = strconv.FormatUint(uint64(getLocale), 10)
searcher, err := session.CreateUpdateSearcher()
if err != nil {
return nil, locale, isDefaultLocale, fmt.Errorf("new searcher: %w", err)
}
return searcher, locale, isDefaultLocale, err
}