Skip to content

Commit 021ff84

Browse files
authored
Merge pull request #182 from UncoderIO/gis-roota-render
Gis roota render
2 parents 94e16aa + 342f8db commit 021ff84

File tree

47 files changed

+681
-411
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+681
-411
lines changed

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

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,14 @@ class LogSourceSignature(ABC):
1919
wildcard_symbol = "*"
2020

2121
@abstractmethod
22-
def is_suitable(self, *args, **kwargs) -> bool:
22+
def is_suitable(self, **kwargs) -> bool:
2323
raise NotImplementedError("Abstract method")
2424

25+
@staticmethod
26+
def _check_conditions(conditions: list[Union[bool, None]]) -> bool:
27+
conditions = [condition for condition in conditions if condition is not None]
28+
return bool(conditions) and all(conditions)
29+
2530
@abstractmethod
2631
def __str__(self) -> str:
2732
raise NotImplementedError("Abstract method")
@@ -147,9 +152,23 @@ def prepare_fields_mapping(field_mapping: dict) -> FieldsMapping:
147152
def prepare_log_source_signature(self, mapping: dict) -> LogSourceSignature:
148153
raise NotImplementedError("Abstract method")
149154

150-
@abstractmethod
151-
def get_suitable_source_mappings(self, *args, **kwargs) -> list[SourceMapping]:
152-
raise NotImplementedError("Abstract method")
155+
def get_suitable_source_mappings(
156+
self, field_names: list[str], log_sources: dict[str, list[Union[int, str]]]
157+
) -> list[SourceMapping]:
158+
by_log_sources_and_fields = []
159+
by_fields = []
160+
for source_mapping in self._source_mappings.values():
161+
if source_mapping.source_id == DEFAULT_MAPPING_NAME:
162+
continue
163+
164+
if source_mapping.fields_mapping.is_suitable(field_names):
165+
by_fields.append(source_mapping)
166+
167+
log_source_signature: LogSourceSignature = source_mapping.log_source_signature
168+
if log_source_signature and log_source_signature.is_suitable(**log_sources):
169+
by_log_sources_and_fields.append(source_mapping)
170+
171+
return by_log_sources_and_fields or by_fields or [self._source_mappings[DEFAULT_MAPPING_NAME]]
153172

154173
def get_source_mapping(self, source_id: str) -> Optional[SourceMapping]:
155174
return self._source_mappings.get(source_id)

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

Lines changed: 32 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,34 @@ 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(
149+
tactics=sorted(tactics_list, key=lambda x: x.name),
150+
techniques=sorted(techniques_list, key=lambda x: x.technique_id),
151+
)

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

Lines changed: 10 additions & 8 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,18 @@ 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"):
37-
if technique := self.mitre_config.get_technique(tag):
38-
result["techniques"].append(technique)
39-
elif tactic := self.mitre_config.get_tactic(tag):
40-
result["tactics"].append(tactic)
41-
return result
40+
parsed_techniques.append(tag)
41+
else:
42+
parsed_tactics.append(tag)
43+
return self.mitre_config.get_mitre_info(tactics=parsed_tactics, techniques=parsed_techniques)
4244

4345

4446
class XMLRuleMixin:

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

Lines changed: 43 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,80 @@
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 = source_mapping_ids or [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)
79+
80+
@property
81+
def source_mapping_ids(self) -> list[str]:
82+
return sorted(self._source_mapping_ids)
83+
84+
@source_mapping_ids.setter
85+
def source_mapping_ids(self, source_mapping_ids: list[str]) -> None:
86+
self._source_mapping_ids = source_mapping_ids
5087

5188

