Skip to content

Commit 9ba370e

Browse files
Merge pull request #23 from UncoderIO/added-mittre-attack-tags-parsing
added mitre-attack parsing + mitre-attack render to platforms
2 parents 66edadc + 1f943a8 commit 9ba370e

File tree

17 files changed

+243
-27
lines changed

17 files changed

+243
-27
lines changed
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import json
2+
import os
3+
import urllib.request
4+
import ssl
5+
from urllib.error import HTTPError
6+
7+
from app.converter.tools.singleton_meta import SingletonMeta
8+
from const import ROOT_PROJECT_PATH
9+
10+
11+
class MitreConfig(metaclass=SingletonMeta):
12+
config_url: str = 'https://raw.githubusercontent.com/mitre/cti/master/enterprise-attack/enterprise-attack.json'
13+
mitre_source_types: tuple = ('mitre-attack', )
14+
tactics: dict = {}
15+
techniques: dict = {}
16+
17+
@staticmethod
18+
def __revoked_or_deprecated(entry: dict) -> bool:
19+
if entry.get("revoked") or entry.get("x_mitre_deprecated"):
20+
return True
21+
return False
22+
23+
def __get_mitre_json(self) -> dict:
24+
ctx = ssl.create_default_context()
25+
ctx.check_hostname = False
26+
ctx.verify_mode = ssl.CERT_NONE
27+
28+
try:
29+
with urllib.request.urlopen(self.config_url, context=ctx) as cti_json:
30+
return json.loads(cti_json.read().decode())
31+
except HTTPError:
32+
return {}
33+
def update_mitre_config(self) -> None:
34+
if not (mitre_json := self.__get_mitre_json()):
35+
self.__load_mitre_configs_from_files()
36+
return
37+
38+
tactic_map = {}
39+
technique_map = {}
40+
41+
# Map the tatics
42+
for entry in mitre_json["objects"]:
43+
if not entry["type"] == "x-mitre-tactic" or self.__revoked_or_deprecated(entry):
44+
continue
45+
for ref in entry["external_references"]:
46+
if ref["source_name"] == 'mitre-attack':
47+
tactic_map[entry["x_mitre_shortname"]] = entry["name"]
48+
self.tactics[entry["name"].replace(' ', '_').lower()] = {
49+
"external_id": ref["external_id"],
50+
"url": ref["url"],
51+
"tactic": entry["name"]
52+
}
53+
break
54+
55+
# Map the techniques
56+
for entry in mitre_json["objects"]:
57+
if not entry["type"] == "attack-pattern" or self.__revoked_or_deprecated(entry):
58+
continue
59+
if entry.get("x_mitre_is_subtechnique"):
60+
continue
61+
for ref in entry["external_references"]:
62+
if ref["source_name"] in self.mitre_source_types:
63+
technique_map[ref["external_id"]] = entry["name"]
64+
sub_tactics = []
65+
# Get Mitre Tactics (Kill-Chains)
66+
for tactic in entry["kill_chain_phases"]:
67+
if tactic["kill_chain_name"] in self.mitre_source_types:
68+
# Map the short phase_name to tactic name
69+
sub_tactics.append(tactic_map[tactic["phase_name"]])
70+
self.techniques[ref["external_id"].lower()] = {
71+
"technique_id": ref["external_id"],
72+
"technique": entry["name"],
73+
"url": ref["url"],
74+
"tactic": sub_tactics
75+
}
76+
break
77+
78+
## Map the sub-techniques
79+
for entry in mitre_json["objects"]:
80+
if not entry["type"] == "attack-pattern" or self.__revoked_or_deprecated(entry):
81+
continue
82+
if entry.get("x_mitre_is_subtechnique"):
83+
for ref in entry["external_references"]:
84+
if ref["source_name"] in self.mitre_source_types:
85+
sub_technique_id = ref["external_id"]
86+
sub_technique_name = entry["name"]
87+
parent_technique_name = technique_map[sub_technique_id.split(".")[0]]
88+
sub_technique_name = "{} : {}".format(parent_technique_name, sub_technique_name)
89+
self.techniques[ref["external_id"].lower()] = {
90+
"technique_id": ref["external_id"],
91+
"technique": sub_technique_name,
92+
"url": ref["url"],
93+
}
94+
break
95+
96+
def __load_mitre_configs_from_files(self) -> None:
97+
with open(os.path.join(ROOT_PROJECT_PATH, 'app/dictionaries/tactics.json'), 'r') as file:
98+
self.tactics = json.load(file)
99+
100+
with open(os.path.join(ROOT_PROJECT_PATH, 'app/dictionaries/techniques.json'), 'r') as file:
101+
self.techniques = json.load(file)
102+
103+
def get_tactic(self, tactic: str) -> dict:
104+
tactic = tactic.replace('.', '_')
105+
return self.tactics.get(tactic, {})
106+
107+
def get_technique(self, technique_id: str) -> dict:
108+
return self.techniques.get(technique_id, {})

