Skip to content

Commit 6973aa3

Browse files
authored
Merge pull request #114 from UncoderIO/gis-7328
Created base platform: aql. And fixes for qradar
2 parents d3dba4e + 87274f8 commit 6973aa3

File tree

49 files changed

+308
-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.

49 files changed

+308
-411
lines changed

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,7 @@ class InvalidYamlStructure(InvalidRuleStructure):
7777

7878
class InvalidJSONStructure(InvalidRuleStructure):
7979
rule_type: str = "JSON"
80+
81+
82+
class InvalidXMLStructure(InvalidRuleStructure):
83+
rule_type: str = "XML"

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

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

4+
import xmltodict
35
import yaml
46

5-
from app.translator.core.exceptions.core import InvalidJSONStructure, InvalidYamlStructure
7+
from app.translator.core.exceptions.core import InvalidJSONStructure, InvalidXMLStructure, InvalidYamlStructure
68
from app.translator.core.mitre import MitreConfig
79

810

@@ -36,5 +38,13 @@ def parse_mitre_attack(self, tags: list[str]) -> dict[str, list]:
3638
result["techniques"].append(technique)
3739
elif tactic := self.mitre_config.get_tactic(tag):
3840
result["tactics"].append(tactic)
39-
4041
return result
42+
43+
44+
class XMLRuleMixin:
45+
@staticmethod
46+
def load_rule(text: Union[str, bytes]) -> dict:
47+
try:
48+
return xmltodict.parse(text)
49+
except Exception as err:
50+
raise InvalidXMLStructure(error=str(err)) from err

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,13 @@ class RawQueryContainer:
5656
meta_info: MetaInfoContainer = field(default_factory=MetaInfoContainer)
5757

5858

59+
@dataclass
60+
class RawQueryDictContainer:
61+
query: dict
62+
language: str
63+
meta_info: dict
64+
65+
5966
@dataclass
6067
class TokenizedQueryContainer:
6168
tokens: list[TOKEN_TYPE]

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,13 @@
3232

3333
class QueryParser(ABC):
3434
wrapped_with_comment_pattern: str = None
35+
details: PlatformDetails = None
3536

3637
def remove_comments(self, text: str) -> str:
37-
return re.sub(self.wrapped_with_comment_pattern, "\n", text, flags=re.MULTILINE).strip()
38+
if self.wrapped_with_comment_pattern:
39+
return re.sub(self.wrapped_with_comment_pattern, "\n", text, flags=re.MULTILINE).strip()
40+
41+
return text
3842

3943
def parse_raw_query(self, text: str, language: str) -> RawQueryContainer:
4044
return RawQueryContainer(query=text, language=language)
@@ -47,7 +51,6 @@ def parse(self, raw_query_container: RawQueryContainer) -> TokenizedQueryContain
4751
class PlatformQueryParser(QueryParser, ABC):
4852
mappings: BasePlatformMappings = None
4953
tokenizer: QueryTokenizer = None
50-
details: PlatformDetails = None
5154
platform_functions: PlatformFunctions = None
5255

5356
def get_fields_tokens(self, tokens: list[Union[FieldValue, Keyword, Identifier]]) -> list[Field]:

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020

2121
from app.translator.core.models.iocs import IocsChunkValue
22+
from app.translator.core.models.platform_details import PlatformDetails
2223

2324

2425
class RenderCTI:
@@ -31,6 +32,7 @@ class RenderCTI:
3132
final_result_for_many: str = "union * | where ({result})\n"
3233
final_result_for_one: str = "union * | where {result}\n"
3334
default_mapping = None
35+
details: PlatformDetails = None
3436

3537
def create_field_value(self, field: str, value: str, generic_field: str) -> str: # noqa: ARG002
3638
return self.field_value_template.format(key=field, value=value)

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ class QueryTokenizer(BaseTokenizer):
5252
single_value_operators_map: ClassVar[dict[str, str]] = {}
5353
# used to generate re pattern. so the keys order is important
5454
multi_value_operators_map: ClassVar[dict[str, str]] = {}
55+
# used to generate re pattern. so the keys order is important
56+
fields_operator_map: ClassVar[dict[str, str]] = {}
5557
operators_map: ClassVar[dict[str, str]] = {} # used to generate re pattern. so the keys order is important
5658

5759
logical_operator_pattern = r"^(?P<logical_operator>and|or|not|AND|OR|NOT)\s+"
@@ -73,7 +75,11 @@ class QueryTokenizer(BaseTokenizer):
7375
def __init_subclass__(cls, **kwargs):
7476
cls._validate_re_patterns()
7577
cls.value_pattern = cls.base_value_pattern.replace("___value_pattern___", cls._value_pattern)
76-
cls.operators_map = {**cls.single_value_operators_map, **cls.multi_value_operators_map}
78+
cls.operators_map = {
79+
**cls.single_value_operators_map,
80+
**cls.multi_value_operators_map,
81+
**cls.fields_operator_map,
82+
}
7783
cls.operator_pattern = rf"""(?:___field___\s*(?P<operator>(?:{'|'.join(cls.operators_map)})))\s*"""
7884

