Merge branch 'master_origin'
This commit is contained in:
@@ -23,18 +23,9 @@ jobs:
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r tools/requirements.txt -r tools/requirements-devel.txt
|
||||
wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add -
|
||||
sudo apt install -y apt-transport-https
|
||||
echo "deb https://artifacts.elastic.co/packages/7.x/apt stable main" | sudo tee -a /etc/apt/sources.list.d/elastic.list
|
||||
sudo apt update
|
||||
sudo apt install -y elasticsearch
|
||||
sudo systemctl start elasticsearch
|
||||
- name: Test Sigma Tools and Rules
|
||||
run: |
|
||||
make test
|
||||
- name: Test Generated Elasticsearch Query Strings
|
||||
run: |
|
||||
make test-backend-es-qs
|
||||
- name: Test SQL(ite) Backend
|
||||
run: |
|
||||
make test-backend-sql
|
||||
|
||||
@@ -8,7 +8,7 @@ clearcov:
|
||||
rm -f .coverage
|
||||
|
||||
finish:
|
||||
$(COVERAGE) report --fail-under=90
|
||||
$(COVERAGE) report --fail-under=80
|
||||
rm -f $(TMPOUT)
|
||||
|
||||
test-rules:
|
||||
@@ -59,6 +59,7 @@ test-sigmac:
|
||||
$(COVERAGE) run -a --include=$(COVSCOPE) tools/sigmac -rvdI -t netwitness -c tools/config/netwitness.yml rules/ > /dev/null
|
||||
$(COVERAGE) run -a --include=$(COVSCOPE) tools/sigmac -rvdI -t netwitness-epl -c netwitness-epl rules/ > /dev/null
|
||||
$(COVERAGE) run -a --include=$(COVSCOPE) tools/sigmac -rvdI -t sumologic -O rulecomment -c tools/config/sumologic.yml rules/ > /dev/null
|
||||
$(COVERAGE) run -a --include=$(COVSCOPE) tools/sigmac -rvdI -t sumologic-cse -O rulecomment -c tools/config/sumologic-cse.yml rules/ > /dev/null
|
||||
$(COVERAGE) run -a --include=$(COVSCOPE) tools/sigmac -rvdI -t humio -O rulecomment -c tools/config/humio.yml rules/ > /dev/null
|
||||
$(COVERAGE) run -a --include=$(COVSCOPE) tools/sigmac -rvdI -t crowdstrike -O rulecomment -c tools/config/crowdstrike.yml rules/ > /dev/null
|
||||
$(COVERAGE) run -a --include=$(COVSCOPE) tools/sigmac -rvdI -t sql -c sysmon rules/ > /dev/null
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
title: Oracle WebLogic Exploit CVE-2020-14882
|
||||
id: 85d466b0-d74c-4514-84d3-2bdd3327588b
|
||||
status: experimental
|
||||
description: Detects exploitation attempts on WebLogic servers
|
||||
author: Florian Roth
|
||||
date: 2020/11/02
|
||||
modified: 2020/11/04
|
||||
references:
|
||||
- https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-14882
|
||||
- https://isc.sans.edu/diary/26734
|
||||
- https://twitter.com/jas502n/status/1321416053050667009?s=20
|
||||
- https://twitter.com/sudo_sudoka/status/1323951871078223874
|
||||
logsource:
|
||||
category: webserver
|
||||
detection:
|
||||
selection:
|
||||
c-uri|contains:
|
||||
- '/console/images/%252E%252E%252Fconsole.portal'
|
||||
- '/console/css/%2e'
|
||||
condition: selection
|
||||
fields:
|
||||
- c-ip
|
||||
- c-dns
|
||||
falsepositives:
|
||||
- Unknown
|
||||
level: high
|
||||
tags:
|
||||
- attack.t1100 # an old one
|
||||
- attack.t1190
|
||||
- attack.initial_access
|
||||
- cve.2020-14882
|
||||
@@ -8,7 +8,7 @@ tags:
|
||||
- attack.defense_evasion
|
||||
- attack.t1036 # an old one
|
||||
- attack.t1036.005
|
||||
author: Trent Liffick (@tliffick)
|
||||
author: Trent Liffick (@tliffick), Bartlomiej Czyz (@bczyz1)
|
||||
date: 2020/06/03
|
||||
logsource:
|
||||
category: process_creation
|
||||
@@ -16,7 +16,7 @@ logsource:
|
||||
detection:
|
||||
selection:
|
||||
Image:
|
||||
- '*\mstdc.exe'
|
||||
- '*\msdtc.exe'
|
||||
- '*\gpvc.exe'
|
||||
filter:
|
||||
Image:
|
||||
|
||||
@@ -10,6 +10,7 @@ tags:
|
||||
- attack.t1073 # an old one
|
||||
author: Florian Roth
|
||||
date: 2019/02/06
|
||||
modified: 2020/11/09
|
||||
logsource:
|
||||
category: process_creation
|
||||
product: windows
|
||||
@@ -17,11 +18,11 @@ detection:
|
||||
selection:
|
||||
Image: '*\GUP.exe'
|
||||
filter:
|
||||
Image:
|
||||
- 'C:\Users\\*\AppData\Local\Notepad++\updater\gup.exe'
|
||||
- 'C:\Users\\*\AppData\Roaming\Notepad++\updater\gup.exe'
|
||||
- 'C:\Program Files\Notepad++\updater\gup.exe'
|
||||
- 'C:\Program Files (x86)\Notepad++\updater\gup.exe'
|
||||
Image|endswith:
|
||||
- ':\Users\\*\AppData\Local\Notepad++\updater\GUP.exe'
|
||||
- ':\Users\\*\AppData\Roaming\Notepad++\updater\GUP.exe'
|
||||
- ':\Program Files\Notepad++\updater\GUP.exe'
|
||||
- ':\Program Files (x86)\Notepad++\updater\GUP.exe'
|
||||
condition: selection and not filter
|
||||
falsepositives:
|
||||
- Execution of tools named GUP.exe and located in folders different than Notepad++\updater
|
||||
|
||||
@@ -40,6 +40,7 @@ detection:
|
||||
- '* -e* IAB*'
|
||||
- '* -e* UwB*'
|
||||
- '* -e* cwB*'
|
||||
- '*.exe -ENCOD *'
|
||||
falsepositive1:
|
||||
CommandLine: '* -ExecutionPolicy remotesigned *'
|
||||
condition: selection and not falsepositive1
|
||||
|
||||
@@ -3,7 +3,7 @@ id: d21374ff-f574-44a7-9998-4a8c8bf33d7d
|
||||
description: Detects wmiprvse spawning processes
|
||||
status: experimental
|
||||
date: 2019/08/15
|
||||
modified: 2019/11/10
|
||||
modified: 2020/11/05
|
||||
author: Roberto Rodriguez @Cyb3rWard0g
|
||||
references:
|
||||
- https://github.com/Cyb3rWard0g/ThreatHunter-Playbook/tree/master/playbooks/windows/02_execution/T1047_windows_management_instrumentation/wmi_win32_process_create_remote.md
|
||||
@@ -19,7 +19,10 @@ detection:
|
||||
filter:
|
||||
- LogonId: '0x3e7' # LUID 999 for SYSTEM
|
||||
- User: 'NT AUTHORITY\SYSTEM' # if we don't have LogonId data, fallback on username detection
|
||||
- Image|endswith:
|
||||
- '\WmiPrvSE.exe'
|
||||
- '\WerFault.exe'
|
||||
condition: selection and not filter
|
||||
falsepositives:
|
||||
- Unknown
|
||||
level: critical
|
||||
level: high
|
||||
|
||||
@@ -5,6 +5,7 @@ backends:
|
||||
- es-dsl
|
||||
- es-rule
|
||||
- kibana
|
||||
- kibana-ndjson
|
||||
- xpack-watcher
|
||||
- elastalert
|
||||
- elastalert-dsl
|
||||
|
||||
@@ -5,6 +5,7 @@ backends:
|
||||
- es-dsl
|
||||
- elasticsearch-rule
|
||||
- kibana
|
||||
- kibana-ndjson
|
||||
- xpack-watcher
|
||||
- elastalert
|
||||
- elastalert-dsl
|
||||
|
||||
@@ -6,6 +6,7 @@ backends:
|
||||
- es-rule
|
||||
- corelight_elasticsearch-rule
|
||||
- kibana
|
||||
- kibana-ndjson
|
||||
- xpack-watcher
|
||||
- elastalert
|
||||
- elastalert-dsl
|
||||
|
||||
@@ -8,6 +8,7 @@ backends:
|
||||
- elasticsearch-rule
|
||||
- corelight_elasticsearch-rule
|
||||
- kibana
|
||||
- kibana-ndjson
|
||||
- corelight_kibana
|
||||
- xpack-watcher
|
||||
- corelight_xpack-watcher
|
||||
|
||||
@@ -5,6 +5,7 @@ backends:
|
||||
- es-dsl
|
||||
- elasticsearch-rule
|
||||
- kibana
|
||||
- kibana-ndjson
|
||||
- xpack-watcher
|
||||
- elastalert
|
||||
- elastalert-dsl
|
||||
|
||||
@@ -5,6 +5,7 @@ backends:
|
||||
- es-dsl
|
||||
- es-rule
|
||||
- kibana
|
||||
- kibana-ndjson
|
||||
- xpack-watcher
|
||||
- elastalert
|
||||
- elastalert-dsl
|
||||
|
||||
@@ -5,6 +5,7 @@ backends:
|
||||
- es-dsl
|
||||
- es-rule
|
||||
- kibana
|
||||
- kibana-ndjson
|
||||
- xpack-watcher
|
||||
- elastalert
|
||||
- elastalert-dsl
|
||||
|
||||
@@ -5,6 +5,7 @@ backends:
|
||||
- es-dsl
|
||||
- es-rule
|
||||
- kibana
|
||||
- kibana-ndjson
|
||||
- xpack-watcher
|
||||
- elastalert
|
||||
- elastalert-dsl
|
||||
|
||||
@@ -5,6 +5,7 @@ backends:
|
||||
- es-dsl
|
||||
- es-rule
|
||||
- kibana
|
||||
- kibana-ndjson
|
||||
- xpack-watcher
|
||||
- elastalert
|
||||
- elastalert-dsl
|
||||
|
||||
@@ -5,6 +5,7 @@ backends:
|
||||
- es-dsl
|
||||
- es-rule
|
||||
- kibana
|
||||
- kibana-ndjson
|
||||
- xpack-watcher
|
||||
- elastalert
|
||||
- elastalert-dsl
|
||||
|
||||
@@ -5,6 +5,7 @@ backends:
|
||||
- es-dsl
|
||||
- elasticsearch-rule
|
||||
- kibana
|
||||
- kibana-ndjson
|
||||
- xpack-watcher
|
||||
- elastalert
|
||||
- elastalert-dsl
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
title: SumoLogic
|
||||
order: 20
|
||||
backends:
|
||||
- sumologic-cse
|
||||
- sumologic-cse-rule
|
||||
# Sumulogic mapping depends on customer configuration. Adapt to your context!
|
||||
# typically rule on _sourceCategory, _index or Field Extraction Rules (FER)
|
||||
# supposing existing FER for service, metdata_vendor, EventID
|
||||
logsources:
|
||||
unix:
|
||||
product: unix
|
||||
index: UNIX
|
||||
linux:
|
||||
product: linux
|
||||
index: Linux
|
||||
linux-sshd:
|
||||
product: linux
|
||||
service: sshd
|
||||
index: Linux
|
||||
linux-auth:
|
||||
product: linux
|
||||
service: auth
|
||||
index: Linux
|
||||
linux-clamav:
|
||||
product: linux
|
||||
service: clamav
|
||||
index: Linux
|
||||
windows:
|
||||
product: windows
|
||||
index: Windows
|
||||
conditions:
|
||||
metdata_vendor: Microsoft
|
||||
windows-sysmon:
|
||||
product: windows
|
||||
service: sysmon
|
||||
conditions:
|
||||
metdata_vendor: Microsoft
|
||||
index: Windows
|
||||
windows-security:
|
||||
product: windows
|
||||
service: security
|
||||
conditions:
|
||||
metdata_vendor: Microsoft
|
||||
index: Security
|
||||
windows-powershell:
|
||||
product: windows
|
||||
service: powershell
|
||||
conditions:
|
||||
metdata_vendor: Microsoft
|
||||
index: Powershell
|
||||
windows-system:
|
||||
product: windows
|
||||
service: system
|
||||
conditions:
|
||||
metdata_vendor: Microsoft
|
||||
index: Windows
|
||||
windows-dhcp:
|
||||
product: windows
|
||||
service: dhcp
|
||||
conditions:
|
||||
metdata_vendor: Microsoft
|
||||
index: Windows
|
||||
microsoft:
|
||||
product: gsuite
|
||||
index: gsuite
|
||||
apache:
|
||||
product: apache
|
||||
service: apache
|
||||
index: Apache
|
||||
apache2:
|
||||
product: apache
|
||||
index: Apache
|
||||
nginx:
|
||||
product: nginx
|
||||
index: Nginx
|
||||
cisco:
|
||||
product: cisco
|
||||
index: Cisco
|
||||
webserver:
|
||||
category: webserver
|
||||
index: WEBSERVER
|
||||
firewall:
|
||||
category: firewall
|
||||
index: FIREWALL
|
||||
firewall2:
|
||||
product: firewall
|
||||
index: FIREWALL
|
||||
network-dns:
|
||||
category: dns
|
||||
index: DNS
|
||||
network-dns2:
|
||||
product: dns
|
||||
index: DNS
|
||||
proxy:
|
||||
category: proxy
|
||||
index: PROXY
|
||||
vpn:
|
||||
product: vpn
|
||||
index: network
|
||||
antivirus:
|
||||
product: antivirus
|
||||
index: ANTIVIRUS
|
||||
azure:
|
||||
product: azure
|
||||
index: Azure
|
||||
conditions:
|
||||
metdata_vendor: Microsoft
|
||||
azuread:
|
||||
product: azuread
|
||||
index: Azure AD
|
||||
conditions:
|
||||
metdata_vendor: Microsoft
|
||||
zeek:
|
||||
product: zeek
|
||||
index: zeek
|
||||
application-sql:
|
||||
product: sql
|
||||
index: DATABASE
|
||||
application-python:
|
||||
product: python
|
||||
index: APPLICATIONS
|
||||
application-django:
|
||||
product: django
|
||||
index: Django
|
||||
application-rails:
|
||||
product: rails
|
||||
index: RAILS
|
||||
application-spring:
|
||||
product: spring
|
||||
index: SPRING
|
||||
# if no index, search in all indexes
|
||||
|
||||
fieldmappings:
|
||||
EventID: metadata_deviceEventId
|
||||
event_id: metadata_deviceEventId
|
||||
Image: baseImage
|
||||
event_data.Image: baseImage
|
||||
TargetImage: changeTarget
|
||||
EventType: changeType
|
||||
CommandLine: commandLine
|
||||
Commandline: commandLine
|
||||
process.args: commandLine
|
||||
event_data.CommandLine: commandLine
|
||||
Description: description
|
||||
|
||||
DestinationHostname: dstDevice_hostname
|
||||
DestinationIp: dstDevice_ip
|
||||
dst_ip: dstDevice_ip
|
||||
dst_mac: dstDevice_mac
|
||||
DestinationPort: dstPort
|
||||
dst_port: dstPort
|
||||
|
||||
FileName: file_basename
|
||||
Imphash: file_hash_imphash
|
||||
hash: file_hash_imphash
|
||||
file_hash: file_hash_imphash
|
||||
# :file_hash_md5
|
||||
# :file_hash_sha1
|
||||
# :file_hash_sha256
|
||||
|
||||
Path: file_path
|
||||
path: file_path
|
||||
|
||||
LogonType: logonType
|
||||
ModuleType: moduleType
|
||||
ObjectType: objectType
|
||||
|
||||
ParentImage: parentBaseImage
|
||||
event_data.ParentImage: parentBaseImage
|
||||
ParentCommandLine: parentCommandLine
|
||||
ParentProcessName: parentPid
|
||||
|
||||
SourceHostname: srcDevice_hostname
|
||||
SourceIp: srcDevice_ip
|
||||
src_ip: srcDevice_ip
|
||||
src_mac: srcDevice_mac
|
||||
SourcePort: srcPort
|
||||
src_port: srcPort
|
||||
User: user_username
|
||||
User-Agent: http_userAgent
|
||||
Protocol: http_url_protocol
|
||||
|
||||
destination.domain: http_url_rootDomain
|
||||
domain: http_url_rootDomain
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
title: Sysmon
|
||||
order: 20
|
||||
backends:
|
||||
- sysmon
|
||||
fieldmappings:
|
||||
event_id: EventID
|
||||
event_data.ParentImage: ParentImage
|
||||
event_data.CommandLine: CommandLine
|
||||
event_data.Image: Image
|
||||
@@ -5,6 +5,7 @@ backends:
|
||||
- es-dsl
|
||||
- es-rule
|
||||
- kibana
|
||||
- kibana-ndjson
|
||||
- xpack-watcher
|
||||
- elastalert
|
||||
- elastalert-dsl
|
||||
|
||||
@@ -5,6 +5,7 @@ backends:
|
||||
- es-dsl
|
||||
- es-rule
|
||||
- kibana
|
||||
- kibana-ndjson
|
||||
- xpack-watcher
|
||||
- elastalert
|
||||
- elastalert-dsl
|
||||
|
||||
@@ -5,6 +5,7 @@ backends:
|
||||
- es-dsl
|
||||
- es-rule
|
||||
- kibana
|
||||
- kibana-ndjson
|
||||
- xpack-watcher
|
||||
- elastalert
|
||||
- elastalert-dsl
|
||||
|
||||
+70
-37
@@ -110,62 +110,95 @@ class AzureLogAnalyticsBackend(SingleTextQueryBackend):
|
||||
if isinstance(val, str):
|
||||
if "*" in val[1:-1]: # value contains * inside string - use regex match
|
||||
op = "matches regex"
|
||||
val = re.sub('\\*', '.*', val)
|
||||
val = re.sub('(\\\\\*|\*)', '.*', val)
|
||||
if "\\" in val:
|
||||
return "%s \"(?i)%s\"" % (op, val)
|
||||
return "%s \"(?i)%s\"" % (op, val)
|
||||
val = "@'(?i)%s'" % (val)
|
||||
else:
|
||||
val = "'(?i)%s'" % (val)
|
||||
return "%s %s" % (op, self.cleanValue(val))
|
||||
elif val.startswith("*") or val.endswith("*"):
|
||||
op = "contains"
|
||||
if val.startswith("*") and val.endswith("*"):
|
||||
op = "contains"
|
||||
elif val.startswith("*"):
|
||||
op = "endswith"
|
||||
elif val.endswith("*"):
|
||||
op = "startswith"
|
||||
val = re.sub('([".^$]|(?![*?]))', '\g<1>', val)
|
||||
val = re.sub('\\*', '', val)
|
||||
val = re.sub('(\\\\\*|\*)', '.*', val)
|
||||
val = re.sub('\\?', '.', val)
|
||||
# if "\\" in val:
|
||||
# return "%s @\"%s\"" % (op, val)
|
||||
return "%s \"%s\"" % (op, val)
|
||||
# elif "\\" in val:
|
||||
# return "%s @\"%s\"" % (op, val)
|
||||
return "%s \"%s\"" % (op, val)
|
||||
if "\\" in val:
|
||||
return "%s @'%s'" % (op, self.cleanValue(val))
|
||||
return "%s '%s'" % (op, self.cleanValue(val))
|
||||
elif "\\" in val:
|
||||
return "%s @'%s'" % (op, self.cleanValue(val))
|
||||
return "%s \"%s\"" % (op, self.cleanValue(val))
|
||||
|
||||
def generate(self, sigmaparser):
|
||||
self.table = None
|
||||
self.category = sigmaparser.parsedyaml['logsource'].setdefault('category', None)
|
||||
self.product = sigmaparser.parsedyaml['logsource'].setdefault('product', None)
|
||||
self.service = sigmaparser.parsedyaml['logsource'].setdefault('service', None)
|
||||
|
||||
detection = sigmaparser.parsedyaml.get("detection", {})
|
||||
if "keywords" in detection.keys():
|
||||
return super().generate(sigmaparser)
|
||||
|
||||
if self.category == "process_creation":
|
||||
self.table = "SecurityEvent"
|
||||
self.eventid = "1"
|
||||
elif self.service == "security":
|
||||
self.table = "SecurityEvent"
|
||||
elif self.service == "sysmon":
|
||||
def getTable(self, sigmaparser):
|
||||
if self.category == "process_creation" and len(set(sigmaparser.values.keys()) - {"Image", "ParentImage",
|
||||
"CommandLine"}) == 0:
|
||||
self.table = "SecurityEvent | where EventID == 4688 "
|
||||
self.eventid = "4688"
|
||||
elif self.category == "process_creation":
|
||||
self.table = "SysmonEvent"
|
||||
elif self.service == "powershell":
|
||||
self.eventid = "1"
|
||||
elif self.service and self.service.lower() == "security":
|
||||
self.table = "SecurityEvent"
|
||||
elif self.service and self.service.lower() == "sysmon":
|
||||
self.table = "SysmonEvent"
|
||||
elif self.service and self.service.lower() == "powershell":
|
||||
self.table = "Event"
|
||||
elif self.service == "office365":
|
||||
elif self.service and self.service.lower() == "office365":
|
||||
self.table = "OfficeActivity"
|
||||
elif self.service == "azuread":
|
||||
elif self.service and self.service.lower() == "azuread":
|
||||
self.table = "AuditLogs"
|
||||
elif self.service == "azureactivity":
|
||||
elif self.service and self.service.lower() == "azureactivity":
|
||||
self.table = "AzureActivity"
|
||||
else:
|
||||
if self.service:
|
||||
if "-" in self.service:
|
||||
self.table = "-".join([item.title() for item in self.service.split("-")])
|
||||
self.table = "-".join([item.capitalize() for item in self.service.split("-")])
|
||||
elif "_" in self.service:
|
||||
self.table = "_".join([item.title() for item in self.service.split("_")])
|
||||
self.table = "_".join([item.capitalize() for item in self.service.split("_")])
|
||||
else:
|
||||
self.table = self.service.title()
|
||||
if self.service.islower() or self.service.isupper():
|
||||
self.table = self.service.capitalize()
|
||||
else:
|
||||
self.table = self.service
|
||||
elif self.product:
|
||||
if "-" in self.product:
|
||||
self.table = "-".join([item.title() for item in self.product.split("-")])
|
||||
self.table = "-".join([item.capitalize() for item in self.product.split("-")])
|
||||
elif "_" in self.product:
|
||||
self.table = "_".join([item.title() for item in self.product.split("_")])
|
||||
self.table = "_".join([item.capitalize() for item in self.product.split("_")])
|
||||
else:
|
||||
self.table = self.product.title()
|
||||
if self.product.islower() or self.product.isupper():
|
||||
self.table = self.product.capitalize()
|
||||
else:
|
||||
self.table = self.product
|
||||
elif self.category:
|
||||
if "-" in self.category:
|
||||
self.table = "-".join([item.capitalize() for item in self.category.split("-")])
|
||||
elif "_" in self.category:
|
||||
self.table = "_".join([item.capitalize() for item in self.category.split("_")])
|
||||
else:
|
||||
if self.category.islower() or self.category.isupper():
|
||||
self.table = self.category.capitalize()
|
||||
else:
|
||||
self.table = self.category
|
||||
|
||||
def generate(self, sigmaparser):
|
||||
try:
|
||||
self.category = sigmaparser.parsedyaml['logsource'].setdefault('category', None)
|
||||
self.product = sigmaparser.parsedyaml['logsource'].setdefault('product', None)
|
||||
self.service = sigmaparser.parsedyaml['logsource'].setdefault('service', None)
|
||||
except KeyError:
|
||||
self.category = None
|
||||
self.product = None
|
||||
self.service = None
|
||||
detection = sigmaparser.parsedyaml.get("detection", {})
|
||||
if "keywords" in detection.keys():
|
||||
return super().generate(sigmaparser)
|
||||
if self.table is None:
|
||||
self.getTable(sigmaparser)
|
||||
|
||||
return super().generate(sigmaparser)
|
||||
|
||||
|
||||
@@ -44,8 +44,7 @@ class CarbonBlackWildcardHandlingMixin:
|
||||
|
||||
|
||||
class CarbonBlackQueryBackend(CarbonBlackWildcardHandlingMixin, SingleTextQueryBackend):
|
||||
"""Converts Sigma rule into CarbonBlack query string. Only searches, no aggregations."""
|
||||
|
||||
"""Converts Sigma rule into CarbonBlack query string. Only searches, no aggregations. Contributed by SOC Prime. https://socprime.com"""
|
||||
identifier = "carbonblack"
|
||||
active = True
|
||||
|
||||
@@ -133,13 +132,13 @@ class CarbonBlackQueryBackend(CarbonBlackWildcardHandlingMixin, SingleTextQueryB
|
||||
|
||||
def cleanIPRange(self, value):
|
||||
new_value = value
|
||||
if type(new_value) is str and value.find('*'):
|
||||
if isinstance(new_value, str) and value.find('*'):
|
||||
sub = value.count('.')
|
||||
if value[-2:] == '.*':
|
||||
value = value[:-2]
|
||||
min_ip = value + '.0' * (4 - sub)
|
||||
new_value = min_ip + '/' + str(8 * (4 - sub))
|
||||
elif type(new_value) is list:
|
||||
elif isinstance(new_value, list):
|
||||
for index, vl in enumerate(new_value):
|
||||
new_value[index] = self.cleanIPRange(vl)
|
||||
|
||||
|
||||
@@ -1331,3 +1331,128 @@ class ElasticSearchRuleBackend(ElasticsearchQuerystringBackend):
|
||||
if references:
|
||||
rule.update({"references": references})
|
||||
return json.dumps(rule)
|
||||
|
||||
class KibanaNdjsonBackend(ElasticsearchQuerystringBackend, MultiRuleOutputMixin):
|
||||
"""Converts Sigma rule into Kibana JSON Configuration files (searches only)."""
|
||||
identifier = "kibana-ndjson"
|
||||
active = True
|
||||
options = ElasticsearchQuerystringBackend.options + (
|
||||
("output", "import", "Output format: import = JSON file manually imported in Kibana, curl = Shell script that imports queries in Kibana via curl (jq is additionally required)", "output_type"),
|
||||
("es", "localhost:9200", "Host and port of Elasticsearch instance", None),
|
||||
("index", ".kibana", "Kibana index", None),
|
||||
("prefix", "Sigma: ", "Title prefix of Sigma queries", None),
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.kibanaconf = list()
|
||||
self.indexsearch = set()
|
||||
|
||||
def generate(self, sigmaparser):
|
||||
description = sigmaparser.parsedyaml.setdefault("description", "")
|
||||
|
||||
columns = list()
|
||||
try:
|
||||
for field in sigmaparser.parsedyaml["fields"]:
|
||||
mapped = sigmaparser.config.get_fieldmapping(field).resolve_fieldname(field, sigmaparser)
|
||||
if type(mapped) == str:
|
||||
columns.append(mapped)
|
||||
elif type(mapped) == list:
|
||||
columns.extend(mapped)
|
||||
else:
|
||||
raise TypeError("Field mapping must return string or list")
|
||||
except KeyError: # no 'fields' attribute
|
||||
pass
|
||||
|
||||
indices = sigmaparser.get_logsource().index
|
||||
if len(indices) == 0: # fallback if no index is given
|
||||
indices = ["*"]
|
||||
|
||||
for parsed in sigmaparser.condparsed:
|
||||
result = self.generateNode(parsed.parsedSearch)
|
||||
|
||||
for index in indices:
|
||||
rulename = self.getRuleName(sigmaparser)
|
||||
if len(indices) > 1: # add index names if rule must be replicated because of ambigiuous index patterns
|
||||
raise NotSupportedError("Multiple target indices are not supported by Kibana")
|
||||
else:
|
||||
title = self.prefix + sigmaparser.parsedyaml["title"]
|
||||
|
||||
self.indexsearch.add(
|
||||
"export {indexvar}=$(curl -s '{es}/{index}/_search?q=index-pattern.title:{indexpattern}' | jq -r '.hits.hits[0]._id | ltrimstr(\"index-pattern:\")')".format(
|
||||
es=self.es,
|
||||
index=self.index,
|
||||
indexpattern=index.replace("*", "\\*"),
|
||||
indexvar=self.index_variable_name(index)
|
||||
)
|
||||
)
|
||||
self.kibanaconf.append({
|
||||
"id": rulename,
|
||||
"type": "search",
|
||||
"attributes": {
|
||||
"title": title,
|
||||
"description": description,
|
||||
"hits": 0,
|
||||
"columns": columns,
|
||||
"sort": ["@timestamp", "desc"],
|
||||
"version": 1,
|
||||
"kibanaSavedObjectMeta": {
|
||||
"searchSourceJSON": {
|
||||
"index": index,
|
||||
"filter": [],
|
||||
"highlight": {
|
||||
"pre_tags": ["@kibana-highlighted-field@"],
|
||||
"post_tags": ["@/kibana-highlighted-field@"],
|
||||
"fields": { "*":{} },
|
||||
"require_field_match": False,
|
||||
"fragment_size": 2147483647
|
||||
},
|
||||
"query": {
|
||||
"query_string": {
|
||||
"query": result,
|
||||
"analyze_wildcard": True
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"id": index,
|
||||
"name": "kibanaSavedObjectMeta.searchSourceJSON.index",
|
||||
"type": "index-pattern"
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
def finalize(self):
|
||||
if self.output_type == "import": # output format that can be imported via Kibana UI
|
||||
for item in self.kibanaconf: # JSONize kibanaSavedObjectMeta.searchSourceJSON
|
||||
item['attributes']['kibanaSavedObjectMeta']['searchSourceJSON'] = json.dumps(item['attributes']['kibanaSavedObjectMeta']['searchSourceJSON'])
|
||||
if self.kibanaconf:
|
||||
ndjson = ""
|
||||
for item in self.kibanaconf:
|
||||
ndjson += json.dumps(item)
|
||||
ndjson += "\n"
|
||||
return ndjson
|
||||
elif self.output_type == "curl":
|
||||
for item in self.indexsearch:
|
||||
return item
|
||||
for item in self.kibanaconf:
|
||||
item['attributes']['kibanaSavedObjectMeta']['searchSourceJSON']['index'] = "$" + self.index_variable_name(item['attributes']['kibanaSavedObjectMeta']['searchSourceJSON']['index']) # replace index pattern with reference to variable that will contain Kibana index UUID at script runtime
|
||||
item['attributes']['kibanaSavedObjectMeta']['searchSourceJSON'] = json.dumps(item['attributes']['kibanaSavedObjectMeta']['searchSourceJSON']) # Convert it to JSON string as expected by Kibana
|
||||
item['attributes']['kibanaSavedObjectMeta']['searchSourceJSON'] = item['attributes']['kibanaSavedObjectMeta']['searchSourceJSON'].replace("\\", "\\\\") # Add further escaping for escaped quotes for shell
|
||||
return "curl -s -XPUT -H 'Content-Type: application/json' --data-binary @- '{es}/{index}/doc/{doc_id}' <<EOF\n{doc}\nEOF".format(
|
||||
es=self.es,
|
||||
index=self.index,
|
||||
doc_id="search:" + item['_id'],
|
||||
doc=json.dumps({
|
||||
"type": "search",
|
||||
"search": item['attributes']
|
||||
}, indent=2)
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError("Output type '%s' not supported" % self.output_type)
|
||||
|
||||
def index_variable_name(self, index):
|
||||
return "index_" + index.replace("-", "__").replace("*", "X")
|
||||
|
||||
@@ -23,7 +23,7 @@ from .base import SingleTextQueryBackend
|
||||
from .mixins import MultiRuleOutputMixin
|
||||
|
||||
class HumioBackend(SingleTextQueryBackend):
|
||||
"""Converts Sigma rule into Humio query."""
|
||||
"""Converts Sigma rule into Humio query. Contributed by SOC Prime. https://socprime.com"""
|
||||
identifier = "humio"
|
||||
active = True
|
||||
|
||||
@@ -117,23 +117,23 @@ class HumioBackend(SingleTextQueryBackend):
|
||||
# return (" | " + " | ".join([self.regexExpression % (key, self.cleanValue(item)) for item in value]) + " | ")
|
||||
if not set([type(val) for val in value]).issubset({str, int}):
|
||||
raise TypeError("List values must be strings or numbers")
|
||||
return (" or ".join(['%s=%s' % (key, self.generateValueNode(item)) for item in value]))
|
||||
return " or ".join(['%s=%s' % (key, self.generateValueNode(item)) for item in value])
|
||||
|
||||
def generateAggregation(self, agg):
|
||||
if agg == None:
|
||||
if agg is None:
|
||||
return ""
|
||||
if agg.aggfunc == SigmaAggregationParser.AGGFUNC_NEAR:
|
||||
raise NotImplementedError("The 'near' aggregation operator is not yet implemented for this backend")
|
||||
if agg.groupfield == None:
|
||||
if agg.groupfield is None:
|
||||
if agg.aggfunc_notrans == 'count':
|
||||
if agg.aggfield == None :
|
||||
if agg.aggfield is None :
|
||||
return " | val := count() | val %s %s" % (agg.cond_op, agg.condition)
|
||||
else:
|
||||
agg.aggfunc_notrans = 'dc'
|
||||
return " | count(field=%s, distinct=true, as=val) | val %s %s" % (agg.aggfield or "", agg.cond_op, agg.condition)
|
||||
else:
|
||||
if agg.aggfunc_notrans == 'count':
|
||||
if agg.aggfield == None :
|
||||
if agg.aggfield is None :
|
||||
return " | val := count(field=%s) | val %s %s" % (agg.groupfield or "", agg.cond_op, agg.condition)
|
||||
else:
|
||||
agg.aggfunc_notrans = 'dc'
|
||||
|
||||
+135
-53
@@ -18,6 +18,10 @@ import re
|
||||
from functools import wraps
|
||||
from .base import SingleTextQueryBackend
|
||||
from .exceptions import NotSupportedError
|
||||
from ..parser.modifiers.base import SigmaTypeModifier
|
||||
from ..parser.modifiers.transform import SigmaContainsModifier, SigmaStartswithModifier, SigmaEndswithModifier
|
||||
from ..parser.modifiers.type import SigmaRegularExpressionModifier
|
||||
|
||||
|
||||
def wrapper(method):
|
||||
@wraps(method)
|
||||
@@ -131,6 +135,25 @@ class WindowsDefenderATPBackend(SingleTextQueryBackend):
|
||||
"User": (self.decompose_user, ),
|
||||
}
|
||||
}
|
||||
self.current_table = ""
|
||||
|
||||
def generateANDNode(self, node):
|
||||
generated = [ self.generateNode(val) for val in node ]
|
||||
filtered = []
|
||||
for g in generated:
|
||||
if g and g.startswith("ActionType"):
|
||||
if not any([i for i in filtered if i.startswith("ActionType")]):
|
||||
filtered.append(g)
|
||||
else:
|
||||
continue
|
||||
elif g is not None:
|
||||
filtered.append(g)
|
||||
if filtered:
|
||||
if self.sort_condition_lists:
|
||||
filtered = sorted(filtered)
|
||||
return self.andToken.join(filtered)
|
||||
else:
|
||||
return None
|
||||
|
||||
def id_mapping(self, src):
|
||||
"""Identity mapping, source == target field name"""
|
||||
@@ -186,25 +209,95 @@ class WindowsDefenderATPBackend(SingleTextQueryBackend):
|
||||
return (("InititatingProcessAccountName", self.default_value_mapping(src_value)))
|
||||
|
||||
def generate(self, sigmaparser):
|
||||
self.table = None
|
||||
self.category = sigmaparser.parsedyaml['logsource'].get('category')
|
||||
self.product = sigmaparser.parsedyaml['logsource'].get('product')
|
||||
self.service = sigmaparser.parsedyaml['logsource'].get('service')
|
||||
self.tables = []
|
||||
try:
|
||||
self.category = sigmaparser.parsedyaml['logsource'].setdefault('category', None)
|
||||
self.product = sigmaparser.parsedyaml['logsource'].setdefault('product', None)
|
||||
self.service = sigmaparser.parsedyaml['logsource'].setdefault('service', None)
|
||||
except KeyError:
|
||||
self.category = None
|
||||
self.product = None
|
||||
self.service = None
|
||||
|
||||
if (self.category, self.product, self.service) == ("process_creation", "windows", None):
|
||||
self.table = "DeviceProcessEvents"
|
||||
self.tables.append("DeviceProcessEvents")
|
||||
self.current_table = "DeviceProcessEvents"
|
||||
elif (self.category, self.product, self.service) == (None, "windows", "powershell"):
|
||||
self.table = "DeviceEvents"
|
||||
self.tables.append("DeviceEvents")
|
||||
self.current_table = "DeviceEvents"
|
||||
self.orToken = ", "
|
||||
elif (self.category, self.product, self.service) == (None, "windows", "security"):
|
||||
self.tables.append("DeviceAlertEvents")
|
||||
self.current_table = "DeviceAlertEvents"
|
||||
|
||||
return super().generate(sigmaparser)
|
||||
|
||||
def generateBefore(self, parsed):
|
||||
if self.table is None:
|
||||
if not any(self.tables):
|
||||
raise NotSupportedError("No MDATP table could be determined from Sigma rule")
|
||||
if self.table == "DeviceEvents" and self.service == "powershell":
|
||||
return "%s | where tostring(extractjson('$.Command', AdditionalFields)) in~ " % self.table
|
||||
return "%s | where " % self.table
|
||||
# if self.tables in "DeviceEvents" and self.service == "powershell":
|
||||
# return "%s | where tostring(extractjson('$.Command', AdditionalFields)) in~ " % self.tables
|
||||
if len(self.tables) == 1:
|
||||
if self.tables[0] == "DeviceEvents" and self.service == "powershell":
|
||||
return "%s | where tostring(extractjson('$.Command', AdditionalFields)) in~ " % self.tables
|
||||
return "%s | where " % self.tables[0]
|
||||
else:
|
||||
if "DeviceEvents" in self.tables and self.service == "powershell":
|
||||
return "union %s | where tostring(extractjson('$.Command', AdditionalFields)) in~ " % ", ".join(self.tables)
|
||||
return "union %s | where " % ", ".join(self.tables)
|
||||
|
||||
def generateORNode(self, node):
|
||||
generated = super().generateORNode(node)
|
||||
if generated:
|
||||
return "%s" % generated
|
||||
return generated
|
||||
|
||||
|
||||
def mapEventId(self, event_id):
|
||||
if self.product == "windows":
|
||||
if self.service == "sysmon" and event_id == 1 \
|
||||
or self.service == "security" and event_id == 4688: # Process Execution
|
||||
self.tables.append("DeviceProcessEvents")
|
||||
self.current_table = "DeviceProcessEvents"
|
||||
return None
|
||||
elif self.service == "sysmon" and event_id == 3: # Network Connection
|
||||
self.tables.append("DeviceNetworkEvents")
|
||||
self.current_table = "DeviceNetworkEvents"
|
||||
return None
|
||||
elif self.service == "sysmon" and event_id == 7: # Image Load
|
||||
self.tables.append("DeviceImageLoadEvents")
|
||||
self.current_table = "DeviceImageLoadEvents"
|
||||
return None
|
||||
elif self.service == "sysmon" and event_id == 8: # Create Remote Thread
|
||||
self.tables.append("DeviceEvents")
|
||||
self.current_table = "DeviceEvents"
|
||||
return "ActionType == \"CreateRemoteThreadApiCall\""
|
||||
elif self.service == "sysmon" and event_id == 11: # File Creation
|
||||
self.tables.append("DeviceFileEvents")
|
||||
self.current_table = "DeviceFileEvents"
|
||||
return "ActionType == \"FileCreated\""
|
||||
elif self.service == "sysmon" and event_id == 23: # File Deletion
|
||||
self.tables.append("DeviceFileEvents")
|
||||
self.current_table = "DeviceFileEvents"
|
||||
return "ActionType == \"FileDeleted\""
|
||||
elif self.service == "sysmon" and event_id == 12: # Create/Delete Registry Value
|
||||
self.tables.append("DeviceRegistryEvents")
|
||||
self.current_table = "DeviceRegistryEvents"
|
||||
return None
|
||||
elif self.service == "sysmon" and event_id == 13 \
|
||||
or self.service == "security" and event_id == 4657: # Set Registry Value
|
||||
self.tables.append("DeviceRegistryEvents")
|
||||
self.current_table = "DeviceRegistryEvents"
|
||||
return "ActionType == \"RegistryValueSet\""
|
||||
elif self.service == "security" and event_id == 4624:
|
||||
self.tables.append("DeviceLogonEvents")
|
||||
self.current_table = "DeviceLogonEvents"
|
||||
return None
|
||||
else:
|
||||
if not self.tables:
|
||||
raise NotSupportedError("No sysmon Event ID provided")
|
||||
else:
|
||||
raise NotSupportedError("No mapping for Event ID %s" % event_id)
|
||||
|
||||
@wrapper
|
||||
def generateMapItemNode(self, node):
|
||||
@@ -213,67 +306,56 @@ class WindowsDefenderATPBackend(SingleTextQueryBackend):
|
||||
and creates an appropriate table reference.
|
||||
"""
|
||||
key, value = node
|
||||
# handle map items with values list like multiple OR-chained conditions
|
||||
if type(value) == list:
|
||||
return self.generateORNode([(key, v) for v in value])
|
||||
elif key == "EventID": # EventIDs are not reflected in condition but in table selection
|
||||
if self.product == "windows":
|
||||
if self.service == "sysmon" and value == 1 \
|
||||
or self.service == "security" and value == 4688: # Process Execution
|
||||
self.table = "DeviceProcessEvents"
|
||||
return None
|
||||
elif self.service == "sysmon" and value == 3: # Network Connection
|
||||
self.table = "DeviceNetworkEvents"
|
||||
return None
|
||||
elif self.service == "sysmon" and value == 7: # Image Load
|
||||
self.table = "DeviceImageLoadEvents"
|
||||
return None
|
||||
elif self.service == "sysmon" and value == 8: # Create Remote Thread
|
||||
self.table = "DeviceEvents"
|
||||
return "ActionType == \"CreateRemoteThreadApiCall\""
|
||||
elif self.service == "sysmon" and value == 11: # File Creation
|
||||
self.table = "DeviceFileEvents"
|
||||
return "ActionType == \"FileCreated\""
|
||||
elif self.service == "sysmon" and value == 23: # File Deletion
|
||||
self.table = "DeviceFileEvents"
|
||||
return "ActionType == \"FileDeleted\""
|
||||
elif self.service == "sysmon" and value == 12: # Create/Delete Registry Value
|
||||
self.table = "DeviceRegistryEvents"
|
||||
return None
|
||||
elif self.service == "sysmon" and value == 13 \
|
||||
or self.service == "security" and value == 4657: # Set Registry Value
|
||||
self.table = "DeviceRegistryEvents"
|
||||
return "ActionType == \"RegistryValueSet\""
|
||||
elif self.service == "security" and value == 4624:
|
||||
self.table = "DeviceLogonEvents"
|
||||
if key == "EventID":
|
||||
# EventIDs are not reflected in condition but in table selection
|
||||
if isinstance(value, str) or isinstance(value, int):
|
||||
value = int(value) if isinstance(value, str) else value
|
||||
return self.mapEventId(value)
|
||||
elif isinstance(value, list):
|
||||
return_payload = []
|
||||
for event_id in value:
|
||||
res = self.mapEventId(event_id)
|
||||
if res:
|
||||
return_payload.append(res)
|
||||
if len(return_payload) == 1:
|
||||
return return_payload[0]
|
||||
elif not any(return_payload):
|
||||
return None
|
||||
else:
|
||||
if not self.table:
|
||||
raise NotSupportedError("No sysmon Event ID provided")
|
||||
else:
|
||||
raise NotSupportedError("No mapping for Event ID %s" % value)
|
||||
return "(%s)" % self.generateORNode(
|
||||
[(key, v) for v in value]
|
||||
)
|
||||
if type(value) == list: # handle map items with values list like multiple OR-chained conditions
|
||||
return "(%s)" % self.generateORNode(
|
||||
[(key, self.cleanValue(v)) for v in value]
|
||||
)
|
||||
elif type(value) in (str, int): # default value processing
|
||||
try:
|
||||
mapping = self.fieldMappings[self.table][key]
|
||||
mapping = self.fieldMappings[self.current_table][key]
|
||||
except KeyError:
|
||||
raise NotSupportedError("No mapping defined for field '%s' in '%s'" % (key, self.table))
|
||||
raise NotSupportedError("No mapping defined for field '%s' in '%s'" % (key, self.tables))
|
||||
if len(mapping) == 1:
|
||||
mapping = mapping[0]
|
||||
if type(mapping) == str:
|
||||
return mapping
|
||||
elif callable(mapping):
|
||||
conds = mapping(key, value)
|
||||
conds = mapping(key, self.cleanValue(value))
|
||||
return self.andToken.join(["{} {}".format(*cond) for cond in conds])
|
||||
elif len(mapping) == 2:
|
||||
result = list()
|
||||
# iterate mapping and mapping source value synchronously over key and value
|
||||
for mapitem, val in zip(mapping, node):
|
||||
for mapitem, val in zip(mapping, node): # iterate mapping and mapping source value synchronously over key and value
|
||||
if type(mapitem) == str:
|
||||
result.append(mapitem)
|
||||
elif callable(mapitem):
|
||||
result.append(mapitem(val))
|
||||
result.append(mapitem(self.cleanValue(val)))
|
||||
return "{} {}".format(*result)
|
||||
else:
|
||||
raise TypeError("Backend does not support map values of type " + str(type(value)))
|
||||
elif isinstance(value, SigmaTypeModifier):
|
||||
try:
|
||||
mapping = self.fieldMappings[self.current_table][key]
|
||||
except KeyError:
|
||||
raise NotSupportedError("No mapping defined for field '%s' in '%s'" % (key, self.tables))
|
||||
return self.generateMapItemTypedNode(mapping[0], value)
|
||||
|
||||
return super().generateMapItemNode(node)
|
||||
|
||||
@@ -68,7 +68,7 @@ class SplunkBackend(SingleTextQueryBackend):
|
||||
agg.aggfunc_notrans = 'dc'
|
||||
return " | eventstats %s(%s) as val by %s | search val %s %s" % (agg.aggfunc_notrans, agg.aggfield or "", agg.groupfield or "", agg.cond_op, agg.condition)
|
||||
|
||||
|
||||
|
||||
def generate(self, sigmaparser):
|
||||
"""Method is called for each sigma rule and receives the parsed rule (SigmaParser)"""
|
||||
columns = list()
|
||||
@@ -106,7 +106,7 @@ class SplunkBackend(SingleTextQueryBackend):
|
||||
result += fields
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class SplunkXMLBackend(SingleTextQueryBackend, MultiRuleOutputMixin):
|
||||
"""Converts Sigma rule into XML used for Splunk Dashboard Panels"""
|
||||
identifier = "splunkxml"
|
||||
@@ -208,3 +208,6 @@ class CrowdStrikeBackend(SplunkBackend):
|
||||
return super().generate(sigmaparser)
|
||||
else:
|
||||
raise NotImplementedError("Not supported logsources!")
|
||||
|
||||
def generateMapItemTypedNode(self, fieldname, value):
|
||||
return super().generateMapItemTypedNode(fieldname=fieldname, value=value)
|
||||
@@ -13,11 +13,14 @@
|
||||
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sigma
|
||||
from sigma.parser.condition import ConditionOR
|
||||
from .base import SingleTextQueryBackend
|
||||
import sys
|
||||
|
||||
from sigma.backends.base import SingleTextQueryBackend
|
||||
from sigma.backends.exceptions import NotSupportedError
|
||||
from sigma.parser.condition import ConditionOR, SigmaAggregationParser
|
||||
|
||||
# Sumo specifics
|
||||
# https://help.sumologic.com/05Search/Search-Query-Language
|
||||
@@ -29,13 +32,13 @@ from .base import SingleTextQueryBackend
|
||||
|
||||
|
||||
class SumoLogicBackend(SingleTextQueryBackend):
|
||||
"""Converts Sigma rule into SumoLogic query"""
|
||||
"""Converts Sigma rule into SumoLogic query. Contributed by SOC Prime. https://socprime.com"""
|
||||
identifier = "sumologic"
|
||||
active = True
|
||||
config_required = False
|
||||
default_config = ["sysmon", "sumologic"]
|
||||
|
||||
index_field = "_index"
|
||||
index_field = "_sourceCategory"
|
||||
reClear = None
|
||||
andToken = " AND "
|
||||
orToken = " OR "
|
||||
@@ -59,22 +62,46 @@ class SumoLogicBackend(SingleTextQueryBackend):
|
||||
agg.groupfield = 'hostname'
|
||||
if agg.aggfunc_notrans == 'count() by':
|
||||
agg.aggfunc_notrans = 'count by'
|
||||
if agg.aggfunc == sigma.parser.condition.SigmaAggregationParser.AGGFUNC_NEAR:
|
||||
if agg.aggfunc == SigmaAggregationParser.AGGFUNC_NEAR:
|
||||
raise NotImplementedError("The 'near' aggregation operator is not yet implemented for this backend")
|
||||
# WIP
|
||||
# ex:
|
||||
# (QUERY) | timeslice 5m
|
||||
# | count_distinct(process) _timeslice,hostname
|
||||
# | where _count_distinct > 5
|
||||
# return " | timeslice %s | count_distinct(%s) %s | where _count_distinct > 0" % (self.interval, agg.aggfunc_notrans or "", agg.aggfield or "", agg.groupfield or "")
|
||||
# return " | timeslice %s | count_distinct(%s) %s | where _count_distinct %s %s" % (self.interval, agg.aggfunc_notrans, agg.aggfield or "", agg.groupfield or "", agg.cond_op, agg.condition)
|
||||
if not agg.groupfield:
|
||||
# return " | %s(%s) | when _count %s %s" % (agg.aggfunc_notrans, agg.aggfield or "", agg.cond_op, agg.condition)
|
||||
return " | %s %s | where _count %s %s" % (agg.aggfunc_notrans, agg.aggfield or "", agg.cond_op, agg.condition)
|
||||
elif agg.groupfield:
|
||||
return " | %s by %s | where _count %s %s" % (agg.aggfunc_notrans, agg.groupfield or "", agg.cond_op, agg.condition)
|
||||
if self.keypresent:
|
||||
if not agg.groupfield:
|
||||
if agg.aggfield:
|
||||
agg.aggfunc_notrans = "count_distinct"
|
||||
return " \n| %s(%s) \n| where _count_distinct %s %s" % (
|
||||
agg.aggfunc_notrans, agg.aggfield, agg.cond_op, agg.condition)
|
||||
else:
|
||||
return " \n| %s | where _count %s %s" % (
|
||||
agg.aggfunc_notrans, agg.cond_op, agg.condition)
|
||||
elif agg.groupfield:
|
||||
if agg.aggfield:
|
||||
agg.aggfunc_notrans = "count_distinct"
|
||||
return " \n| %s(%s) by %s \n| where _count_distinct %s %s" % (
|
||||
agg.aggfunc_notrans, agg.aggfield, agg.groupfield, agg.cond_op, agg.condition)
|
||||
else:
|
||||
return " \n| %s by %s \n| where _count %s %s" % (
|
||||
agg.aggfunc_notrans, agg.groupfield, agg.cond_op, agg.condition)
|
||||
else:
|
||||
return " \n| %s | where _count %s %s" % (agg.aggfunc_notrans, agg.cond_op, agg.condition)
|
||||
else:
|
||||
return " | %s(%s) by %s | where _count %s %s" % (agg.aggfunc_notrans, agg.aggfield or "", agg.groupfield or "", agg.cond_op, agg.condition)
|
||||
if not agg.groupfield:
|
||||
if agg.aggfield:
|
||||
agg.aggfunc_notrans = "count_distinct"
|
||||
return " \n| parse \"[%s=*]\" as searched nodrop\n| %s(searched) \n| where _count_distinct %s %s" % (
|
||||
agg.aggfield, agg.aggfunc_notrans, agg.cond_op, agg.condition)
|
||||
else:
|
||||
return " \n| %s | where _count %s %s" % (
|
||||
agg.aggfunc_notrans, agg.cond_op, agg.condition)
|
||||
elif agg.groupfield:
|
||||
if agg.aggfield:
|
||||
agg.aggfunc_notrans = "count_distinct"
|
||||
return " \n| parse \"[%s=*]\" as searched nodrop\n| parse \"[%s=*]\" as grpd nodrop\n| %s(searched) by grpd \n| where _count_distinct %s %s" % (
|
||||
agg.aggfield, agg.groupfield, agg.aggfunc_notrans, agg.cond_op, agg.condition)
|
||||
else:
|
||||
return " \n| parse \"[%s=*]\" as grpd nodrop\n| %s by grpd \n| where _count %s %s" % (
|
||||
agg.groupfield, agg.aggfunc_notrans, agg.cond_op, agg.condition)
|
||||
else:
|
||||
return " \n| %s | where _count %s %s" % (agg.aggfunc_notrans, agg.cond_op, agg.condition)
|
||||
|
||||
def generateBefore(self, parsed):
|
||||
# not required but makes query faster, especially if no FER or _index/_sourceCategory
|
||||
@@ -126,13 +153,17 @@ class SumoLogicBackend(SingleTextQueryBackend):
|
||||
if '|' in result:
|
||||
return result
|
||||
else:
|
||||
return "(" + result + ")"
|
||||
return result
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# TODO/FIXME! depending on deployment configuration, existing FER must be populate here (or backend config?)
|
||||
# aFL = ["EventID"]
|
||||
aFL = ["_index", "_sourceCategory", "_view", "EventID", "sourcename", "CommandLine", "NewProcessName", "Image", "ParentImage", "ParentCommandLine", "ParentProcessName"]
|
||||
aFL = ["_sourceCategory", "_view", "_sourceName"]
|
||||
if self.sigmaconfig.config.get("afl_fields"):
|
||||
self.keypresent = True
|
||||
aFL.extend(self.sigmaconfig.config.get("afl_fields"))
|
||||
else:
|
||||
self.keypresent = False
|
||||
for item in self.sigmaconfig.fieldmappings.values():
|
||||
if item.target_type is list:
|
||||
aFL.extend(item.target)
|
||||
@@ -146,25 +177,22 @@ class SumoLogicBackend(SingleTextQueryBackend):
|
||||
|
||||
# Clearing values from special characters.
|
||||
# Sumologic: only removing '*' (in quotes, is litteral. without, is wildcard) and '"'
|
||||
def CleanNode(self, node):
|
||||
search_ptrn = re.compile(r"[*\"\\]")
|
||||
replace_ptrn = re.compile(r"[*\"\\]")
|
||||
match = search_ptrn.search(str(node))
|
||||
new_node = list()
|
||||
if match:
|
||||
replaced_str = replace_ptrn.sub('*', node)
|
||||
node = [x for x in replaced_str.split('*') if x]
|
||||
new_node.extend(node)
|
||||
else:
|
||||
new_node.append(node)
|
||||
node = new_node
|
||||
|
||||
def cleanNode(self, node, key=None):
|
||||
if "*" in node and key and not re.search("[\s]", node):
|
||||
return node
|
||||
elif "*" in node and not key:
|
||||
return [x for x in node.split("*") if x]
|
||||
return node
|
||||
|
||||
# Clearing values from special characters.
|
||||
def generateMapItemNode(self, node):
|
||||
key, value = node
|
||||
if key in self.allowedFieldsList:
|
||||
if not self.mapListsSpecialHandling and type(value) in (
|
||||
if key in ["_sourceCategory", "_sourceName"]:
|
||||
value = "*%s*" % value.lower()
|
||||
return self.mapExpression % (key, value)
|
||||
elif not self.mapListsSpecialHandling and type(value) in (
|
||||
str, int, list) or self.mapListsSpecialHandling and type(value) in (str, int):
|
||||
if key in ("LogName", "source"):
|
||||
self.logname = value
|
||||
@@ -181,29 +209,29 @@ class SumoLogicBackend(SingleTextQueryBackend):
|
||||
str, int, list) or self.mapListsSpecialHandling and type(value) in (str, int):
|
||||
if type(value) is str:
|
||||
new_value = list()
|
||||
value = self.CleanNode(value)
|
||||
value = self.cleanNode(value)
|
||||
if type(value) == list:
|
||||
new_value.append(self.andToken.join([self.valueExpression % val for val in value]))
|
||||
new_value.append(self.andToken.join([self.cleanValue(val) for val in value]))
|
||||
else:
|
||||
new_value.append(value)
|
||||
if len(new_value) == 1:
|
||||
if self.generateANDNode(new_value):
|
||||
return "(" + self.generateANDNode(new_value) + ")"
|
||||
return self.generateANDNode(new_value)
|
||||
else:
|
||||
# if after cleaning node, it is empty but there is AND statement... make it true.
|
||||
return "true"
|
||||
else:
|
||||
return "(" + self.generateORNode(new_value) + ")"
|
||||
return self.generateORNode(new_value)
|
||||
else:
|
||||
return self.generateValueNode(value)
|
||||
elif type(value) is list:
|
||||
new_value = list()
|
||||
for item in value:
|
||||
item = self.CleanNode(item)
|
||||
item = self.cleanNode(item)
|
||||
if type(item) is list and len(item) == 1:
|
||||
new_value.append(self.valueExpression % item[0])
|
||||
new_value.append(item[0])
|
||||
elif type(item) is list:
|
||||
new_value.append(self.andToken.join([self.valueExpression % val for val in item]))
|
||||
new_value.append(self.andToken.join([self.cleanValue(val) for val in item]))
|
||||
else:
|
||||
new_value.append(item)
|
||||
return self.generateORNode(new_value)
|
||||
@@ -217,35 +245,25 @@ class SumoLogicBackend(SingleTextQueryBackend):
|
||||
# => OK only if field entry with list, not string
|
||||
# => generateNode: call cleanValue
|
||||
def cleanValue(self, val, key=''):
|
||||
# in sumologic, if key, can use wildcard outside of double quotes. if inside, it's litteral
|
||||
if key:
|
||||
val = re.sub(r'\"', '\\"', str(val))
|
||||
val = re.sub(r'(.+)\*(.+)', '"\g<1>"*"\g<2>"', val, 0)
|
||||
val = re.sub(r'^\*', '*"', val)
|
||||
val = re.sub(r'\*$', '"*', val)
|
||||
# if unbalanced wildcard?
|
||||
if val.startswith('*"') and not (val.endswith('"*') or val.endswith('"')):
|
||||
val = val + '"'
|
||||
if val.endswith('"*') and not (val.startswith('*"') or val.startswith('"')):
|
||||
val = '"' + val
|
||||
# double escape if end quote
|
||||
if val.endswith('\\"*') and not val.endswith('\\\\"*'):
|
||||
val = re.sub(r'\\"\*$', '\\\\\\"*', val)
|
||||
# if not key and not (val.startswith('"') and val.endswith('"')) and not (val.startswith('(') and val.endswith(')')) and not ('|' in val) and val:
|
||||
# apt_babyshark.yml
|
||||
if not (val.startswith('"') and val.endswith('"')) and not (val.startswith('(') and val.endswith(')')) and not ('|' in val) and not ('*' in val) and val and not '_index' in key and not '_sourceCategory' in key and not '_view' in key:
|
||||
val = '"%s"' % val
|
||||
if isinstance(val, str):
|
||||
val = re.sub("[^\\\"](\")", "\\\"", val)
|
||||
if re.search("[\W\s]", val):# and not val.startswith('"') and not val.endswith('"'): # or "\\" in node in [] or "/" in node:
|
||||
return self.valueExpression % val
|
||||
return val
|
||||
|
||||
# for keywords values with space
|
||||
def generateValueNode(self, node, key=''):
|
||||
cV = self.cleanValue(str(node), key)
|
||||
cV = self.cleanNode(str(node), key)
|
||||
if type(node) is int:
|
||||
return cV
|
||||
if type(cV) is list:
|
||||
return "(%s)" % "AND".join([self.cleanValue(item) for item in cV])
|
||||
if 'AND' in node and cV:
|
||||
return "(" + cV + ")"
|
||||
else:
|
||||
elif isinstance(node, str) and node.startswith('"') and node.endswith('"'):
|
||||
return cV
|
||||
else:
|
||||
return self.cleanValue(cV)
|
||||
|
||||
def generateMapItemListNode(self, key, value):
|
||||
itemslist = list()
|
||||
@@ -256,15 +274,209 @@ class SumoLogicBackend(SingleTextQueryBackend):
|
||||
itemslist.append('%s' % (self.generateValueNode(item)))
|
||||
return "(" + " OR ".join(itemslist) + ")"
|
||||
|
||||
# generateORNode algorithm for ArcSightBackend & SumoLogicBackend class.
|
||||
# generateORNode algorithm for SumoLogicBackend class.
|
||||
def generateORNode(self, node):
|
||||
if type(node) == ConditionOR and all(isinstance(item, str) for item in node):
|
||||
new_value = list()
|
||||
for value in node:
|
||||
value = self.CleanNode(value)
|
||||
value = self.cleanNode(value)
|
||||
if type(value) is list:
|
||||
new_value.append(self.andToken.join([self.valueExpression % val for val in value]))
|
||||
else:
|
||||
new_value.append(value)
|
||||
return "(" + self.orToken.join([self.generateNode(val) for val in new_value]) + ")"
|
||||
return "(" + self.orToken.join([self.generateNode(val) for val in node]) + ")"
|
||||
|
||||
|
||||
class SumoLogicCSE(SumoLogicBackend):
|
||||
"""Converts Sigma rule into SumoLogic CSE query. Contributed by SOC Prime. https://socprime.com"""
|
||||
identifier = "sumologic-cse"
|
||||
active = True
|
||||
config_required = False
|
||||
default_config = ["sysmon"]
|
||||
|
||||
index_field = "metdata_product"
|
||||
reClear = None
|
||||
#reEscape = re.compile('[\\\\"]')
|
||||
andToken = " and "
|
||||
orToken = " or "
|
||||
notToken = "!"
|
||||
subExpression = "(%s)"
|
||||
listExpression = "(%s)"
|
||||
listSeparator = ", "
|
||||
valueExpression = "\"%s\""
|
||||
nullExpression = "isEmpty(%s)"
|
||||
notNullExpression = "!isEmpty(%s)"
|
||||
mapExpression = "%s=%s"
|
||||
mapListsSpecialHandling = True
|
||||
mapListValueExpression = "%s IN %s"
|
||||
interval = None
|
||||
logname = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.allowedFieldsList.extend(["metdata_product", "metdata_vendor"])
|
||||
|
||||
def cleanValue(self, val, key=''):
|
||||
if key == 'metadata_deviceEventId' or isinstance(val, int) or val.isdigit():
|
||||
return val
|
||||
return self.valueExpression % val
|
||||
|
||||
def cleanNode(self, node, key=None):
|
||||
return node
|
||||
|
||||
# Clearing values from special characters.
|
||||
def generateMapItemNode(self, node):
|
||||
key, value = node
|
||||
if key:
|
||||
if not self.mapListsSpecialHandling and type(value) in (
|
||||
str, int, list) or self.mapListsSpecialHandling and type(value) in (str, int):
|
||||
if key in ("LogName", "source"):
|
||||
self.logname = value
|
||||
# need cleanValue if sigma entry with single quote
|
||||
return self.mapExpression % (key, self.cleanValue(value, key))
|
||||
elif type(value) is list:
|
||||
return self.generateMapItemListNode(key, value)
|
||||
elif value is None:
|
||||
return self.nullExpression % (key,)
|
||||
else:
|
||||
raise TypeError("Backend does not support map values of type " + str(type(value)))
|
||||
raise TypeError("Backend does not support query without key.")
|
||||
|
||||
def generateMapItemListNode(self, key, value):
|
||||
if len(value) == 1:
|
||||
return self.mapExpression % (key, value[0])
|
||||
return "%s IN (%s)" % (key, ", ".join([self.cleanValue(item, key) for item in value]))
|
||||
|
||||
|
||||
class SumoLogicCSERule(SumoLogicCSE):
|
||||
"""Converts Sigma rule into SumoLogic CSE query"""
|
||||
identifier = "sumologic-cse-rule"
|
||||
active = True
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize field mappings"""
|
||||
super().__init__(*args, **kwargs)
|
||||
self.techniques = self._load_mitre_file("techniques")
|
||||
self.allowedCategories = ["Threat Intelligence", "Initial Access", "Execution", "Persistence", "Privilege Escalation",
|
||||
"Defense Evasion", "Credential Access", "Discovery", "Lateral Movement", "Collection",
|
||||
"Command and Control", "Exfiltration", "Impact"]
|
||||
self.defaultCategory = "Unknown/Other"
|
||||
self.results = []
|
||||
|
||||
def find_technique(self, key_ids):
|
||||
for key_id in set(key_ids):
|
||||
if not key_id:
|
||||
continue
|
||||
for technique in self.techniques:
|
||||
if key_id == technique.get("technique_id", ""):
|
||||
yield technique
|
||||
|
||||
def _load_mitre_file(self, mitre_type):
|
||||
try:
|
||||
backend_dir = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "config", "mitre"))
|
||||
path = os.path.join(backend_dir, "{}.json".format(mitre_type))
|
||||
with open(path) as config_file:
|
||||
config = json.load(config_file)
|
||||
return config
|
||||
except (IOError, OSError) as e:
|
||||
print("Failed to open {} configuration file '%s': %s".format(path, str(e)), file=sys.stderr)
|
||||
return []
|
||||
except json.JSONDecodeError as e:
|
||||
print("Failed to parse {} configuration file '%s' as valid YAML: %s" % (path, str(e)), file=sys.stderr)
|
||||
return []
|
||||
|
||||
def skip_tactics_or_techniques(self, src_technics, src_tactics):
|
||||
tactics = set()
|
||||
technics = set()
|
||||
|
||||
local_storage_techniques = {item["technique_id"]: item for item in self.find_technique(src_technics)}
|
||||
|
||||
for key_id in src_technics:
|
||||
src_tactic = local_storage_techniques.get(key_id, {}).get("tactic")
|
||||
if not src_tactic:
|
||||
continue
|
||||
src_tactic = set(src_tactic)
|
||||
|
||||
for item in src_tactics:
|
||||
if item in src_tactic:
|
||||
technics.add(key_id)
|
||||
tactics.add(item)
|
||||
|
||||
return sorted(tactics), sorted(technics)
|
||||
|
||||
def parse_severity(self, old_severity):
|
||||
if old_severity.lower() == "critical":
|
||||
return "high"
|
||||
return old_severity
|
||||
|
||||
def get_tactics_and_techniques(self, tags):
|
||||
tactics = list()
|
||||
technics = list()
|
||||
|
||||
for tag in tags:
|
||||
tag = tag.replace("attack.", "")
|
||||
if re.match("[t][0-9]{4}", tag, re.IGNORECASE):
|
||||
technics.append(tag.title())
|
||||
elif re.match("[s][0-9]{4}", tag, re.IGNORECASE):
|
||||
continue
|
||||
else:
|
||||
if "_" in tag:
|
||||
tag = tag.replace("_", " ")
|
||||
tag = tag.title()
|
||||
tactics.append(tag)
|
||||
|
||||
return tactics, technics
|
||||
|
||||
def map_risk_score(self, level):
|
||||
if level == "critical":
|
||||
return 5
|
||||
elif level == "high":
|
||||
return 4
|
||||
elif level == "medium":
|
||||
return 3
|
||||
elif level == "low":
|
||||
return 2
|
||||
return 1
|
||||
|
||||
def create_rule(self, config):
|
||||
tags = config.get("tags", [])
|
||||
|
||||
tactics, technics = self.get_tactics_and_techniques(tags)
|
||||
tactics, technics = self.skip_tactics_or_techniques(technics, tactics)
|
||||
tactics = list(map(lambda s: s.replace(" ", ""), tactics))
|
||||
score = self.map_risk_score(config.get("level", "medium"))
|
||||
rule = {
|
||||
"name": "{} by {}".format(config.get("title"), config.get('author')),
|
||||
"description": "{} {}".format(config.get("description"), "Technique: {}.".format(",".join(technics))),
|
||||
"enabled": True,
|
||||
"expression": """{}""".format(config.get("translation", "")),
|
||||
"assetField": "device_hostname",
|
||||
"score": score,
|
||||
"stream": "record"
|
||||
}
|
||||
if tactics and tactics[0] in self.allowedCategories:
|
||||
rule.update({"category": tactics[0]})
|
||||
else:
|
||||
rule.update({"category": "Unknown/Other"})
|
||||
self.results.append(rule)
|
||||
#return json.dumps(rule, indent=4, sort_keys=False)
|
||||
|
||||
def generate(self, sigmaparser):
|
||||
translation = super().generate(sigmaparser)
|
||||
if translation:
|
||||
configs = sigmaparser.parsedyaml
|
||||
configs.update({"translation": translation})
|
||||
rule = self.create_rule(configs)
|
||||
return rule
|
||||
else:
|
||||
raise NotSupportedError("No table could be determined from Sigma rule")
|
||||
|
||||
def finalize(self):
|
||||
if len(self.results) == 1:
|
||||
return json.dumps(self.results[0], indent=4, sort_keys=False)
|
||||
elif len(self.results) > 1:
|
||||
return json.dumps(self.results, indent=4, sort_keys=False)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ class SysmonConfigBackend(SingleTextQueryBackend, MultiRuleOutputMixin):
|
||||
orToken = " OR "
|
||||
notToken = "NOT "
|
||||
subExpression = "(%s)"
|
||||
config_required = False
|
||||
config_required = True
|
||||
INCLUDE = "include"
|
||||
EXCLUDE = "exclude"
|
||||
conditionDict = {
|
||||
|
||||
@@ -207,6 +207,14 @@ class FieldMappingChain(object):
|
||||
def resolve(self, key, value, sigmaparser):
|
||||
if type(self.fieldmappings) == str: # one field mapping
|
||||
return (self.fieldmappings, value)
|
||||
elif isinstance(self.fieldmappings, ConditionalFieldMapping):
|
||||
logsource = sigmaparser.parsedyaml.get("logsource")
|
||||
condition = self.fieldmappings.conditions
|
||||
for source_type, logsource_item in logsource.items():
|
||||
if condition.get(source_type) and condition.get(source_type, {}).get(logsource_item):
|
||||
new_field = condition.get(source_type, {}).get(logsource_item)
|
||||
self.fieldmappings.default = new_field
|
||||
return self.fieldmappings.resolve(self.fieldmappings.source, value, sigmaparser)
|
||||
elif isinstance(self.fieldmappings, SimpleFieldMapping):
|
||||
return self.fieldmappings.resolve(key, value, sigmaparser)
|
||||
elif type(self.fieldmappings) == set:
|
||||
|
||||
@@ -21,7 +21,7 @@ def main():
|
||||
num_rules_used = 0
|
||||
for rule_file in rule_files:
|
||||
try:
|
||||
rule = yaml.safe_load(open(rule_file).read())
|
||||
rule = yaml.safe_load(open(rule_file, encoding="utf-8").read())
|
||||
except yaml.YAMLError:
|
||||
sys.stderr.write("Ignoring rule " + rule_file + " (parsing failed)\n")
|
||||
continue
|
||||
@@ -61,7 +61,10 @@ def main():
|
||||
"maxValue": curr_max_technique_count,
|
||||
"minValue": 0
|
||||
},
|
||||
"version": "2.2",
|
||||
"versions": {
|
||||
"navigator": "4.0",
|
||||
"layer": "4.0"
|
||||
},
|
||||
"techniques": scores,
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user