siem-converter/app/converter/core/mixins/rule.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import json
2+
from typing import List
23

34
import yaml
45

56
from app.converter.core.exceptions.core import InvalidYamlStructure, InvalidJSONStructure
7+
from app.converter.core.mitre import MitreConfig
68

79

810
class JsonRuleMixin:
@@ -15,9 +17,28 @@ def load_rule(self, text):
1517

1618

1719
class YamlRuleMixin:
20+
mitre_config: MitreConfig = MitreConfig()
1821

1922
def load_rule(self, text):
2023
try:
2124
return yaml.safe_load(text)
2225
except yaml.YAMLError as err:
2326
raise InvalidYamlStructure(error=str(err))
27+
28+
def parse_mitre_attack(self, tags: List[str]) -> dict[str, list]:
29+
result = {
30+
'tactics': [],
31+
'techniques': []
32+
}
33+
for tag in tags:
34+
tag = tag.lower()
35+
if tag.startswith('attack.'):
36+
tag = tag[7::]
37+
if tag.startswith('t'):
38+
if technique := self.mitre_config.get_technique(tag):
39+
result['techniques'].append(technique)
40+
else:
41+
if tactic := self.mitre_config.get_tactic(tag):
42+
result['tactics'].append(tactic)
43+
44+
return result

