From db390cf39b5365cfdaf4271a232cbbb64f84ff6d Mon Sep 17 00:00:00 2001 From: Rebecca Mahany-Horton Date: Fri, 28 Jun 2024 12:32:12 -0400 Subject: [PATCH] Add source path constraint filtering so we don't run queries against unmatched sources --- ee/katc/config.go | 17 +----- ee/katc/sqlite.go | 12 +++- ee/katc/table.go | 139 ++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 141 insertions(+), 27 deletions(-) diff --git a/ee/katc/config.go b/ee/katc/config.go index f1a2b71b7a..3c3ac17347 100644 --- a/ee/katc/config.go +++ b/ee/katc/config.go @@ -19,7 +19,7 @@ TODOs: type katcSourceType struct { name string - dataFunc func(ctx context.Context, slogger *slog.Logger, path string, query string) ([]sourceData, error) + dataFunc func(ctx context.Context, slogger *slog.Logger, path string, query string, sourceConstraints *table.ConstraintList) ([]sourceData, error) } type sourceData struct { @@ -109,20 +109,7 @@ func ConstructKATCTables(config map[string]string, slogger *slog.Logger) []osque continue } - columns := []table.ColumnDefinition{ - { - Name: sourcePathColumnName, - Type: table.ColumnTypeText, - }, - } - for i := 0; i < len(cfg.Columns); i += 1 { - columns = append(columns, table.ColumnDefinition{ - Name: cfg.Columns[i], - Type: table.ColumnTypeText, - }) - } - - t := newKatcTable(tableName, cfg, slogger) + t, columns := newKatcTable(tableName, cfg, slogger) plugins = append(plugins, table.NewPlugin(tableName, columns, t.generate)) } diff --git a/ee/katc/sqlite.go b/ee/katc/sqlite.go index f392a68f98..1d041de7aa 100644 --- a/ee/katc/sqlite.go +++ b/ee/katc/sqlite.go @@ -7,11 +7,12 @@ import ( "log/slog" "path/filepath" + "github.com/osquery/osquery-go/plugin/table" _ "modernc.org/sqlite" ) // sqliteData is the dataFunc for sqlite KATC tables -func sqliteData(ctx context.Context, slogger *slog.Logger, pathPattern string, query string) ([]sourceData, error) { +func sqliteData(ctx context.Context, slogger *slog.Logger, pathPattern string, query string, sourceConstraints *table.ConstraintList) ([]sourceData, error) { sqliteDbs, err := filepath.Glob(pathPattern) if err != nil { return nil, fmt.Errorf("globbing for files with pattern %s: %w", pathPattern, err) @@ -19,6 +20,15 @@ func sqliteData(ctx context.Context, slogger *slog.Logger, pathPattern string, q results := make([]sourceData, 0) for _, sqliteDb := range sqliteDbs { + // Check to make sure `sqliteDb` adheres to sourceConstraints + valid, err := sourcePathAdheresToSourceConstraints(sqliteDb, sourceConstraints) + if err != nil { + return nil, fmt.Errorf("checking source path constraints: %w", err) + } + if !valid { + continue + } + rowsFromDb, err := querySqliteDb(ctx, slogger, sqliteDb, query) if err != nil { return nil, fmt.Errorf("querying %s: %w", sqliteDb, err) diff --git a/ee/katc/table.go b/ee/katc/table.go index f8f63f0156..93f9db8518 100644 --- a/ee/katc/table.go +++ b/ee/katc/table.go @@ -4,6 +4,8 @@ import ( "context" "fmt" "log/slog" + "regexp" + "strings" "github.com/osquery/osquery-go/plugin/table" ) @@ -11,30 +13,49 @@ import ( const sourcePathColumnName = "source_path" type katcTable struct { - cfg katcTableConfig - slogger *slog.Logger + cfg katcTableConfig + columnLookup map[string]struct{} + slogger *slog.Logger } -func newKatcTable(tableName string, cfg katcTableConfig, slogger *slog.Logger) *katcTable { +func newKatcTable(tableName string, cfg katcTableConfig, slogger *slog.Logger) (*katcTable, []table.ColumnDefinition) { + columns := []table.ColumnDefinition{ + { + Name: sourcePathColumnName, + Type: table.ColumnTypeText, + }, + } + columnLookup := map[string]struct{}{ + sourcePathColumnName: {}, + } + for i := 0; i < len(cfg.Columns); i += 1 { + columns = append(columns, table.ColumnDefinition{ + Name: cfg.Columns[i], + Type: table.ColumnTypeText, + }) + columnLookup[cfg.Columns[i]] = struct{}{} + } + return &katcTable{ - cfg: cfg, + cfg: cfg, + columnLookup: columnLookup, slogger: slogger.With( "table_name", tableName, "table_type", cfg.Source, "table_path", cfg.Path, ), - } + }, columns } func (k *katcTable) generate(ctx context.Context, queryContext table.QueryContext) ([]map[string]string, error) { // Fetch data from our table source - dataRaw, err := k.cfg.Source.dataFunc(ctx, k.slogger, k.cfg.Path, k.cfg.Query) + dataRaw, err := k.cfg.Source.dataFunc(ctx, k.slogger, k.cfg.Path, k.cfg.Query, getSourceConstraint(queryContext)) if err != nil { return nil, fmt.Errorf("fetching data: %w", err) } // Process data - results := make([]map[string]string, 0) + transformedResults := make([]map[string]string, 0) for _, s := range dataRaw { for _, dataRawRow := range s.rows { // Make sure source is included in row data @@ -55,12 +76,108 @@ func (k *katcTable) generate(ctx context.Context, queryContext table.QueryContex for key, val := range dataRawRow { rowData[key] = string(val) } - results = append(results, rowData) + transformedResults = append(transformedResults, rowData) + } + } + + // Now, filter data to ensure we only return columns in k.columnLookup + filteredResults := make([]map[string]string, 0) + for _, row := range transformedResults { + includeRow := true + filteredRow := make(map[string]string) + for column, data := range row { + if _, expectedColumn := k.columnLookup[column]; !expectedColumn { + k.slogger.Log(ctx, slog.LevelWarn, + "results contained unknown column, discarding", + "column", column, + ) + continue + } + + filteredRow[column] = data + + // No need to check the rest of the row + if !includeRow { + break + } } + + if includeRow { + filteredResults = append(filteredResults, filteredRow) + } + } + + return filteredResults, nil +} + +func getSourceConstraint(queryContext table.QueryContext) *table.ConstraintList { + sourceConstraint, sourceConstraintExists := queryContext.Constraints[sourcePathColumnName] + if sourceConstraintExists { + return &sourceConstraint + } + return nil +} + +func sourcePathAdheresToSourceConstraints(sourcePath string, sourceConstraints *table.ConstraintList) (bool, error) { + if sourceConstraints == nil { + return true, nil } - // Now, filter data as needed - // TODO queryContext + validPath := true + for _, sourceConstraint := range sourceConstraints.Constraints { + switch sourceConstraint.Operator { + case table.OperatorEquals: + if sourcePath != sourceConstraint.Expression { + validPath = false + } + case table.OperatorLike: + // Transform the expression into a regex to test if we have a match. + likeRegexpStr := regexp.QuoteMeta(sourceConstraint.Expression) + // % matches zero or more characters + likeRegexpStr = strings.Replace(likeRegexpStr, "%", `.*`, -1) + // _ matches a single character + likeRegexpStr = strings.Replace(likeRegexpStr, "_", `.`, -1) + // LIKE is case-insensitive + likeRegexpStr = `(?i)` + likeRegexpStr + r, err := regexp.Compile(likeRegexpStr) + if err != nil { + return false, fmt.Errorf("invalid LIKE statement: %w", err) + } + if !r.MatchString(sourcePath) { + validPath = false + } + case table.OperatorGlob: + // Transform the expression into a regex to test if we have a match. + // Unlike LIKE, GLOB is case-sensitive. + globRegexpStr := regexp.QuoteMeta(sourceConstraint.Expression) + // * matches zero or more characters + globRegexpStr = strings.Replace(globRegexpStr, `\*`, `.*`, -1) + // ? matches a single character + globRegexpStr = strings.Replace(globRegexpStr, `\?`, `.`, -1) + r, err := regexp.Compile(globRegexpStr) + if err != nil { + return false, fmt.Errorf("invalid GLOB statement: %w", err) + } + if !r.MatchString(sourcePath) { + validPath = false + } + case table.OperatorRegexp: + r, err := regexp.Compile(sourceConstraint.Expression) + if err != nil { + return false, fmt.Errorf("invalid regex: %w", err) + } + if !r.MatchString(sourcePath) { + validPath = false + } + default: + return false, fmt.Errorf("operator %v not valid source constraint", sourceConstraint.Operator) + } + + // No need to check other constraints + if !validPath { + break + } + } - return results, nil + return validPath, nil }