4f3f07d4dd
- Vendor shuffle-shared v1.2.51 as backend/go-app/shuffle-shared - Add replace directive in go.mod to use the local moduled copy - In HandleCheckLicense, force org.Licensed=true and set every SyncFeatures limit to 1e9, skipping all license-key logic - Update Dockerfile to ADD the local shuffle-shared before go build - Verified: backend image builds successfully via docker
271 lines
8.5 KiB
Go
271 lines
8.5 KiB
Go
package shuffle
|
|
|
|
// Basic classifier that tries to look for similarities without requiring a lot of resources.
|
|
// FIXME: Not doing loops at all yet.
|
|
|
|
import (
|
|
//"bytes"
|
|
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/adrg/strutil"
|
|
"github.com/adrg/strutil/metrics"
|
|
//"github.com/rcrowley/go-metrics"
|
|
"reflect"
|
|
)
|
|
|
|
func findSimilarity(blob1, blob2 string, onlyItems []string) (int64, []string) {
|
|
var blob1Map map[string]interface{}
|
|
var blob2Map map[string]interface{}
|
|
|
|
err := json.Unmarshal([]byte(blob1), &blob1Map)
|
|
if err != nil {
|
|
//log.Printf("[WARNING] Something went wrong for blob1: %s", err)
|
|
blob1Map = map[string]interface{}{
|
|
"default": blob1,
|
|
}
|
|
|
|
blob2Map = map[string]interface{}{
|
|
"default": blob2,
|
|
}
|
|
|
|
} else {
|
|
err = json.Unmarshal([]byte(blob2), &blob2Map)
|
|
if err != nil {
|
|
//log.Printf("[WARNING] Something went wrong for blob2: %s", err)
|
|
blob2Map = map[string]interface{}{
|
|
"default": blob2,
|
|
}
|
|
}
|
|
}
|
|
|
|
allValues, skippedValues := findSimilarityInterface("", blob1Map, blob2Map, onlyItems)
|
|
log.Printf("Allvalues: %#v", allValues)
|
|
log.Printf("SkippedValues: %#v", skippedValues)
|
|
|
|
if len(allValues) == 0 {
|
|
return 0, skippedValues
|
|
}
|
|
|
|
avg := int64(0)
|
|
for _, value := range allValues {
|
|
avg = avg + value
|
|
}
|
|
|
|
return avg / int64(len(allValues)), skippedValues
|
|
}
|
|
|
|
func cleanupText(input string) string {
|
|
typesToRemove := []string{",", ".", "\n"}
|
|
for _, val := range typesToRemove {
|
|
input = strings.Replace(input, val, "", -1)
|
|
}
|
|
|
|
input = strings.ToLower(input)
|
|
input = strings.TrimSpace(input)
|
|
return input
|
|
}
|
|
|
|
func findSimilarityInterface(rootNode string, blob1Map, blob2Map map[string]interface{}, onlyItems []string) ([]int64, []string) {
|
|
// clean up data first: stopwords, dots - halvor :)
|
|
log.Printf("[DEBUG] Root: %s", rootNode)
|
|
// "reflect"
|
|
badKeys := []string{"id"}
|
|
avgValues := []int64{}
|
|
skippedValues := []string{}
|
|
for key, value := range blob1Map {
|
|
if fmt.Sprintf("%s", reflect.TypeOf(value)) == "string" {
|
|
if len(onlyItems) > 0 && !ArrayContains(onlyItems, strings.ToLower(key)) {
|
|
skippedValues = append(skippedValues, key)
|
|
continue
|
|
}
|
|
|
|
if ArrayContains(badKeys, key) {
|
|
skippedValues = append(skippedValues, key)
|
|
continue
|
|
}
|
|
|
|
newValue1 := cleanupText(fmt.Sprintf("%#v", blob1Map[key]))
|
|
newValue2 := cleanupText(fmt.Sprintf("%#v", blob2Map[key]))
|
|
|
|
//log.Printf("Val1: %#v, Val2: %#v", newValue1, newValue2)
|
|
//fmt.Printf("%.2f\n", similarity) // Output: 0.43
|
|
similarity := strutil.Similarity(newValue1, newValue2, metrics.NewLevenshtein())
|
|
|
|
avgValues = append(avgValues, int64(similarity*100))
|
|
|
|
} else if fmt.Sprintf("%s", reflect.TypeOf(value)) == "map[string]interface {}" {
|
|
var mappedValue2 map[string]interface{}
|
|
if val, ok := blob2Map[key]; ok {
|
|
mappedValue2 = val.(map[string]interface{})
|
|
} else {
|
|
continue
|
|
}
|
|
|
|
mappedValue1 := value.(map[string]interface{})
|
|
|
|
newRootnode := fmt.Sprintf("%s/%s", rootNode, key)
|
|
parentValue, parentSkipped := findSimilarityInterface(newRootnode, mappedValue1, mappedValue2, onlyItems)
|
|
avgValues = append(avgValues, parentValue...)
|
|
skippedValues = append(skippedValues, parentSkipped...)
|
|
|
|
} else {
|
|
log.Printf("%s: %s", key, reflect.TypeOf(value))
|
|
}
|
|
}
|
|
|
|
return avgValues, skippedValues
|
|
}
|
|
|
|
// Checks same workflow's executions if it has had something similar happening in the last 10 workflows
|
|
func RunTextClassifier(ctx context.Context, workflowExecution WorkflowExecution) {
|
|
// Onlyitems is here in case we JUST want to look for specific keys. Could be per action, app or workflow
|
|
onlyItems := []string{}
|
|
maxCheck := 10
|
|
workflowExecutions, err := GetAllWorkflowExecutions(ctx, workflowExecution.Workflow.ID, 50)
|
|
if err != nil {
|
|
log.Printf("[WARNING] Failed getting executions for %s in text classifier: %s", workflowExecution.Workflow.ID, err)
|
|
return
|
|
}
|
|
|
|
// Compare with at most 10 previous
|
|
if len(workflowExecutions) > maxCheck {
|
|
workflowExecutions = workflowExecutions[0 : maxCheck-1]
|
|
}
|
|
|
|
updatedExecutions := []string{}
|
|
for mainResultKey, mainResult := range workflowExecution.Results {
|
|
if len(mainResult.Result) == 0 {
|
|
continue
|
|
}
|
|
|
|
for executionKey, execution := range workflowExecutions {
|
|
if execution.ExecutionId == mainResult.ExecutionId {
|
|
continue
|
|
}
|
|
|
|
// Need to be same length
|
|
if len(execution.Results) != len(workflowExecution.Results) {
|
|
continue
|
|
}
|
|
|
|
executionAdded := false
|
|
for subResultKey, result := range execution.Results {
|
|
if mainResult.Action.ID != result.Action.ID {
|
|
continue
|
|
}
|
|
|
|
// FIXME: 100% match for this action
|
|
//log.Printf("[DEBUG] Checking action %s (%s)", mainResult.Action.Name, mainResult.Action.ID)
|
|
similarity := int64(0)
|
|
if mainResult.Result == result.Result {
|
|
//log.Printf("[DEBUG] They are exactly equal\n")
|
|
// Skip exactly equal for now
|
|
//similarity = 100
|
|
} else {
|
|
similarity, skippedItems := findSimilarity(mainResult.Result, result.Result, onlyItems)
|
|
log.Printf("[DEBUG] Similarity: %d, Skipped: %#v\n", similarity, skippedItems)
|
|
}
|
|
|
|
if similarity > 0 {
|
|
workflowExecution.Results[mainResultKey].SimilarActions = append(workflowExecution.Results[mainResultKey].SimilarActions, SimilarAction{
|
|
ExecutionId: execution.ExecutionId,
|
|
Similarity: similarity,
|
|
})
|
|
|
|
workflowExecutions[executionKey].Results[subResultKey].SimilarActions = append(workflowExecutions[executionKey].Results[subResultKey].SimilarActions, SimilarAction{
|
|
ExecutionId: workflowExecution.ExecutionId,
|
|
Similarity: similarity,
|
|
})
|
|
|
|
if !executionAdded {
|
|
executionAdded = true
|
|
updatedExecutions = append(updatedExecutions, execution.ExecutionId)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, execution := range workflowExecutions {
|
|
if execution.ExecutionId == workflowExecution.ExecutionId {
|
|
continue
|
|
}
|
|
|
|
if !ArrayContains(updatedExecutions, execution.ExecutionId) {
|
|
continue
|
|
}
|
|
|
|
//log.Printf("Should update %s", execution.ExecutionId)
|
|
err := SetWorkflowExecution(ctx, execution, true)
|
|
if err != nil {
|
|
log.Printf("[WARNING] Failed to update execution %s", execution.ExecutionId)
|
|
}
|
|
}
|
|
|
|
// Means main one is also updated
|
|
if len(updatedExecutions) > 0 {
|
|
//log.Printf("Should current: %s", execution.ExecutionId)
|
|
err := SetWorkflowExecution(ctx, workflowExecution, true)
|
|
if err != nil {
|
|
log.Printf("[WARNING] Failed to update main execution %s", workflowExecution.ExecutionId)
|
|
}
|
|
}
|
|
}
|
|
|
|
func runDedup(inputArr []string) []string {
|
|
newarr := []string{}
|
|
for _, value := range inputArr {
|
|
if !ArrayContains(newarr, value) {
|
|
newarr = append(newarr, value)
|
|
}
|
|
}
|
|
|
|
return newarr
|
|
}
|
|
|
|
// Finds IPs, domains and hashes
|
|
// Point is to test out how we can create a structured database of these, correlate with, and store them
|
|
func RunIOCFinder(ctx context.Context, workflowExecution WorkflowExecution) {
|
|
|
|
numBlock := "(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])"
|
|
regexPattern := numBlock + "\\." + numBlock + "\\." + numBlock + "\\." + numBlock
|
|
ips := regexp.MustCompile(regexPattern)
|
|
|
|
domains := regexp.MustCompile(`^(([a-zA-Z]{1})|([a-zA-Z]{1}[a-zA-Z]{1})|([a-zA-Z]{1}[0-9]{1})|([0-9]{1}[a-zA-Z]{1})|([a-zA-Z0-9][a-zA-Z0-9-_]{1,61}[a-zA-Z0-9]))\.([a-zA-Z]{2,6}|[a-zA-Z0-9-]{2,30}\.[a-zA-Z]{2,3})$`)
|
|
|
|
//urls := regexp.MustCompile(`(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})`)
|
|
//urls := regexp.MustCompile(`(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})`)
|
|
|
|
md5s := regexp.MustCompile(`[a-f0-9]{32}`)
|
|
sha256s := regexp.MustCompile(`[A-Fa-f0-9]{64}`)
|
|
|
|
foundIps := []string{}
|
|
foundDomains := []string{}
|
|
foundMd5s := []string{}
|
|
foundSha256s := []string{}
|
|
for _, result := range workflowExecution.Results {
|
|
// Too big?
|
|
//if len(result.Result) > 1000000 {
|
|
// continue
|
|
//}
|
|
|
|
foundIps = append(foundIps, ips.FindAllString(result.Result, -1)...)
|
|
foundDomains = append(foundDomains, domains.FindAllString(result.Result, -1)...)
|
|
foundMd5s = append(foundMd5s, md5s.FindAllString(result.Result, -1)...)
|
|
foundSha256s = append(foundSha256s, sha256s.FindAllString(result.Result, -1)...)
|
|
}
|
|
|
|
foundIps = runDedup(foundIps)
|
|
foundDomains = runDedup(foundDomains)
|
|
foundMd5s = runDedup(foundMd5s)
|
|
foundSha256s = runDedup(foundSha256s)
|
|
|
|
//fmt.Printf("[DEBUG][%s] IPS: %#v, Domains: %#v, Md5s: %#v, Sha256s: %#v", workflowExecution.ExecutionId, foundIps, foundDomains, foundMd5s, foundSha256s)
|
|
}
|