7985
@classmethod

uncoder-core/app/translator/managers.py

Lines changed: 43 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,16 @@
11
from abc import ABC
22
from functools import cached_property
3+
from typing import ClassVar, Union
34

45
from app.models.translation import TranslatorPlatform
5-
from app.translator.core.exceptions.core import UnsupportedRootAParser
6+
from app.translator.core.exceptions.core import UnsupportedPlatform, UnsupportedRootAParser
7+
from app.translator.core.parser import QueryParser
8+
from app.translator.core.render import QueryRender
9+
from app.translator.core.render_cti import RenderCTI
610

711

8-
class Manager(ABC):
9-
platforms = {}
10-
11-
def register(self, cls):
12-
self.platforms[cls.details.platform_id] = cls()
13-
return cls
14-
15-
def get(self, platform_id: str): # noqa: ANN201
16-
if platform := self.platforms.get(platform_id):
17-
return platform
18-
raise UnsupportedRootAParser(parser=platform_id)
12+
class PlatformManager(ABC):
13+
platforms: ClassVar[dict[str, Union[QueryParser, QueryRender, RenderCTI]]] = {}
1914

2015
def all_platforms(self) -> list:
2116
return list(self.platforms.keys())
@@ -40,54 +35,61 @@ def get_platforms_details(self) -> list[TranslatorPlatform]:
4035
return sorted(platforms, key=lambda platform: platform.group_name)
4136

4237

43-
class ParserManager(Manager):
44-
platforms = {}
45-
supported_by_roota_platforms = {}
46-
main_platforms = {}
38+
class ParserManager(PlatformManager):
39+
supported_by_roota_platforms: ClassVar[dict[str, QueryParser]] = {}
40+
main_platforms: ClassVar[dict[str, QueryParser]] = {}
4741

48-
def get_supported_by_roota(self, platform_id: str): # noqa: ANN201
42+
def get(self, platform_id: str) -> QueryParser:
43+
if platform := self.platforms.get(platform_id):
44+
return platform
45+
raise UnsupportedPlatform(platform=platform_id, is_parser=True)
46+
47+
def register(self, cls: type[QueryParser]) -> type[QueryParser]:
48+
self.platforms[cls.details.platform_id] = cls()
49+
return cls
50+
51+
def get_supported_by_roota(self, platform_id: str) -> QueryParser:
4952
if platform := self.supported_by_roota_platforms.get(platform_id):
5053
return platform
5154
raise UnsupportedRootAParser(parser=platform_id)
5255

53-
def register_supported_by_roota(self, cls):
56+
def register_supported_by_roota(self, cls: type[QueryParser]) -> type[QueryParser]:
5457
parser = cls()
5558
self.supported_by_roota_platforms[cls.details.platform_id] = parser
5659
self.platforms[cls.details.platform_id] = parser
5760
return cls
5861

59-
def register_main(self, cls):
62+
def register_main(self, cls: type[QueryParser]) -> type[QueryParser]:
6063
parser = cls()
6164
self.main_platforms[cls.details.platform_id] = parser
6265
self.platforms[cls.details.platform_id] = parser
6366
return cls
6467

65-
@cached_property
66-
def get_platforms_details(self) -> list[TranslatorPlatform]:
67-
platforms = [
68-
TranslatorPlatform(
69-
id=platform.details.platform_id,
70-
name=platform.details.name,
71-
code=platform.details.platform_id,
72-
group_name=platform.details.group_name,
73-
group_id=platform.details.group_id,
74-
platform_name=platform.details.platform_name,
75-
platform_id=platform.details.platform_id,
76-
alt_platform_name=platform.details.alt_platform_name,
77-
alt_platform=platform.details.alt_platform,
78-
first_choice=platform.details.first_choice,
79-
)
80-
for platform in self.platforms.values()
81-
]
82-
return sorted(platforms, key=lambda platform: platform.group_name)
8368

69+
class RenderManager(PlatformManager):
70+
platforms: ClassVar[dict[str, QueryRender]] = {}
71+
72+
def get(self, platform_id: str) -> QueryRender:
73+
if platform := self.platforms.get(platform_id):
74+
return platform
75+
raise UnsupportedPlatform(platform=platform_id)
76+
77+
def register(self, cls: type[QueryRender]) -> type[QueryRender]:
78+
self.platforms[cls.details.platform_id] = cls()
79+
return cls
8480

85-
class RenderManager(Manager):
86-
platforms = {}
8781

