clue-api 1.3.0.dev15__tar.gz → 1.3.0.dev28__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.3.0.dev15 → clue_api-1.3.0.dev28}/PKG-INFO +1 -1
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/api/v1/lookup.py +4 -1
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/constants/supported_types.py +1 -1
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/models/config.py +1 -1
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/models/selector.py +4 -4
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/services/auth_service.py +7 -16
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/services/jwt_service.py +23 -2
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/pyproject.toml +1 -1
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/LICENSE +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/README.md +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/.gitignore +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/__init__.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/api/__init__.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/api/base.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/api/v1/__init__.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/api/v1/actions.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/api/v1/auth.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/api/v1/configs.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/api/v1/fetchers.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/api/v1/registration.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/api/v1/static.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/app.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/cache/__init__.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/common/__init__.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/common/classification.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/common/classification.yml +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/common/dict_utils.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/common/exceptions.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/common/forge.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/common/json_utils.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/common/list_utils.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/common/logging/__init__.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/common/logging/audit.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/common/logging/format.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/common/regex.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/common/str_utils.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/common/swagger.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/common/uid.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/config.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/constants/__init__.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/cronjobs/__init__.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/cronjobs/plugins.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/error.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/extensions/__init__.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/extensions/config.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/gunicorn_config.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/healthz.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/helper/discover.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/helper/headers.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/helper/oauth.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/models/__init__.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/models/actions.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/models/fetchers.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/models/graph.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/models/model_list.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/models/network.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/models/results/__init__.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/models/results/base.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/models/results/graph.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/models/results/image.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/models/results/status.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/models/results/validation.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/models/validators.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/patched.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/plugin/__init__.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/plugin/helpers/__init__.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/plugin/helpers/central_server.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/plugin/helpers/email_render.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/plugin/helpers/token.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/plugin/helpers/trino.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/plugin/models.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/plugin/utils.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/py.typed +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/remote/__init__.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/remote/datatypes/__init__.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/remote/datatypes/cache.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/remote/datatypes/events.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/remote/datatypes/hash.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/remote/datatypes/queues/__init__.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/remote/datatypes/queues/comms.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/remote/datatypes/set.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/remote/datatypes/user_quota_tracker.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/security/__init__.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/security/obo.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/security/utils.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/services/action_service.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/services/config_service.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/services/fetcher_service.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/services/lookup_service.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/services/type_service.py +0 -0
- {clue_api-1.3.0.dev15 → clue_api-1.3.0.dev28}/clue/services/user_service.py +0 -0
|
@@ -205,13 +205,16 @@ def enrich(type_name: str, value: str, **kwargs) -> dict[str, QueryResult]:
|
|
|
205
205
|
user = kwargs["user"]
|
|
206
206
|
|
|
207
207
|
# For backwards compatability, if eml is used it is replaced with email
|
|
208
|
-
type_name = type_name.replace("eml", "email")
|
|
208
|
+
type_name = type_name.lower().replace("eml", "email")
|
|
209
209
|
|
|
210
210
|
if type_name == "telemetry":
|
|
211
211
|
try:
|
|
212
212
|
json.loads(urllib.parse.unquote(value))
|
|
213
213
|
except json.JSONDecodeError:
|
|
214
214
|
return bad_request(err="If type is telemetry, value must be a valid JSON object.")
|
|
215
|
+
else:
|
|
216
|
+
# Normalize to lowercase all non-telemetry inputs
|
|
217
|
+
value = value.lower()
|
|
215
218
|
|
|
216
219
|
# re-encode the type after being decoded going through flask/wsgi route
|
|
217
220
|
value = urllib.parse.quote(value, safe="")
|
|
@@ -69,7 +69,7 @@ class OAuthProvider(BaseModel):
|
|
|
69
69
|
classification_map: dict[str, str] = Field(
|
|
70
70
|
default={}, description="A mapping of OAuth groups to classification levels"
|
|
71
71
|
)
|
|
72
|
-
access_token_url: str
|
|
72
|
+
access_token_url: str = Field(description="URL to get access token")
|
|
73
73
|
authorize_url: str | None = Field(description="URL used to authorize access to a resource")
|
|
74
74
|
api_base_url: str | None = Field(description="Base URL for downloading the user's and groups info")
|
|
75
75
|
audience: str | None = Field(
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
# ruff: noqa: D101
|
|
2
2
|
import ipaddress
|
|
3
3
|
import json
|
|
4
|
+
from typing import Annotated
|
|
4
5
|
|
|
5
6
|
from flask import request
|
|
6
|
-
from pydantic import BaseModel, Field, model_validator
|
|
7
|
+
from pydantic import BaseModel, Field, StringConstraints, model_validator
|
|
7
8
|
from typing_extensions import Self
|
|
8
9
|
|
|
9
10
|
from clue.common.logging import get_logger
|
|
@@ -14,7 +15,7 @@ logger = get_logger(__file__)
|
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
class Selector(BaseModel):
|
|
17
|
-
type: str
|
|
18
|
+
type: Annotated[str, StringConstraints(to_lower=True, strip_whitespace=True)]
|
|
18
19
|
value: str
|
|
19
20
|
classification: str | None = Field(default=None)
|
|
20
21
|
sources: list[str] | None = Field(default=None)
|
|
@@ -41,8 +42,7 @@ class Selector(BaseModel):
|
|
|
41
42
|
json.loads(self.value)
|
|
42
43
|
except json.JSONDecodeError as e:
|
|
43
44
|
raise AssertionError("If type is telemetry, value must be a valid JSON object.") from e
|
|
44
|
-
|
|
45
|
-
if self.type in CASE_INSENSITIVE_TYPES:
|
|
45
|
+
elif self.type in CASE_INSENSITIVE_TYPES:
|
|
46
46
|
self.value = self.value.lower()
|
|
47
47
|
|
|
48
48
|
if not self.classification:
|
|
@@ -2,7 +2,7 @@ import base64
|
|
|
2
2
|
import hashlib
|
|
3
3
|
from typing import Any, Optional, Union
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
from elasticapm.traces import capture_span
|
|
6
6
|
from flask import request
|
|
7
7
|
|
|
8
8
|
from clue.common.exceptions import (
|
|
@@ -17,7 +17,7 @@ from clue.config import config, get_redis
|
|
|
17
17
|
from clue.models.config import ExternalSource
|
|
18
18
|
from clue.remote.datatypes.set import ExpiringSet
|
|
19
19
|
from clue.security.obo import get_obo_token
|
|
20
|
-
from clue.security.utils import
|
|
20
|
+
from clue.security.utils import generate_random_secret
|
|
21
21
|
from clue.services import jwt_service, user_service
|
|
22
22
|
|
|
23
23
|
logger = get_logger(__file__)
|
|
@@ -123,7 +123,7 @@ def validate_token(username: str, token: str) -> Optional[list[str]]:
|
|
|
123
123
|
return None
|
|
124
124
|
|
|
125
125
|
|
|
126
|
-
@
|
|
126
|
+
@capture_span(span_type="authentication")
|
|
127
127
|
def bearer_auth(
|
|
128
128
|
data: str, skip_jwt: bool = False, skip_internal: bool = False
|
|
129
129
|
) -> tuple[Optional[dict[str, Any]], Optional[list[str]]]:
|
|
@@ -164,7 +164,7 @@ def bearer_auth(
|
|
|
164
164
|
raise InvalidDataException("Not a valid authentication type for this endpoint.")
|
|
165
165
|
|
|
166
166
|
|
|
167
|
-
@
|
|
167
|
+
@capture_span(span_type="authentication")
|
|
168
168
|
def validate_apikey(name: str, apikey: str) -> tuple[Optional[dict[str, Any]], Optional[list[str]]]:
|
|
169
169
|
"""This function identifies the user via the internal API key functionality.
|
|
170
170
|
|
|
@@ -243,7 +243,7 @@ def decode_b64(b64_str: str) -> str:
|
|
|
243
243
|
raise InvalidDataException("Basic authentication data must be base64 encoded") from e
|
|
244
244
|
|
|
245
245
|
|
|
246
|
-
@
|
|
246
|
+
@capture_span(span_type="authentication")
|
|
247
247
|
def basic_auth(
|
|
248
248
|
data: str, is_base64: bool = True, skip_apikey: bool = False, skip_password: bool = False
|
|
249
249
|
) -> tuple[Optional[dict[str, Any]], Optional[list[str]]]:
|
|
@@ -267,6 +267,7 @@ def basic_auth(
|
|
|
267
267
|
[username, data] = key_pair.split(":", maxsplit=1)
|
|
268
268
|
|
|
269
269
|
validated_user = None
|
|
270
|
+
priv = None
|
|
270
271
|
if not skip_apikey:
|
|
271
272
|
validated_user, priv = validate_apikey(username, data)
|
|
272
273
|
|
|
@@ -299,16 +300,6 @@ def basic_auth(
|
|
|
299
300
|
return validated_user, priv
|
|
300
301
|
|
|
301
302
|
|
|
302
|
-
def extract_audience(access_token: str) -> list[str]:
|
|
303
|
-
"Extract the audience from an encoded JWT."
|
|
304
|
-
audience: list[str] | str | None = decode_jwt_payload(access_token).get("aud", None)
|
|
305
|
-
|
|
306
|
-
if not audience:
|
|
307
|
-
return []
|
|
308
|
-
|
|
309
|
-
return [audience] if not isinstance(audience, list) else audience
|
|
310
|
-
|
|
311
|
-
|
|
312
303
|
# TODO: sa-clue support
|
|
313
304
|
def check_obo(source: ExternalSource, access_token: str, username: str) -> tuple[Optional[str], Optional[str]]:
|
|
314
305
|
"""Checks whether a token's audience matches the source, and if it doesn't, tries to get an OBO token for the source
|
|
@@ -333,7 +324,7 @@ def check_obo(source: ExternalSource, access_token: str, username: str) -> tuple
|
|
|
333
324
|
|
|
334
325
|
access_token = sa_token
|
|
335
326
|
|
|
336
|
-
audience = extract_audience(access_token)
|
|
327
|
+
audience = jwt_service.extract_audience(access_token)
|
|
337
328
|
|
|
338
329
|
# Check if this is a standard clue token
|
|
339
330
|
if jwt_service.get_audience(jwt_service.get_provider(access_token)) in audience:
|
|
@@ -11,6 +11,7 @@ from jwt.api_jwk import PyJWK
|
|
|
11
11
|
from clue.common.exceptions import ClueKeyError, ClueValueError
|
|
12
12
|
from clue.common.logging import get_logger
|
|
13
13
|
from clue.config import cache, config
|
|
14
|
+
from clue.security.utils import decode_jwt_payload
|
|
14
15
|
|
|
15
16
|
logger = get_logger(__file__)
|
|
16
17
|
|
|
@@ -19,6 +20,9 @@ def get_jwk(access_token: str) -> PyJWK:
|
|
|
19
20
|
"""Get the JSON Web Key associated with the given JWT"""
|
|
20
21
|
# "kid" is the JSON Web Key's identifier. It tells us which key was used to validate the token.
|
|
21
22
|
kid = jwt.get_unverified_header(access_token).get("kid")
|
|
23
|
+
if not kid or not isinstance(kid, str):
|
|
24
|
+
raise ClueValueError("Unexpected kid value in access token: %s", kid)
|
|
25
|
+
|
|
22
26
|
jwks, _ = get_jwks()
|
|
23
27
|
|
|
24
28
|
try:
|
|
@@ -50,6 +54,9 @@ def get_provider(access_token: str) -> str:
|
|
|
50
54
|
"""
|
|
51
55
|
# "kid" is the JSON Web Key's identifier. It tells us which key was used to validate the token.
|
|
52
56
|
kid = jwt.get_unverified_header(access_token).get("kid")
|
|
57
|
+
if not kid or not isinstance(kid, str):
|
|
58
|
+
raise ClueValueError("Unexpected kid value in access token: %s", kid)
|
|
59
|
+
|
|
53
60
|
_, providers = get_jwks()
|
|
54
61
|
|
|
55
62
|
try:
|
|
@@ -93,6 +100,16 @@ def get_jwks() -> tuple[dict[str, dict[str, Any]], dict[str, str]]:
|
|
|
93
100
|
return (jwks, providers)
|
|
94
101
|
|
|
95
102
|
|
|
103
|
+
def extract_audience(access_token: str) -> list[str]:
|
|
104
|
+
"Extract the audience from an encoded JWT."
|
|
105
|
+
audience: list[str] | str | None = decode_jwt_payload(access_token).get("aud", None)
|
|
106
|
+
|
|
107
|
+
if not audience:
|
|
108
|
+
return []
|
|
109
|
+
|
|
110
|
+
return [audience] if not isinstance(audience, list) else audience
|
|
111
|
+
|
|
112
|
+
|
|
96
113
|
def get_audience(oauth_provider: str) -> str:
|
|
97
114
|
"""Get the audience for the specified OAuth provider
|
|
98
115
|
|
|
@@ -156,7 +173,7 @@ def decode(
|
|
|
156
173
|
options={"verify_aud": validate_audience},
|
|
157
174
|
**kwargs,
|
|
158
175
|
) # type: ignore
|
|
159
|
-
except jwt.InvalidAudienceError:
|
|
176
|
+
except jwt.exceptions.InvalidAudienceError:
|
|
160
177
|
logger.debug("Default audience did not match - checking additional audiences")
|
|
161
178
|
if config.auth.oauth.other_audiences is not None:
|
|
162
179
|
# The main audience isn't valid, let's try the others
|
|
@@ -174,7 +191,11 @@ def decode(
|
|
|
174
191
|
except jwt.InvalidAudienceError:
|
|
175
192
|
continue
|
|
176
193
|
|
|
177
|
-
logger.
|
|
194
|
+
logger.warning(
|
|
195
|
+
"Default and additional audiences failed to validate. Expected: %s, Actual: %s",
|
|
196
|
+
audience,
|
|
197
|
+
",".join(extract_audience(access_token)),
|
|
198
|
+
)
|
|
178
199
|
raise
|
|
179
200
|
|
|
180
201
|
|
|
@@ -146,7 +146,7 @@ log_cli_level = "WARN"
|
|
|
146
146
|
[tool.poetry]
|
|
147
147
|
package-mode = true
|
|
148
148
|
name = "clue-api"
|
|
149
|
-
version = "1.3.0.
|
|
149
|
+
version = "1.3.0.dev28"
|
|
150
150
|
description = "Clue distributed enrichment service"
|
|
151
151
|
authors = ["Canadian Centre for Cyber Security <contact@cyber.gc.ca>"]
|
|
152
152
|
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
|