Skip to content

Commit fa2a565

Browse files
committed
resolve conflicts
1 parent 1843327 commit fa2a565

File tree

24 files changed

+524
-107
lines changed

24 files changed

+524
-107
lines changed

uncoder-core/app/translator/core/mitre.py

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
import ssl
44
import urllib.request
55
from json import JSONDecodeError
6+
from typing import Optional
67
from urllib.error import HTTPError
78

9+
from app.translator.core.models.query_container import MitreInfoContainer, MitreTacticContainer, MitreTechniqueContainer
810
from app.translator.tools.singleton_meta import SingletonMeta
911
from const import ROOT_PROJECT_PATH
1012

@@ -116,9 +118,31 @@ def __load_mitre_configs_from_files(self) -> None:
116118
except JSONDecodeError:
117119
self.techniques = {}
118120

119-
def get_tactic(self, tactic: str) -> dict:
121+
def get_tactic(self, tactic: str) -> Optional[MitreTacticContainer]:
120122
tactic = tactic.replace(".", "_")
121-
return self.tactics.get(tactic, {})
122-
123-
def get_technique(self, technique_id: str) -> dict:
124-
return self.techniques.get(technique_id, {})
123+
if tactic_found := self.tactics.get(tactic):
124+
return MitreTacticContainer(
125+
external_id=tactic_found["external_id"], url=tactic_found["url"], name=tactic_found["tactic"]
126+
)
127+
128+
def get_technique(self, technique_id: str) -> Optional[MitreTechniqueContainer]:
129+
if technique_found := self.techniques.get(technique_id):
130+
return MitreTechniqueContainer(
131+
technique_id=technique_found["technique_id"],
132+
name=technique_found["technique"],
133+
url=technique_found["url"],
134+
tactic=technique_found["tactic"],
135+
)
136+
137+
def get_mitre_info(
138+
self, tactics: Optional[list[str]] = None, techniques: Optional[list[str]] = None
139+
) -> MitreInfoContainer:
140+
tactics_list = []
141+
techniques_list = []
142+
for tactic in tactics or []:
143+
if tactic_found := self.get_tactic(tactic=tactic.lower()):
144+
tactics_list.append(tactic_found)
145+
for technique in techniques or []:
146+
if technique_found := self.get_technique(technique_id=technique.lower()):
147+
techniques_list.append(technique_found)
148+
return MitreInfoContainer(tactics=tactics_list, techniques=techniques_list)

uncoder-core/app/translator/core/mixins/rule.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
import yaml
66

77
from app.translator.core.exceptions.core import InvalidJSONStructure, InvalidXMLStructure, InvalidYamlStructure
8-
from app.translator.core.mitre import MitreConfig
8+
from app.translator.core.mitre import MitreConfig, MitreInfoContainer
99

1010

1111
class JsonRuleMixin:
12+
mitre_config: MitreConfig = MitreConfig()
13+
1214
@staticmethod
1315
def load_rule(text: str) -> dict:
1416
try:
@@ -27,18 +29,19 @@ def load_rule(text: str) -> dict:
2729
except yaml.YAMLError as err:
2830
raise InvalidYamlStructure(error=str(err)) from err
2931

30-
def parse_mitre_attack(self, tags: list[str]) -> dict[str, list]:
31-
result = {"tactics": [], "techniques": []}
32+
def parse_mitre_attack(self, tags: list[str]) -> MitreInfoContainer:
33+
parsed_techniques = []
34+
parsed_tactics = []
3235
for tag in set(tags):
3336
tag = tag.lower()
3437
if tag.startswith("attack."):
3538
tag = tag[7::]
3639
if tag.startswith("t"):
3740
if technique := self.mitre_config.get_technique(tag):
38-
result["techniques"].append(technique)
41+
parsed_techniques.append(technique)
3942
elif tactic := self.mitre_config.get_tactic(tag):
40-
result["tactics"].append(tactic)
41-
return result
43+
parsed_tactics.append(tactic)
44+
return MitreInfoContainer(tactics=parsed_tactics, techniques=parsed_techniques)
4245

4346

4447
class XMLRuleMixin:

