karton-core
Advanced tools
| import sys, types, os;has_mfs = sys.version_info > (3, 5);p = os.path.join(sys._getframe(1).f_locals['sitedir'], *('karton',));importlib = has_mfs and __import__('importlib.util');has_mfs and __import__('importlib.machinery');m = has_mfs and sys.modules.setdefault('karton', importlib.util.module_from_spec(importlib.machinery.PathFinder.find_spec('karton', [os.path.dirname(p)])));m = m or sys.modules.setdefault('karton', types.ModuleType('karton'));mp = (m or []) and m.__dict__.setdefault('__path__',[]);(p not in mp) and mp.append(p) |
| import fnmatch | ||
| import re | ||
| from collections.abc import Mapping, Sequence | ||
| from typing import Dict, Type | ||
| # Source code adopted from https://github.com/kapouille/mongoquery | ||
| # Original licenced under "The Unlicense" license. | ||
| class QueryError(Exception): | ||
| """Query error exception""" | ||
| pass | ||
| class _Undefined(object): | ||
| pass | ||
| def is_non_string_sequence(entry): | ||
| """Returns True if entry is a Python sequence iterable, and not a string""" | ||
| return isinstance(entry, Sequence) and not isinstance(entry, str) | ||
| class Query(object): | ||
| """The Query class is used to match an object against a MongoDB-like query""" | ||
| def __init__(self, definition): | ||
| self._definition = definition | ||
| def match(self, entry): | ||
| """Matches the entry object against the query specified on instanciation""" | ||
| return self._match(self._definition, entry) | ||
| def _match(self, condition, entry): | ||
| if isinstance(condition, Mapping): | ||
| return all( | ||
| self._process_condition(sub_operator, sub_condition, entry) | ||
| for sub_operator, sub_condition in condition.items() | ||
| ) | ||
| if is_non_string_sequence(entry): | ||
| return condition in entry | ||
| return condition == entry | ||
| def _extract(self, entry, path): | ||
| if not path: | ||
| return entry | ||
| if entry is None: | ||
| return entry | ||
| if is_non_string_sequence(entry): | ||
| try: | ||
| index = int(path[0]) | ||
| return self._extract(entry[index], path[1:]) | ||
| except ValueError: | ||
| return [self._extract(item, path) for item in entry] | ||
| elif isinstance(entry, Mapping) and path[0] in entry: | ||
| return self._extract(entry[path[0]], path[1:]) | ||
| else: | ||
| return _Undefined() | ||
| def _path_exists(self, operator, condition, entry): | ||
| keys_list = list(operator.split(".")) | ||
| for i, k in enumerate(keys_list): | ||
| if isinstance(entry, Sequence) and not k.isdigit(): | ||
| for elem in entry: | ||
| operator = ".".join(keys_list[i:]) | ||
| if self._path_exists(operator, condition, elem) == condition: | ||
| return condition | ||
| return not condition | ||
| elif isinstance(entry, Sequence): | ||
| k = int(k) | ||
| try: | ||
| entry = entry[k] | ||
| except (TypeError, IndexError, KeyError): | ||
| return not condition | ||
| return condition | ||
| def _process_condition(self, operator, condition, entry): | ||
| if isinstance(condition, Mapping) and "$exists" in condition: | ||
| if isinstance(operator, str) and operator.find(".") != -1: | ||
| return self._path_exists(operator, condition["$exists"], entry) | ||
| elif condition["$exists"] != (operator in entry): | ||
| return False | ||
| elif tuple(condition.keys()) == ("$exists",): | ||
| return True | ||
| if isinstance(operator, str): | ||
| if operator.startswith("$"): | ||
| try: | ||
| return getattr(self, "_" + operator[1:])(condition, entry) | ||
| except AttributeError: | ||
| raise QueryError(f"{operator} operator isn't supported") | ||
| else: | ||
| try: | ||
| extracted_data = self._extract(entry, operator.split(".")) | ||
| except IndexError: | ||
| extracted_data = _Undefined() | ||
| else: | ||
| if operator not in entry: | ||
| return False | ||
| extracted_data = entry[operator] | ||
| return self._match(condition, extracted_data) | ||
| @staticmethod | ||
| def _not_implemented(*_): | ||
| raise NotImplementedError | ||
| @staticmethod | ||
| def _noop(*_): | ||
| return True | ||
| @staticmethod | ||
| def _eq(condition, entry): | ||
| try: | ||
| return entry == condition | ||
| except TypeError: | ||
| return False | ||
| @staticmethod | ||
| def _gt(condition, entry): | ||
| try: | ||
| return entry > condition | ||
| except TypeError: | ||
| return False | ||
| @staticmethod | ||
| def _gte(condition, entry): | ||
| try: | ||
| return entry >= condition | ||
| except TypeError: | ||
| return False | ||
| @staticmethod | ||
| def _in(condition, entry): | ||
| if is_non_string_sequence(condition): | ||
| for elem in condition: | ||
| if is_non_string_sequence(entry) and elem in entry: | ||
| return True | ||
| elif not is_non_string_sequence(entry) and elem == entry: | ||
| return True | ||
| return False | ||
| else: | ||
| raise TypeError("condition must be a list") | ||
| @staticmethod | ||
| def _lt(condition, entry): | ||
| try: | ||
| return entry < condition | ||
| except TypeError: | ||
| return False | ||
| @staticmethod | ||
| def _lte(condition, entry): | ||
| try: | ||
| return entry <= condition | ||
| except TypeError: | ||
| return False | ||
| @staticmethod | ||
| def _ne(condition, entry): | ||
| return entry != condition | ||
| def _nin(self, condition, entry): | ||
| return not self._in(condition, entry) | ||
| def _and(self, condition, entry): | ||
| if isinstance(condition, Sequence): | ||
| return all(self._match(sub_condition, entry) for sub_condition in condition) | ||
| raise QueryError(f"$and has been attributed incorrect argument {condition}") | ||
| def _nor(self, condition, entry): | ||
| if isinstance(condition, Sequence): | ||
| return all( | ||
| not self._match(sub_condition, entry) for sub_condition in condition | ||
| ) | ||
| raise QueryError(f"$nor has been attributed incorrect argument {condition}") | ||
| def _not(self, condition, entry): | ||
| return not self._match(condition, entry) | ||
| def _or(self, condition, entry): | ||
| if isinstance(condition, Sequence): | ||
| return any(self._match(sub_condition, entry) for sub_condition in condition) | ||
| raise QueryError(f"$or has been attributed incorrect argument {condition}") | ||
| @staticmethod | ||
| def _type(condition, entry): | ||
| bson_type: Dict[int, Type] = { | ||
| 1: float, | ||
| 2: str, | ||
| 3: Mapping, | ||
| 4: Sequence, | ||
| 5: bytearray, | ||
| 7: str, # object id (uuid) | ||
| 8: bool, | ||
| 9: str, # date (UTC datetime) | ||
| 10: type(None), | ||
| 11: re.Pattern, # regex, | ||
| 13: str, # Javascript | ||
| 15: str, # JavaScript (with scope) | ||
| 16: int, # 32-bit integer | ||
| 17: int, # Timestamp | ||
| 18: int, # 64-bit integer | ||
| } | ||
| bson_alias = { | ||
| "double": 1, | ||
| "string": 2, | ||
| "object": 3, | ||
| "array": 4, | ||
| "binData": 5, | ||
| "objectId": 7, | ||
| "bool": 8, | ||
| "date": 9, | ||
| "null": 10, | ||
| "regex": 11, | ||
| "javascript": 13, | ||
| "javascriptWithScope": 15, | ||
| "int": 16, | ||
| "timestamp": 17, | ||
| "long": 18, | ||
| } | ||
| if condition == "number": | ||
| return any( | ||
| [ | ||
| isinstance(entry, bson_type[bson_alias[alias]]) | ||
| for alias in ["double", "int", "long"] | ||
| ] | ||
| ) | ||
| # resolves bson alias, or keeps original condition value | ||
| condition = bson_alias.get(condition, condition) | ||
| if condition not in bson_type: | ||
| raise QueryError(f"$type has been used with unknown type {condition}") | ||
| return isinstance(entry, bson_type[condition]) | ||
| _exists = _noop | ||
| @staticmethod | ||
| def _mod(condition, entry): | ||
| return entry % condition[0] == condition[1] | ||
| @staticmethod | ||
| def _regex(condition, entry): | ||
| if not isinstance(entry, str): | ||
| return False | ||
| # If the caller has supplied a compiled regex, assume options are already | ||
| # included. | ||
| if isinstance(condition, re.Pattern): | ||
| return bool(re.search(condition, entry)) | ||
| try: | ||
| regex = re.match(r"\A/(.+)/([imsx]{,4})\Z", condition, flags=re.DOTALL) | ||
| except TypeError: | ||
| raise QueryError( | ||
| f"{condition} is not a regular expression and should be a string" | ||
| ) | ||
| flags = 0 | ||
| if regex: | ||
| options = regex.group(2) | ||
| for option in options: | ||
| flags |= getattr(re, option.upper()) | ||
| exp = regex.group(1) | ||
| else: | ||
| exp = condition | ||
| try: | ||
| match = re.search(exp, entry, flags=flags) | ||
| except Exception as error: | ||
| raise QueryError(f"{condition} failed to execute with error {error!r}") | ||
| return bool(match) | ||
| _options = _text = _where = _not_implemented | ||
| def _all(self, condition, entry): | ||
| return all(self._match(item, entry) for item in condition) | ||
| def _elemMatch(self, condition, entry): | ||
| if not isinstance(entry, Sequence): | ||
| return False | ||
| return any( | ||
| all( | ||
| self._process_condition(sub_operator, sub_condition, element) | ||
| for sub_operator, sub_condition in condition.items() | ||
| ) | ||
| for element in entry | ||
| ) | ||
| @staticmethod | ||
| def _size(condition, entry): | ||
| if not isinstance(condition, int): | ||
| raise QueryError( | ||
| f"$size has been attributed incorrect argument {condition}" | ||
| ) | ||
| if is_non_string_sequence(entry): | ||
| return len(entry) == condition | ||
| return False | ||
| def __repr__(self): | ||
| return f"<Query({self._definition})>" | ||
| def toregex(wildcard): | ||
| if not isinstance(wildcard, str): | ||
| raise QueryError(f"Unexpected value in the regex conversion: {wildcard}") | ||
| # If is not neessary, but we avoid unnecessary regular expressions. | ||
| if any(c in wildcard for c in "?*[]!"): | ||
| return {"$regex": fnmatch.translate(wildcard)} | ||
| return wildcard | ||
| def convert(filters): | ||
| """Convert filters to the mongo query syntax. | ||
| A special care is taken to handle old-style negative filters correctly | ||
| """ | ||
| # Negative_filters are old-style negative assertions, and behave differently. | ||
| # See issue #246 for the original bug report. | ||
| # | ||
| # For a short example: | ||
| # [{"platform": "!win32"}, {"platform": "!linux"}] | ||
| # will match all non-linux non-windows samples, but: | ||
| # [{"platform": {"$not": "win32"}}, {"platform": {"$not": "linux"}}] | ||
| # means `platform != "win32" or "platform != "linux"` and will match everything. | ||
| # To get equivalent behaviour with mongo syntax, you should use: | ||
| # [{"platform": {"$not": {"$or": ["win32", "linux"]}}}] | ||
| regular_filter, negative_filter = [], [] | ||
| for rule in filters: | ||
| positive_checks, negative_checks = [], [] | ||
| for key, value in rule.items(): | ||
| if isinstance(value, str): | ||
| if value and value[0] == "!": # negative check | ||
| negative_checks.append({key: toregex(value[1:])}) | ||
| else: | ||
| positive_checks.append({key: toregex(value)}) | ||
| else: | ||
| positive_checks.append({key: value}) | ||
| regular_filter.append({"$and": positive_checks}) | ||
| negative_filter.append({"$and": positive_checks + [{"$or": negative_checks}]}) | ||
| return Query( | ||
| { | ||
| "$and": [ | ||
| {"$not": {"$or": negative_filter}}, | ||
| {"$or": regular_filter}, | ||
| ] | ||
| } | ||
| ) |
@@ -1,1 +0,1 @@ | ||
| __version__ = "5.4.0" | ||
| __version__ = "5.5.0" |
@@ -11,2 +11,3 @@ """ | ||
| from . import query | ||
| from .__version__ import __version__ | ||
@@ -126,2 +127,5 @@ from .backend import KartonBackend, KartonBind, KartonMetrics | ||
| # Dummy conversion to make sure the filters are well-formed. | ||
| query.convert(self.filters) | ||
| self.persistent = ( | ||
@@ -128,0 +132,0 @@ self.config.getboolean("karton", "persistent", self.persistent) |
+3
-70
| import enum | ||
| import fnmatch | ||
| import json | ||
@@ -19,2 +18,3 @@ import time | ||
| from . import query | ||
| from .resource import RemoteResource, ResourceBase | ||
@@ -227,72 +227,5 @@ from .utils import recursive_iter, recursive_iter_with_keys, recursive_map | ||
| def matches_filters(self, filters: List[Dict[str, Any]]) -> bool: | ||
| """ | ||
| Checks whether provided task headers match filters | ||
| """Check if a task matches the given filters""" | ||
| return query.convert(filters).match(self.headers) | ||
| :param filters: Task header filters | ||
| :return: True if task headers match specific filters | ||
| :meta private: | ||
| """ | ||
| def test_filter(headers: Dict[str, Any], filter: Dict[str, Any]) -> int: | ||
| """ | ||
| Filter match follows AND logic, but it's non-boolean because filters may be | ||
| negated (task:!platform). | ||
| Result values are as follows: | ||
| - 1 - positive match, no mismatched values in headers | ||
| (all matched) | ||
| - 0 - no match, found value that doesn't match to the filter | ||
| (some are not matched) | ||
| - -1 - negative match, found value that matches negated filter value | ||
| (all matched but found negative matches) | ||
| """ | ||
| matches = 1 | ||
| for filter_key, filter_value in filter.items(): | ||
| # Coerce filter value to string | ||
| filter_value_str = str(filter_value) | ||
| negated = False | ||
| if filter_value_str.startswith("!"): | ||
| negated = True | ||
| filter_value_str = filter_value_str[1:] | ||
| # If expected key doesn't exist in headers | ||
| if filter_key not in headers: | ||
| # Negated filter ignores non-existent values | ||
| if negated: | ||
| continue | ||
| # But positive filter doesn't | ||
| return 0 | ||
| # Coerce header value to string | ||
| header_value_str = str(headers[filter_key]) | ||
| # fnmatch is great for handling simple wildcard patterns (?, *, [abc]) | ||
| match = fnmatch.fnmatchcase(header_value_str, filter_value_str) | ||
| # If matches, but it's negated: it's negative match | ||
| if match and negated: | ||
| matches = -1 | ||
| # If doesn't match but filter is not negated: it's not a match | ||
| if not match and not negated: | ||
| return 0 | ||
| # If there are no mismatched values: filter is matched | ||
| return matches | ||
| # List of filter matches follow OR logic, but -1 is special | ||
| # If there is any -1, result is False | ||
| # (any matched, but it's negative match) | ||
| # If there is any 1, but no -1's: result is True | ||
| # (any matched, no negative match) | ||
| # If there are only 0's: result is False | ||
| # (none matched) | ||
| matches = False | ||
| for task_filter in filters: | ||
| match_result = test_filter(self.headers, task_filter) | ||
| if match_result == -1: | ||
| # Any negative match results in False | ||
| return False | ||
| if match_result == 1: | ||
| # Any positive match but without negative matches results in True | ||
| matches = True | ||
| return matches | ||
| def set_task_parent(self, parent: "Task"): | ||
@@ -299,0 +232,0 @@ """ |
@@ -6,2 +6,3 @@ import argparse | ||
| from karton.core import query | ||
| from karton.core.__version__ import __version__ | ||
@@ -179,3 +180,8 @@ from karton.core.backend import ( | ||
| identity = bind.identity | ||
| if task.matches_filters(bind.filters): | ||
| try: | ||
| is_match = task.matches_filters(bind.filters) | ||
| except query.QueryError: | ||
| self.log.error("Task matching failed - invalid filters?") | ||
| continue | ||
| if is_match: | ||
| routed_task = task.fork_task() | ||
@@ -182,0 +188,0 @@ routed_task.status = TaskState.SPAWNED |
+1
-1
| Metadata-Version: 2.1 | ||
| Name: karton-core | ||
| Version: 5.4.0 | ||
| Version: 5.5.0 | ||
| Summary: Distributed malware analysis orchestration framework | ||
@@ -5,0 +5,0 @@ Home-page: https://github.com/CERT-Polska/karton |
+13
-12
@@ -1,4 +0,4 @@ | ||
| karton_core-5.4.0-nspkg.pth,sha256=vHa-jm6pBTeInFrmnsHMg9AOeD88czzQy-6QCFbpRcM,539 | ||
| karton_core-5.5.0-nspkg.pth,sha256=vHa-jm6pBTeInFrmnsHMg9AOeD88czzQy-6QCFbpRcM,539 | ||
| karton/core/__init__.py,sha256=QuT0BWZyp799eY90tK3H1OD2hwuusqMJq8vQwpB3kG4,337 | ||
| karton/core/__version__.py,sha256=xjYaBGUFGg0kGZj_WhuoFyPD8NILPsr79SaMwmYQGSg,22 | ||
| karton/core/__version__.py,sha256=zFTHldBmR5ReiC3uSZ8VkZOEirtsq_l6QbUJYRBHlTs,22 | ||
| karton/core/backend.py,sha256=-sQG7utnaWLJOEcafeSwEDLnkflPqtSCwg_mn_nnFhg,36727 | ||
@@ -9,8 +9,9 @@ karton/core/base.py,sha256=C6Lco3E0XCsxvEjeVOLR9fxh_IWJ1vjC9BqUYsQyewE,8083 | ||
| karton/core/inspect.py,sha256=aIJQEOEkD5q2xLlV8nhxY5qL5zqcnprP-2DdP6ecKlE,6150 | ||
| karton/core/karton.py,sha256=9SOAviG42kSsPqc3EuaHzWtA_KywMtc01hmU6FaJpHo,15007 | ||
| karton/core/karton.py,sha256=cXLleTEPCVBIXkj09kKu2hjd1XNUSpTAk87-BES1WlA,15133 | ||
| karton/core/logger.py,sha256=J3XAyG88U0cwYC9zR6E3QD1uJenrQh7zS9-HgxhqeAs,2040 | ||
| karton/core/main.py,sha256=ir1-dhn3vbwfh2YHiM6ZYfRBbjwLvJSz0d8tuK1mb_4,8310 | ||
| karton/core/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 | ||
| karton/core/query.py,sha256=Ay0VzfrBQwdJzcZ27JbOlUc1ZZdOl6A8sh4iIYTmLyE,11493 | ||
| karton/core/resource.py,sha256=tA3y_38H9HVKIrCeAU70zHUkQUv0BuCQWMC470JLxxc,20321 | ||
| karton/core/task.py,sha256=diwg8uUl57NEYNRjT1l5CPiNw3EQcU11BnrLul33fx0,21350 | ||
| karton/core/task.py,sha256=1E_d60XbzqX0O9gFhYe_8aNGH7vuXDHe-bir5cRot_0,18515 | ||
| karton/core/test.py,sha256=tms-YM7sUKQDHN0vm2_W7DIvHnO_ld_VPsWHnsbKSfk,9102 | ||
@@ -20,9 +21,9 @@ karton/core/utils.py,sha256=sEVqGdVPyYswWuVn8wYXBQmln8Az826N_2HgC__pmW8,4090 | ||
| karton/system/__main__.py,sha256=QJkwIlSwaPRdzwKlNmCAL41HtDAa73db9MZKWmOfxGM,56 | ||
| karton/system/system.py,sha256=yF_d71a8w7JYA7IXUt63d5_QBH6x1QplB-xcrzQTXL4,13792 | ||
| karton_core-5.4.0.dist-info/LICENSE,sha256=o8h7hYhn7BJC_-DmrfqWwLjaR_Gbe0TZOOQJuN2ca3I,1519 | ||
| karton_core-5.4.0.dist-info/METADATA,sha256=kopeYFCI9EoFQbc7J7woZWjI_5egy29-lYUW7UzEQ2I,6847 | ||
| karton_core-5.4.0.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92 | ||
| karton_core-5.4.0.dist-info/entry_points.txt,sha256=FJj5EZuvFP0LkagjX_dLbRGBUnuLjgBiSyiFfq4c86U,99 | ||
| karton_core-5.4.0.dist-info/namespace_packages.txt,sha256=X8SslCPsqXDCnGZqrYYolzT3xPzJMq1r-ZQSc0jfAEA,7 | ||
| karton_core-5.4.0.dist-info/top_level.txt,sha256=X8SslCPsqXDCnGZqrYYolzT3xPzJMq1r-ZQSc0jfAEA,7 | ||
| karton_core-5.4.0.dist-info/RECORD,, | ||
| karton/system/system.py,sha256=tptar24RuXUnlII1xKbuJtfNkQsSxTtS3g4O8S99tbg,14011 | ||
| karton_core-5.5.0.dist-info/LICENSE,sha256=o8h7hYhn7BJC_-DmrfqWwLjaR_Gbe0TZOOQJuN2ca3I,1519 | ||
| karton_core-5.5.0.dist-info/METADATA,sha256=h4-M_JnMm8z_An5IDFPHAkQ4YuR_-YpwekETiNMjIxQ,6847 | ||
| karton_core-5.5.0.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92 | ||
| karton_core-5.5.0.dist-info/entry_points.txt,sha256=FJj5EZuvFP0LkagjX_dLbRGBUnuLjgBiSyiFfq4c86U,99 | ||
| karton_core-5.5.0.dist-info/namespace_packages.txt,sha256=X8SslCPsqXDCnGZqrYYolzT3xPzJMq1r-ZQSc0jfAEA,7 | ||
| karton_core-5.5.0.dist-info/top_level.txt,sha256=X8SslCPsqXDCnGZqrYYolzT3xPzJMq1r-ZQSc0jfAEA,7 | ||
| karton_core-5.5.0.dist-info/RECORD,, |
| import sys, types, os;has_mfs = sys.version_info > (3, 5);p = os.path.join(sys._getframe(1).f_locals['sitedir'], *('karton',));importlib = has_mfs and __import__('importlib.util');has_mfs and __import__('importlib.machinery');m = has_mfs and sys.modules.setdefault('karton', importlib.util.module_from_spec(importlib.machinery.PathFinder.find_spec('karton', [os.path.dirname(p)])));m = m or sys.modules.setdefault('karton', types.ModuleType('karton'));mp = (m or []) and m.__dict__.setdefault('__path__',[]);(p not in mp) and mp.append(p) |
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.