dissect.util 3.23.dev7__tar.gz → 3.23.dev8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of dissect.util might be problematic. Click here for more details.
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/PKG-INFO +1 -1
- dissect_util-3.23.dev8/dissect/util/ldap.py +237 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/dissect.util.egg-info/PKG-INFO +1 -1
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/dissect.util.egg-info/SOURCES.txt +2 -0
- dissect_util-3.23.dev8/tests/test_ldap.py +652 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/.devcontainer/devcontainer.json +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/COPYRIGHT +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/LICENSE +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/MANIFEST.in +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/README.md +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/dissect/util/__init__.py +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/dissect/util/_build.py +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/dissect/util/_native/__init__.pyi +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/dissect/util/_native/compression/__init__.pyi +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/dissect/util/_native/compression/lz4.pyi +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/dissect/util/_native/compression/lzo.pyi +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/dissect/util/_native/hash/__init__.py +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/dissect/util/_native/hash/crc32c.py +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/dissect/util/_native.src/Cargo.lock +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/dissect/util/_native.src/Cargo.toml +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/dissect/util/_native.src/src/compression/lz4.rs +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/dissect/util/_native.src/src/compression/lzo.rs +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/dissect/util/_native.src/src/compression.rs +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/dissect/util/_native.src/src/hash/crc32c.rs +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/dissect/util/_native.src/src/hash.rs +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/dissect/util/_native.src/src/lib.rs +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/dissect/util/compression/__init__.py +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/dissect/util/compression/lz4.py +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/dissect/util/compression/lzbitmap.py +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/dissect/util/compression/lzfse.py +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/dissect/util/compression/lznt1.py +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/dissect/util/compression/lzo.py +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/dissect/util/compression/lzvn.py +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/dissect/util/compression/lzxpress.py +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/dissect/util/compression/lzxpress_huffman.py +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/dissect/util/compression/sevenbit.py +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/dissect/util/compression/xz.py +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/dissect/util/cpio.py +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/dissect/util/encoding/__init__.py +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/dissect/util/encoding/surrogateescape.py +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/dissect/util/exceptions.py +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/dissect/util/feature.py +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/dissect/util/hash/__init__.py +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/dissect/util/hash/crc32c.py +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/dissect/util/hash/jenkins.py +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/dissect/util/plist.py +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/dissect/util/sid.py +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/dissect/util/stream.py +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/dissect/util/tools/__init__.py +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/dissect/util/tools/dump_nskeyedarchiver.py +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/dissect/util/ts.py +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/dissect/util/xmemoryview.py +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/dissect.util.egg-info/dependency_links.txt +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/dissect.util.egg-info/entry_points.txt +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/dissect.util.egg-info/top_level.txt +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/pyproject.toml +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/setup.cfg +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/tests/__init__.py +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/tests/_data/bin.cpio.gz +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/tests/_data/crc.cpio.gz +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/tests/_data/hpbin.cpio.gz +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/tests/_data/hpodc.cpio.gz +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/tests/_data/newc.cpio.gz +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/tests/_data/odc.cpio.gz +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/tests/_docs/Makefile +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/tests/_docs/conf.py +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/tests/_docs/index.rst +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/tests/compression/__init__.py +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/tests/compression/test_lz4.py +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/tests/compression/test_lzbitmap.py +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/tests/compression/test_lzfse.py +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/tests/compression/test_lznt1.py +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/tests/compression/test_lzo.py +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/tests/compression/test_lzvn.py +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/tests/compression/test_lzxpress.py +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/tests/compression/test_lzxpress_huffman.py +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/tests/compression/test_sevenbit.py +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/tests/compression/test_xz.py +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/tests/conftest.py +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/tests/test_cpio.py +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/tests/test_feature.py +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/tests/test_hash.py +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/tests/test_plist.py +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/tests/test_sid.py +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/tests/test_stream.py +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/tests/test_ts.py +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/tests/test_xmemoryview.py +0 -0
- {dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/tox.ini +0 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import operator
|
|
4
|
+
import re
|
|
5
|
+
from enum import Enum
|
|
6
|
+
|
|
7
|
+
from dissect.util.exceptions import Error
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class InvalidQueryError(Error):
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class LogicalOperator(Enum):
|
|
15
|
+
AND = "&"
|
|
16
|
+
OR = "|"
|
|
17
|
+
NOT = "!"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
_LOGICAL_OPERATORS = tuple(op.value for op in LogicalOperator)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ComparisonOperator(Enum):
|
|
24
|
+
GE = ">="
|
|
25
|
+
LE = "<="
|
|
26
|
+
GT = ">"
|
|
27
|
+
LT = "<"
|
|
28
|
+
EQ = "="
|
|
29
|
+
APPROX = "~="
|
|
30
|
+
BIT = ":="
|
|
31
|
+
EXTENDED = ":"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
_NORMAL_COMPARISON_OPERATORS = [op for op in ComparisonOperator if op != ComparisonOperator.EXTENDED]
|
|
35
|
+
_SORTED_COMPARISON_OPERATORS = sorted(_NORMAL_COMPARISON_OPERATORS, key=lambda op: len(op.value), reverse=True)
|
|
36
|
+
|
|
37
|
+
_RE_EXTENDED = re.compile(r"(.+?):(.+?):=(.+)?")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class SearchFilter:
|
|
41
|
+
"""Represents an LDAP search filter (simple or nested).
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
query: The LDAP search filter string.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, query: str) -> None:
|
|
48
|
+
self.query: str = query
|
|
49
|
+
|
|
50
|
+
self.children: list[SearchFilter] = []
|
|
51
|
+
self.operator: LogicalOperator | ComparisonOperator | None = None
|
|
52
|
+
self.attribute: str | None = None
|
|
53
|
+
self.value: str | None = None
|
|
54
|
+
self._extended_rule: str | None = None
|
|
55
|
+
|
|
56
|
+
_validate_syntax(query)
|
|
57
|
+
|
|
58
|
+
if query[1:-1].startswith(_LOGICAL_OPERATORS):
|
|
59
|
+
self._parse_nested()
|
|
60
|
+
else:
|
|
61
|
+
self._parse_simple()
|
|
62
|
+
|
|
63
|
+
def __repr__(self) -> str:
|
|
64
|
+
if self.is_nested():
|
|
65
|
+
return f"<SearchFilter nested operator={self.operator.value!r} children={self.children}>"
|
|
66
|
+
return f"<SearchFilter attribute={self.attribute!r} operator={self.operator.value!r} value={self.value}>"
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def parse(cls, query: str, optimize: bool = True) -> SearchFilter:
|
|
70
|
+
"""Parse an LDAP query into a filter object, with optional optimization."""
|
|
71
|
+
result = cls(query)
|
|
72
|
+
if optimize:
|
|
73
|
+
return optimize_ldap_query(result)[0]
|
|
74
|
+
return result
|
|
75
|
+
|
|
76
|
+
def is_nested(self) -> bool:
|
|
77
|
+
"""Return whether the filter is nested (i.e., contains logical operators and child filters)."""
|
|
78
|
+
return isinstance(self.operator, LogicalOperator)
|
|
79
|
+
|
|
80
|
+
def format(self) -> str:
|
|
81
|
+
"""Format the search filter back into an LDAP query string."""
|
|
82
|
+
if self.is_nested():
|
|
83
|
+
childs = "".join([child.format() for child in self.children])
|
|
84
|
+
return f"({self.operator.value}{childs})"
|
|
85
|
+
|
|
86
|
+
if self.operator == ComparisonOperator.EXTENDED:
|
|
87
|
+
return f"({self.attribute}:{self._extended_rule}:={self.value})"
|
|
88
|
+
|
|
89
|
+
return f"({self.attribute}{self.operator.value}{self.value})"
|
|
90
|
+
|
|
91
|
+
def _parse_simple(self) -> None:
|
|
92
|
+
"""Parse simple filter."""
|
|
93
|
+
query = self.query[1:-1]
|
|
94
|
+
|
|
95
|
+
# Check for extended matching rules first
|
|
96
|
+
if ":" in query and (match := _RE_EXTENDED.match(query)):
|
|
97
|
+
self.operator = ComparisonOperator.EXTENDED
|
|
98
|
+
self.attribute, self._extended_rule, self.value = match.groups()
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
# Regular operator parsing
|
|
102
|
+
test = query
|
|
103
|
+
operators: list[ComparisonOperator] = []
|
|
104
|
+
for op in _SORTED_COMPARISON_OPERATORS:
|
|
105
|
+
if op.value not in test:
|
|
106
|
+
continue
|
|
107
|
+
|
|
108
|
+
if test.count(op.value) > 1:
|
|
109
|
+
raise InvalidQueryError(f"Comparison operator {op.value} found multiple times in query: {self.query}")
|
|
110
|
+
|
|
111
|
+
operators.append(op)
|
|
112
|
+
test = test.replace(op.value, "")
|
|
113
|
+
|
|
114
|
+
if len(operators) == 0:
|
|
115
|
+
raise InvalidQueryError(
|
|
116
|
+
f"No comparison operator found in query: {self.query}. "
|
|
117
|
+
f"Expected one of {[op.value for op in _NORMAL_COMPARISON_OPERATORS]}."
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
if len(operators) > 1:
|
|
121
|
+
raise InvalidQueryError(
|
|
122
|
+
f"Multiple comparison operators found in query: {self.query} -> {[o.value for o in operators]} "
|
|
123
|
+
f"Expected only one of {[op.value for op in _NORMAL_COMPARISON_OPERATORS]}."
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
self.operator = operators[0]
|
|
127
|
+
self.attribute, _, self.value = query.partition(self.operator.value)
|
|
128
|
+
|
|
129
|
+
def _parse_nested(self) -> None:
|
|
130
|
+
"""Parse nested filter."""
|
|
131
|
+
query = self.query[1:-1]
|
|
132
|
+
self.operator = LogicalOperator(query[0])
|
|
133
|
+
|
|
134
|
+
start = 1
|
|
135
|
+
while start < len(query):
|
|
136
|
+
end = start + 1
|
|
137
|
+
depth = 1
|
|
138
|
+
|
|
139
|
+
while end < len(query) and depth > 0:
|
|
140
|
+
if query[end] == "(":
|
|
141
|
+
depth += 1
|
|
142
|
+
elif query[end] == ")":
|
|
143
|
+
depth -= 1
|
|
144
|
+
end += 1
|
|
145
|
+
|
|
146
|
+
self.children.append(SearchFilter(query[start:end]))
|
|
147
|
+
start = end
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
_ATTRIBUTE_WEIGHTS = {
|
|
151
|
+
"objectGUID": 1,
|
|
152
|
+
"distinguishedName": 1,
|
|
153
|
+
"sAMAccountName": 2,
|
|
154
|
+
"userPrincipalName": 2,
|
|
155
|
+
"mail": 2,
|
|
156
|
+
"sAMAccountType": 3,
|
|
157
|
+
"servicePrincipalName": 3,
|
|
158
|
+
"userAccountControl": 4,
|
|
159
|
+
"memberOf": 5,
|
|
160
|
+
"member": 5,
|
|
161
|
+
"pwdLastSet": 5,
|
|
162
|
+
"primaryGroupID": 6,
|
|
163
|
+
"whenCreated": 6,
|
|
164
|
+
"ou": 6,
|
|
165
|
+
"lastLogonTimestamp": 6,
|
|
166
|
+
"cn": 7,
|
|
167
|
+
"givenName": 7,
|
|
168
|
+
"name": 7,
|
|
169
|
+
"telephoneNumber": 7,
|
|
170
|
+
"objectCategory": 8,
|
|
171
|
+
"description": 9,
|
|
172
|
+
"objectClass": 10,
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def optimize_ldap_query(query: SearchFilter) -> tuple[SearchFilter, int]:
|
|
177
|
+
"""Optimize an LDAP query in-place.
|
|
178
|
+
|
|
179
|
+
Removes redundant conditions and sorts filters and conditions based on how specific they are.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
query: The LDAP query to optimize.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
A tuple containing the optimized LDAP query and its weight.
|
|
186
|
+
"""
|
|
187
|
+
# Simplify single-child AND/OR
|
|
188
|
+
if query.is_nested() and len(query.children) == 1 and query.operator in (LogicalOperator.AND, LogicalOperator.OR):
|
|
189
|
+
return optimize_ldap_query(query.children[0])
|
|
190
|
+
|
|
191
|
+
# Sort nested children by weight
|
|
192
|
+
if query.is_nested() and len(query.children) > 1:
|
|
193
|
+
children = sorted((optimize_ldap_query(child) for child in query.children), key=operator.itemgetter(1))
|
|
194
|
+
|
|
195
|
+
query.children = [child for child, _ in children]
|
|
196
|
+
query.query = query.format()
|
|
197
|
+
|
|
198
|
+
return query, max(weight for _, weight in children)
|
|
199
|
+
|
|
200
|
+
# Handle NOT
|
|
201
|
+
if query.is_nested() and len(query.children) == 1 and query.operator == LogicalOperator.NOT:
|
|
202
|
+
child, weight = optimize_ldap_query(query.children[0])
|
|
203
|
+
|
|
204
|
+
query.children[0] = child
|
|
205
|
+
query.query = query.format()
|
|
206
|
+
|
|
207
|
+
return query, weight
|
|
208
|
+
|
|
209
|
+
# Base case: simple filter
|
|
210
|
+
if not query.is_nested():
|
|
211
|
+
return query, _ATTRIBUTE_WEIGHTS.get(query.attribute, max(_ATTRIBUTE_WEIGHTS.values()))
|
|
212
|
+
|
|
213
|
+
return query, max(_ATTRIBUTE_WEIGHTS.values())
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _validate_syntax(query: str) -> None:
|
|
217
|
+
"""Validate basic LDAP query syntax.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
query: The LDAP query to validate.
|
|
221
|
+
"""
|
|
222
|
+
if not query:
|
|
223
|
+
raise InvalidQueryError("Empty query")
|
|
224
|
+
|
|
225
|
+
if not query.startswith("(") or not query.endswith(")"):
|
|
226
|
+
raise InvalidQueryError(f"Query must be wrapped in parentheses: {query}")
|
|
227
|
+
|
|
228
|
+
if query.count("(") != query.count(")"):
|
|
229
|
+
raise InvalidQueryError(f"Unbalanced parentheses in query: {query}")
|
|
230
|
+
|
|
231
|
+
# Check for empty parentheses
|
|
232
|
+
if "()" in query:
|
|
233
|
+
raise InvalidQueryError(f"Empty parentheses found in query: {query}")
|
|
234
|
+
|
|
235
|
+
# Check for queries that start with double opening parentheses
|
|
236
|
+
if query.startswith("(("):
|
|
237
|
+
raise InvalidQueryError(f"Invalid query structure: {query}")
|
|
@@ -15,6 +15,7 @@ dissect/util/_build.py
|
|
|
15
15
|
dissect/util/cpio.py
|
|
16
16
|
dissect/util/exceptions.py
|
|
17
17
|
dissect/util/feature.py
|
|
18
|
+
dissect/util/ldap.py
|
|
18
19
|
dissect/util/plist.py
|
|
19
20
|
dissect/util/sid.py
|
|
20
21
|
dissect/util/stream.py
|
|
@@ -57,6 +58,7 @@ tests/conftest.py
|
|
|
57
58
|
tests/test_cpio.py
|
|
58
59
|
tests/test_feature.py
|
|
59
60
|
tests/test_hash.py
|
|
61
|
+
tests/test_ldap.py
|
|
60
62
|
tests/test_plist.py
|
|
61
63
|
tests/test_sid.py
|
|
62
64
|
tests/test_stream.py
|
|
@@ -0,0 +1,652 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from dissect.util.ldap import (
|
|
6
|
+
_ATTRIBUTE_WEIGHTS,
|
|
7
|
+
ComparisonOperator,
|
|
8
|
+
InvalidQueryError,
|
|
9
|
+
LogicalOperator,
|
|
10
|
+
SearchFilter,
|
|
11
|
+
optimize_ldap_query,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.mark.parametrize(
|
|
16
|
+
("query", "match"),
|
|
17
|
+
[
|
|
18
|
+
("(&(objectClass=user)(name=Henk)))", "Unbalanced parentheses"),
|
|
19
|
+
("(objectClass=user)(name=Henk)", "Comparison operator = found multiple times in query"),
|
|
20
|
+
("", "Empty query"),
|
|
21
|
+
("name=test", "Query must be wrapped in parentheses"),
|
|
22
|
+
("()", "Empty parentheses found"),
|
|
23
|
+
("((test))", "Invalid query structure"),
|
|
24
|
+
],
|
|
25
|
+
)
|
|
26
|
+
def test_invalid_query(query: str, match: str) -> None:
|
|
27
|
+
"""Test against various invalid LDAP queries."""
|
|
28
|
+
with pytest.raises(InvalidQueryError, match=match):
|
|
29
|
+
SearchFilter.parse(query)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@pytest.mark.parametrize(
|
|
33
|
+
("query", "attribute", "value"),
|
|
34
|
+
[
|
|
35
|
+
("(name=Henk*)", "name", "Henk*"),
|
|
36
|
+
("(name=*son)", "name", "*son"),
|
|
37
|
+
("(givenName=*Jan*)", "givenName", "*Jan*"),
|
|
38
|
+
("(mail=*@*.com)", "mail", "*@*.com"),
|
|
39
|
+
],
|
|
40
|
+
)
|
|
41
|
+
def test_wildcards(query: str, attribute: str, value: str) -> None:
|
|
42
|
+
"""Test wildcard support in various positions."""
|
|
43
|
+
parsed = SearchFilter.parse(query, optimize=False)
|
|
44
|
+
assert parsed.attribute == attribute
|
|
45
|
+
assert parsed.value == value
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_wildcards_nested() -> None:
|
|
49
|
+
"""Test wildcards in nested filters."""
|
|
50
|
+
query = "(&(objectClass=user)(name=*Henk*))"
|
|
51
|
+
parsed = SearchFilter.parse(query, optimize=False)
|
|
52
|
+
assert parsed.children[1].attribute == "name"
|
|
53
|
+
assert parsed.children[1].value == "*Henk*"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_parsing_and_no_optimization() -> None:
|
|
57
|
+
"""Test parsing without optimization."""
|
|
58
|
+
query = "(&(objectClass=user)(|(name=Henk)(name=Jan)))"
|
|
59
|
+
parsed = SearchFilter.parse(query, optimize=False)
|
|
60
|
+
assert parsed.query == query, "Query was optimized"
|
|
61
|
+
|
|
62
|
+
assert len(parsed.children) == 2
|
|
63
|
+
assert parsed.operator == LogicalOperator.AND
|
|
64
|
+
assert isinstance(parsed.children[0], SearchFilter)
|
|
65
|
+
assert parsed.children[0].attribute == "objectClass"
|
|
66
|
+
assert parsed.children[0].value == "user"
|
|
67
|
+
assert parsed.children[0].operator.value == "="
|
|
68
|
+
|
|
69
|
+
assert isinstance(parsed.children[1], SearchFilter)
|
|
70
|
+
assert parsed.children[1].operator == LogicalOperator.OR
|
|
71
|
+
assert parsed.children[1].query == "(|(name=Henk)(name=Jan))"
|
|
72
|
+
assert parsed.children[1].children[0].attribute == "name"
|
|
73
|
+
assert parsed.children[1].children[0].operator.value == "="
|
|
74
|
+
assert parsed.children[1].children[0].value == "Henk"
|
|
75
|
+
assert parsed.children[1].children[1].attribute == "name"
|
|
76
|
+
assert parsed.children[1].children[1].operator.value == "="
|
|
77
|
+
assert parsed.children[1].children[1].value == "Jan"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_parsing_and_suboptimal() -> None:
|
|
81
|
+
"""Test parsing with optimization."""
|
|
82
|
+
query = "(&(objectClass=user)(|(name=Henk)(name=Jan)))"
|
|
83
|
+
parsed = SearchFilter.parse(query)
|
|
84
|
+
assert parsed.query != query, "Query was not optimized"
|
|
85
|
+
|
|
86
|
+
assert len(parsed.children) == 2
|
|
87
|
+
assert parsed.operator == LogicalOperator.AND
|
|
88
|
+
assert isinstance(parsed.children[1], SearchFilter)
|
|
89
|
+
assert parsed.children[1].attribute == "objectClass"
|
|
90
|
+
assert parsed.children[1].operator.value == "="
|
|
91
|
+
assert parsed.children[1].value == "user"
|
|
92
|
+
|
|
93
|
+
assert isinstance(parsed.children[0], SearchFilter)
|
|
94
|
+
assert parsed.children[0].operator == LogicalOperator.OR
|
|
95
|
+
assert parsed.children[0].query == "(|(name=Henk)(name=Jan))"
|
|
96
|
+
assert parsed.children[0].children[0].attribute == "name"
|
|
97
|
+
assert parsed.children[0].children[0].operator.value == "="
|
|
98
|
+
assert parsed.children[0].children[0].value == "Henk"
|
|
99
|
+
assert parsed.children[0].children[1].attribute == "name"
|
|
100
|
+
assert parsed.children[0].children[1].operator.value == "="
|
|
101
|
+
assert parsed.children[0].children[1].value == "Jan"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_parsing_or() -> None:
|
|
105
|
+
"""Test parsing OR filters."""
|
|
106
|
+
query = "(|(name=Henk)(name=Jan))"
|
|
107
|
+
parsed = SearchFilter.parse(query)
|
|
108
|
+
assert parsed.query == query, "Query was optimized"
|
|
109
|
+
|
|
110
|
+
assert len(parsed.children) == 2
|
|
111
|
+
assert parsed.operator == LogicalOperator.OR
|
|
112
|
+
assert isinstance(parsed.children[0], SearchFilter)
|
|
113
|
+
assert parsed.children[0].attribute == "name"
|
|
114
|
+
assert parsed.children[0].operator.value == "="
|
|
115
|
+
assert parsed.children[0].value == "Henk"
|
|
116
|
+
assert isinstance(parsed.children[1], SearchFilter)
|
|
117
|
+
assert parsed.children[1].attribute == "name"
|
|
118
|
+
assert parsed.children[1].operator.value == "="
|
|
119
|
+
assert parsed.children[1].value == "Jan"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_parsing_or_2() -> None:
|
|
123
|
+
"""Test parsing OR filters with AND parent."""
|
|
124
|
+
query = "(&(|(name=Henk)(name=Jan))(surname=de Vries))"
|
|
125
|
+
parsed = SearchFilter.parse(query)
|
|
126
|
+
assert parsed.query == query, "Query was optimized"
|
|
127
|
+
|
|
128
|
+
assert len(parsed.children) == 2
|
|
129
|
+
assert parsed.operator == LogicalOperator.AND
|
|
130
|
+
assert isinstance(parsed.children[0], SearchFilter)
|
|
131
|
+
assert parsed.children[0].query == "(|(name=Henk)(name=Jan))"
|
|
132
|
+
assert len(parsed.children[0].children) == 2
|
|
133
|
+
assert isinstance(parsed.children[0].children[0], SearchFilter)
|
|
134
|
+
assert parsed.children[0].operator == LogicalOperator.OR
|
|
135
|
+
assert parsed.children[0].children[0].attribute == "name"
|
|
136
|
+
assert parsed.children[0].children[0].operator.value == "="
|
|
137
|
+
assert parsed.children[0].children[0].value == "Henk"
|
|
138
|
+
assert isinstance(parsed.children[0].children[1], SearchFilter)
|
|
139
|
+
assert parsed.children[0].children[1].attribute == "name"
|
|
140
|
+
assert parsed.children[0].children[1].operator.value == "="
|
|
141
|
+
assert parsed.children[0].children[1].value == "Jan"
|
|
142
|
+
assert isinstance(parsed.children[1], SearchFilter)
|
|
143
|
+
assert parsed.children[1].attribute == "surname"
|
|
144
|
+
assert parsed.children[1].operator.value == "="
|
|
145
|
+
assert parsed.children[1].value == "de Vries"
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_parsing_nested_and_or() -> None:
|
|
149
|
+
"""Test parsing nested AND/OR filters."""
|
|
150
|
+
query = (
|
|
151
|
+
"(|(objectClass=container)(objectClass=organizationalUnit)"
|
|
152
|
+
"(sAMAccountType>=805306369)(objectClass=group)(&(objectCategory=person)(objectClass=user)))"
|
|
153
|
+
)
|
|
154
|
+
parsed = SearchFilter.parse(query)
|
|
155
|
+
assert parsed.query != query, "Query was not optimized"
|
|
156
|
+
|
|
157
|
+
assert len(parsed.children) == 5
|
|
158
|
+
|
|
159
|
+
assert isinstance(parsed.children[0], SearchFilter)
|
|
160
|
+
assert parsed.children[0].query == "(sAMAccountType>=805306369)"
|
|
161
|
+
assert parsed.children[0].attribute == "sAMAccountType"
|
|
162
|
+
assert parsed.children[0].operator.value == ">="
|
|
163
|
+
assert parsed.children[0].value == "805306369"
|
|
164
|
+
|
|
165
|
+
assert isinstance(parsed.children[2], SearchFilter)
|
|
166
|
+
assert parsed.children[2].query == "(objectClass=organizationalUnit)"
|
|
167
|
+
assert parsed.children[2].attribute == "objectClass"
|
|
168
|
+
assert parsed.children[2].operator.value == "="
|
|
169
|
+
assert parsed.children[2].value == "organizationalUnit"
|
|
170
|
+
|
|
171
|
+
assert isinstance(parsed.children[1], SearchFilter)
|
|
172
|
+
assert parsed.children[1].query == "(objectClass=container)"
|
|
173
|
+
assert parsed.children[1].attribute == "objectClass"
|
|
174
|
+
assert parsed.children[1].operator.value == "="
|
|
175
|
+
assert parsed.children[1].value == "container"
|
|
176
|
+
|
|
177
|
+
assert isinstance(parsed.children[3], SearchFilter)
|
|
178
|
+
assert parsed.children[3].query == "(objectClass=group)"
|
|
179
|
+
assert parsed.children[3].attribute == "objectClass"
|
|
180
|
+
assert parsed.children[3].operator.value == "="
|
|
181
|
+
assert parsed.children[3].value == "group"
|
|
182
|
+
|
|
183
|
+
assert isinstance(parsed.children[4], SearchFilter)
|
|
184
|
+
assert parsed.children[4].query == "(&(objectCategory=person)(objectClass=user))"
|
|
185
|
+
assert parsed.children[4].operator == LogicalOperator.AND
|
|
186
|
+
assert len(parsed.children[4].children) == 2
|
|
187
|
+
assert isinstance(parsed.children[4].children[0], SearchFilter)
|
|
188
|
+
assert parsed.children[4].children[0].query == "(objectCategory=person)"
|
|
189
|
+
assert parsed.children[4].children[0].attribute == "objectCategory"
|
|
190
|
+
assert parsed.children[4].children[0].operator.value == "="
|
|
191
|
+
assert parsed.children[4].children[0].value == "person"
|
|
192
|
+
assert isinstance(parsed.children[4].children[1], SearchFilter)
|
|
193
|
+
assert parsed.children[4].children[1].query == "(objectClass=user)"
|
|
194
|
+
assert parsed.children[4].children[1].attribute == "objectClass"
|
|
195
|
+
assert parsed.children[4].children[1].operator.value == "="
|
|
196
|
+
assert parsed.children[4].children[1].value == "user"
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def test_parsing_single_not() -> None:
|
|
200
|
+
"""Test parsing single NOT filter."""
|
|
201
|
+
query = "(!(sAMAccountType=805306369))"
|
|
202
|
+
parsed = SearchFilter.parse(query)
|
|
203
|
+
assert parsed.query == query, "Query was optimized"
|
|
204
|
+
|
|
205
|
+
assert len(parsed.children) == 1
|
|
206
|
+
assert parsed.operator == LogicalOperator.NOT
|
|
207
|
+
assert isinstance(parsed.children[0], SearchFilter)
|
|
208
|
+
assert parsed.children[0].attribute == "sAMAccountType"
|
|
209
|
+
assert parsed.children[0].operator.value == "="
|
|
210
|
+
assert parsed.children[0].value == "805306369"
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def test_parsing_single_filter_in_condition() -> None:
|
|
214
|
+
"""Test parsing single filter in AND condition."""
|
|
215
|
+
query = "(&(objectClass=user))"
|
|
216
|
+
parsed = SearchFilter.parse(query)
|
|
217
|
+
assert parsed.query != query, "Query was not optimized"
|
|
218
|
+
|
|
219
|
+
assert parsed.query == "(objectClass=user)"
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@pytest.mark.parametrize(
|
|
223
|
+
("query", "attribute", "operator", "value"),
|
|
224
|
+
[
|
|
225
|
+
pytest.param("(age>=18)", "age", ComparisonOperator.GE, "18", id=">="),
|
|
226
|
+
pytest.param("(age<=30)", "age", ComparisonOperator.LE, "30", id="<="),
|
|
227
|
+
pytest.param("(priority>5)", "priority", ComparisonOperator.GT, "5", id=">"),
|
|
228
|
+
pytest.param("(score<100)", "score", ComparisonOperator.LT, "100", id="<"),
|
|
229
|
+
pytest.param("(name=Henk)", "name", ComparisonOperator.EQ, "Henk", id="="),
|
|
230
|
+
pytest.param("(name~=Henk)", "name", ComparisonOperator.APPROX, "Henk", id="~="),
|
|
231
|
+
pytest.param("(userAccountControl:=2)", "userAccountControl", ComparisonOperator.BIT, "2", id=":="),
|
|
232
|
+
],
|
|
233
|
+
)
|
|
234
|
+
def test_comparison_operators(query: str, attribute: str, operator: ComparisonOperator, value: str) -> None:
|
|
235
|
+
"""Test various comparison operators."""
|
|
236
|
+
parsed = SearchFilter.parse(query, optimize=False)
|
|
237
|
+
|
|
238
|
+
assert not parsed.is_nested()
|
|
239
|
+
assert parsed.attribute == attribute
|
|
240
|
+
assert parsed.operator == operator
|
|
241
|
+
assert parsed.value == value
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def test_all_comparison_operators_in_nested_filter() -> None:
|
|
245
|
+
"""Test all comparison operators in nested filter."""
|
|
246
|
+
query = "(&(name=Henk)(age>=18)(score<=100)(priority>5)(weight<75)(description~=admin)(flags:=1024))"
|
|
247
|
+
parsed = SearchFilter.parse(query, optimize=False)
|
|
248
|
+
|
|
249
|
+
assert parsed.is_nested()
|
|
250
|
+
assert parsed.operator == LogicalOperator.AND
|
|
251
|
+
assert len(parsed.children) == 7
|
|
252
|
+
|
|
253
|
+
# name = Henk
|
|
254
|
+
assert parsed.children[0].attribute == "name"
|
|
255
|
+
assert parsed.children[0].operator.value == "="
|
|
256
|
+
assert parsed.children[0].value == "Henk"
|
|
257
|
+
|
|
258
|
+
# age >= 18
|
|
259
|
+
assert parsed.children[1].attribute == "age"
|
|
260
|
+
assert parsed.children[1].operator.value == ">="
|
|
261
|
+
assert parsed.children[1].value == "18"
|
|
262
|
+
|
|
263
|
+
# score <= 100
|
|
264
|
+
assert parsed.children[2].attribute == "score"
|
|
265
|
+
assert parsed.children[2].operator.value == "<="
|
|
266
|
+
assert parsed.children[2].value == "100"
|
|
267
|
+
|
|
268
|
+
# priority > 5
|
|
269
|
+
assert parsed.children[3].attribute == "priority"
|
|
270
|
+
assert parsed.children[3].operator.value == ">"
|
|
271
|
+
assert parsed.children[3].value == "5"
|
|
272
|
+
|
|
273
|
+
# weight < 75
|
|
274
|
+
assert parsed.children[4].attribute == "weight"
|
|
275
|
+
assert parsed.children[4].operator.value == "<"
|
|
276
|
+
assert parsed.children[4].value == "75"
|
|
277
|
+
|
|
278
|
+
# description ~= admin
|
|
279
|
+
assert parsed.children[5].attribute == "description"
|
|
280
|
+
assert parsed.children[5].operator.value == "~="
|
|
281
|
+
assert parsed.children[5].value == "admin"
|
|
282
|
+
|
|
283
|
+
# flags := 1024
|
|
284
|
+
assert parsed.children[6].attribute == "flags"
|
|
285
|
+
assert parsed.children[6].operator.value == ":="
|
|
286
|
+
assert parsed.children[6].value == "1024"
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def test_nested_not_with_different_comparison_operators() -> None:
|
|
290
|
+
"""Test NOT with various comparison operators."""
|
|
291
|
+
query = "(!(|(age<18)(score>=90)(userAccountControl:=512)))"
|
|
292
|
+
parsed = SearchFilter.parse(query, optimize=False)
|
|
293
|
+
|
|
294
|
+
assert parsed.is_nested()
|
|
295
|
+
assert parsed.operator == LogicalOperator.NOT
|
|
296
|
+
assert len(parsed.children) == 1
|
|
297
|
+
|
|
298
|
+
# Inner OR filter
|
|
299
|
+
inner_filter = parsed.children[0]
|
|
300
|
+
assert inner_filter.operator == LogicalOperator.OR
|
|
301
|
+
assert len(inner_filter.children) == 3
|
|
302
|
+
|
|
303
|
+
# age < 18
|
|
304
|
+
assert inner_filter.children[0].attribute == "age"
|
|
305
|
+
assert inner_filter.children[0].operator.value == "<"
|
|
306
|
+
assert inner_filter.children[0].value == "18"
|
|
307
|
+
|
|
308
|
+
# score >= 90
|
|
309
|
+
assert inner_filter.children[1].attribute == "score"
|
|
310
|
+
assert inner_filter.children[1].operator.value == ">="
|
|
311
|
+
assert inner_filter.children[1].value == "90"
|
|
312
|
+
|
|
313
|
+
# userAccountControl := 512
|
|
314
|
+
assert inner_filter.children[2].attribute == "userAccountControl"
|
|
315
|
+
assert inner_filter.children[2].operator.value == ":="
|
|
316
|
+
assert inner_filter.children[2].value == "512"
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def test_complex_nested_with_all_operators() -> None:
|
|
320
|
+
"""Test complex nested filters with multiple operators."""
|
|
321
|
+
query = "(&(objectClass=person)(!(department~=temp))(|(age>=21)(|(salary>9000)(title<=manager))))"
|
|
322
|
+
parsed = SearchFilter.parse(query, optimize=False)
|
|
323
|
+
|
|
324
|
+
assert parsed.is_nested()
|
|
325
|
+
assert parsed.operator == LogicalOperator.AND
|
|
326
|
+
assert len(parsed.children) == 3
|
|
327
|
+
|
|
328
|
+
# First: (objectClass=person)
|
|
329
|
+
assert parsed.children[0].attribute == "objectClass"
|
|
330
|
+
assert parsed.children[0].operator.value == "="
|
|
331
|
+
assert parsed.children[0].value == "person"
|
|
332
|
+
|
|
333
|
+
# Second: (!(department~=temp))
|
|
334
|
+
not_filter = parsed.children[1]
|
|
335
|
+
assert not_filter.operator == LogicalOperator.NOT
|
|
336
|
+
assert not_filter.children[0].attribute == "department"
|
|
337
|
+
assert not_filter.children[0].operator.value == "~="
|
|
338
|
+
assert not_filter.children[0].value == "temp"
|
|
339
|
+
|
|
340
|
+
# Third: (|(age>=21)(|(salary>50000)(title<=manager)))
|
|
341
|
+
or_filter = parsed.children[2]
|
|
342
|
+
assert or_filter.operator == LogicalOperator.OR
|
|
343
|
+
assert len(or_filter.children) == 2
|
|
344
|
+
|
|
345
|
+
# age >= 21
|
|
346
|
+
assert or_filter.children[0].attribute == "age"
|
|
347
|
+
assert or_filter.children[0].operator.value == ">="
|
|
348
|
+
assert or_filter.children[0].value == "21"
|
|
349
|
+
|
|
350
|
+
# Inner OR: (|(salary>50000)(title<=manager))
|
|
351
|
+
inner_or = or_filter.children[1]
|
|
352
|
+
assert inner_or.operator == LogicalOperator.OR
|
|
353
|
+
assert len(inner_or.children) == 2
|
|
354
|
+
|
|
355
|
+
# salary > 50000
|
|
356
|
+
assert inner_or.children[0].attribute == "salary"
|
|
357
|
+
assert inner_or.children[0].operator.value == ">"
|
|
358
|
+
assert inner_or.children[0].value == "9000"
|
|
359
|
+
|
|
360
|
+
# title <= manager (this tests string values with comparison operators)
|
|
361
|
+
assert inner_or.children[1].attribute == "title"
|
|
362
|
+
assert inner_or.children[1].operator.value == "<="
|
|
363
|
+
assert inner_or.children[1].value == "manager"
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def test_presence_filters() -> None:
|
|
367
|
+
"""Test presence filters."""
|
|
368
|
+
# Presence filter - attribute exists
|
|
369
|
+
query = "(mailNickName=*)"
|
|
370
|
+
parsed = SearchFilter.parse(query, optimize=False)
|
|
371
|
+
|
|
372
|
+
assert not parsed.is_nested()
|
|
373
|
+
assert parsed.attribute == "mailNickName"
|
|
374
|
+
assert parsed.operator.value == "="
|
|
375
|
+
assert parsed.value == "*"
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def test_absence_filters() -> None:
|
|
379
|
+
"""Test absence filters."""
|
|
380
|
+
# Absence filter - attribute does not exist
|
|
381
|
+
query = "(!(proxyAddresses=*))"
|
|
382
|
+
parsed = SearchFilter.parse(query, optimize=False)
|
|
383
|
+
|
|
384
|
+
assert parsed.is_nested()
|
|
385
|
+
assert parsed.operator == LogicalOperator.NOT
|
|
386
|
+
assert len(parsed.children) == 1
|
|
387
|
+
|
|
388
|
+
inner = parsed.children[0]
|
|
389
|
+
assert inner.attribute == "proxyAddresses"
|
|
390
|
+
assert inner.operator.value == "="
|
|
391
|
+
assert inner.value == "*"
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
@pytest.mark.parametrize(
|
|
395
|
+
("query", "attribute", "value"),
|
|
396
|
+
[
|
|
397
|
+
("(name=Henk*)", "name", "Henk*"),
|
|
398
|
+
("(name=*Henk)", "name", "*Henk"),
|
|
399
|
+
("(name=*Jo*hn*)", "name", "*Jo*hn*"),
|
|
400
|
+
("(email=*@domain.com)", "email", "*@domain.com"),
|
|
401
|
+
],
|
|
402
|
+
)
|
|
403
|
+
def test_wildcard_with_different_operators(query: str, attribute: str, value: str) -> None:
|
|
404
|
+
"""Test wildcards with = operator. Wildcards should work with = operator in all positions."""
|
|
405
|
+
parsed = SearchFilter.parse(query, optimize=False)
|
|
406
|
+
|
|
407
|
+
assert not parsed.is_nested()
|
|
408
|
+
assert parsed.attribute == attribute
|
|
409
|
+
assert parsed.operator.value == "="
|
|
410
|
+
assert parsed.value == value
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
@pytest.mark.parametrize(
|
|
414
|
+
("expected", "attribute", "op", "value"),
|
|
415
|
+
[
|
|
416
|
+
pytest.param("(name=value)", "name", "=", "value", id="="),
|
|
417
|
+
pytest.param("(age>=18)", "age", ">=", "18", id=">="),
|
|
418
|
+
pytest.param("(score<=100)", "score", "<=", "100", id="<="),
|
|
419
|
+
pytest.param("(priority>5)", "priority", ">", "5", id=">"),
|
|
420
|
+
pytest.param("(weight<75)", "weight", "<", "75", id="<"),
|
|
421
|
+
pytest.param("(desc~=admin)", "desc", "~=", "admin", id="!="),
|
|
422
|
+
pytest.param("(flags:=1024)", "flags", ":=", "1024", id=":="),
|
|
423
|
+
],
|
|
424
|
+
)
|
|
425
|
+
def test_format_with_all_operators(expected: str, attribute: str, op: str, value: str) -> None:
|
|
426
|
+
"""Test format() method with all operators."""
|
|
427
|
+
query = f"({attribute}{op}{value})"
|
|
428
|
+
parsed = SearchFilter.parse(query, optimize=False)
|
|
429
|
+
assert parsed.format() == expected, f"Failed for {query}"
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def test_error_no_comparison_operator() -> None:
|
|
433
|
+
"""Test error for missing comparison operator."""
|
|
434
|
+
with pytest.raises(InvalidQueryError, match="No comparison operator found in query: .+ Expected one of .*"):
|
|
435
|
+
SearchFilter.parse("(attributename)", optimize=False)
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def test_error_multiple_comparison_operators() -> None:
|
|
439
|
+
"""Test error for multiple comparison operators."""
|
|
440
|
+
# This creates a query with both >= and <= operators
|
|
441
|
+
with pytest.raises(InvalidQueryError, match="Multiple comparison operators found"):
|
|
442
|
+
SearchFilter.parse("(attribute>=value<=other)", optimize=False)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def test_repr_nested_filter() -> None:
|
|
446
|
+
"""Test ``__repr__`` for nested filters."""
|
|
447
|
+
query = "(&(name=Henk)(age>=18))"
|
|
448
|
+
parsed = SearchFilter.parse(query, optimize=False)
|
|
449
|
+
|
|
450
|
+
# Test repr of the main nested filter
|
|
451
|
+
assert (
|
|
452
|
+
repr(parsed)
|
|
453
|
+
== "<SearchFilter nested operator='&' children=[<SearchFilter attribute='name' operator='=' value=Henk>, <SearchFilter attribute='age' operator='>=' value=18>]>" # noqa: E501
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def test_repr_simple_filter() -> None:
|
|
458
|
+
"""Test ``__repr__`` for simple filters."""
|
|
459
|
+
query = "(name=Henk)"
|
|
460
|
+
parsed = SearchFilter.parse(query, optimize=False)
|
|
461
|
+
|
|
462
|
+
assert repr(parsed) == "<SearchFilter attribute='name' operator='=' value=Henk>"
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def test_optimizer_fallback_case() -> None:
|
|
466
|
+
"""Test optimizer fallback case."""
|
|
467
|
+
# Create a nested filter that doesn't match other optimization cases
|
|
468
|
+
|
|
469
|
+
obj = SearchFilter.__new__(SearchFilter)
|
|
470
|
+
obj.query = "(&)"
|
|
471
|
+
obj.children = []
|
|
472
|
+
obj.operator = LogicalOperator.AND
|
|
473
|
+
obj.attribute = None
|
|
474
|
+
obj.value = None
|
|
475
|
+
|
|
476
|
+
# This should hit the fallback case
|
|
477
|
+
_, weight = optimize_ldap_query(obj)
|
|
478
|
+
assert weight == max(_ATTRIBUTE_WEIGHTS.values())
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def test_extended_matching_rules() -> None:
|
|
482
|
+
"""Test extended matching rules."""
|
|
483
|
+
# BIT_AND rule (1.2.840.113556.1.4.803)
|
|
484
|
+
query = "(groupType:1.2.840.113556.1.4.803:=8)"
|
|
485
|
+
parsed = SearchFilter.parse(query, optimize=False)
|
|
486
|
+
|
|
487
|
+
assert not parsed.is_nested()
|
|
488
|
+
assert parsed.attribute == "groupType"
|
|
489
|
+
assert parsed.operator == ComparisonOperator.EXTENDED
|
|
490
|
+
assert parsed.value == "8"
|
|
491
|
+
assert parsed._extended_rule == "1.2.840.113556.1.4.803"
|
|
492
|
+
|
|
493
|
+
# Test formatting of extended matching rules
|
|
494
|
+
assert parsed.format() == query
|
|
495
|
+
|
|
496
|
+
# BIT_OR rule (1.2.840.113556.1.4.804)
|
|
497
|
+
query = "(userAccountControl:1.2.840.113556.1.4.804:=65568)"
|
|
498
|
+
parsed = SearchFilter.parse(query, optimize=False)
|
|
499
|
+
|
|
500
|
+
assert not parsed.is_nested()
|
|
501
|
+
assert parsed.attribute == "userAccountControl"
|
|
502
|
+
assert parsed.operator == ComparisonOperator.EXTENDED
|
|
503
|
+
assert parsed.value == "65568"
|
|
504
|
+
assert parsed._extended_rule == "1.2.840.113556.1.4.804"
|
|
505
|
+
|
|
506
|
+
# Test formatting of extended matching rules
|
|
507
|
+
assert parsed.format() == query
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def test_extended_matching_in_nested_query() -> None:
|
|
511
|
+
"""Test extended matching in nested queries."""
|
|
512
|
+
query = "(&(objectClass=group)(groupType:1.2.840.113556.1.4.803:=2147483648))"
|
|
513
|
+
parsed = SearchFilter.parse(query, optimize=False)
|
|
514
|
+
|
|
515
|
+
assert parsed.is_nested()
|
|
516
|
+
assert parsed.operator == LogicalOperator.AND
|
|
517
|
+
assert len(parsed.children) == 2
|
|
518
|
+
|
|
519
|
+
# First child - regular filter
|
|
520
|
+
assert parsed.children[0].attribute == "objectClass"
|
|
521
|
+
assert parsed.children[0].operator.value == "="
|
|
522
|
+
assert parsed.children[0].value == "group"
|
|
523
|
+
|
|
524
|
+
# Second child - extended matching rule
|
|
525
|
+
assert parsed.children[1].attribute == "groupType"
|
|
526
|
+
assert parsed.children[1].operator == ComparisonOperator.EXTENDED
|
|
527
|
+
assert parsed.children[1].value == "2147483648"
|
|
528
|
+
assert parsed.children[1]._extended_rule == "1.2.840.113556.1.4.803"
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def test_not_operator_optimization() -> None:
|
|
532
|
+
"""Test that NOT operators properly optimize their children."""
|
|
533
|
+
# Create a NOT filter with a redundant single-child AND that should be optimized
|
|
534
|
+
query = "(!(&(objectClass=user)))"
|
|
535
|
+
|
|
536
|
+
# Parse with optimization enabled
|
|
537
|
+
parsed_optimized = SearchFilter.parse(query, optimize=True)
|
|
538
|
+
|
|
539
|
+
# Parse without optimization to compare
|
|
540
|
+
parsed_unoptimized = SearchFilter.parse(query, optimize=False)
|
|
541
|
+
|
|
542
|
+
# The optimized version should have simplified the inner (&(objectClass=user)) to just (objectClass=user)
|
|
543
|
+
assert parsed_optimized.operator == LogicalOperator.NOT
|
|
544
|
+
assert len(parsed_optimized.children) == 1
|
|
545
|
+
|
|
546
|
+
# The child should be a simple filter (not nested) after optimization
|
|
547
|
+
inner_child = parsed_optimized.children[0]
|
|
548
|
+
assert not inner_child.is_nested()
|
|
549
|
+
assert inner_child.attribute == "objectClass"
|
|
550
|
+
assert inner_child.value == "user"
|
|
551
|
+
|
|
552
|
+
# Verify the query strings are different (optimization occurred)
|
|
553
|
+
assert parsed_optimized.query != parsed_unoptimized.query
|
|
554
|
+
assert parsed_optimized.query == "(!(objectClass=user))"
|
|
555
|
+
|
|
556
|
+
# Test with a more complex case: NOT with nested AND containing OR
|
|
557
|
+
complex_query = "(!(&(|(name=Henk)(name=Jan))))"
|
|
558
|
+
complex_parsed = SearchFilter.parse(complex_query, optimize=True)
|
|
559
|
+
|
|
560
|
+
# Should optimize to: (!(|(name=Henk)(name=Jan)))
|
|
561
|
+
assert complex_parsed.operator == LogicalOperator.NOT
|
|
562
|
+
inner = complex_parsed.children[0]
|
|
563
|
+
assert inner.operator == LogicalOperator.OR
|
|
564
|
+
assert len(inner.children) == 2
|
|
565
|
+
assert complex_parsed.query == "(!(|(name=Henk)(name=Jan)))"
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def test_real_world_examples() -> None:
|
|
569
|
+
"""Test real-world LDAP filter examples."""
|
|
570
|
+
query1 = "(&(objectclass=user)(displayName=Henk))"
|
|
571
|
+
parsed = SearchFilter.parse(query1, optimize=False)
|
|
572
|
+
assert parsed.operator == LogicalOperator.AND
|
|
573
|
+
assert len(parsed.children) == 2
|
|
574
|
+
|
|
575
|
+
query2 = "(!(objectClass=group))"
|
|
576
|
+
parsed = SearchFilter.parse(query2, optimize=False)
|
|
577
|
+
assert parsed.operator == LogicalOperator.NOT
|
|
578
|
+
assert parsed.children[0].attribute == "objectClass"
|
|
579
|
+
assert parsed.children[0].value == "group"
|
|
580
|
+
|
|
581
|
+
query = "(mail=*@henk.nl)"
|
|
582
|
+
parsed = SearchFilter.parse(query, optimize=False)
|
|
583
|
+
assert parsed.attribute == "mail"
|
|
584
|
+
assert parsed.value == "*@henk.nl"
|
|
585
|
+
|
|
586
|
+
query = "(mdbStorageQuota>=100000)"
|
|
587
|
+
parsed = SearchFilter.parse(query, optimize=False)
|
|
588
|
+
assert parsed.attribute == "mdbStorageQuota"
|
|
589
|
+
assert parsed.operator.value == ">="
|
|
590
|
+
assert parsed.value == "100000"
|
|
591
|
+
|
|
592
|
+
query = "(displayName~=Henk)"
|
|
593
|
+
parsed = SearchFilter.parse(query, optimize=False)
|
|
594
|
+
assert parsed.attribute == "displayName"
|
|
595
|
+
assert parsed.operator.value == "~="
|
|
596
|
+
assert parsed.value == "Henk"
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def test_anr_style_filters() -> None:
|
|
600
|
+
"""Test ANR-style filters."""
|
|
601
|
+
# Simple ANR-style filter (though ANR itself is server-side)
|
|
602
|
+
query1 = "(anr=Henk)"
|
|
603
|
+
parsed = SearchFilter.parse(query1, optimize=False)
|
|
604
|
+
assert parsed.attribute == "anr"
|
|
605
|
+
assert parsed.value == "Henk"
|
|
606
|
+
|
|
607
|
+
# ANR with partial match
|
|
608
|
+
query2 = "(anr=Henk*)"
|
|
609
|
+
parsed = SearchFilter.parse(query2, optimize=False)
|
|
610
|
+
assert parsed.attribute == "anr"
|
|
611
|
+
assert parsed.value == "Henk*"
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
def test_complex_real_world_filter() -> None:
|
|
615
|
+
"""Test complex real-world filter."""
|
|
616
|
+
# Complex filter for finding specific user accounts
|
|
617
|
+
query = (
|
|
618
|
+
"(&"
|
|
619
|
+
"(objectClass=user)"
|
|
620
|
+
"(!(userAccountControl:1.2.840.113556.1.4.803:=2))"
|
|
621
|
+
"(|(sAMAccountName=admin*)(mail=*@admin.*))"
|
|
622
|
+
"(pwdLastSet>=0)"
|
|
623
|
+
"(!(description=*))"
|
|
624
|
+
")"
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
parsed = SearchFilter.parse(query, optimize=False)
|
|
628
|
+
assert parsed.operator == LogicalOperator.AND
|
|
629
|
+
assert len(parsed.children) == 5
|
|
630
|
+
|
|
631
|
+
# Check that all components are parsed correctly
|
|
632
|
+
# objectClass=user
|
|
633
|
+
assert any(child.attribute == "objectClass" and child.value == "user" for child in parsed.children)
|
|
634
|
+
|
|
635
|
+
# Extended matching rule for userAccountControl
|
|
636
|
+
bit_filter = next(
|
|
637
|
+
(
|
|
638
|
+
child
|
|
639
|
+
for child in parsed.children
|
|
640
|
+
if child.operator == LogicalOperator.NOT and child.children[0].operator == ComparisonOperator.EXTENDED
|
|
641
|
+
),
|
|
642
|
+
None,
|
|
643
|
+
)
|
|
644
|
+
assert bit_filter is not None
|
|
645
|
+
assert bit_filter.children[0].attribute == "userAccountControl"
|
|
646
|
+
|
|
647
|
+
# OR condition with wildcards
|
|
648
|
+
or_filter = next((child for child in parsed.children if child.operator == LogicalOperator.OR), None)
|
|
649
|
+
assert or_filter is not None
|
|
650
|
+
assert len(or_filter.children) == 2
|
|
651
|
+
assert "admin*" in [child.value for child in or_filter.children]
|
|
652
|
+
assert "*@admin.*" in [child.value for child in or_filter.children]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/dissect/util/_native/compression/__init__.pyi
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/dissect/util/_native.src/src/compression/lz4.rs
RENAMED
|
File without changes
|
{dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/dissect/util/_native.src/src/compression/lzo.rs
RENAMED
|
File without changes
|
{dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/dissect/util/_native.src/src/compression.rs
RENAMED
|
File without changes
|
{dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/dissect/util/_native.src/src/hash/crc32c.rs
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/dissect/util/compression/lzxpress_huffman.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/dissect/util/tools/dump_nskeyedarchiver.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/dissect.util.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dissect_util-3.23.dev7 → dissect_util-3.23.dev8}/tests/compression/test_lzxpress_huffman.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|