5289
@dataclass

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,10 @@ def get_field_tokens(
7777
return field_tokens
7878

7979
def get_source_mappings(
80-
self, field_tokens: list[Field], log_sources: dict[str, Union[str, list[str]]]
80+
self, field_tokens: list[Field], log_sources: dict[str, list[Union[int, str]]]
8181
) -> list[SourceMapping]:
8282
field_names = [field.source_name for field in field_tokens]
83-
source_mappings = self.mappings.get_suitable_source_mappings(field_names=field_names, **log_sources)
83+
source_mappings = self.mappings.get_suitable_source_mappings(field_names=field_names, log_sources=log_sources)
8484
self.tokenizer.set_field_tokens_generic_names_map(field_tokens, source_mappings, self.mappings.default_mapping)
8585
return source_mappings
86+

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/mappings/platforms/qradar/default.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@ field_mapping:
1414
- DstPort
1515
- DestinationPort
1616
- remoteport
17-
dst-hostname: DstHost
18-
src-hostname: SrcHost
1917
src-port:
2018
- SourcePort
2119
- localport
@@ -25,13 +23,15 @@ field_mapping:
2523
- source_ip
2624
- SourceIP
2725
- sourceIP
26+
- SrcHost
2827
dst-ip:
2928
- DestinationIP
3029
- destinationip
3130
- destination_ip
3231
- destinationIP
3332
- destinationaddress
3433
- destination
34+
- DstHost
3535
User:
3636
- userName
3737
- EventUserName

uncoder-core/app/translator/mappings/utils/load_from_files.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from app.translator.const import APP_PATH
77

88
COMMON_FIELD_MAPPING_FILE_NAME = "common.yml"
9+
DEFAULT_FIELD_MAPPING_FILE_NAME = "default.yml"
910

1011

1112
class LoaderFileMappings:
@@ -23,8 +24,9 @@ def load_mapping(mapping_file_path: str) -> dict:
2324
def load_platform_mappings(self, platform_dir: str) -> Generator[dict, None, None]:
2425
platform_path = os.path.join(self.base_mapping_filepath, platform_dir)
2526
for mapping_file in os.listdir(platform_path):
26-
if mapping_file != COMMON_FIELD_MAPPING_FILE_NAME:
27+
if mapping_file not in (COMMON_FIELD_MAPPING_FILE_NAME, DEFAULT_FIELD_MAPPING_FILE_NAME):
2728
yield self.load_mapping(mapping_file_path=os.path.join(platform_path, mapping_file))
29+
yield self.load_mapping(mapping_file_path=os.path.join(platform_path, DEFAULT_FIELD_MAPPING_FILE_NAME))
2830

2931
def load_common_mapping(self, platform_dir: str) -> dict:
3032
platform_path = os.path.join(self.base_mapping_filepath, platform_dir)
Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from typing import Optional
22

3-
from app.translator.core.mapping import DEFAULT_MAPPING_NAME, BasePlatformMappings, LogSourceSignature, SourceMapping
3+
from app.translator.core.mapping import BasePlatformMappings, LogSourceSignature
44
from app.translator.platforms.athena.const import athena_query_details
55

66

@@ -22,23 +22,5 @@ def prepare_log_source_signature(self, mapping: dict) -> AthenaLogSourceSignatur
2222
default_log_source = mapping["default_log_source"]
2323
return AthenaLogSourceSignature(tables=tables, default_source=default_log_source)
2424

25-
def get_suitable_source_mappings(self, field_names: list[str], table: Optional[str]) -> list[SourceMapping]:
26-
suitable_source_mappings = []
27-
for source_mapping in self._source_mappings.values():
28-
if source_mapping.source_id == DEFAULT_MAPPING_NAME:
29-
continue
30-
31-
log_source_signature: AthenaLogSourceSignature = source_mapping.log_source_signature
32-
if table and log_source_signature.is_suitable(table=table):
33-
if source_mapping.fields_mapping.is_suitable(field_names):
34-
suitable_source_mappings.append(source_mapping)
35-
elif source_mapping.fields_mapping.is_suitable(field_names):
36-
suitable_source_mappings.append(source_mapping)
37-
38-
if not suitable_source_mappings:
39-
suitable_source_mappings = [self._source_mappings[DEFAULT_MAPPING_NAME]]
40-
41-
return suitable_source_mappings
42-
4325

4426
athena_query_mappings = AthenaMappings(platform_dir="athena", platform_details=athena_query_details)

0 commit comments

Comments
 (0)