82+
class RenderCTIManager(PlatformManager):
83+
platforms: ClassVar[dict[str, RenderCTI]] = {}
8884

89-
class RenderCTIManager(Manager):
90-
platforms = {}
85+
def get(self, platform_id: str) -> RenderCTI:
86+
if platform := self.platforms.get(platform_id):
87+
return platform
88+
raise UnsupportedPlatform(platform=platform_id)
89+
90+
def register(self, cls: type[RenderCTI]) -> type[RenderCTI]:
91+
self.platforms[cls.details.platform_id] = cls()
92+
return cls
9193

9294

9395
parser_manager = ParserManager()
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
from app.translator.platforms.arcsight.renders.arcsight_cti import ArcsightKeyword
1+
from app.translator.platforms.arcsight.renders.arcsight_cti import ArcsightKeyword # noqa: F401
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
ARCSIGHT_QUERY_DETAILS = {
2+
"platform_id": "arcsight",
3+
"name": "ArcSight Query",
4+
"group_name": "ArcSight",
5+
"group_id": "arcsight",
6+
"platform_name": "Query",
7+
"alt_platform_name": "CEF",
8+
}

uncoder-core/app/translator/platforms/arcsight/mappings/__init__.py

Whitespace-only changes.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
DEFAULT_ARCSIGHT_MAPPING = {
2+
"SourceIP": "sourceAddress",
3+
"DestinationIP": "destinationAddress",
4+
"Domain": "destinationDnsDomain",
5+
"URL": "requestUrl",
6+
"HashMd5": "fileHash",
7+
"HashSha1": "fileHash",
8+
"HashSha256": "fileHash",
9+
"HashSha512": "fileHash",
10+
"Emails": "sender-address",
11+
"Files": "winlog.event_data.TargetFilename",
12+
}

uncoder-core/app/translator/platforms/arcsight/renders/__init__.py

Whitespace-only changes.
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
from app.translator.platforms.athena.parsers.athena import AthenaQueryParser
2-
from app.translator.platforms.athena.renders.athena import AthenaQueryRender
3-
from app.translator.platforms.athena.renders.athena_cti import AthenaCTI
1+
from app.translator.platforms.athena.parsers.athena import AthenaQueryParser # noqa: F401
2+
from app.translator.platforms.athena.renders.athena import AthenaQueryRender # noqa: F401
3+
from app.translator.platforms.athena.renders.athena_cti import AthenaCTI # noqa: F401

uncoder-core/app/translator/platforms/base/aql/__init__.py

Whitespace-only changes.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
UTF8_PAYLOAD_PATTERN = r"UTF8\(payload\)"
2+
NUM_VALUE_PATTERN = r"(?P<num_value>\d+(?:\.\d+)*)"
3+
SINGLE_QUOTES_VALUE_PATTERN = r"""'(?P<s_q_value>(?:[:a-zA-Z\*0-9=+%#\-\/\\,_".$&^@!\(\)\{\}\s]|'')*)'"""
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from app.translator.core.escape_manager import EscapeManager
2+
3+
4+
class AQLEscapeManager(EscapeManager):
5+
...
6+
7+
8+
aql_escape_manager = AQLEscapeManager()

uncoder-core/app/translator/platforms/base/aql/parsers/__init__.py

Whitespace-only changes.

uncoder-core/app/translator/platforms/base/aql/renders/__init__.py

