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

Strelka Rule License Fallback #377

Merged
merged 2 commits into from
Mar 8, 2024
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
81 changes: 64 additions & 17 deletions server/modules/strelka/strelka.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,19 @@ type IOManager interface {
ExecCommand(cmd *exec.Cmd) ([]byte, int, time.Duration, error)
}

type yaraRepo struct {
Repo string `json:"repo"`
License string `json:"license"`
}

type StrelkaEngine struct {
srv *server.Server
isRunning bool
thread *sync.WaitGroup
communityRulesImportFrequencySeconds int
yaraRulesFolder string
reposFolder string
rulesRepos []string
rulesRepos []*yaraRepo
compileYaraPythonScriptPath string
allowRegex *regexp.Regexp
denyRegex *regexp.Regexp
Expand All @@ -68,22 +73,25 @@ func (e *StrelkaEngine) PrerequisiteModules() []string {
return nil
}

func (e *StrelkaEngine) Init(config module.ModuleConfig) error {
func (e *StrelkaEngine) Init(config module.ModuleConfig) (err error) {
e.thread = &sync.WaitGroup{}

e.communityRulesImportFrequencySeconds = module.GetIntDefault(config, "communityRulesImportFrequencySeconds", 600)
e.yaraRulesFolder = module.GetStringDefault(config, "yaraRulesFolder", "/opt/so/conf/strelka/rules")
e.reposFolder = module.GetStringDefault(config, "reposFolder", "/opt/so/conf/strelka/repos")
e.rulesRepos = module.GetStringArrayDefault(config, "rulesRepos", []string{"github.com/Security-Onion-Solutions/securityonion-yara"})
e.compileYaraPythonScriptPath = module.GetStringDefault(config, "compileYaraPythonScriptPath", "/opt/so/conf/strelka/compile_yara.py")
e.compileRules = module.GetBoolDefault(config, "compileRules", true)
e.autoUpdateEnabled = module.GetBoolDefault(config, "autoUpdateEnabled", false)

e.rulesRepos, err = getYaraRepos(config)
if err != nil {
return fmt.Errorf("unable to parse Strelka's rulesRepos: %w", err)
}

allow := module.GetStringDefault(config, "allowRegex", "")
deny := module.GetStringDefault(config, "denyRegex", "")

if allow != "" {
var err error
e.allowRegex, err = regexp.Compile(allow)
if err != nil {
return fmt.Errorf("unable to compile Strelka's allowRegex: %w", err)
Expand All @@ -101,6 +109,39 @@ func (e *StrelkaEngine) Init(config module.ModuleConfig) error {
return nil
}

func getYaraRepos(cfg module.ModuleConfig) ([]*yaraRepo, error) {
repoMaps, ok := cfg["rulesRepos"].([]interface{})
if !ok {
return nil, fmt.Errorf(`top level config value "rulesRepos" is not an array of objects`)
}

repos := make([]*yaraRepo, 0, len(repoMaps))

for _, repoMap := range repoMaps {
obj, ok := repoMap.(map[string]interface{})
if !ok {
return nil, fmt.Errorf(`"rulesRepo" entry is not an object`)
}

repo, ok := obj["repo"].(string)
if !ok {
return nil, fmt.Errorf(`missing "repo" link from "rulesRepo" entry`)
}

license, ok := obj["license"].(string)
if !ok {
return nil, fmt.Errorf(`missing "license" from "rulesRepo" entry`)
}

repos = append(repos, &yaraRepo{
Repo: repo,
License: license,
})
}

return repos, nil
}

func (e *StrelkaEngine) Start() error {
e.srv.DetectionEngines[model.EngineNameStrelka] = e
e.isRunning = true
Expand Down Expand Up @@ -200,12 +241,12 @@ func (e *StrelkaEngine) startCommunityRuleImport() {
existingRepos[entry.Name()] = struct{}{}
}

upToDate := map[string]struct{}{}
upToDate := map[string]*yaraRepo{}

if e.autoUpdateEnabled {
// pull or clone repos
for _, repo := range e.rulesRepos {
parser, err := url.Parse(repo)
parser, err := url.Parse(repo.Repo)
if err != nil {
log.WithError(err).WithField("repo", repo).Error("Failed to parse repo URL, doing nothing with it")
continue
Expand All @@ -216,15 +257,15 @@ func (e *StrelkaEngine) startCommunityRuleImport() {

if _, ok := existingRepos[lastFolder]; ok {
// repo already exists, pull
repo, err := git.PlainOpen(repoPath)
gitrepo, err := git.PlainOpen(repoPath)
if err != nil {
log.WithError(err).WithField("repo", repo).Error("Failed to open repo, doing nothing with it")
log.WithError(err).WithField("repo", gitrepo).Error("Failed to open repo, doing nothing with it")
continue
}

work, err := repo.Worktree()
work, err := gitrepo.Worktree()
if err != nil {
log.WithError(err).WithField("repo", repo).Error("Failed to get worktree, doing nothing with it")
log.WithError(err).WithField("repo", gitrepo).Error("Failed to get worktree, doing nothing with it")
continue
}

Expand All @@ -242,21 +283,21 @@ func (e *StrelkaEngine) startCommunityRuleImport() {
cancel()

if err == nil {
upToDate[repoPath] = struct{}{}
upToDate[repoPath] = repo
}
} else {
// repo does not exist, clone
_, err = git.PlainClone(repoPath, false, &git.CloneOptions{
Depth: 1,
SingleBranch: true,
URL: repo,
URL: repo.Repo,
})
if err != nil {
log.WithError(err).WithField("repo", repo).Error("Failed to clone repo, doing nothing with it")
continue
}

upToDate[repoPath] = struct{}{}
upToDate[repoPath] = repo
}
}
} else {
Expand All @@ -281,8 +322,8 @@ func (e *StrelkaEngine) startCommunityRuleImport() {
}

// parse *.yar files in repos
for repo := range upToDate {
err = filepath.WalkDir(repo, func(path string, d fs.DirEntry, err error) error {
for repopath, repo := range upToDate {
err = filepath.WalkDir(repopath, func(path string, d fs.DirEntry, err error) error {
if err != nil {
log.WithError(err).WithField("path", path).Error("Failed to walk path")
return nil
Expand Down Expand Up @@ -330,7 +371,12 @@ func (e *StrelkaEngine) startCommunityRuleImport() {
sev = model.SeverityCritical
}

ruleset := filepath.Base(repo)
license, ok := rule.Meta.Rest["license"]
if !ok {
license = repo.License
}

ruleset := filepath.Base(repopath)

det := &model.Detection{
Engine: model.EngineNameStrelka,
Expand All @@ -341,6 +387,7 @@ func (e *StrelkaEngine) startCommunityRuleImport() {
IsCommunity: true,
Language: model.SigLangYara,
Ruleset: util.Ptr(ruleset),
License: license,
}

comRule, exists := communityDetections[det.PublicID]
Expand Down Expand Up @@ -377,7 +424,7 @@ func (e *StrelkaEngine) startCommunityRuleImport() {
return nil
})
if err != nil {
log.WithError(err).WithField("repo", repo).Error("Failed to walk repo")
log.WithError(err).WithField("repo", repopath).Error("Failed to walk repo")
continue
}
}
Expand Down
99 changes: 99 additions & 0 deletions server/modules/strelka/strelka_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -553,3 +553,102 @@ func TestExtractDetails(t *testing.T) {
})
}
}

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

table := []struct {
Name string
Config map[string]interface{}
Expected []*yaraRepo
Error *string
}{
{
Name: "Valid",
Config: map[string]interface{}{
"rulesRepos": []interface{}{
map[string]interface{}{
"repo": "repo1",
"license": "MIT",
},
map[string]interface{}{
"repo": "repo2",
"license": "GPL2",
},
map[string]interface{}{
"repo": "repo3",
"license": "DRL",
},
},
},
Expected: []*yaraRepo{
{
Repo: "repo1",
License: "MIT",
},
{
Repo: "repo2",
License: "GPL2",
},
{
Repo: "repo3",
License: "DRL",
},
},
},
{
Name: "Missing License",
Config: map[string]interface{}{
"rulesRepos": []interface{}{
map[string]interface{}{
"repo": "repo1",
},
},
},
Error: util.Ptr(`missing "license" from "rulesRepo" entry`),
},
{
Name: "Missing Repo",
Config: map[string]interface{}{
"rulesRepos": []interface{}{
map[string]interface{}{
"license": "DRL",
},
},
},
Error: util.Ptr(`missing "repo" link from "rulesRepo" entry`),
},
{
Name: "Wrong Structure A",
Config: map[string]interface{}{
"rulesRepos": "repo",
},
Error: util.Ptr(`top level config value "rulesRepos" is not an array of objects`),
},
{
Name: "Wrong Structure B",
Config: map[string]interface{}{
"rulesRepos": []interface{}{
"github",
},
},
Error: util.Ptr(`"rulesRepo" entry is not an object`),
},
}

for _, test := range table {
test := test
t.Run(test.Name, func(t *testing.T) {
t.Parallel()

repos, err := getYaraRepos(test.Config)
if test.Error == nil {
assert.NoError(t, err)
} else {
assert.Contains(t, err.Error(), *test.Error)
}

assert.Equal(t, test.Expected, repos)
})
}
}
Loading