clue-api 1.6.0.dev320__tar.gz → 1.6.0.dev335__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.6.0.dev320 → clue_api-1.6.0.dev335}/PKG-INFO +1 -1
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/models/network.py +5 -6
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/services/fetcher_service.py +56 -35
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/pyproject.toml +1 -1
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/LICENSE +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/README.md +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/.gitignore +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/__init__.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/api/__init__.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/api/base.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/api/v1/__init__.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/api/v1/actions.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/api/v1/auth.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/api/v1/configs.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/api/v1/fetchers.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/api/v1/lookup.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/api/v1/registration.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/api/v1/static.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/api/v1/sync.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/app.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/cache/__init__.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/common/__init__.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/common/bytes_utils.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/common/classification.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/common/classification.yml +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/common/dict_utils.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/common/exceptions.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/common/forge.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/common/json_utils.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/common/list_utils.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/common/logging/__init__.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/common/logging/audit.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/common/logging/format.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/common/regex.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/common/str_utils.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/common/swagger.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/common/uid.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/config.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/constants/__init__.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/constants/env.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/constants/supported_types.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/cronjobs/__init__.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/cronjobs/plugins.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/error.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/extensions/__init__.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/extensions/config.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/gunicorn_config.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/healthz.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/helper/discover.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/helper/headers.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/helper/oauth.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/models/__init__.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/models/actions.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/models/config.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/models/fetchers.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/models/graph.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/models/model_list.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/models/results/__init__.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/models/results/base.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/models/results/file.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/models/results/graph.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/models/results/image.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/models/results/status.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/models/results/validation.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/models/schema.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/models/selector.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/models/sync.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/models/validators.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/patched.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/plugin/__init__.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/plugin/celery_app.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/plugin/helpers/__init__.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/plugin/helpers/central_server.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/plugin/helpers/email_render.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/plugin/helpers/token.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/plugin/helpers/trino.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/plugin/models.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/plugin/utils.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/py.typed +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/remote/__init__.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/remote/datatypes/__init__.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/remote/datatypes/cache.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/remote/datatypes/events.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/remote/datatypes/hash.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/remote/datatypes/queues/__init__.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/remote/datatypes/queues/comms.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/remote/datatypes/set.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/remote/datatypes/user_quota_tracker.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/security/__init__.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/security/obo.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/security/utils.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/services/action_service.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/services/auth_service.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/services/config_service.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/services/jwt_service.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/services/lookup_service.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/services/mongo_service.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/services/type_service.py +0 -0
- {clue_api-1.6.0.dev320 → clue_api-1.6.0.dev335}/clue/services/user_service.py +0 -0
|
@@ -76,11 +76,6 @@ class ClueResponse(BaseModel):
|
|
|
76
76
|
Timestamp = Annotated[
|
|
77
77
|
datetime,
|
|
78
78
|
BeforeValidator(parse_datetime),
|
|
79
|
-
Field(
|
|
80
|
-
description="A timestamp describing when the knowledge was generated.",
|
|
81
|
-
default_factory=lambda: datetime.now(timezone.utc),
|
|
82
|
-
examples=[datetime.now(timezone.utc), datetime.now(timezone.utc) - timedelta(weeks=2)],
|
|
83
|
-
),
|
|
84
79
|
]
|
|
85
80
|
|
|
86
81
|
|
|
@@ -121,7 +116,11 @@ class Annotation(BaseModel):
|
|
|
121
116
|
examples=["v0.0.1", "1.0.0", None],
|
|
122
117
|
),
|
|
123
118
|
] = None
|
|
124
|
-
timestamp: Timestamp
|
|
119
|
+
timestamp: Timestamp = Field(
|
|
120
|
+
description="A timestamp describing when the knowledge was generated.",
|
|
121
|
+
default_factory=lambda: datetime.now(timezone.utc),
|
|
122
|
+
examples=[datetime.now(timezone.utc), datetime.now(timezone.utc) - timedelta(weeks=2)],
|
|
123
|
+
)
|
|
125
124
|
type: Annotated[
|
|
126
125
|
Literal["opinion", "frequency", "assessment", "mitigation", "context"],
|
|
127
126
|
Field(
|
|
@@ -3,7 +3,7 @@ from urllib.parse import urljoin
|
|
|
3
3
|
|
|
4
4
|
import elasticapm
|
|
5
5
|
import requests
|
|
6
|
-
from flask import request
|
|
6
|
+
from flask import has_request_context, request
|
|
7
7
|
from pydantic import TypeAdapter, ValidationError
|
|
8
8
|
from requests import JSONDecodeError, exceptions
|
|
9
9
|
|
|
@@ -11,6 +11,7 @@ from clue.common.exceptions import (
|
|
|
11
11
|
AuthenticationException,
|
|
12
12
|
ClueException,
|
|
13
13
|
ClueValueError,
|
|
14
|
+
InvalidDataException,
|
|
14
15
|
NotFoundException,
|
|
15
16
|
)
|
|
16
17
|
from clue.common.logging import get_logger
|
|
@@ -26,6 +27,26 @@ logger = get_logger(__file__)
|
|
|
26
27
|
CACHE_TIMEOUT: int = 1 if DEBUG else 5 * 60
|
|
27
28
|
|
|
28
29
|
|
|
30
|
+
def get_obo_access_token(
|
|
31
|
+
source: ExternalSource, user: dict[str, Any], access_token: Optional[str] = None
|
|
32
|
+
) -> tuple[Optional[str], Optional[str]]:
|
|
33
|
+
"""Get the caller access token and an OBO token for an external source when needed."""
|
|
34
|
+
if access_token is None and has_request_context():
|
|
35
|
+
auth_header = request.headers.get("Authorization", type=str)
|
|
36
|
+
if auth_header:
|
|
37
|
+
parts = auth_header.split(" ", 1)
|
|
38
|
+
access_token = parts[1] if len(parts) == 2 and parts[0].lower() == "bearer" else auth_header
|
|
39
|
+
if not access_token:
|
|
40
|
+
return None, None
|
|
41
|
+
|
|
42
|
+
obo_access_token, error = auth_service.check_obo(source, access_token, user["uname"])
|
|
43
|
+
if error:
|
|
44
|
+
logger.error("%s: %s", source.name, error)
|
|
45
|
+
raise AuthenticationException("Invalid token provided for this enrichment.")
|
|
46
|
+
|
|
47
|
+
return access_token, obo_access_token
|
|
48
|
+
|
|
49
|
+
|
|
29
50
|
@cache.memoize(timeout=1 if DEBUG else 5 * 60, args_to_ignore=["access_token"]) # Cached for 5 minutes
|
|
30
51
|
def get_supported_fetchers(
|
|
31
52
|
source: ExternalSource, user: dict[str, Any], access_token: Optional[str] = None
|
|
@@ -43,13 +64,10 @@ def get_supported_fetchers(
|
|
|
43
64
|
|
|
44
65
|
url = urljoin(source.url, "fetchers/")
|
|
45
66
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if error:
|
|
51
|
-
logger.error("%s: %s", source.name, error)
|
|
52
|
-
return {}
|
|
67
|
+
try:
|
|
68
|
+
access_token, obo_access_token = get_obo_access_token(source, user, access_token)
|
|
69
|
+
except AuthenticationException:
|
|
70
|
+
return {}
|
|
53
71
|
|
|
54
72
|
headers = {"Accept": "application/json"}
|
|
55
73
|
if obo_access_token or access_token:
|
|
@@ -130,6 +148,15 @@ def get_plugins_supported_fetchers(user: dict[str, Any]) -> dict[str, FetcherDef
|
|
|
130
148
|
return available_fetchers
|
|
131
149
|
|
|
132
150
|
|
|
151
|
+
def _validate_fetcher_classification(fetcher: FetcherDefinition | None, selector: Selector, fetcher_id: str) -> None:
|
|
152
|
+
if fetcher and not CLASSIFICATION.is_accessible(fetcher.classification, selector.classification):
|
|
153
|
+
raise InvalidDataException(
|
|
154
|
+
f"Cannot send data classified as {selector.classification} to fetcher {fetcher_id} "
|
|
155
|
+
f"at classification {fetcher.classification}.",
|
|
156
|
+
status_code=400,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
133
160
|
def run_fetcher(plugin_id: str, fetcher_id: str, user: dict[str, Any]) -> FetcherResult:
|
|
134
161
|
"""Executes a specified fetcher.
|
|
135
162
|
|
|
@@ -150,30 +177,34 @@ def run_fetcher(plugin_id: str, fetcher_id: str, user: dict[str, Any]) -> Fetche
|
|
|
150
177
|
if not plugin:
|
|
151
178
|
raise NotFoundException(f"Plugin {plugin_id} does not exist.")
|
|
152
179
|
|
|
153
|
-
access_token =
|
|
154
|
-
if access_token:
|
|
155
|
-
access_token = access_token.split(" ")[1]
|
|
156
|
-
|
|
157
|
-
obo_access_token = None
|
|
158
|
-
if access_token:
|
|
159
|
-
obo_access_token, error = auth_service.check_obo(plugin, access_token, user["uname"])
|
|
160
|
-
|
|
161
|
-
if error:
|
|
162
|
-
logger.error("%s: %s", plugin.name, error)
|
|
163
|
-
raise AuthenticationException("Invalid token provided for this enrichment.")
|
|
180
|
+
access_token, obo_access_token = get_obo_access_token(plugin, user)
|
|
164
181
|
|
|
165
182
|
headers = {"Accept": "application/json"}
|
|
166
183
|
if obo_access_token or access_token:
|
|
167
184
|
headers["Authorization"] = f"Bearer {obo_access_token or access_token}"
|
|
168
185
|
|
|
169
|
-
if request.
|
|
186
|
+
if request.is_json:
|
|
170
187
|
parameters = request.json
|
|
171
188
|
else:
|
|
172
|
-
|
|
173
|
-
|
|
189
|
+
logger.error(
|
|
190
|
+
"Invalid content-type detected: %s",
|
|
191
|
+
)
|
|
192
|
+
raise ClueValueError(
|
|
193
|
+
"The request body must be of type application/json.",
|
|
194
|
+
status_code=400,
|
|
195
|
+
)
|
|
174
196
|
|
|
175
197
|
try:
|
|
176
|
-
Selector.model_validate(parameters)
|
|
198
|
+
selector = Selector.model_validate(parameters)
|
|
199
|
+
supported_fetchers = get_supported_fetchers(plugin, user, access_token=access_token)
|
|
200
|
+
|
|
201
|
+
if len(supported_fetchers) < 1:
|
|
202
|
+
raise NotFoundException(f"{plugin_id} does not support any fetchers.")
|
|
203
|
+
|
|
204
|
+
fetcher = supported_fetchers.get(fetcher_id)
|
|
205
|
+
if fetcher is None:
|
|
206
|
+
raise NotFoundException(f"Fetcher {fetcher_id} does not exist", status_code=404)
|
|
207
|
+
_validate_fetcher_classification(fetcher, selector, fetcher_id)
|
|
177
208
|
|
|
178
209
|
response = requests.post(
|
|
179
210
|
urljoin(plugin.url, f"fetchers/{fetcher_id}"),
|
|
@@ -192,7 +223,7 @@ def run_fetcher(plugin_id: str, fetcher_id: str, user: dict[str, Any]) -> Fetche
|
|
|
192
223
|
return FetcherResult.model_validate(result["api_response"], context={"is_response": True})
|
|
193
224
|
except ValidationError as err:
|
|
194
225
|
logger.exception("Invalid Request Body:")
|
|
195
|
-
raise
|
|
226
|
+
raise InvalidDataException(
|
|
196
227
|
"Validation error encountered on request body. Ensure your request body is properly formatted.",
|
|
197
228
|
status_code=400,
|
|
198
229
|
) from err
|
|
@@ -224,17 +255,7 @@ def get_fetcher_status(plugin_id: str, fetcher_id: str, task_id: str, user: dict
|
|
|
224
255
|
if not plugin:
|
|
225
256
|
raise NotFoundException(f"Plugin {plugin_id} does not exist.")
|
|
226
257
|
|
|
227
|
-
access_token =
|
|
228
|
-
if access_token:
|
|
229
|
-
access_token = access_token.split(" ")[1]
|
|
230
|
-
|
|
231
|
-
obo_access_token = None
|
|
232
|
-
if access_token:
|
|
233
|
-
obo_access_token, error = auth_service.check_obo(plugin, access_token, user["uname"])
|
|
234
|
-
|
|
235
|
-
if error:
|
|
236
|
-
logger.error("%s: %s", plugin.name, error)
|
|
237
|
-
raise AuthenticationException("Invalid token provided for this enrichment.")
|
|
258
|
+
access_token, obo_access_token = get_obo_access_token(plugin, user)
|
|
238
259
|
|
|
239
260
|
headers = {"Accept": "application/json"}
|
|
240
261
|
if obo_access_token or access_token:
|
|
@@ -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.6.0.
|
|
144
|
+
version = "1.6.0.dev335"
|
|
145
145
|
description = "Clue distributed enrichment service"
|
|
146
146
|
authors = ["Canadian Centre for Cyber Security <contact@cyber.gc.ca>"]
|
|
147
147
|
license = "MIT"
|
|
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
|
|
File without changes
|