2023-11-02 02:11:35 +00:00
|
|
|
//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"
|
2024-06-27 17:26:20 +00:00
|
|
|
"github.com/rs/zerolog"
|
2023-11-02 02:11:35 +00:00
|
|
|
"github.com/scjalliance/comshim"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type tableMode int
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
UpdatesTable tableMode = iota
|
|
|
|
|
HistoryTable
|
|
|
|
|
)
|
|
|
|
|
|
2025-04-28 22:23:13 +00:00
|
|
|
type windowsUpdatesSearcher interface {
|
|
|
|
|
QueryHistoryAll() ([]*windowsupdate.IUpdateHistoryEntry, error)
|
|
|
|
|
Search(criteria string) (*windowsupdate.ISearchResult, error)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type queryFuncType func(searcher windowsUpdatesSearcher) (interface{}, error)
|
|
|
|
|
|
2023-11-02 02:11:35 +00:00
|
|
|
type Table struct {
|
2024-06-27 17:26:20 +00:00
|
|
|
logger zerolog.Logger
|
2023-11-02 02:11:35 +00:00
|
|
|
queryFunc queryFuncType
|
|
|
|
|
name string
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-27 17:26:20 +00:00
|
|
|
func TablePlugin(mode tableMode, logger zerolog.Logger) *table.Plugin {
|
2023-11-02 02:11:35 +00:00
|
|
|
columns := dataflattentable.Columns(
|
|
|
|
|
table.TextColumn("locale"),
|
|
|
|
|
table.IntegerColumn("is_default"),
|
|
|
|
|
)
|
|
|
|
|
|
2024-06-27 17:26:20 +00:00
|
|
|
t := &Table{}
|
2023-11-02 02:11:35 +00:00
|
|
|
|
|
|
|
|
switch mode {
|
|
|
|
|
case UpdatesTable:
|
|
|
|
|
t.queryFunc = queryUpdates
|
|
|
|
|
t.name = "windows_updates"
|
|
|
|
|
case HistoryTable:
|
|
|
|
|
t.queryFunc = queryHistory
|
|
|
|
|
t.name = "windows_update_history"
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-27 17:26:20 +00:00
|
|
|
t.logger = logger.With().Str("table", t.name).Logger()
|
|
|
|
|
|
2023-11-02 02:11:35 +00:00
|
|
|
return table.NewPlugin(t.name, columns, t.generate)
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-28 22:23:13 +00:00
|
|
|
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
|
2023-11-02 02:11:35 +00:00
|
|
|
}
|
|
|
|
|
|
2025-04-28 22:23:13 +00:00
|
|
|
func queryHistory(searcher windowsUpdatesSearcher) (interface{}, error) {
|
2023-11-02 02:11:35 +00:00
|
|
|
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 {
|
2024-06-27 17:26:20 +00:00
|
|
|
t.logger.Info().Err(err).Str("locale", locale).Msg("got error searching")
|
2023-11-02 02:11:35 +00:00
|
|
|
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 {
|
2024-06-27 17:26:20 +00:00
|
|
|
t.logger.Err(err).Msg("flatten failed")
|
2023-11-02 02:11:35 +00:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-02 20:11:26 +00:00
|
|
|
locale = strconv.FormatUint(uint64(getLocale), 10)
|
|
|
|
|
|
2023-11-02 02:11:35 +00:00
|
|
|
searcher, err := session.CreateUpdateSearcher()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, locale, isDefaultLocale, fmt.Errorf("new searcher: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return searcher, locale, isDefaultLocale, err
|
|
|
|
|
}
|