Skip to content

Commit

Permalink
Add source path constraint filtering so we don't run queries against …
Browse files Browse the repository at this point in the history
…unmatched sources
  • Loading branch information
RebeccaMahany committed Jun 28, 2024
1 parent f52843e commit db390cf
Show file tree
Hide file tree
Showing 3 changed files with 141 additions and 27 deletions.
17 changes: 2 additions & 15 deletions ee/katc/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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))
}

Expand Down
12 changes: 11 additions & 1 deletion ee/katc/sqlite.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,28 @@ 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)
}

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)
Expand Down
139 changes: 128 additions & 11 deletions ee/katc/table.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,58 @@ import (
"context"
"fmt"
"log/slog"
"regexp"
"strings"

"github.com/osquery/osquery-go/plugin/table"
)

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
Expand All @@ -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
}

0 comments on commit db390cf

Please sign in to comment.