uncoder-core/app/translator/core/models/query_container.py

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import uuid
22
from dataclasses import dataclass, field
3-
from datetime import datetime
3+
from datetime import datetime, timedelta
44
from typing import Optional
55

66
from app.translator.core.const import QUERY_TOKEN_TYPE
@@ -10,43 +10,72 @@
1010
from app.translator.core.models.query_tokens.field import Field
1111

1212

13+
@dataclass
14+
class MitreTechniqueContainer:
15+
technique_id: str
16+
name: str
17+
url: str
18+
tactic: list[str]
19+
20+
21+
@dataclass
22+
class MitreTacticContainer:
23+
external_id: str
24+
url: str
25+
name: str
26+
27+
28+
@dataclass
29+
class MitreInfoContainer:
30+
tactics: list[MitreTacticContainer] = field(default_factory=list)
31+
techniques: list[MitreTechniqueContainer] = field(default_factory=list)
32+
33+
1334
class MetaInfoContainer:
1435
def __init__(
1536
self,
1637
*,
1738
id_: Optional[str] = None,
1839
title: Optional[str] = None,
1940
description: Optional[str] = None,
20-
author: Optional[str] = None,
41+
author: Optional[list[str]] = None,
2142
date: Optional[str] = None,
2243
output_table_fields: Optional[list[Field]] = None,
2344
query_fields: Optional[list[Field]] = None,
2445
license_: Optional[str] = None,
2546
severity: Optional[str] = None,
2647
references: Optional[list[str]] = None,
2748
tags: Optional[list[str]] = None,
28-
mitre_attack: Optional[dict[str, list]] = None,
49+
raw_mitre_attack: Optional[list[str]] = None,
2950
status: Optional[str] = None,
3051
false_positives: Optional[list[str]] = None,
3152
source_mapping_ids: Optional[list[str]] = None,
3253
parsed_logsources: Optional[dict] = None,
54+
timeframe: Optional[timedelta] = None,
55+
mitre_attack: MitreInfoContainer = MitreInfoContainer(),
3356
) -> None:
3457
self.id = id_ or str(uuid.uuid4())
3558
self.title = title or ""
3659
self.description = description or ""
37-
self.author = author or ""
60+
self.author = [v.strip() for v in author] if author else []
3861
self.date = date or datetime.now().date().strftime("%Y-%m-%d")
3962
self.output_table_fields = output_table_fields or []
4063
self.query_fields = query_fields or []
4164
self.license = license_ or "DRL 1.1"
4265
self.severity = severity or SeverityType.low
4366
self.references = references or []
4467
self.tags = tags or []
45-
self.mitre_attack = mitre_attack or {}
68+
self.mitre_attack = mitre_attack or None
69+
self.raw_mitre_attack = raw_mitre_attack or []
4670
self.status = status or "stable"
4771
self.false_positives = false_positives or []
48-
self.source_mapping_ids = source_mapping_ids or [DEFAULT_MAPPING_NAME]
72+
self.source_mapping_ids = sorted(source_mapping_ids) if source_mapping_ids else [DEFAULT_MAPPING_NAME]
4973
self.parsed_logsources = parsed_logsources or {}
74+
self.timeframe = timeframe
75+
76+
@property
77+
def author_str(self) -> str:
78+
return ", ".join(self.author)
5079

5180

5281
@dataclass

