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)

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy