Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[KATC] Add support for .indexeddb.leveldb files #1769

Merged
Merged
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
4 changes: 2 additions & 2 deletions ee/indexeddb/values.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ const (
tokenNull byte = 0x30
)

// deserializeChrome deserializes a JS object that has been stored by Chrome
// DeserializeChrome deserializes a JS object that has been stored by Chrome
// in IndexedDB LevelDB-backed databases.
func deserializeChrome(_ context.Context, _ *slog.Logger, row map[string][]byte) (map[string][]byte, error) {
func DeserializeChrome(_ context.Context, _ *slog.Logger, row map[string][]byte) (map[string][]byte, error) {
data, ok := row["data"]
if !ok {
return nil, errors.New("row missing top-level data key")
Expand Down
2 changes: 1 addition & 1 deletion ee/indexeddb/values_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func Test_deserializeIndexeddbValue(t *testing.T) {
0x01, // properties_written
}

obj, err := deserializeChrome(context.TODO(), multislogger.NewNopLogger(), map[string][]byte{"data": testBytes})
obj, err := DeserializeChrome(context.TODO(), multislogger.NewNopLogger(), map[string][]byte{"data": testBytes})
require.NoError(t, err, "deserializing object")

// Confirm we got an id property for the object
Expand Down
15 changes: 13 additions & 2 deletions ee/katc/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"log/slog"
"runtime"

"github.com/kolide/launcher/ee/indexeddb"
"github.com/osquery/osquery-go"
"github.com/osquery/osquery-go/plugin/table"
)
Expand All @@ -16,7 +17,7 @@ import (
// that performs the query against the source.
type katcSourceType struct {
name string
dataFunc func(ctx context.Context, slogger *slog.Logger, path string, query string, sourceConstraints *table.ConstraintList) ([]sourceData, error)
dataFunc func(ctx context.Context, slogger *slog.Logger, sourcePattern string, query string, sourceConstraints *table.ConstraintList) ([]sourceData, error)
}

// sourceData holds the result of calling `katcSourceType.dataFunc`. It maps the
Expand All @@ -28,7 +29,8 @@ type sourceData struct {
}

const (
sqliteSourceType = "sqlite"
sqliteSourceType = "sqlite"
indexeddbLeveldbSourceType = "indexeddb_leveldb"
)

func (kst *katcSourceType) UnmarshalJSON(data []byte) error {
Expand All @@ -43,6 +45,10 @@ func (kst *katcSourceType) UnmarshalJSON(data []byte) error {
kst.name = sqliteSourceType
kst.dataFunc = sqliteData
return nil
case indexeddbLeveldbSourceType:
kst.name = indexeddbLeveldbSourceType
kst.dataFunc = indexeddbLeveldbData
return nil
default:
return fmt.Errorf("unknown table type %s", s)
}
Expand All @@ -59,6 +65,7 @@ type rowTransformStep struct {
const (
snappyDecodeTransformStep = "snappy"
deserializeFirefoxTransformStep = "deserialize_firefox"
deserializeChromeTransformStep = "deserialize_chrome"
camelToSnakeTransformStep = "camel_to_snake"
)

Expand All @@ -78,6 +85,10 @@ func (r *rowTransformStep) UnmarshalJSON(data []byte) error {
r.name = deserializeFirefoxTransformStep
r.transformFunc = deserializeFirefox
return nil
case deserializeChromeTransformStep:
r.name = deserializeChromeTransformStep
r.transformFunc = indexeddb.DeserializeChrome
return nil
case camelToSnakeTransformStep:
r.name = camelToSnakeTransformStep
r.transformFunc = camelToSnake
Expand Down
14 changes: 14 additions & 0 deletions ee/katc/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,20 @@ func TestConstructKATCTables(t *testing.T) {
},
expectedPluginCount: 1,
},
{
testCaseName: "indexeddb_leveldb",
katcConfig: map[string]string{
"kolide_indexeddb_leveldb_test": fmt.Sprintf(`{
"source_type": "indexeddb_leveldb",
"platform": "%s",
"columns": ["data"],
"source": "/some/path/to/db.indexeddb.leveldb",
"query": "db.store",
"row_transform_steps": ["deserialize_chrome"]
}`, runtime.GOOS),
},
expectedPluginCount: 1,
},
{
testCaseName: "multiple plugins",
katcConfig: map[string]string{
Expand Down
71 changes: 71 additions & 0 deletions ee/katc/indexeddb_leveldb.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package katc

import (
"context"
"fmt"
"log/slog"
"path/filepath"
"strings"

"github.com/kolide/launcher/ee/indexeddb"
"github.com/osquery/osquery-go/plugin/table"
)

// indexeddbLeveldbData retrieves data from the LevelDB-backed IndexedDB instances
// found at the filepath in `sourcePattern`. It retrieves all rows from the database
// and object store specified in `query`, which it expects to be in the format
// `<db name>.<object store name>`.
func indexeddbLeveldbData(ctx context.Context, slogger *slog.Logger, sourcePattern string, query string, sourceConstraints *table.ConstraintList) ([]sourceData, error) {
pathPattern := sourcePatternToGlobbablePattern(sourcePattern)
leveldbs, err := filepath.Glob(pathPattern)
if err != nil {
return nil, fmt.Errorf("globbing for leveldb files: %w", err)
}

// Extract database and table from query
dbName, objectStoreName, err := extractQueryTargets(query)
if err != nil {
return nil, fmt.Errorf("getting db and object store names: %w", err)
}

// Query databases
results := make([]sourceData, 0)
for _, db := range leveldbs {
// Check to make sure `db` adheres to sourceConstraints
valid, err := checkSourceConstraints(db, sourceConstraints)
if err != nil {
return nil, fmt.Errorf("checking source path constraints: %w", err)
}
if !valid {
continue
}

rowsFromDb, err := indexeddb.QueryIndexeddbObjectStore(db, dbName, objectStoreName)
if err != nil {
return nil, fmt.Errorf("querying %s: %w", db, err)
}
results = append(results, sourceData{
path: db,
rows: rowsFromDb,
})
}

return results, nil
}

// extractQueryTargets retrieves the targets of the query (the database name and the object store name)
// from the query. IndexedDB is a NoSQL database, so we expect to retrieve all rows from the given
// object store within the given database name.
func extractQueryTargets(query string) (string, string, error) {
parts := strings.Split(query, ".")
if len(parts) != 2 {
return "", "", fmt.Errorf("unable to extract query targets from query: expected `<db name>.<obj store name>`, got `%s`", query)
}
if len(parts[0]) == 0 {
return "", "", fmt.Errorf("missing db name in query `%s`", query)
}
if len(parts[1]) == 0 {
return "", "", fmt.Errorf("missing object store name in query `%s`", query)
}
return parts[0], parts[1], nil
}
62 changes: 62 additions & 0 deletions ee/katc/indexeddb_leveldb_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package katc

import (
"testing"

"github.com/stretchr/testify/require"
)

func Test_extractQueryTargets(t *testing.T) {
t.Parallel()

for _, tt := range []struct {
testCaseName string
query string
expectedDbName string
expectedObjectStoreName string
expectErr bool
}{
{
testCaseName: "correctly formed query",
query: "some_db.some_obj_store",
expectedDbName: "some_db",
expectedObjectStoreName: "some_obj_store",
expectErr: false,
},
{
testCaseName: "missing db name",
query: ".some_obj_store",
expectErr: true,
},
{
testCaseName: "missing object store name",
query: "some_db.",
expectErr: true,
},
{
testCaseName: "query missing separator",
query: "some_db some_obj_store",
expectErr: true,
},
{
testCaseName: "query has too many components",
query: "some_db.some_obj_store.some_other_component",
expectErr: true,
},
} {
tt := tt
t.Run(tt.testCaseName, func(t *testing.T) {
t.Parallel()

dbName, objStoreName, err := extractQueryTargets(tt.query)

if tt.expectErr {
require.Error(t, err)
} else {
require.NoError(t, err)
require.Equal(t, tt.expectedDbName, dbName)
require.Equal(t, tt.expectedObjectStoreName, objStoreName)
}
})
}
}
11 changes: 4 additions & 7 deletions ee/katc/sqlite.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,12 @@ func sqliteData(ctx context.Context, slogger *slog.Logger, sourcePattern string,
return results, nil
}

// sourcePatternToGlobbablePattern translates the source pattern, which adheres to LIKE
// sqlite syntax for consistency with other osquery tables, into a pattern that can be
// sourcePatternToGlobbablePattern translates the source pattern, which allows for
// using % wildcards for consistency with other osquery tables, into a pattern that can be
// accepted by filepath.Glob.
func sourcePatternToGlobbablePattern(sourcePattern string) string {
// % matches zero or more characters in LIKE, corresponds to * in glob syntax
globbablePattern := strings.Replace(sourcePattern, "%", `*`, -1)
// _ matches a single character in LIKE, corresponds to ? in glob syntax
globbablePattern = strings.Replace(globbablePattern, "_", `?`, -1)
return globbablePattern
// % matches zero or more characters, corresponds to * in glob syntax
return strings.Replace(sourcePattern, "%", `*`, -1)
}

// querySqliteDb queries the database at the given path, returning rows of results
Expand Down
9 changes: 2 additions & 7 deletions ee/katc/sqlite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,15 +121,10 @@ func TestSourcePatternToGlobbablePattern(t *testing.T) {
sourcePattern: filepath.Join(rootDir, "path", "to", "%", "directory", "db.sqlite"),
expectedPattern: filepath.Join(rootDir, "path", "to", "*", "directory", "db.sqlite"),
},
{
testCaseName: "underscore wildcard",
sourcePattern: filepath.Join(rootDir, "path", "to", "_", "directory", "db.sqlite"),
expectedPattern: filepath.Join(rootDir, "path", "to", "?", "directory", "db.sqlite"),
},
{
testCaseName: "multiple wildcards",
sourcePattern: filepath.Join(rootDir, "path", "to", "_", "directory", "%.sqlite"),
expectedPattern: filepath.Join(rootDir, "path", "to", "?", "directory", "*.sqlite"),
sourcePattern: filepath.Join(rootDir, "path", "to", "*", "directory", "%.sqlite"),
expectedPattern: filepath.Join(rootDir, "path", "to", "*", "directory", "*.sqlite"),
},
} {
tt := tt
Expand Down
Loading