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

Add capability to serve YARA rules via authenticated Fleet endpoints #23343

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions changes/14899-yara-rules
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Add capability for Fleet to serve yara rules to agents over HTTPS authenticated via node key.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use past tense.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's clarify it needs osquery 5.14.X on the hosts.

49 changes: 49 additions & 0 deletions server/datastore/mysql/app_configs.go
Original file line number Diff line number Diff line change
Expand Up @@ -288,3 +288,52 @@ func (ds *Datastore) getConfigEnableDiskEncryption(ctx context.Context, teamID *
}
return ac.MDM.EnableDiskEncryption.Value, nil
}

func (ds *Datastore) ApplyYaraRules(ctx context.Context, rules []fleet.YaraRule) error {
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
return applyYaraRulesDB(ctx, tx, rules)
})
}

func applyYaraRulesDB(ctx context.Context, q sqlx.ExtContext, rules []fleet.YaraRule) error {
const delStmt = "DELETE FROM yara_rules"
if _, err := q.ExecContext(ctx, delStmt); err != nil {
return ctxerr.Wrap(ctx, err, "clear before insert")
}

if len(rules) > 0 {
const insStmt = `INSERT INTO yara_rules (name, contents) VALUES %s`
var args []interface{}
sql := fmt.Sprintf(insStmt, strings.TrimSuffix(strings.Repeat(`(?, ?),`, len(rules)), ","))
for _, r := range rules {
args = append(args, r.Name, r.Contents)
}

if _, err := q.ExecContext(ctx, sql, args...); err != nil {
return ctxerr.Wrap(ctx, err, "insert yara rules")
}
}

return nil
}

func (ds *Datastore) GetYaraRules(ctx context.Context) ([]fleet.YaraRule, error) {
sql := "SELECT name, contents FROM yara_rules"
rules := []fleet.YaraRule{}
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &rules, sql); err != nil {
return nil, ctxerr.Wrap(ctx, err, "get yara rules")
}
return rules, nil
}

func (ds *Datastore) YaraRuleByName(ctx context.Context, name string) (*fleet.YaraRule, error) {
query := "SELECT name, contents FROM yara_rules WHERE name = ?"
rule := fleet.YaraRule{}
if err := sqlx.GetContext(ctx, ds.reader(ctx), &rule, query, name); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ctxerr.Wrap(ctx, notFound("YaraRule"), "no yara rule with provided name")
}
return nil, ctxerr.Wrap(ctx, err, "get yara rule by name")
}
return &rule, nil
}
102 changes: 101 additions & 1 deletion server/datastore/mysql/app_configs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
{"GetConfigEnableDiskEncryption", testGetConfigEnableDiskEncryption},
{"IsEnrollSecretAvailable", testIsEnrollSecretAvailable},
{"NDESSCEPProxyPassword", testNDESSCEPProxyPassword},
{"YaraRulesRoundtrip", testYaraRulesRoundtrip},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
Expand Down Expand Up @@ -533,7 +534,6 @@
},
)
}

}

func testNDESSCEPProxyPassword(t *testing.T, ds *Datastore) {
Expand Down Expand Up @@ -606,5 +606,105 @@
require.NoError(t, err)
checkProxyConfig()
checkPassword()
}

func testYaraRulesRoundtrip(t *testing.T, ds *Datastore) {
ctx := context.Background()
defer TruncateTables(t, ds)

// Empty insert
expectedRules := []fleet.YaraRule{}
err := ds.ApplyYaraRules(ctx, expectedRules)
require.NoError(t, err)
rules, err := ds.GetYaraRules(ctx)
require.NoError(t, err)
assert.Equal(t, expectedRules, rules)

// Insert values
expectedRules = []fleet.YaraRule{
{
Name: "wildcard.yar",
Contents: `rule WildcardExample
{
strings:
$hex_string = { E2 34 ?? C8 A? FB }

condition:
$hex_string
}`,
},
{
Name: "jump.yar",
Contents: `rule JumpExample
{
strings:
$hex_string = { F4 23 [4-6] 62 B4 }

condition:
$hex_string
}`,
},
}
err = ds.ApplyYaraRules(ctx, expectedRules)
require.NoError(t, err)
rules, err = ds.GetYaraRules(ctx)
require.NoError(t, err)
assert.Equal(t, expectedRules, rules)

rule, err := ds.YaraRuleByName(ctx, expectedRules[0].Name)
require.NoError(t, err)
assert.Equal(t, &expectedRules[0], rule)
rule, err = ds.YaraRuleByName(ctx, expectedRules[1].Name)
require.NoError(t, err)
assert.Equal(t, &expectedRules[1], rule)

// Update rules
expectedRules = []fleet.YaraRule{
{
Name: "wildcard.yar",
Contents: `rule WildcardExample
{
strings:
$hex_string = { E2 34 ?? C8 A? FB }

condition:
$hex_string
}`,
},
{
Name: "jump-modified.yar",
Contents: `rule JumpExample
{
strings:
$hex_string = true

condition:
$hex_string
}`,
},
}
err = ds.ApplyYaraRules(ctx, expectedRules)
require.NoError(t, err)
rules, err = ds.GetYaraRules(ctx)
require.NoError(t, err)
assert.Equal(t, expectedRules, rules)

rule, err = ds.YaraRuleByName(ctx, expectedRules[0].Name)
require.NoError(t, err)
assert.Equal(t, &expectedRules[0], rule)
rule, err = ds.YaraRuleByName(ctx, expectedRules[1].Name)
require.NoError(t, err)
assert.Equal(t, &expectedRules[1], rule)

// Clear rules
expectedRules = []fleet.YaraRule{}
err = ds.ApplyYaraRules(ctx, expectedRules)
require.NoError(t, err)
rules, err = ds.GetYaraRules(ctx)
require.NoError(t, err)
assert.Equal(t, expectedRules, rules)

// Get rule that doesn't exist
rule, err = ds.YaraRuleByName(ctx, "wildcard.yar")

Check failure on line 708 in server/datastore/mysql/app_configs_test.go

View workflow job for this annotation

GitHub Actions / lint (ubuntu-latest)

ineffectual assignment to rule (ineffassign)

Check failure on line 708 in server/datastore/mysql/app_configs_test.go

View workflow job for this annotation

GitHub Actions / lint (macos-latest)

ineffectual assignment to rule (ineffassign)
require.Error(t, err)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package tables

import (
"database/sql"
"fmt"
)

func init() {
MigrationClient.AddMigration(Up_20241016155452, Down_20241016155452)
}

func Up_20241016155452(tx *sql.Tx) error {
_, err := tx.Exec(`
CREATE TABLE yara_rules (
id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
contents TEXT NOT NULL,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How big can these get?

https://github.com/Yara-Rules/rules/blob/master/crypto/crypto_signatures.yar listed in the osquery docs https://osquery.readthedocs.io/en/stable/deployment/yara/#continuous-monitoring-using-the-yara_events-table is ~76KB.

Eventually if customers/users need bigger rules we can migrate and store them in S3.

PRIMARY KEY (id),
UNIQUE KEY idx_yara_rules_name (name)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci;`)
if err != nil {
return fmt.Errorf("failed to create yara_rules table: %w", err)
}
return nil
}

func Down_20241016155452(tx *sql.Tx) error {
return nil
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove this file.

Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package tables

import "testing"

func TestUp_20241025141856(t *testing.T) {
db := applyUpToPrev(t)

//
// Insert data to test the migration
//
// ...

// Apply current migration.
applyNext(t, db)

//
// Check data, insert new entries, e.g. to verify migration is safe.
//
// ...
}
Loading
Loading