siem-converter/app/converter/core/models/parser_output.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ def __init__(self, *,
1717
license_: str = None,
1818
severity: str = None,
1919
references: List[str] = None,
20-
tags: List[str] = None,
21-
mitre_attack: List[str] = None,
20+
tags: list[str] = None,
21+
mitre_attack: dict[str, list] = None,
2222
status: str = None,
2323
false_positives: List[str] = None,
2424
source_mapping_ids: List[str] = None

siem-converter/app/converter/platforms/chronicle/const.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77
version = "0.01"
88
rule_id = "<rule_id_place_holder>"
99
status = "<status_place_holder>"
10-
severity = "<severity_place_holder>"
10+
tags = "<tags_place_holder>"
1111
falsepositives = "<falsepositives_place_holder>"
12+
severity = "<severity_place_holder>"
13+
created = "<created_place_holder>"
1214
1315
events:
1416
<query_placeholder>

siem-converter/app/converter/platforms/chronicle/renders/chronicle_rule.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,4 +96,6 @@ def finalize_query(self, prefix: str, query: str, functions: str, meta_info: Met
9696
rule = rule.replace("<severity_place_holder>", meta_info.severity)
9797
rule = rule.replace("<status_place_holder>", meta_info.status)
9898
rule = rule.replace("<falsepositives_place_holder>", ', '.join(meta_info.false_positives))
99+
rule = rule.replace("<tags_place_holder>", ", ".join(meta_info.tags))
100+
rule = rule.replace("<created_place_holder>", str(meta_info.date))
99101
return rule

siem-converter/app/converter/platforms/elasticsearch/renders/detection_rule.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import copy
2121
import json
22+
from typing import Union
2223

2324
from app.converter.platforms.elasticsearch.const import ELASTICSEARCH_DETECTION_RULE, elasticsearch_rule_details
2425
from app.converter.platforms.elasticsearch.mapping import ElasticSearchMappings, elasticsearch_mappings
@@ -27,6 +28,7 @@
2728
from app.converter.core.models.platform_details import PlatformDetails
2829
from app.converter.core.models.parser_output import MetaInfoContainer
2930
from app.converter.tools.utils import concatenate_str, get_mitre_attack_str
31+
from app.converter.core.mitre import MitreConfig
3032

3133

3234
class ElasticSearchRuleFieldValue(ElasticSearchFieldValue):
@@ -36,6 +38,7 @@ class ElasticSearchRuleFieldValue(ElasticSearchFieldValue):
3638
class ElasticSearchRuleRender(ElasticSearchQueryRender):
3739
details: PlatformDetails = elasticsearch_rule_details
3840
mappings: ElasticSearchMappings = elasticsearch_mappings
41+
mitre: MitreConfig = MitreConfig()
3942

4043
or_token = "OR"
4144
and_token = "AND"
@@ -44,6 +47,46 @@ class ElasticSearchRuleRender(ElasticSearchQueryRender):
4447
field_value_map = ElasticSearchRuleFieldValue(or_token=or_token)
4548
query_pattern = "{prefix} {query} {functions}"
4649

50+
def __create_mitre_threat(self, mitre_attack: dict) -> Union[list, list[dict]]:
51+
if not mitre_attack.get('techniques'):
52+
return []
53+
threat = []
54+
55+
if not mitre_attack.get('tactics'):
56+
for technique in mitre_attack.get('techniques'):
57+
technique_name = technique['technique']
58+
if '.' in technique_name:
59+
technique_name = technique_name[:technique_name.index('.')]
60+
threat.append(technique_name)
61+
return threat
62+
63+
for tactic in mitre_attack['tactics']:
64+
tactic_render = {
65+
'id': tactic['external_id'],
66+
'name': tactic['tactic'],
67+
'reference': tactic['url']
68+
}
69+
sub_threat = {
70+
'tactic': tactic_render,
71+
'framework': 'MITRE ATT&CK',
72+
'technique': []
73+
}
74+
for technique in mitre_attack['techniques']:
75+
technique_id = technique['technique_id'].lower()
76+
if '.' in technique_id:
77+
technique_id = technique_id[:technique['technique_id'].index('.')]
78+
main_technique = self.mitre.get_technique(technique_id)
79+
if tactic['tactic'] in main_technique['tactic']:
80+
sub_threat['technique'].append({
81+
"id": main_technique['technique_id'],
82+
"name": main_technique['technique'],
83+
"reference": main_technique['url']
84+
})
85+
if len(sub_threat['technique']) > 0:
86+
threat.append(sub_threat)
87+
88+
return threat
89+
4790
def finalize_query(self, prefix: str, query: str, functions: str, meta_info: MetaInfoContainer,
4891
source_mapping: SourceMapping = None, not_supported_functions: list = None):
4992
query = super().finalize_query(prefix=prefix, query=query, functions=functions, meta_info=meta_info)
@@ -61,7 +104,8 @@ def finalize_query(self, prefix: str, query: str, functions: str, meta_info: Met
61104
"severity": meta_info.severity,
62105
"references": meta_info.references,
63106
"license": meta_info.license,
64-
"tags": meta_info.mitre_attack,
107+
"tags": meta_info.tags,
108+
"threat": self.__create_mitre_threat(meta_info.mitre_attack),
65109
"false_positives": meta_info.false_positives
66110
})
67111
rule_str = json.dumps(rule, indent=4, sort_keys=False, ensure_ascii=False)

siem-converter/app/converter/platforms/elasticsearch/renders/elast_alert.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,7 @@ def finalize_query(self, prefix: str, query: str, functions: str, meta_info: Met
5252
"<description_place_holder>",
5353
get_rule_description_str(
5454
description=meta_info.description,
55-
license=meta_info.license,
56-
mitre_attack=meta_info.mitre_attack
55+
license=meta_info.license
5756
)
5857
)
5958
rule = rule.replace("<title_place_holder>", meta_info.title)

siem-converter/app/converter/platforms/elasticsearch/renders/kibana.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,7 @@ def finalize_query(self, prefix: str, query: str, functions: str, meta_info: Met
5353
author=meta_info.author,
5454
rule_id=meta_info.id,
5555
license=meta_info.license,
56-
references=meta_info.references,
57-
mitre_attack=meta_info.mitre_attack
56+
references=meta_info.references
5857
)
5958
rule_str = json.dumps(rule, indent=4, sort_keys=False)
6059
if not_supported_functions:

siem-converter/app/converter/platforms/logscale/renders/logscale_alert.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from app.converter.core.mapping import SourceMapping
2626
from app.converter.core.models.platform_details import PlatformDetails
2727
from app.converter.core.models.parser_output import MetaInfoContainer
28-
from app.converter.tools.utils import get_rule_description_str
28+
from app.converter.tools.utils import get_rule_description_str, get_mitre_attack_str
2929

3030

3131
_AUTOGENERATED_TITLE = "Autogenerated Falcon LogScale Alert"
@@ -45,10 +45,14 @@ def finalize_query(self, prefix: str, query: str, functions: str, meta_info: Met
4545
rule = copy.deepcopy(DEFAULT_LOGSCALE_ALERT)
4646
rule['query']['queryString'] = query
4747
rule['name'] = meta_info.title or _AUTOGENERATED_TITLE
48+
mitre_attack = []
49+
if meta_info.mitre_attack:
50+
mitre_attack = [f"ATTACK.{i['tactic']}" for i in meta_info.mitre_attack.get('tactics', [])]
51+
mitre_attack.extend([f"ATTACK.{i['technique_id']}" for i in meta_info.mitre_attack.get('techniques', [])])
4852
rule['description'] = get_rule_description_str(
4953
description=meta_info.description,
5054
license=meta_info.license,
51-
mitre_attack=meta_info.mitre_attack,
55+
mitre_attack=mitre_attack,
5256
author=meta_info.author
5357
)
5458

siem-converter/app/converter/platforms/microsoft/renders/microsoft_sentinel_rule.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,18 @@ class MicrosoftSentinelRuleRender(MicrosoftSentinelQueryRender):
4040
or_token = "or"
4141
field_value_map = MicrosoftSentinelRuleFieldValue(or_token=or_token)
4242

43+
def __create_mitre_threat(self, meta_info: MetaInfoContainer) -> tuple[list, list]:
44+
tactics = []
45+
techniques = []
46+
47+
for tactic in meta_info.mitre_attack.get('tactics'):
48+
tactics.append(tactic['tactic'])
49+
50+
for technique in meta_info.mitre_attack.get('techniques'):
51+
techniques.append(technique['technique_id'])
52+
53+
return tactics, techniques
54+
4355
def finalize_query(self, prefix: str, query: str, functions: str, meta_info: MetaInfoContainer,
4456
source_mapping: SourceMapping = None, not_supported_functions: list = None):
4557
query = super().finalize_query(prefix=prefix, query=query, functions=functions, meta_info=meta_info)
@@ -52,7 +64,9 @@ def finalize_query(self, prefix: str, query: str, functions: str, meta_info: Met
5264
license=meta_info.license
5365
)
5466
rule["severity"] = meta_info.severity
55-
rule["techniques"] = [el.upper() for el in meta_info.mitre_attack]
67+
mitre_tactics, mitre_techniques = self.__create_mitre_threat(meta_info=meta_info)
68+
rule['tactics'] = mitre_tactics
69+
rule['techniques'] = mitre_techniques
5670
json_rule = json.dumps(rule, indent=4, sort_keys=False)
5771
if not_supported_functions:
5872
rendered_not_supported = self.render_not_supported_functions(not_supported_functions)

siem-converter/app/converter/platforms/roota/parsers/roota.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1616
-----------------------------------------------------------------
1717
"""
18+
import re
1819

1920
from app.converter.core.exceptions.core import UnsupportedRootAParser, RootARuleValidationException
2021
from app.converter.core.mixins.rule import YamlRuleMixin
@@ -27,16 +28,19 @@ class RootAParser(YamlRuleMixin):
2728
parsers = parser_manager
2829
mandatory_fields = {"name", "details", "author", "severity", "mitre-attack", "detection", "references", "license"}
2930

30-
@staticmethod
31-
def __update_meta_info(meta_info: MetaInfoContainer, rule: dict) -> MetaInfoContainer:
31+
def __update_meta_info(self, meta_info: MetaInfoContainer, rule: dict) -> MetaInfoContainer:
3232
mitre_attack = rule.get("mitre-attack") or []
33-
mitre_attack = [i.strip("") for i in mitre_attack.split(",")] if isinstance(mitre_attack, str) else mitre_attack
33+
mitre_tags = [i.strip("") for i in mitre_attack.split(",")] if isinstance(mitre_attack, str) else mitre_attack
34+
mitre_attack = self.parse_mitre_attack(mitre_tags)
35+
rule_tags = rule.get('tags', [])
36+
rule_tags += mitre_tags
37+
3438
meta_info.title = rule.get("name")
3539
meta_info.description = rule.get("details")
3640
meta_info.id = rule.get("uuid", meta_info.id)
3741
meta_info.references = rule.get("references")
3842
meta_info.license = rule.get("license", meta_info.license)
39-
meta_info.tags = rule.get("tags", meta_info.tags)
43+
meta_info.tags = rule_tags or meta_info.tags
4044
meta_info.mitre_attack = mitre_attack
4145
meta_info.date = rule.get("date", meta_info.date)
4246
meta_info.author = rule.get("author", meta_info.author)

siem-converter/app/converter/platforms/sigma/parsers/sigma.py

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,6 @@ class SigmaParser(YamlRuleMixin):
3838
mappings: SigmaMappings = sigma_mappings
3939
mandatory_fields = {"title", "description", "references", "logsource", "detection"}
4040

41-
@staticmethod
42-
def __parse_mitre_attack(tags: List[str]) -> List[str]:
43-
result = []
44-
for tag in tags:
45-
if search := re.search(r"[tT]\d{4}(?:\.\d{3})?", tag):
46-
result.append(search.group())
47-
48-
return result
49-
5041
@staticmethod
5142
def __parse_false_positives(false_positives: Union[str, List[str], None]) -> list:
5243
if isinstance(false_positives, str):
@@ -62,9 +53,10 @@ def _get_meta_info(self, rule: dict, source_mapping_ids: List[str]) -> MetaInfoC
6253
date=rule.get("date"),
6354
references=rule.get("references", []),
6455
license_=rule.get("license"),
65-
mitre_attack=self.__parse_mitre_attack(rule.get("tags", [])),
56+
mitre_attack=self.parse_mitre_attack(rule.get("tags", [])),
6657
severity=rule.get("level"),
6758
status=rule.get("status"),
59+
tags=rule.get("tags"),
6860
false_positives=self.__parse_false_positives(rule.get("falsepositives")),
6961
source_mapping_ids=source_mapping_ids
7062
)

0 commit comments

Comments
 (0)