Whitespace-only changes.
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
"""
2+
Uncoder IO Community Edition License
3+
-----------------------------------------------------------------
4+
Copyright (c) 2024 SOC Prime, Inc.
5+
6+
Licensed under the Apache License, Version 2.0 (the "License");
7+
you may not use this file except in compliance with the License.
8+
You may obtain a copy of the License at
9+
10+
http://www.apache.org/licenses/LICENSE-2.0
11+
12+
Unless required by applicable law or agreed to in writing, software
13+
distributed under the License is distributed on an "AS IS" BASIS,
14+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
See the License for the specific language governing permissions and
16+
limitations under the License.
17+
-----------------------------------------------------------------
18+
"""
19+
from typing import Union
20+
21+
from app.translator.const import DEFAULT_VALUE_TYPE
22+
from app.translator.core.custom_types.values import ValueType
23+
from app.translator.core.render import BaseQueryFieldValue, PlatformQueryRender
24+
from app.translator.platforms.base.aql.escape_manager import aql_escape_manager
25+
from app.translator.platforms.base.aql.mapping import AQLLogSourceSignature, AQLMappings, aql_mappings
26+
27+
28+
class AQLFieldValue(BaseQueryFieldValue):
29+
escape_manager = aql_escape_manager
30+
31+
def apply_value(self, value: Union[str, int], value_type: str = ValueType.value) -> Union[str, int]: # noqa: ARG002
32+
if isinstance(value, str):
33+
value = value.replace("_", "__").replace("%", "%%").replace("\\'", "%").replace("'", '"')
34+
if value.endswith("\\\\%"):
35+
value = value.replace("\\\\%", "\\%")
36+
return value
37+
38+
def _apply_value(self, value: Union[str, int]) -> Union[str, int]:
39+
if isinstance(value, str) and "\\" in value:
40+
return value
41+
return self.apply_value(value)
42+
43+
def equal_modifier(self, field: str, value: DEFAULT_VALUE_TYPE) -> str:
44+
if isinstance(value, list):
45+
return f"({self.or_token.join([self.equal_modifier(field=field, value=v) for v in value])})"
46+
if field == "UTF8(payload)":
47+
return f"UTF8(payload) ILIKE '{self.apply_value(value)}'"
48+
if isinstance(value, int):
49+
return f'"{field}"={value}'
50+
51+
return f"\"{field}\"='{self._apply_value(value)}'"
52+
53+
def less_modifier(self, field: str, value: Union[int, str]) -> str:
54+
if isinstance(value, int):
55+
return f'"{field}"<{value}'
56+
return f"\"{field}\"<'{self._apply_value(value)}'"
57+
58+
def less_or_equal_modifier(self, field: str, value: Union[int, str]) -> str:
59+
if isinstance(value, int):
60+
return f'"{field}"<={value}'
61+
return f"\"{field}\"<='{self._apply_value(value)}'"
62+
63+
def greater_modifier(self, field: str, value: Union[int, str]) -> str:
64+
if isinstance(value, int):
65+
return f'"{field}">{value}'
66+
return f"\"{field}\">'{self._apply_value(value)}'"
67+
68+
def greater_or_equal_modifier(self, field: str, value: Union[int, str]) -> str:
69+
if isinstance(value, int):
70+
return f'"{field}">={value}'
71+
return f"\"{field}\">='{self._apply_value(value)}'"
72+
73+
def not_equal_modifier(self, field: str, value: DEFAULT_VALUE_TYPE) -> str:
74+
if isinstance(value, list):
75+
return f"({self.or_token.join([self.not_equal_modifier(field=field, value=v) for v in value])})"
76+
if isinstance(value, int):
77+
return f'"{field}"!={value}'
78+
return f"\"{field}\"!='{self._apply_value(value)}'"
79+
80+
def contains_modifier(self, field: str, value: DEFAULT_VALUE_TYPE) -> str:
81+
if isinstance(value, list):
82+
return f"({self.or_token.join(self.contains_modifier(field=field, value=v) for v in value)})"
83+
return f"\"{field}\" ILIKE '%{self._apply_value(value)}%'"
84+
85+
def endswith_modifier(self, field: str, value: DEFAULT_VALUE_TYPE) -> str:
86+
if isinstance(value, list):
87+
return f"({self.or_token.join(self.endswith_modifier(field=field, value=v) for v in value)})"
88+
return f"\"{field}\" ILIKE '%{self._apply_value(value)}'"
89+
90+
def startswith_modifier(self, field: str, value: DEFAULT_VALUE_TYPE) -> str:
91+
if isinstance(value, list):
92+
return f"({self.or_token.join(self.startswith_modifier(field=field, value=v) for v in value)})"
93+
return f"\"{field}\" ILIKE '{self._apply_value(value)}%'"
94+
95+
def regex_modifier(self, field: str, value: DEFAULT_VALUE_TYPE) -> str:
96+
if isinstance(value, list):
97+
return f"({self.or_token.join(self.regex_modifier(field=field, value=v) for v in value)})"
98+
return f"\"{field}\" IMATCHES '{value}'"
99+
100+
def keywords(self, field: str, value: DEFAULT_VALUE_TYPE) -> str:
101+
if isinstance(value, list):
102+
return f"({self.or_token.join(self.keywords(field=field, value=v) for v in value)})"
103+
return f"UTF8(payload) ILIKE '%{self.apply_value(value)}%'"
104+
105+
106+
class AQLQueryRender(PlatformQueryRender):
107+
mappings: AQLMappings = aql_mappings
108+
109+
or_token = "OR"
110+
and_token = "AND"
111+
not_token = "NOT"
112+
113+
field_value_map = AQLFieldValue(or_token=or_token)
114+
query_pattern = "{prefix} AND {query} {functions}"
115+
116+
def generate_prefix(self, log_source_signature: AQLLogSourceSignature) -> str:
117+
table = str(log_source_signature)
118+
extra_condition = log_source_signature.extra_condition
119+
return f"SELECT UTF8(payload) FROM {table} WHERE {extra_condition}"
120+
121+
def wrap_with_comment(self, value: str) -> str:
122+
return f"/* {value} */"

0 commit comments

Comments
 (0)