-
-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
55cf012
commit 6be5655
Showing
9 changed files
with
404 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
package ioc | ||
|
||
import ( | ||
"gopkg.in/yaml.v3" | ||
) | ||
|
||
// Config is a struct that defines the IOC configuration | ||
type Config struct { | ||
Title string // A short description of what this configuration does | ||
Order int // Defines the order of expansion when multiple config files are applicable | ||
FieldMappings map[string]FieldMapping | ||
Placeholders map[string][]interface{} // Defines values for placeholders that might appear in IOCs | ||
} | ||
|
||
// FieldMapping is a struct that defines the target fields to be matched in IOCs | ||
type FieldMapping struct { | ||
TargetNames []string // The name(s) that appear in the events being matched | ||
} | ||
|
||
// UnmarshalYAML is a custom method for unmarshaling YAML data into FieldMapping | ||
func (f *FieldMapping) UnmarshalYAML(value *yaml.Node) error { | ||
switch value.Kind { | ||
case yaml.ScalarNode: | ||
// If the YAML value is a scalar (single value), set it as the only element in the slice | ||
f.TargetNames = []string{value.Value} | ||
|
||
case yaml.SequenceNode: | ||
// If the YAML value is a sequence (list), decode it into a slice | ||
var values []string | ||
err := value.Decode(&values) | ||
if err != nil { | ||
return err | ||
} | ||
f.TargetNames = values | ||
} | ||
return nil | ||
} | ||
|
||
// ParseConfig takes a byte slice of YAML data and returns a Config struct or an error if unmarshaling fails | ||
func ParseConfig(contents []byte) (Config, error) { | ||
config := Config{} | ||
return config, yaml.Unmarshal(contents, &config) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
title: Conversion of IOCs into CRYPTTECH Specific Queries | ||
order: 10 | ||
fieldmappings: | ||
ip: dst_ip | ||
domain: dst_host | ||
hash: hash | ||
url: url |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
95.8.80.107 | ||
https://www.whatismyip.com |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
{ | ||
"Name": "IOC Rule ip, domain, url", | ||
"Description": "", | ||
"Query": "sourcetype=\"*\" eql select * from _source_ where _condition_ and (dst_ip = '95.8.80.107' OR dst_host = 'www.whatismyip.com' OR url = 'https://www.whatismyip.com')", | ||
"InsertDate": "2024-07-17T20:25:33Z", | ||
"LastUpdateDate": "2024-07-17T20:25:33Z", | ||
"Tags": [ | ||
"ip", | ||
"domain", | ||
"url" | ||
], | ||
"Level": "info" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
package ievaluator | ||
|
||
import ( | ||
"fmt" | ||
"strings" | ||
|
||
"github.com/mtnmunuklu/alterix/ioc" | ||
) | ||
|
||
type IOCEvaluator struct { | ||
*ioc.IOC | ||
config []ioc.Config | ||
fieldmappings map[string][]string | ||
} | ||
|
||
// ForIOC constructs a new IOCEvaluator with the given IOC and evaluation options. | ||
// It applies any provided options to the new IOCEvaluator and returns it. | ||
func ForIOC(ioc *ioc.IOC, options ...Option) *IOCEvaluator { | ||
e := &IOCEvaluator{IOC: ioc} | ||
for _, option := range options { | ||
option(e) | ||
} | ||
return e | ||
} | ||
|
||
// Result represents the evaluation result of an IOC. | ||
type Result struct { | ||
QueryResult string // The query result as a string | ||
Tags []string // The list of tags associated with the query | ||
} | ||
|
||
// Alters function generates a query string using the IOC information. | ||
func (ioc IOCEvaluator) Alters() (Result, error) { | ||
var conditions []string | ||
var tags []string | ||
|
||
// Generate query parts for each field type | ||
ipConditions := generateCondition(ioc.fieldmappings["ip"], ioc.IOC.IPs) | ||
domainConditions := generateCondition(ioc.fieldmappings["domain"], ioc.IOC.Domains) | ||
urlConditions := generateCondition(ioc.fieldmappings["url"], ioc.IOC.URLs) | ||
hashConditions := generateCondition(ioc.fieldmappings["hash"], ioc.IOC.Hashes) | ||
|
||
conditions = append(conditions, ipConditions...) | ||
conditions = append(conditions, domainConditions...) | ||
conditions = append(conditions, urlConditions...) | ||
conditions = append(conditions, hashConditions...) | ||
|
||
// Add tags based on the conditions present | ||
if len(ipConditions) > 0 { | ||
tags = append(tags, "ip") | ||
} | ||
if len(domainConditions) > 0 { | ||
tags = append(tags, "domain") | ||
} | ||
if len(urlConditions) > 0 { | ||
tags = append(tags, "url") | ||
} | ||
if len(hashConditions) > 0 { | ||
tags = append(tags, "hash") | ||
} | ||
|
||
// Join the conditions with " OR " and wrap in parentheses if there are multiple conditions | ||
condition := strings.Join(conditions, " OR ") | ||
if len(conditions) > 1 { | ||
condition = fmt.Sprintf("(%s)", condition) | ||
} | ||
|
||
query := fmt.Sprintf(`sourcetype="*" eql select * from _source_ where _condition_ and %s`, condition) | ||
result := Result{ | ||
QueryResult: query, | ||
Tags: tags, | ||
} | ||
|
||
return result, nil | ||
} | ||
|
||
// generateCondition generates the condition part of the query for a given field and values. | ||
func generateCondition(fields []string, values []string) []string { | ||
var conditions []string | ||
for _, field := range fields { | ||
var condition string | ||
if len(values) == 1 { | ||
condition = fmt.Sprintf("%s = '%s'", field, values[0]) | ||
} else if len(values) > 1 { | ||
condition = fmt.Sprintf("%s in (%s)", field, joinValues(values)) | ||
} | ||
if condition != "" { | ||
conditions = append(conditions, condition) | ||
} | ||
} | ||
return conditions | ||
} | ||
|
||
// joinValues joins a slice of values into a single comma-separated string with quotes. | ||
func joinValues(values []string) string { | ||
quotedValues := make([]string, len(values)) | ||
for i, v := range values { | ||
quotedValues[i] = fmt.Sprintf("'%s'", v) | ||
} | ||
return strings.Join(quotedValues, ", ") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
package ievaluator | ||
|
||
// calculateFieldMappings compiles a mapping from the ioc fieldnames to possible event fieldnames | ||
func (ioc *IOCEvaluator) calculateFieldMappings() { | ||
// If no config is supplied, no field mapping is needed. | ||
if ioc.config == nil { | ||
return | ||
} | ||
|
||
// mappings is a map from ioc fieldnames to possible event fieldnames. | ||
mappings := map[string][]string{} | ||
|
||
// Loop through each config that is supplied. | ||
for _, config := range ioc.config { | ||
// For each field in the config, add the mapping target names to the mappings. | ||
for field, mapping := range config.FieldMappings { | ||
// TODO: trim duplicates and only care about fields that are actually checked by this ioc | ||
mappings[field] = append(mappings[field], mapping.TargetNames...) | ||
} | ||
} | ||
|
||
// Set the field mappings of the RuleEvaluator to the compiled mappings. | ||
ioc.fieldmappings = mappings | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
package ievaluator | ||
|
||
import ( | ||
"github.com/mtnmunuklu/alterix/ioc" | ||
) | ||
|
||
// Option is a function that takes a IOCEvaluator pointer and modifies its configuration | ||
type Option func(*IOCEvaluator) | ||
|
||
// WithConfig returns an Option that sets the provided IOC configs to the IOCEvaluator. | ||
func WithConfig(config ...ioc.Config) Option { | ||
return func(e *IOCEvaluator) { | ||
// TODO: assert that the configs are in the correct order | ||
e.config = append(e.config, config...) | ||
e.calculateFieldMappings() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
package ioc | ||
|
||
import ( | ||
"errors" | ||
"net" | ||
"net/url" | ||
"regexp" | ||
"strings" | ||
) | ||
|
||
// IOC struct contains IPs, domains, URLs, and hashes. | ||
type IOC struct { | ||
IPs []string | ||
Domains []string | ||
URLs []string | ||
Hashes []string | ||
} | ||
|
||
// checkIfIP checks if the input string is a valid IP address. | ||
func checkIfIP(input string) bool { | ||
return net.ParseIP(input) != nil | ||
} | ||
|
||
// checkIfDomain checks if the input string is a valid domain. | ||
func checkIfDomain(input string) bool { | ||
domainRegex := regexp.MustCompile(`^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$`) | ||
return domainRegex.MatchString(input) | ||
} | ||
|
||
// checkIfURL checks if the input string is a valid URL. | ||
func checkIfURL(input string) bool { | ||
u, err := url.ParseRequestURI(input) | ||
return err == nil && (u.Scheme == "http" || u.Scheme == "https") | ||
} | ||
|
||
// checkIfHash checks if the input string is a valid hash (MD5, SHA1, SHA256). | ||
func checkIfHash(input string) bool { | ||
hashRegex := regexp.MustCompile(`^[a-fA-F0-9]{32}$|^[a-fA-F0-9]{40}$|^[a-fA-F0-9]{64}$`) | ||
return hashRegex.MatchString(input) | ||
} | ||
|
||
func classifyInput(input string) (string, map[string][]string) { | ||
extra := make(map[string][]string) | ||
switch { | ||
case checkIfIP(input): | ||
return "ip", extra | ||
case checkIfDomain(input): | ||
return "domain", extra | ||
case checkIfURL(input): | ||
u, _ := url.Parse(input) | ||
host := u.Hostname() | ||
if checkIfIP(host) { | ||
extra["ip"] = append(extra["ip"], host) | ||
} else if checkIfDomain(host) { | ||
extra["domain"] = append(extra["domain"], host) | ||
} | ||
return "url", extra | ||
case checkIfHash(input): | ||
return "hash", extra | ||
default: | ||
return "unknown", extra | ||
} | ||
} | ||
|
||
func processData(data []byte) (*IOC, error) { | ||
if len(data) == 0 { | ||
return nil, errors.New("input data is empty") | ||
} | ||
|
||
result := &IOC{} | ||
input := string(data) | ||
parts := strings.Fields(input) | ||
|
||
if len(parts) == 0 { | ||
return nil, errors.New("no valid input found") | ||
} | ||
|
||
for _, part := range parts { | ||
classification, extra := classifyInput(part) | ||
switch classification { | ||
case "ip": | ||
result.IPs = append(result.IPs, part) | ||
case "domain": | ||
result.Domains = append(result.Domains, part) | ||
case "url": | ||
result.URLs = append(result.URLs, part) | ||
case "hash": | ||
result.Hashes = append(result.Hashes, part) | ||
} | ||
for key, items := range extra { | ||
switch key { | ||
case "ip": | ||
result.IPs = append(result.IPs, items...) | ||
case "domain": | ||
result.Domains = append(result.Domains, items...) | ||
} | ||
} | ||
} | ||
|
||
return result, nil | ||
} | ||
|
||
// ParseIOC parses the input byte slice and returns an IOC struct. | ||
func ParseIOC(input []byte) (*IOC, error) { | ||
return processData(input) | ||
} |
Oops, something went wrong.