clue-api 1.7.0.dev459__tar.gz → 1.7.0.dev464__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.
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/PKG-INFO +2 -2
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/api/__init__.py +17 -10
- clue_api-1.7.0.dev459/clue/api/v1/__init__.py → clue_api-1.7.0.dev464/clue/api/documentation.py +8 -11
- clue_api-1.7.0.dev464/clue/api/v1/__init__.py +8 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/api/v1/lookup.py +1 -1
- clue_api-1.7.0.dev464/clue/api/v2/__init__.py +8 -0
- clue_api-1.7.0.dev464/clue/api/v2/lookup.py +41 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/app.py +4 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/common/regex.py +12 -9
- clue_api-1.7.0.dev464/clue/constants/supported_types.py +108 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/services/type_service.py +15 -7
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/pyproject.toml +2 -2
- clue_api-1.7.0.dev459/clue/constants/supported_types.py +0 -85
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/LICENSE +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/README.md +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/.gitignore +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/__init__.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/api/base.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/api/v1/actions.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/api/v1/auth.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/api/v1/configs.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/api/v1/fetchers.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/api/v1/registration.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/api/v1/static.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/api/v1/sync.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/cache/__init__.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/common/__init__.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/common/bytes_utils.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/common/classification.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/common/classification.yml +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/common/dict_utils.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/common/exceptions.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/common/forge.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/common/json_utils.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/common/list_utils.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/common/logging/__init__.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/common/logging/audit.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/common/logging/format.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/common/str_utils.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/common/swagger.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/common/uid.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/config.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/constants/__init__.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/constants/env.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/cronjobs/__init__.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/cronjobs/plugins.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/error.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/extensions/__init__.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/extensions/config.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/gunicorn_config.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/healthz.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/helper/discover.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/helper/headers.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/helper/oauth.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/helper/obo.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/helper/plugin_requests.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/models/__init__.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/models/actions.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/models/auth_user.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/models/config.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/models/fetchers.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/models/graph.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/models/jwks.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/models/model_list.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/models/network.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/models/results/__init__.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/models/results/base.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/models/results/file.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/models/results/graph.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/models/results/image.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/models/results/status.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/models/results/validation.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/models/schema.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/models/selector.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/models/sync.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/models/validators.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/patched.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/plugin/__init__.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/plugin/celery_app.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/plugin/helpers/__init__.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/plugin/helpers/central_server.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/plugin/helpers/email_render.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/plugin/helpers/token.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/plugin/helpers/trino.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/plugin/models.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/plugin/utils.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/py.typed +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/remote/__init__.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/remote/datatypes/__init__.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/remote/datatypes/cache.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/remote/datatypes/events.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/remote/datatypes/hash.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/remote/datatypes/queues/__init__.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/remote/datatypes/queues/comms.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/remote/datatypes/set.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/remote/datatypes/user_quota_tracker.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/security/__init__.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/security/obo.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/security/utils.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/services/action_service.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/services/auth_service.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/services/config_service.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/services/fetcher_service.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/services/jwt_service.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/services/lookup_service.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/services/mongo_service.py +0 -0
- {clue_api-1.7.0.dev459 → clue_api-1.7.0.dev464}/clue/services/user_service.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: clue-api
|
|
3
|
-
Version: 1.7.0.
|
|
3
|
+
Version: 1.7.0.dev464
|
|
4
4
|
Summary: Clue distributed enrichment service
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -36,7 +36,7 @@ Requires-Dist: gevent (>=24.2.1,<25.0.0)
|
|
|
36
36
|
Requires-Dist: gunicorn (>=22,<24)
|
|
37
37
|
Requires-Dist: imgkit (>=1.2.3,<2.0.0)
|
|
38
38
|
Requires-Dist: passlib (>=1.7.4,<2.0.0) ; extra == "server"
|
|
39
|
-
Requires-Dist: pillow (>=11.1,<
|
|
39
|
+
Requires-Dist: pillow (>=11.1,<14.0)
|
|
40
40
|
Requires-Dist: prometheus-client (>=0.20.0,<0.21.0) ; extra == "server"
|
|
41
41
|
Requires-Dist: pydantic (>=2.7.1,<3.0.0)
|
|
42
42
|
Requires-Dist: pydantic-settings[yaml] (>=2.3.4,<3.0.0)
|
|
@@ -2,7 +2,7 @@ from sys import exc_info
|
|
|
2
2
|
from traceback import format_tb
|
|
3
3
|
from typing import Any, Union, cast
|
|
4
4
|
|
|
5
|
-
from flask import Blueprint, Response, make_response, request
|
|
5
|
+
from flask import Blueprint, Response, current_app, make_response, request
|
|
6
6
|
from prometheus_client import Counter
|
|
7
7
|
from pydantic import BaseModel
|
|
8
8
|
|
|
@@ -21,13 +21,19 @@ RAW_API_COUNTER = Counter(
|
|
|
21
21
|
logger = get_logger(__file__)
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
def make_subapi_blueprint(name, api_version=1):
|
|
24
|
+
def make_subapi_blueprint(name: str, api_version: int = 1, *, blueprint_name: str | None = None):
|
|
25
25
|
"""Create a flask Blueprint for a subapi in a standard way."""
|
|
26
|
-
return Blueprint(name, name, url_prefix="/".join([API_PREFIX, f"v{api_version}", name]))
|
|
26
|
+
return Blueprint(blueprint_name or name, name, url_prefix="/".join([API_PREFIX, f"v{api_version}", name]))
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
def _make_api_response(
|
|
30
|
-
data: Any,
|
|
30
|
+
data: Any,
|
|
31
|
+
err: Union[str, Exception] = "",
|
|
32
|
+
warnings: list[str] = [],
|
|
33
|
+
status_code: int = 200,
|
|
34
|
+
cookies: Any = None,
|
|
35
|
+
*,
|
|
36
|
+
sort_keys: bool | None = None,
|
|
31
37
|
) -> Response:
|
|
32
38
|
if isinstance(err, Exception): # pragma: no cover
|
|
33
39
|
trace = exc_info()[2]
|
|
@@ -41,11 +47,12 @@ def _make_api_response(
|
|
|
41
47
|
entry.model_dump(mode="json", by_alias=True, exclude_none=True) for entry in cast(list[BaseModel], data)
|
|
42
48
|
]
|
|
43
49
|
|
|
50
|
+
payload = ClueResponse(response=data, error_message=err, warning=warnings, status_code=status_code).model_dump(
|
|
51
|
+
mode="json", by_alias=True, exclude_none=True
|
|
52
|
+
)
|
|
53
|
+
# Keep existing endpoint serialization unchanged unless ordering is explicitly requested.
|
|
44
54
|
resp = make_response(
|
|
45
|
-
|
|
46
|
-
mode="json", by_alias=True, exclude_none=True
|
|
47
|
-
),
|
|
48
|
-
status_code,
|
|
55
|
+
payload if sort_keys is None else current_app.json.dumps(payload, sort_keys=sort_keys), status_code
|
|
49
56
|
)
|
|
50
57
|
|
|
51
58
|
resp.headers["Content-Type"] = "application/json"
|
|
@@ -65,9 +72,9 @@ def _make_api_response(
|
|
|
65
72
|
DEFAULT_DATA = {True: {"success": True}, False: {"success": False}}
|
|
66
73
|
|
|
67
74
|
|
|
68
|
-
def ok(data=DEFAULT_DATA[True], cookies=None):
|
|
75
|
+
def ok(data: Any = DEFAULT_DATA[True], cookies: Any = None, *, sort_keys: bool | None = None):
|
|
69
76
|
"""Returns response with status code 200"""
|
|
70
|
-
return _make_api_response(data, status_code=200, cookies=cookies)
|
|
77
|
+
return _make_api_response(data, status_code=200, cookies=cookies, sort_keys=sort_keys)
|
|
71
78
|
|
|
72
79
|
|
|
73
80
|
def created(data=DEFAULT_DATA[True], warnings=[], cookies=None):
|
clue_api-1.7.0.dev459/clue/api/v1/__init__.py → clue_api-1.7.0.dev464/clue/api/documentation.py
RENAMED
|
@@ -1,19 +1,14 @@
|
|
|
1
1
|
from textwrap import dedent
|
|
2
2
|
|
|
3
|
-
from flask import
|
|
3
|
+
from flask import current_app, request
|
|
4
4
|
|
|
5
5
|
from clue.api import ok
|
|
6
|
-
from clue.
|
|
7
|
-
|
|
8
|
-
API_PREFIX = "/api/v1"
|
|
9
|
-
apiv1 = Blueprint("apiv1", __name__, url_prefix=API_PREFIX)
|
|
10
|
-
apiv1._doc = "Api Documentation - Verison 1" # type: ignore[attr-defined]
|
|
6
|
+
from clue.security import api_login
|
|
11
7
|
|
|
12
8
|
|
|
13
9
|
#####################################
|
|
14
10
|
# API DOCUMENTATION
|
|
15
11
|
# noinspection PyProtectedMember,PyBroadException
|
|
16
|
-
@apiv1.route("/")
|
|
17
12
|
@api_login(audit=False)
|
|
18
13
|
def get_api_documentation(**_):
|
|
19
14
|
"""Full API doc. Loop through all registered API paths and display their documentation.
|
|
@@ -38,17 +33,19 @@ def get_api_documentation(**_):
|
|
|
38
33
|
'complete' : True}, # Is the API stable?
|
|
39
34
|
...]
|
|
40
35
|
"""
|
|
36
|
+
api_prefix = f"{request.path.rstrip('/')}/"
|
|
37
|
+
api_version = api_prefix.strip("/").split("/")[-1]
|
|
41
38
|
api_blueprints = {}
|
|
42
39
|
api_list = []
|
|
43
40
|
for rule in current_app.url_map.iter_rules():
|
|
44
|
-
if rule.rule.startswith(
|
|
41
|
+
if rule.rule.startswith(api_prefix):
|
|
45
42
|
methods = [item for item in (rule.methods or []) if item != "OPTIONS" and item != "HEAD"]
|
|
46
43
|
|
|
47
44
|
func = current_app.view_functions[rule.endpoint]
|
|
48
45
|
doc_string = func.__doc__
|
|
49
46
|
func_title = " ".join([x.capitalize() for x in rule.endpoint[rule.endpoint.rindex(".") + 1 :].split("_")])
|
|
50
47
|
blueprint = rule.endpoint[: rule.endpoint.rindex(".")]
|
|
51
|
-
if blueprint ==
|
|
48
|
+
if blueprint == request.blueprint:
|
|
52
49
|
blueprint = "documentation"
|
|
53
50
|
|
|
54
51
|
if blueprint not in api_blueprints:
|
|
@@ -64,14 +61,14 @@ def get_api_documentation(**_):
|
|
|
64
61
|
else:
|
|
65
62
|
description = "[INCOMPLETE]\n\nTHIS API HAS NOT BEEN DOCUMENTED YET!"
|
|
66
63
|
|
|
67
|
-
api_id = rule.endpoint.replace("
|
|
64
|
+
api_id = rule.endpoint.replace(f"{request.blueprint}.", "").replace(".", "_")
|
|
68
65
|
|
|
69
66
|
api_list.append(
|
|
70
67
|
{
|
|
71
68
|
"protected": func.__dict__.get("protected", False),
|
|
72
69
|
"name": func_title,
|
|
73
70
|
"id": api_id,
|
|
74
|
-
"function": f"api.
|
|
71
|
+
"function": f"api.{api_version}.{rule.endpoint}",
|
|
75
72
|
"path": rule.rule,
|
|
76
73
|
"methods": methods,
|
|
77
74
|
"description": description,
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from flask import Blueprint
|
|
2
|
+
|
|
3
|
+
from clue.api.documentation import get_api_documentation
|
|
4
|
+
|
|
5
|
+
API_PREFIX = "/api/v1"
|
|
6
|
+
apiv1 = Blueprint("apiv1", __name__, url_prefix=API_PREFIX)
|
|
7
|
+
apiv1._doc = "Api Documentation - Version 1" # type: ignore[attr-defined]
|
|
8
|
+
apiv1.add_url_rule("/", view_func=get_api_documentation)
|
|
@@ -61,7 +61,7 @@ def get_types(**kwargs) -> dict[str, list[str]]:
|
|
|
61
61
|
@generate_swagger_docs(responses={200: "A list of types and their regex detectors"})
|
|
62
62
|
@lookup_api.route("/types_detection/", methods=["GET"])
|
|
63
63
|
@api_login()
|
|
64
|
-
def get_types_detection(**kwargs) -> dict[str, str]:
|
|
64
|
+
def get_types_detection(**kwargs) -> dict[str, str | None]:
|
|
65
65
|
"""Return the regular expression to detect the different types
|
|
66
66
|
|
|
67
67
|
Variables:
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from flask import Blueprint
|
|
2
|
+
|
|
3
|
+
from clue.api.documentation import get_api_documentation
|
|
4
|
+
|
|
5
|
+
API_PREFIX = "/api/v2"
|
|
6
|
+
apiv2 = Blueprint("apiv2", __name__, url_prefix=API_PREFIX)
|
|
7
|
+
apiv2._doc = "Api Documentation - Version 2" # type: ignore[attr-defined]
|
|
8
|
+
apiv2.add_url_rule("/", view_func=get_api_documentation)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Version 2 type detection metadata for enrichment lookups."""
|
|
2
|
+
|
|
3
|
+
from flask_cors import CORS
|
|
4
|
+
|
|
5
|
+
from clue.api import make_subapi_blueprint, ok
|
|
6
|
+
from clue.common.swagger import generate_swagger_docs
|
|
7
|
+
from clue.config import config
|
|
8
|
+
from clue.constants.supported_types import SupportedType
|
|
9
|
+
from clue.security import api_login
|
|
10
|
+
from clue.services import type_service
|
|
11
|
+
|
|
12
|
+
lookup_api = make_subapi_blueprint("lookup", api_version=2, blueprint_name="lookup_v2")
|
|
13
|
+
lookup_api._doc = "Type detection metadata with case sensitivity and priority."
|
|
14
|
+
|
|
15
|
+
CORS(lookup_api, origins=config.ui.cors_origins, supports_credentials=True)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@generate_swagger_docs(responses={200: "Supported types and their detection metadata"})
|
|
19
|
+
@lookup_api.route("/types_detection/", methods=["GET"])
|
|
20
|
+
@api_login()
|
|
21
|
+
def get_types_detection(**kwargs) -> dict[str, SupportedType]:
|
|
22
|
+
"""Return detection metadata for types exposed by accessible plugins.
|
|
23
|
+
|
|
24
|
+
Variables:
|
|
25
|
+
None
|
|
26
|
+
|
|
27
|
+
Arguments:
|
|
28
|
+
None
|
|
29
|
+
|
|
30
|
+
Result Example:
|
|
31
|
+
{
|
|
32
|
+
"ipv4": {"regex": "<ipv4 regex>", "case_sensitive": True, "priority": 10},
|
|
33
|
+
"domain": {"regex": "<domain regex>", "case_sensitive": False},
|
|
34
|
+
"ip": {"regex": None, "case_sensitive": False}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
Higher priorities win. Omitted priorities default to zero; equal priorities
|
|
38
|
+
use the registry insertion order preserved in this response. Null regexes
|
|
39
|
+
indicate types that must be specified explicitly rather than auto-detected.
|
|
40
|
+
"""
|
|
41
|
+
return ok(type_service.get_types_detection(kwargs["user"]), sort_keys=False)
|
|
@@ -60,6 +60,8 @@ from clue.api.v1.lookup import lookup_api
|
|
|
60
60
|
from clue.api.v1.registration import registration_api
|
|
61
61
|
from clue.api.v1.static import static_api
|
|
62
62
|
from clue.api.v1.sync import sync_api
|
|
63
|
+
from clue.api.v2 import apiv2
|
|
64
|
+
from clue.api.v2.lookup import lookup_api as lookup_v2_api
|
|
63
65
|
from clue.common.logging import get_logger
|
|
64
66
|
from clue.cronjobs import setup_jobs as setup_cron_jobs
|
|
65
67
|
from clue.error import errors
|
|
@@ -125,12 +127,14 @@ if SESSION_COOKIE_SAMESITE:
|
|
|
125
127
|
app.register_blueprint(healthz)
|
|
126
128
|
app.register_blueprint(api)
|
|
127
129
|
app.register_blueprint(apiv1)
|
|
130
|
+
app.register_blueprint(apiv2)
|
|
128
131
|
app.register_blueprint(errors)
|
|
129
132
|
app.register_blueprint(auth_api)
|
|
130
133
|
app.register_blueprint(actions_api)
|
|
131
134
|
app.register_blueprint(configs_api)
|
|
132
135
|
app.register_blueprint(fetchers_api)
|
|
133
136
|
app.register_blueprint(lookup_api)
|
|
137
|
+
app.register_blueprint(lookup_v2_api)
|
|
134
138
|
app.register_blueprint(registration_api)
|
|
135
139
|
app.register_blueprint(static_api)
|
|
136
140
|
app.register_blueprint(sync_api)
|
|
@@ -5,15 +5,23 @@ DOMAIN_REGEX = (
|
|
|
5
5
|
)
|
|
6
6
|
DOMAIN_ONLY_REGEX = f"^{DOMAIN_REGEX}$"
|
|
7
7
|
DOMAIN_EXCLUDED_NORM_CHARS = "./?@#"
|
|
8
|
-
|
|
8
|
+
# Accept either case in hosts without lowercasing email local parts or URI paths.
|
|
9
|
+
CASE_INSENSITIVE_DOMAIN_REGEX = (
|
|
10
|
+
r"(?:(?=[a-zA-Z0-9-]{1,63}\.)"
|
|
11
|
+
r"(?:xn--[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?|[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?)"
|
|
12
|
+
r"\.)+[a-zA-Z]{2,63}"
|
|
13
|
+
)
|
|
14
|
+
EMAIL_REGEX = (
|
|
15
|
+
f"^[a-zA-Z0-9!#$%&'*+/=?^_‘{{|}}~-]+(?:\\.[a-zA-Z0-9!#$%&'*+/=?^_‘{{|}}~-]+)*@({CASE_INSENSITIVE_DOMAIN_REGEX})$"
|
|
16
|
+
)
|
|
9
17
|
IPV4_REGEX = r"(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)"
|
|
10
18
|
IPV6_REGEX = (
|
|
11
19
|
r"(?:(?:[0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,7}:|"
|
|
12
20
|
r"(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|"
|
|
13
21
|
r"(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}|"
|
|
14
22
|
r"(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:(?:(?::[0-9a-fA-F]{1,4}){1,6})|"
|
|
15
|
-
r":(?:(?::[0-9a-fA-F]{1,4}){1,7}|:)|
|
|
16
|
-
r"::(?:
|
|
23
|
+
r":(?:(?::[0-9a-fA-F]{1,4}){1,7}|:)|[fF][eE]80:(?::[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|"
|
|
24
|
+
r"::(?:[fF]{4}(?::0{1,4}){0,1}:){0,1}(?:(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(?:25[0-5]|"
|
|
17
25
|
r"(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])|(?:[0-9a-fA-F]{1,4}:){1,4}:(?:(?:25[0-5]|"
|
|
18
26
|
r"(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9]))"
|
|
19
27
|
)
|
|
@@ -26,12 +34,7 @@ MD5_REGEX = r"^[a-fA-F0-9]{32}$"
|
|
|
26
34
|
SHA1_REGEX = r"^[a-fA-F0-9]{40}$"
|
|
27
35
|
SHA256_REGEX = r"^[a-fA-F0-9]{64}$"
|
|
28
36
|
URI_PATH = r"([/?#]\S*)"
|
|
29
|
-
|
|
30
|
-
URI_DOMAIN_REGEX = (
|
|
31
|
-
r"(?:(?=[a-zA-Z0-9-]{1,63}\.)"
|
|
32
|
-
r"(?:xn--[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?|[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?)"
|
|
33
|
-
r"\.)+[a-zA-Z]{2,63}"
|
|
34
|
-
)
|
|
37
|
+
URI_DOMAIN_REGEX = CASE_INSENSITIVE_DOMAIN_REGEX
|
|
35
38
|
URI_REGEX = (
|
|
36
39
|
f"((?:(?:[A-Za-z][A-Za-z0-9+.-]*:)//(?:[^/?#\\s]+@)?)?({IPV4_REGEX}|\\[{IPV6_REGEX}\\]|{URI_DOMAIN_REGEX})"
|
|
37
40
|
f"(?::\\d{{1,5}})?{URI_PATH}?)"
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from typing import NotRequired, TypedDict
|
|
2
|
+
|
|
3
|
+
from clue.common.logging import get_logger
|
|
4
|
+
from clue.common.regex import (
|
|
5
|
+
COMMUNITY_ID_REGEX,
|
|
6
|
+
DOMAIN_ONLY_REGEX,
|
|
7
|
+
EMAIL_PATH_REGEX,
|
|
8
|
+
EMAIL_REGEX,
|
|
9
|
+
IPV4_ONLY_REGEX,
|
|
10
|
+
IPV6_ONLY_REGEX,
|
|
11
|
+
JA3_REGEX,
|
|
12
|
+
JA4_REGEX,
|
|
13
|
+
MAC_ADDRESS_REGEX,
|
|
14
|
+
MD5_REGEX,
|
|
15
|
+
PORT_REGEX,
|
|
16
|
+
SHA1_REGEX,
|
|
17
|
+
SHA256_REGEX,
|
|
18
|
+
URI_ONLY,
|
|
19
|
+
UUID4_REGEX,
|
|
20
|
+
ZEEK_ID_REGEX,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
logger = get_logger(__file__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SupportedType(TypedDict):
|
|
27
|
+
"""Type detection metadata; an omitted priority is equivalent to zero."""
|
|
28
|
+
|
|
29
|
+
regex: str | None
|
|
30
|
+
case_sensitive: bool
|
|
31
|
+
priority: NotRequired[int]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
SUPPORTED_TYPES: dict[str, SupportedType] = {
|
|
35
|
+
"ipv4": {"regex": IPV4_ONLY_REGEX, "case_sensitive": True, "priority": 10},
|
|
36
|
+
"ipv6": {"regex": IPV6_ONLY_REGEX, "case_sensitive": True, "priority": 10},
|
|
37
|
+
# We don't auto-detect ip types, as it's redundant with ipv4/v6. This is just a convenience/backwards compat thing
|
|
38
|
+
"ip": {"regex": None, "case_sensitive": False},
|
|
39
|
+
"asn": {"regex": None, "case_sensitive": True},
|
|
40
|
+
"domain": {"regex": DOMAIN_ONLY_REGEX, "case_sensitive": False},
|
|
41
|
+
"port": {"regex": PORT_REGEX, "case_sensitive": False},
|
|
42
|
+
"url": {"regex": URI_ONLY, "case_sensitive": True},
|
|
43
|
+
"userid": {"regex": None, "case_sensitive": True},
|
|
44
|
+
"user_agent": {"regex": None, "case_sensitive": True},
|
|
45
|
+
"email_address": {"regex": EMAIL_REGEX, "case_sensitive": True},
|
|
46
|
+
"email_id": {"regex": None, "case_sensitive": True},
|
|
47
|
+
"email_path": {"regex": EMAIL_PATH_REGEX, "case_sensitive": True},
|
|
48
|
+
"md5": {"regex": MD5_REGEX, "case_sensitive": True},
|
|
49
|
+
"sha1": {"regex": SHA1_REGEX, "case_sensitive": True},
|
|
50
|
+
"sha256": {"regex": SHA256_REGEX, "case_sensitive": True},
|
|
51
|
+
"telemetry": {"regex": None, "case_sensitive": True},
|
|
52
|
+
"hostname": {"regex": None, "case_sensitive": True},
|
|
53
|
+
"tenant_id": {"regex": UUID4_REGEX, "case_sensitive": False},
|
|
54
|
+
"mac_address": {"regex": MAC_ADDRESS_REGEX, "case_sensitive": False},
|
|
55
|
+
"command_line": {"regex": None, "case_sensitive": True},
|
|
56
|
+
"community_id": {"regex": COMMUNITY_ID_REGEX, "case_sensitive": True},
|
|
57
|
+
"ja3": {"regex": JA3_REGEX, "case_sensitive": True},
|
|
58
|
+
"ja4": {"regex": JA4_REGEX, "case_sensitive": True},
|
|
59
|
+
"zeek_id": {"regex": ZEEK_ID_REGEX, "case_sensitive": True},
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# Retained for selector normalization and extensions importing the legacy list.
|
|
64
|
+
CASE_INSENSITIVE_TYPES = [
|
|
65
|
+
type_name for type_name, metadata in SUPPORTED_TYPES.items() if not metadata["case_sensitive"]
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def add_supported_type(
|
|
70
|
+
type: str,
|
|
71
|
+
regex: str | None = None,
|
|
72
|
+
namespace: str | None = None,
|
|
73
|
+
case_insensitive: bool = False,
|
|
74
|
+
priority: int = 0,
|
|
75
|
+
) -> None:
|
|
76
|
+
r"""Add a supported type to the SUPPORTED_TYPES registry.
|
|
77
|
+
|
|
78
|
+
This function registers a new type with an optional regex pattern for validation.
|
|
79
|
+
The type can be added to either the default namespace or a custom namespace.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
type (str): The name of the type to be added.
|
|
83
|
+
regex (str | None, optional): A regex pattern for validating the type. Defaults to None.
|
|
84
|
+
namespace (str | None, optional): The namespace for the type. If None, adds to default namespace.
|
|
85
|
+
Defaults to None.
|
|
86
|
+
case_insensitive (bool, optional): Whether matching and selector normalization ignore case. Defaults to False.
|
|
87
|
+
priority (int, optional): Detection priority; higher values win, with registry insertion order breaking ties.
|
|
88
|
+
Defaults to 0. Negative values are allowed.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
None
|
|
92
|
+
|
|
93
|
+
Examples:
|
|
94
|
+
>>> add_supported_type("email", r"^[\w\.-]+@[\w\.-]+\.\w+$")
|
|
95
|
+
>>> add_supported_type("custom_id", r"^\d{5}$", namespace="myapp")
|
|
96
|
+
"""
|
|
97
|
+
if not namespace:
|
|
98
|
+
logger.info("Adding new type %s to the default namespace with regex %s", type, regex)
|
|
99
|
+
new_entry = type
|
|
100
|
+
else:
|
|
101
|
+
logger.info("Adding type %s to namespace %s with regex %s", type, namespace, regex)
|
|
102
|
+
new_entry = f"{namespace}_{type}"
|
|
103
|
+
|
|
104
|
+
SUPPORTED_TYPES[new_entry] = {"regex": regex, "case_sensitive": not case_insensitive, "priority": priority}
|
|
105
|
+
if case_insensitive and new_entry not in CASE_INSENSITIVE_TYPES:
|
|
106
|
+
CASE_INSENSITIVE_TYPES.append(new_entry)
|
|
107
|
+
elif not case_insensitive and new_entry in CASE_INSENSITIVE_TYPES:
|
|
108
|
+
CASE_INSENSITIVE_TYPES.remove(new_entry)
|
|
@@ -2,14 +2,13 @@ from typing import Any
|
|
|
2
2
|
|
|
3
3
|
import requests
|
|
4
4
|
from elasticapm.traces import capture_span
|
|
5
|
-
from flask import request
|
|
6
5
|
from requests import exceptions
|
|
7
6
|
|
|
8
7
|
from clue.common.exceptions import AuthenticationException
|
|
9
8
|
from clue.common.logging import get_logger
|
|
10
9
|
from clue.config import CLASSIFICATION, DEBUG, cache, config
|
|
11
10
|
from clue.constants.env import DISABLE_CACHE
|
|
12
|
-
from clue.constants.supported_types import SUPPORTED_TYPES
|
|
11
|
+
from clue.constants.supported_types import SUPPORTED_TYPES, SupportedType
|
|
13
12
|
from clue.helper.headers import generate_headers
|
|
14
13
|
from clue.helper.obo import get_obo_access_token
|
|
15
14
|
from clue.helper.plugin_requests import request_with_safe_redirects
|
|
@@ -23,12 +22,13 @@ CACHE_TIMEOUT: int = 1 if DEBUG else 5 * 60
|
|
|
23
22
|
CACHE = RedisCache(prefix="clue_types", ttl=CACHE_TIMEOUT)
|
|
24
23
|
|
|
25
24
|
|
|
26
|
-
def get_types_regular_expressions(user: dict[str, Any]):
|
|
27
|
-
"""Return the
|
|
28
|
-
|
|
29
|
-
if access_token:
|
|
30
|
-
access_token = access_token.split(" ")[1]
|
|
25
|
+
def get_types_regular_expressions(user: dict[str, Any]) -> dict[str, str | None]:
|
|
26
|
+
"""Return the legacy V1 regex mapping in plugin discovery order."""
|
|
27
|
+
return {type_name: metadata["regex"] for type_name, metadata in _get_accessible_type_definitions(user).items()}
|
|
31
28
|
|
|
29
|
+
|
|
30
|
+
def _get_accessible_type_definitions(user: dict[str, Any]) -> dict[str, SupportedType]:
|
|
31
|
+
"""Collect definitions for types exposed by plugins accessible to the user."""
|
|
32
32
|
all_types = all_supported_types(user)
|
|
33
33
|
|
|
34
34
|
type_detection = {}
|
|
@@ -44,6 +44,14 @@ def get_types_regular_expressions(user: dict[str, Any]):
|
|
|
44
44
|
return type_detection
|
|
45
45
|
|
|
46
46
|
|
|
47
|
+
def get_types_detection(user: dict[str, Any]) -> dict[str, SupportedType]:
|
|
48
|
+
"""Return V2 detection metadata in registry order for deterministic priority ties."""
|
|
49
|
+
accessible_types = _get_accessible_type_definitions(user)
|
|
50
|
+
return {
|
|
51
|
+
type_name: metadata.copy() for type_name, metadata in SUPPORTED_TYPES.items() if type_name in accessible_types
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
47
55
|
@cache.memoize(timeout=CACHE_TIMEOUT)
|
|
48
56
|
def get_supported_types(
|
|
49
57
|
source_url: str, access_token: str | None = None, obo_access_token: str | None = None
|
|
@@ -141,7 +141,7 @@ log_cli_level = "WARN"
|
|
|
141
141
|
[tool.poetry]
|
|
142
142
|
package-mode = true
|
|
143
143
|
name = "clue-api"
|
|
144
|
-
version = "1.7.0.
|
|
144
|
+
version = "1.7.0.dev464"
|
|
145
145
|
description = "Clue distributed enrichment service"
|
|
146
146
|
authors = ["Canadian Centre for Cyber Security <contact@cyber.gc.ca>"]
|
|
147
147
|
license = "MIT"
|
|
@@ -184,7 +184,7 @@ pydantic-settings = { extras = ["yaml"], version = "^2.3.4" }
|
|
|
184
184
|
redis = { version = "^5.0.3" }
|
|
185
185
|
beautifulsoup4 = "^4.13.3"
|
|
186
186
|
imgkit = "^1.2.3"
|
|
187
|
-
pillow = ">=11.1,<
|
|
187
|
+
pillow = ">=11.1,<14.0"
|
|
188
188
|
cart = "^1.2.3"
|
|
189
189
|
setuptools = "<84.0.0"
|
|
190
190
|
requests = "^2.32.5"
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
from clue.common.logging import get_logger
|
|
2
|
-
from clue.common.regex import (
|
|
3
|
-
COMMUNITY_ID_REGEX,
|
|
4
|
-
DOMAIN_ONLY_REGEX,
|
|
5
|
-
EMAIL_PATH_REGEX,
|
|
6
|
-
EMAIL_REGEX,
|
|
7
|
-
IPV4_ONLY_REGEX,
|
|
8
|
-
IPV6_ONLY_REGEX,
|
|
9
|
-
JA3_REGEX,
|
|
10
|
-
JA4_REGEX,
|
|
11
|
-
MAC_ADDRESS_REGEX,
|
|
12
|
-
MD5_REGEX,
|
|
13
|
-
PORT_REGEX,
|
|
14
|
-
SHA1_REGEX,
|
|
15
|
-
SHA256_REGEX,
|
|
16
|
-
URI_ONLY,
|
|
17
|
-
UUID4_REGEX,
|
|
18
|
-
ZEEK_ID_REGEX,
|
|
19
|
-
)
|
|
20
|
-
|
|
21
|
-
logger = get_logger(__file__)
|
|
22
|
-
|
|
23
|
-
SUPPORTED_TYPES = {
|
|
24
|
-
"ipv4": IPV4_ONLY_REGEX,
|
|
25
|
-
"ipv6": IPV6_ONLY_REGEX,
|
|
26
|
-
# We don't auto-detect ip types, as it's redundant with ipv4/v6. This is just a convenience/backwards compat thing
|
|
27
|
-
"ip": None,
|
|
28
|
-
"asn": None,
|
|
29
|
-
"domain": DOMAIN_ONLY_REGEX,
|
|
30
|
-
"port": PORT_REGEX,
|
|
31
|
-
"url": URI_ONLY,
|
|
32
|
-
"userid": None,
|
|
33
|
-
"user_agent": None,
|
|
34
|
-
"email_address": EMAIL_REGEX,
|
|
35
|
-
"email_id": None,
|
|
36
|
-
"email_path": EMAIL_PATH_REGEX,
|
|
37
|
-
"md5": MD5_REGEX,
|
|
38
|
-
"sha1": SHA1_REGEX,
|
|
39
|
-
"sha256": SHA256_REGEX,
|
|
40
|
-
"telemetry": None,
|
|
41
|
-
"hostname": None,
|
|
42
|
-
"tenant_id": UUID4_REGEX,
|
|
43
|
-
"mac_address": MAC_ADDRESS_REGEX,
|
|
44
|
-
"command_line": None,
|
|
45
|
-
"community_id": COMMUNITY_ID_REGEX,
|
|
46
|
-
"ja3": JA3_REGEX,
|
|
47
|
-
"ja4": JA4_REGEX,
|
|
48
|
-
"zeek_id": ZEEK_ID_REGEX,
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
CASE_INSENSITIVE_TYPES = ["ip", "domain", "port", "tenant_id", "mac_address"]
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def add_supported_type(
|
|
56
|
-
type: str, regex: str | None = None, namespace: str | None = None, case_insensitive: bool = False
|
|
57
|
-
):
|
|
58
|
-
r"""Add a supported type to the SUPPORTED_TYPES registry.
|
|
59
|
-
|
|
60
|
-
This function registers a new type with an optional regex pattern for validation.
|
|
61
|
-
The type can be added to either the default namespace or a custom namespace.
|
|
62
|
-
|
|
63
|
-
Args:
|
|
64
|
-
type (str): The name of the type to be added.
|
|
65
|
-
regex (str | None, optional): A regex pattern for validating the type. Defaults to None.
|
|
66
|
-
namespace (str | None, optional): The namespace for the type. If None, adds to default namespace.
|
|
67
|
-
Defaults to None.
|
|
68
|
-
|
|
69
|
-
Returns:
|
|
70
|
-
None
|
|
71
|
-
|
|
72
|
-
Examples:
|
|
73
|
-
>>> add_supported_type("email", r"^[\w\.-]+@[\w\.-]+\.\w+$")
|
|
74
|
-
>>> add_supported_type("custom_id", r"^\d{5}$", namespace="myapp")
|
|
75
|
-
"""
|
|
76
|
-
if not namespace:
|
|
77
|
-
logger.info("Adding new type %s to the default namespace with regex %s", type, regex)
|
|
78
|
-
new_entry = type
|
|
79
|
-
else:
|
|
80
|
-
logger.info("Adding type %s to namespace %s with regex %s", type, namespace, regex)
|
|
81
|
-
new_entry = f"{namespace}_{type}"
|
|
82
|
-
|
|
83
|
-
SUPPORTED_TYPES[new_entry] = regex
|
|
84
|
-
if case_insensitive:
|
|
85
|
-
CASE_INSENSITIVE_TYPES.append(new_entry)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|