clue-api 1.0.0.dev7__py3-none-any.whl
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/.gitignore +21 -0
- clue/__init__.py +0 -0
- clue/api/__init__.py +211 -0
- clue/api/base.py +99 -0
- clue/api/v1/__init__.py +82 -0
- clue/api/v1/actions.py +92 -0
- clue/api/v1/auth.py +243 -0
- clue/api/v1/configs.py +83 -0
- clue/api/v1/fetchers.py +94 -0
- clue/api/v1/lookup.py +221 -0
- clue/api/v1/registration.py +109 -0
- clue/api/v1/static.py +94 -0
- clue/app.py +166 -0
- clue/cache/__init__.py +129 -0
- clue/common/__init__.py +0 -0
- clue/common/classification.py +1006 -0
- clue/common/classification.yml +130 -0
- clue/common/dict_utils.py +130 -0
- clue/common/exceptions.py +199 -0
- clue/common/forge.py +152 -0
- clue/common/json_utils.py +10 -0
- clue/common/list_utils.py +11 -0
- clue/common/logging/__init__.py +291 -0
- clue/common/logging/audit.py +157 -0
- clue/common/logging/format.py +42 -0
- clue/common/regex.py +31 -0
- clue/common/str_utils.py +213 -0
- clue/common/swagger.py +139 -0
- clue/common/uid.py +47 -0
- clue/config.py +60 -0
- clue/constants/__init__.py +0 -0
- clue/constants/supported_types.py +38 -0
- clue/cronjobs/__init__.py +30 -0
- clue/cronjobs/plugins.py +32 -0
- clue/error.py +129 -0
- clue/gunicorn_config.py +29 -0
- clue/healthz.py +74 -0
- clue/helper/discover.py +53 -0
- clue/helper/headers.py +30 -0
- clue/helper/oauth.py +128 -0
- clue/models/__init__.py +0 -0
- clue/models/actions.py +243 -0
- clue/models/config.py +456 -0
- clue/models/fetchers.py +136 -0
- clue/models/graph.py +162 -0
- clue/models/model_list.py +52 -0
- clue/models/network.py +430 -0
- clue/models/results/__init__.py +34 -0
- clue/models/results/base.py +10 -0
- clue/models/results/graph.py +26 -0
- clue/models/results/image.py +22 -0
- clue/models/results/status.py +55 -0
- clue/models/results/validation.py +57 -0
- clue/models/selector.py +67 -0
- clue/models/utils.py +52 -0
- clue/models/validators.py +19 -0
- clue/patched.py +8 -0
- clue/plugin/__init__.py +1008 -0
- clue/plugin/helpers/__init__.py +0 -0
- clue/plugin/helpers/central_server.py +27 -0
- clue/plugin/helpers/email_render.py +228 -0
- clue/plugin/helpers/token.py +34 -0
- clue/plugin/helpers/trino.py +103 -0
- clue/plugin/interactive.py +270 -0
- clue/plugin/models.py +19 -0
- clue/plugin/utils.py +78 -0
- clue/remote/__init__.py +0 -0
- clue/remote/datatypes/__init__.py +130 -0
- clue/remote/datatypes/cache.py +62 -0
- clue/remote/datatypes/events.py +118 -0
- clue/remote/datatypes/hash.py +193 -0
- clue/remote/datatypes/queues/__init__.py +0 -0
- clue/remote/datatypes/queues/comms.py +62 -0
- clue/remote/datatypes/set.py +96 -0
- clue/remote/datatypes/user_quota_tracker.py +54 -0
- clue/security/__init__.py +211 -0
- clue/security/obo.py +95 -0
- clue/security/utils.py +34 -0
- clue/services/action_service.py +186 -0
- clue/services/auth_service.py +348 -0
- clue/services/config_service.py +38 -0
- clue/services/fetcher_service.py +203 -0
- clue/services/jwt_service.py +233 -0
- clue/services/lookup_service.py +786 -0
- clue/services/type_service.py +165 -0
- clue/services/user_service.py +152 -0
- clue_api-1.0.0.dev7.dist-info/METADATA +111 -0
- clue_api-1.0.0.dev7.dist-info/RECORD +91 -0
- clue_api-1.0.0.dev7.dist-info/WHEEL +4 -0
- clue_api-1.0.0.dev7.dist-info/entry_points.txt +8 -0
- clue_api-1.0.0.dev7.dist-info/licenses/LICENSE +11 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from typing import override
|
|
2
|
+
|
|
3
|
+
from pydantic import Field
|
|
4
|
+
|
|
5
|
+
from clue.models.graph import BaseGraphModel, Metadata, Node
|
|
6
|
+
from clue.models.results.base import Result
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class GraphResult(BaseGraphModel, Result):
|
|
10
|
+
"A result describing a graph that should be returned"
|
|
11
|
+
|
|
12
|
+
@override
|
|
13
|
+
@staticmethod
|
|
14
|
+
def format():
|
|
15
|
+
"Return the clue format for this result"
|
|
16
|
+
return "graph"
|
|
17
|
+
|
|
18
|
+
id: str = Field(description="An ID for this generated graph.")
|
|
19
|
+
metadata: Metadata = Metadata()
|
|
20
|
+
data: list[list[Node]] = Field(
|
|
21
|
+
description=(
|
|
22
|
+
"A list of lists of nodes to render. The outer list breaks the nodes into columns, "
|
|
23
|
+
"while the inner list breaks the nodes into rows."
|
|
24
|
+
)
|
|
25
|
+
)
|
|
26
|
+
# related: dict[str, str] = Field(description="A dictionary of other related graphs.")
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# ruff: noqa: D101
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from pydantic import Field
|
|
5
|
+
|
|
6
|
+
from clue.common.logging import get_logger
|
|
7
|
+
from clue.models.results.base import Result
|
|
8
|
+
|
|
9
|
+
logger = get_logger(__file__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ImageResult(Result):
|
|
13
|
+
@staticmethod
|
|
14
|
+
def format():
|
|
15
|
+
"Return the clue format for this result"
|
|
16
|
+
return "image"
|
|
17
|
+
|
|
18
|
+
image: str = Field(
|
|
19
|
+
description="An image URL, either redirecting to a valid network location or encoded image data in "
|
|
20
|
+
"base64 format."
|
|
21
|
+
)
|
|
22
|
+
alt: str = Field(description="The label for the image.")
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
from typing import Self
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field, model_validator
|
|
6
|
+
from pydantic_core import Url
|
|
7
|
+
|
|
8
|
+
from clue.common.exceptions import ClueValueError
|
|
9
|
+
from clue.common.logging import get_logger
|
|
10
|
+
from clue.models.results.base import Result
|
|
11
|
+
|
|
12
|
+
logger = get_logger(__file__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class StatusLabel(BaseModel):
|
|
16
|
+
"Labels for the status, tied to s specific localization"
|
|
17
|
+
|
|
18
|
+
language: str = Field(description="Localization language for this label (i.e., en, fr)")
|
|
19
|
+
label: str = Field(description="The localized label text.")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class StatusResult(Result):
|
|
23
|
+
"Information about the status of a selector"
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def format():
|
|
27
|
+
"Return the clue format for this result"
|
|
28
|
+
return "status"
|
|
29
|
+
|
|
30
|
+
empty: bool = Field(
|
|
31
|
+
description="Is this status empty (i.e., if there's no applicable status result)?", default=False
|
|
32
|
+
)
|
|
33
|
+
labels: list[StatusLabel] = Field(description="A list of status labels in various languages", default=[])
|
|
34
|
+
link: Url | None = Field(description="An optional link for more information", default=None)
|
|
35
|
+
icon: str | None = Field(description="An optional icon to style the status", default=None)
|
|
36
|
+
color: str | None = Field(description="An optional hexadecimal colour to style the status", default=None)
|
|
37
|
+
|
|
38
|
+
@model_validator(mode="after")
|
|
39
|
+
def validate_model(self: Self) -> Self:
|
|
40
|
+
"Ensure the model has correct localizations, and the color is a valid hex string"
|
|
41
|
+
if self.empty:
|
|
42
|
+
return self
|
|
43
|
+
|
|
44
|
+
for language in os.environ.get("LOCALIZATION_LANGUAGES", "en,fr").split(","):
|
|
45
|
+
if not language:
|
|
46
|
+
continue
|
|
47
|
+
|
|
48
|
+
if not any(label.language == language for label in self.labels):
|
|
49
|
+
raise ClueValueError(f"Status is missing a localized result for the localization '{language}'")
|
|
50
|
+
|
|
51
|
+
if self.color:
|
|
52
|
+
if not re.match(r"^#[0-9a-f]{6}$", self.color, re.IGNORECASE):
|
|
53
|
+
raise ClueValueError("Invalid hex color value provided. Shorthand colors are not supported.")
|
|
54
|
+
|
|
55
|
+
return self
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import cast
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, ValidationInfo
|
|
5
|
+
|
|
6
|
+
from clue.common.exceptions import ClueValueError
|
|
7
|
+
from clue.common.logging import get_logger
|
|
8
|
+
from clue.models.results import DATA, FORMAT_MAPPINGS, FORMAT_MAPPINGS_REVERSE
|
|
9
|
+
|
|
10
|
+
logger = get_logger(__file__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def validate_result(_format: str, data: DATA | None, info: ValidationInfo) -> DATA | None: # noqa: C901
|
|
14
|
+
"Validate a result in a model"
|
|
15
|
+
if isinstance(data, BaseModel) and data.__class__ in FORMAT_MAPPINGS:
|
|
16
|
+
expected_format = FORMAT_MAPPINGS[data.__class__]
|
|
17
|
+
if expected_format != _format:
|
|
18
|
+
raise ClueValueError(
|
|
19
|
+
f"Format should be {expected_format} if data is of type {data.__class__.__name__}, "
|
|
20
|
+
f"but is set to {_format}"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
if _format in FORMAT_MAPPINGS_REVERSE and not any(
|
|
24
|
+
isinstance(data, _type) for _type in FORMAT_MAPPINGS_REVERSE[_format]
|
|
25
|
+
):
|
|
26
|
+
resolved = False
|
|
27
|
+
for expected_type in FORMAT_MAPPINGS_REVERSE[_format]:
|
|
28
|
+
if info.context and info.context.get("is_response", False) and issubclass(expected_type, BaseModel):
|
|
29
|
+
data = cast(DATA, cast(BaseModel, expected_type).model_validate(data))
|
|
30
|
+
resolved = True
|
|
31
|
+
elif _format == "json" and isinstance(data, str):
|
|
32
|
+
data = json.loads(data)
|
|
33
|
+
resolved = True
|
|
34
|
+
elif _format == "json" and isinstance(data, BaseModel):
|
|
35
|
+
data = cast(DATA, data.model_dump(mode="json", exclude_none=True))
|
|
36
|
+
resolved = True
|
|
37
|
+
|
|
38
|
+
if resolved:
|
|
39
|
+
break
|
|
40
|
+
|
|
41
|
+
if not resolved:
|
|
42
|
+
raise ClueValueError(
|
|
43
|
+
f"data should be of type {getattr(expected_type, '__name__', str(expected_type))}, "
|
|
44
|
+
f"but is set to {data.__class__.__name__}"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
if _format == "graph" and isinstance(data, BaseModel):
|
|
48
|
+
data = cast(DATA, data.model_dump(mode="json", exclude_none=True, by_alias=True))
|
|
49
|
+
|
|
50
|
+
if _format == "json":
|
|
51
|
+
try:
|
|
52
|
+
json.dumps(data)
|
|
53
|
+
except Exception:
|
|
54
|
+
logger.exception("Exception on serialization")
|
|
55
|
+
raise ClueValueError("Data is not JSON serializable, or is not valid JSON.")
|
|
56
|
+
|
|
57
|
+
return data
|
clue/models/selector.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# ruff: noqa: D101
|
|
2
|
+
import ipaddress
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
from flask import request
|
|
6
|
+
from pydantic import BaseModel, Field, model_validator
|
|
7
|
+
from typing_extensions import Self
|
|
8
|
+
|
|
9
|
+
from clue.common.logging import get_logger
|
|
10
|
+
from clue.config import CLASSIFICATION
|
|
11
|
+
from clue.constants.supported_types import CASE_INSENSITIVE_TYPES
|
|
12
|
+
|
|
13
|
+
logger = get_logger(__file__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Selector(BaseModel):
|
|
17
|
+
type: str
|
|
18
|
+
value: str
|
|
19
|
+
classification: str | None = Field(default=None)
|
|
20
|
+
sources: list[str] | None = Field(default=None)
|
|
21
|
+
|
|
22
|
+
@model_validator(mode="after")
|
|
23
|
+
def validate_model(self: Self) -> Self: # noqa: C901
|
|
24
|
+
"""Validates the entire model.
|
|
25
|
+
|
|
26
|
+
Raises:
|
|
27
|
+
AssertionError: Raised whenever a field is invalid on the model.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Self: The validated model.
|
|
31
|
+
"""
|
|
32
|
+
# For backwards compatability, if eml is used it is replaced with email
|
|
33
|
+
self.type = self.type.replace("eml", "email")
|
|
34
|
+
|
|
35
|
+
if self.type == "ip":
|
|
36
|
+
is_ipv4 = isinstance(ipaddress.ip_address(self.value), ipaddress.IPv4Address)
|
|
37
|
+
self.type = "ipv4" if is_ipv4 else "ipv6"
|
|
38
|
+
|
|
39
|
+
if self.type == "telemetry":
|
|
40
|
+
try:
|
|
41
|
+
json.loads(self.value)
|
|
42
|
+
except json.JSONDecodeError as e:
|
|
43
|
+
raise AssertionError("If type is telemetry, value must be a valid JSON object.") from e
|
|
44
|
+
|
|
45
|
+
if self.type in CASE_INSENSITIVE_TYPES:
|
|
46
|
+
self.value = self.value.lower()
|
|
47
|
+
|
|
48
|
+
if not self.classification:
|
|
49
|
+
try:
|
|
50
|
+
self.classification = request.args.get("classification", CLASSIFICATION.UNRESTRICTED)
|
|
51
|
+
except RuntimeError:
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
if self.sources is None:
|
|
55
|
+
try:
|
|
56
|
+
if query_sources_str := request.args.get("sources", None):
|
|
57
|
+
if "|" in query_sources_str:
|
|
58
|
+
self.sources = query_sources_str.split("|")
|
|
59
|
+
else:
|
|
60
|
+
self.sources = query_sources_str.split(",")
|
|
61
|
+
except RuntimeError:
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
return self
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
BulkEnrich = Selector
|
clue/models/utils.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import random
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import Union, get_args, get_origin
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
from pydantic_core import Url
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def generate_example_model(model: type[BaseModel], as_list: bool = False): # noqa: C901
|
|
10
|
+
"""Populate fields with example values"""
|
|
11
|
+
result = {}
|
|
12
|
+
|
|
13
|
+
if as_list:
|
|
14
|
+
full_list = []
|
|
15
|
+
for _ in range(random.randint(1, 3)): # noqa: S311
|
|
16
|
+
full_list.append(generate_example_model(model))
|
|
17
|
+
|
|
18
|
+
return full_list
|
|
19
|
+
|
|
20
|
+
for name, field_info in model.model_fields.items():
|
|
21
|
+
field_type = field_info.annotation
|
|
22
|
+
if field_type:
|
|
23
|
+
if get_origin(field_type) is Union and type(None) in get_args(field_type):
|
|
24
|
+
field_type = (
|
|
25
|
+
next((_type for _type in get_args(field_type) if _type is not type(None)), None) or field_type
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
_as_list = False
|
|
29
|
+
if get_origin(field_type) is list:
|
|
30
|
+
_as_list = True
|
|
31
|
+
field_type = get_args(field_type)[0]
|
|
32
|
+
|
|
33
|
+
_issubclass = False
|
|
34
|
+
try:
|
|
35
|
+
if field_type:
|
|
36
|
+
_issubclass = issubclass(field_type, BaseModel)
|
|
37
|
+
except TypeError:
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
if field_type and _issubclass:
|
|
41
|
+
result[name] = generate_example_model(field_type, as_list=_as_list)
|
|
42
|
+
elif field_info.examples:
|
|
43
|
+
result[name] = random.choice(field_info.examples) # noqa: S311
|
|
44
|
+
elif field_info.default:
|
|
45
|
+
result[name] = field_info.default
|
|
46
|
+
|
|
47
|
+
if isinstance(result[name], datetime):
|
|
48
|
+
result[name] = result[name].isoformat()
|
|
49
|
+
elif isinstance(result[name], Url):
|
|
50
|
+
result[name] = str(result[name])
|
|
51
|
+
|
|
52
|
+
return dict(result)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from clue.config import CLASSIFICATION
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def validate_classification(classification: str):
|
|
5
|
+
"""Validates the provided classification.
|
|
6
|
+
|
|
7
|
+
Args:
|
|
8
|
+
classification (str): The classification to validate.
|
|
9
|
+
|
|
10
|
+
Raises:
|
|
11
|
+
AssertionError: Raised whenever the provided classification is not valid.
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
str: The validated classification.
|
|
15
|
+
"""
|
|
16
|
+
if not CLASSIFICATION.is_valid(classification):
|
|
17
|
+
raise AssertionError(f"{classification} is not a valid classification.")
|
|
18
|
+
|
|
19
|
+
return classification
|