uncoder-core/app/translator/core/render.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -208,15 +208,17 @@ def wrap_with_not_supported_functions(self, query: str, not_supported_functions:
208208
return query
209209

210210
def wrap_with_unmapped_fields(self, query: str, fields: Optional[list[str]]) -> str:
211-
if fields:
211+
if wrap_query_with_meta_info_ctx_var.get() and fields:
212212
return query + "\n\n" + self.wrap_with_comment(f"{self.unmapped_fields_text}{', '.join(fields)}")
213213
return query
214214

215215
def wrap_with_comment(self, value: str) -> str:
216216
return f"{self.comment_symbol} {value}"
217217

218218
@abstractmethod
219-
def generate(self, query_container: Union[RawQueryContainer, TokenizedQueryContainer]) -> str:
219+
def generate(
220+
self, raw_query_container: RawQueryContainer, tokenized_query_container: Optional[TokenizedQueryContainer]
221+
) -> str:
220222
raise NotImplementedError("Abstract method")
221223

222224

@@ -318,7 +320,7 @@ def wrap_with_meta_info(self, query: str, meta_info: Optional[MetaInfoContainer]
318320
meta_info_dict = {
319321
"name: ": meta_info.title,
320322
"uuid: ": meta_info.id,
321-
"author: ": meta_info.author if meta_info.author else "not defined in query/rule",
323+
"author: ": meta_info.author_str or "not defined in query/rule",
322324
"licence: ": meta_info.license,
323325
}
324326
query_meta_info = "\n".join(
@@ -370,7 +372,7 @@ def finalize(self, queries_map: dict[str, str]) -> str:
370372

371373
return result
372374

373-
def _get_source_mappings(self, source_mapping_ids: list[str]) -> list[SourceMapping]:
375+
def _get_source_mappings(self, source_mapping_ids: list[str]) -> Optional[list[SourceMapping]]:
374376
source_mappings = []
375377
for source_mapping_id in source_mapping_ids:
376378
if source_mapping := self.mappings.get_source_mapping(source_mapping_id):
@@ -468,8 +470,9 @@ def generate_from_tokenized_query_container(self, query_container: TokenizedQuer
468470
raise errors[0]
469471
return self.finalize(queries_map)
470472

471-
def generate(self, query_container: Union[RawQueryContainer, TokenizedQueryContainer]) -> str:
472-
if isinstance(query_container, RawQueryContainer):
473-
return self.generate_from_raw_query_container(query_container)
474-
475-
return self.generate_from_tokenized_query_container(query_container)
473+
def generate(
474+
self, raw_query_container: RawQueryContainer, tokenized_query_container: Optional[TokenizedQueryContainer]
475+
) -> str:
476+
if tokenized_query_container:
477+
return self.generate_from_tokenized_query_container(tokenized_query_container)
478+
return self.generate_from_raw_query_container(raw_query_container)

uncoder-core/app/translator/platforms/chronicle/parsers/chronicle_rule.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,10 @@
3131
@parser_manager.register
3232
class ChronicleRuleParser(ChronicleQueryParser):
3333
details: PlatformDetails = chronicle_rule_details
34-
rule_name_pattern = "rule\s(?P<rule_name>[a-z0-9_]+)\s{"
35-
meta_info_pattern = "meta:\n(?P<meta_info>[a-zA-Z0-9_\\\.*,>–<—~#$’`:;%+^\|?!@\s\"/=\-&'\(\)\[\]]+)\n\s+events:" # noqa: RUF001
36-
rule_pattern = "events:\n\s*(?P<query>[a-zA-Z\w0-9_%{}\|\.,!#^><:~\s\"\/=+?\-–&;$()`\*@\[\]'\\\]+)\n\s+condition:" # noqa: RUF001
37-
event_name_pattern = "condition:\n\s*(?P<event_name>\$[a-zA-Z_0-9]+)\n"
34+
rule_name_pattern = r"rule\s+(?P<rule_name>[a-zA-Z0-9_]+)\s+{"
35+
meta_info_pattern = r"meta:\n(?P<meta_info>[a-zA-Z0-9_\\\.*,>–<—~#$’`:;%+^\|?!@\s\"/=\-&'\(\)\[\]]+)\n\s+events:" # noqa: RUF001
36+
rule_pattern = r"events:\n\s*(?P<query>[a-zA-Z\w0-9_%{}\|\.,!#^><:~\s\"\/=+?\-–&;$()`\*@\[\]'\\]+)\n\s+condition:" # noqa: RUF001
37+
event_name_pattern = r"condition:\n\s*(?P<event_name>\$[a-zA-Z_0-9]+)\n"
3838
mappings: ChronicleMappings = chronicle_rule_mappings
3939
tokenizer = ChronicleRuleTokenizer()
4040

uncoder-core/app/translator/platforms/chronicle/renders/chronicle_rule.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ def finalize_query(
119119
rule = DEFAULT_CHRONICLE_SECURITY_RULE.replace("<query_placeholder>", query)
120120
rule = rule.replace("<title_place_holder>", self.prepare_title(meta_info.title) or _AUTOGENERATED_TEMPLATE)
121121
description = meta_info.description or _AUTOGENERATED_TEMPLATE
122-
rule = rule.replace("<author_place_holder>", meta_info.author)
122+
rule = rule.replace("<author_place_holder>", ", ".join(meta_info.author))
123123
rule = rule.replace("<description_place_holder>", description)
124124
rule = rule.replace("<licence_place_holder>", meta_info.license)
125125
rule = rule.replace("<rule_id_place_holder>", meta_info.id)

uncoder-core/app/translator/platforms/elasticsearch/parsers/detection_rule.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from app.translator.managers import parser_manager
2323
from app.translator.platforms.elasticsearch.const import elasticsearch_rule_details
2424
from app.translator.platforms.elasticsearch.parsers.elasticsearch import ElasticSearchQueryParser
25+
from app.translator.tools.utils import parse_rule_description_str
2526

2627

2728
@parser_manager.register
@@ -30,8 +31,25 @@ class ElasticSearchRuleParser(ElasticSearchQueryParser, JsonRuleMixin):
3031

3132
def parse_raw_query(self, text: str, language: str) -> RawQueryContainer:
3233
rule = self.load_rule(text=text)
34+
parsed_description = parse_rule_description_str(rule.get("description", ""))
35+
36+
mitre_attack = self.mitre_config.get_mitre_info(
37+
tactics=[threat_data["tactic"]["name"].replace(" ", "_").lower() for threat_data in rule.get("threat", [])],
38+
techniques=[threat_data["technique"][0]["id"].lower() for threat_data in rule.get("threat", [])],
39+
)
40+
3341
return RawQueryContainer(
3442
query=rule["query"],
3543
language=language,
36-
meta_info=MetaInfoContainer(title=rule["name"], description=rule["description"]),
44+
meta_info=MetaInfoContainer(
45+
id_=rule.get("rule_id"),
46+
title=rule.get("name"),
47+
description=parsed_description.get("description") or rule.get("description"),
48+
references=rule.get("references", []),
49+
author=parsed_description.get("author") or rule.get("author"),
50+
severity=rule.get("severity"),
51+
license_=parsed_description.get("license"),
52+
tags=rule.get("tags"),
53+
mitre_attack=mitre_attack,
54+
),
3755
)

uncoder-core/app/translator/platforms/elasticsearch/renders/detection_rule.py

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from typing import Optional, Union
2323

2424
from app.translator.core.mapping import SourceMapping
25-
from app.translator.core.mitre import MitreConfig
25+
from app.translator.core.mitre import MitreConfig, MitreInfoContainer
2626
from app.translator.core.models.platform_details import PlatformDetails
2727
from app.translator.core.models.query_container import MetaInfoContainer
2828
from app.translator.managers import render_manager
@@ -33,6 +33,7 @@
3333
ElasticSearchFieldValue,
3434
ElasticSearchQueryRender,
3535
)
36+
from app.translator.tools.utils import get_rule_description_str
3637

3738
_AUTOGENERATED_TEMPLATE = "Autogenerated Elastic Rule"
3839

@@ -53,25 +54,25 @@ class ElasticSearchRuleRender(ElasticSearchQueryRender):
5354

5455
field_value_render = ElasticSearchRuleFieldValue(or_token=or_token)
5556

56-
def __create_mitre_threat(self, mitre_attack: dict) -> Union[list, list[dict]]:
57-
if not mitre_attack.get("techniques"):
57+
def __create_mitre_threat(self, mitre_attack: MitreInfoContainer) -> Union[list, list[dict]]:
58+
if not mitre_attack.techniques:
5859
return []
5960
threat = []
6061

61-
for tactic in mitre_attack["tactics"]:
62-
tactic_render = {"id": tactic["external_id"], "name": tactic["tactic"], "reference": tactic["url"]}
62+
for tactic in mitre_attack.tactics:
63+
tactic_render = {"id": tactic.external_id, "name": tactic.name, "reference": tactic.url}
6364
sub_threat = {"tactic": tactic_render, "framework": "MITRE ATT&CK", "technique": []}
64-
for technique in mitre_attack["techniques"]:
65-
technique_id = technique["technique_id"].lower()
65+
for technique in mitre_attack.techniques:
66+
technique_id = technique.technique_id.lower()
6667
if "." in technique_id:
67-
technique_id = technique_id[: technique["technique_id"].index(".")]
68+
technique_id = technique_id[: technique.technique_id.index(".")]
6869
main_technique = self.mitre.get_technique(technique_id)
69-
if tactic["tactic"] in main_technique["tactic"]:
70+
if tactic.name in main_technique.tactic:
7071
sub_threat["technique"].append(
7172
{
72-
"id": main_technique["technique_id"],
73-
"name": main_technique["technique"],
74-
"reference": main_technique["url"],
73+
"id": main_technique.technique_id,
74+
"name": main_technique.name,
75+
"reference": main_technique.url,
7576
}
7677
)
7778
if len(sub_threat["technique"]) > 0:
@@ -94,13 +95,17 @@ def finalize_query(
9495
query = super().finalize_query(prefix=prefix, query=query, functions=functions)
9596
rule = copy.deepcopy(ELASTICSEARCH_DETECTION_RULE)
9697
index = source_mapping.log_source_signature.default_source.get("index") if source_mapping else None
98+
description_str = get_rule_description_str(
99+
description=meta_info.description or rule["description"] or _AUTOGENERATED_TEMPLATE,
100+
license_=meta_info.license,
101+
)
97102
rule.update(
98103
{
99104
"query": query,
100-
"description": meta_info.description or rule["description"] or _AUTOGENERATED_TEMPLATE,
105+
"description": description_str,
101106
"name": meta_info.title or _AUTOGENERATED_TEMPLATE,
102107
"rule_id": meta_info.id,
103-
"author": [meta_info.author],
108+
"author": meta_info.author,
104109
"severity": meta_info.severity,
105110
"references": meta_info.references,
106111
"license": meta_info.license,

uncoder-core/app/translator/platforms/elasticsearch/renders/elast_alert.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,18 @@ def finalize_query(
6666
) -> str:
6767
query = super().finalize_query(prefix=prefix, query=query, functions=functions)
6868
rule = ELASTICSEARCH_ALERT.replace("<query_placeholder>", query)
69+
mitre_attack = []
70+
if meta_info and meta_info.mitre_attack:
71+
mitre_attack.extend([technique.technique_id for technique in meta_info.mitre_attack.techniques])
72+
mitre_attack.extend([tactic.name for tactic in meta_info.mitre_attack.tactics])
6973
rule = rule.replace(
7074
"<description_place_holder>",
7175
get_rule_description_str(
7276
author=meta_info.author,
7377
description=meta_info.description or _AUTOGENERATED_TEMPLATE,
7478
license_=meta_info.license,
7579
rule_id=meta_info.id,
80+
mitre_attack=mitre_attack,
7681
),
7782
)
7883
rule = rule.replace("<title_place_holder>", meta_info.title or _AUTOGENERATED_TEMPLATE)

uncoder-core/app/translator/platforms/elasticsearch/renders/kibana.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,17 @@ def finalize_query(
6868
rule["_source"]["kibanaSavedObjectMeta"]["searchSourceJSON"] = dumped_rule
6969
rule["_source"]["title"] = meta_info.title or _AUTOGENERATED_TEMPLATE
7070
descr = meta_info.description or rule["_source"]["description"] or _AUTOGENERATED_TEMPLATE
71+
mitre_attack = []
72+
if meta_info and meta_info.mitre_attack:
73+
mitre_attack.extend([technique.technique_id for technique in meta_info.mitre_attack.techniques])
74+
mitre_attack.extend([tactic.name for tactic in meta_info.mitre_attack.tactics])
7175
rule["_source"]["description"] = get_rule_description_str(
7276
description=descr,
7377
author=meta_info.author,
7478
rule_id=meta_info.id,
7579
license_=meta_info.license,
7680
references=meta_info.references,
81+
mitre_attack=mitre_attack,
7782
)
7883
rule_str = json.dumps(rule, indent=4, sort_keys=False)
7984
rule_str = self.wrap_with_unmapped_fields(rule_str, unmapped_fields)

0 commit comments

Comments
 (0)