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.
Files changed (91) hide show
  1. clue/.gitignore +21 -0
  2. clue/__init__.py +0 -0
  3. clue/api/__init__.py +211 -0
  4. clue/api/base.py +99 -0
  5. clue/api/v1/__init__.py +82 -0
  6. clue/api/v1/actions.py +92 -0
  7. clue/api/v1/auth.py +243 -0
  8. clue/api/v1/configs.py +83 -0
  9. clue/api/v1/fetchers.py +94 -0
  10. clue/api/v1/lookup.py +221 -0
  11. clue/api/v1/registration.py +109 -0
  12. clue/api/v1/static.py +94 -0
  13. clue/app.py +166 -0
  14. clue/cache/__init__.py +129 -0
  15. clue/common/__init__.py +0 -0
  16. clue/common/classification.py +1006 -0
  17. clue/common/classification.yml +130 -0
  18. clue/common/dict_utils.py +130 -0
  19. clue/common/exceptions.py +199 -0
  20. clue/common/forge.py +152 -0
  21. clue/common/json_utils.py +10 -0
  22. clue/common/list_utils.py +11 -0
  23. clue/common/logging/__init__.py +291 -0
  24. clue/common/logging/audit.py +157 -0
  25. clue/common/logging/format.py +42 -0
  26. clue/common/regex.py +31 -0
  27. clue/common/str_utils.py +213 -0
  28. clue/common/swagger.py +139 -0
  29. clue/common/uid.py +47 -0
  30. clue/config.py +60 -0
  31. clue/constants/__init__.py +0 -0
  32. clue/constants/supported_types.py +38 -0
  33. clue/cronjobs/__init__.py +30 -0
  34. clue/cronjobs/plugins.py +32 -0
  35. clue/error.py +129 -0
  36. clue/gunicorn_config.py +29 -0
  37. clue/healthz.py +74 -0
  38. clue/helper/discover.py +53 -0
  39. clue/helper/headers.py +30 -0
  40. clue/helper/oauth.py +128 -0
  41. clue/models/__init__.py +0 -0
  42. clue/models/actions.py +243 -0
  43. clue/models/config.py +456 -0
  44. clue/models/fetchers.py +136 -0
  45. clue/models/graph.py +162 -0
  46. clue/models/model_list.py +52 -0
  47. clue/models/network.py +430 -0
  48. clue/models/results/__init__.py +34 -0
  49. clue/models/results/base.py +10 -0
  50. clue/models/results/graph.py +26 -0
  51. clue/models/results/image.py +22 -0
  52. clue/models/results/status.py +55 -0
  53. clue/models/results/validation.py +57 -0
  54. clue/models/selector.py +67 -0
  55. clue/models/utils.py +52 -0
  56. clue/models/validators.py +19 -0
  57. clue/patched.py +8 -0
  58. clue/plugin/__init__.py +1008 -0
  59. clue/plugin/helpers/__init__.py +0 -0
  60. clue/plugin/helpers/central_server.py +27 -0
  61. clue/plugin/helpers/email_render.py +228 -0
  62. clue/plugin/helpers/token.py +34 -0
  63. clue/plugin/helpers/trino.py +103 -0
  64. clue/plugin/interactive.py +270 -0
  65. clue/plugin/models.py +19 -0
  66. clue/plugin/utils.py +78 -0
  67. clue/remote/__init__.py +0 -0
  68. clue/remote/datatypes/__init__.py +130 -0
  69. clue/remote/datatypes/cache.py +62 -0
  70. clue/remote/datatypes/events.py +118 -0
  71. clue/remote/datatypes/hash.py +193 -0
  72. clue/remote/datatypes/queues/__init__.py +0 -0
  73. clue/remote/datatypes/queues/comms.py +62 -0
  74. clue/remote/datatypes/set.py +96 -0
  75. clue/remote/datatypes/user_quota_tracker.py +54 -0
  76. clue/security/__init__.py +211 -0
  77. clue/security/obo.py +95 -0
  78. clue/security/utils.py +34 -0
  79. clue/services/action_service.py +186 -0
  80. clue/services/auth_service.py +348 -0
  81. clue/services/config_service.py +38 -0
  82. clue/services/fetcher_service.py +203 -0
  83. clue/services/jwt_service.py +233 -0
  84. clue/services/lookup_service.py +786 -0
  85. clue/services/type_service.py +165 -0
  86. clue/services/user_service.py +152 -0
  87. clue_api-1.0.0.dev7.dist-info/METADATA +111 -0
  88. clue_api-1.0.0.dev7.dist-info/RECORD +91 -0
  89. clue_api-1.0.0.dev7.dist-info/WHEEL +4 -0
  90. clue_api-1.0.0.dev7.dist-info/entry_points.txt +8 -0
  91. 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
@@ -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
clue/patched.py ADDED
@@ -0,0 +1,8 @@
1
+ from gevent.monkey import patch_all
2
+
3
+ patch_all()
4
+
5
+ from clue.app import app, main # noqa: F401, E402
6
+
7
+ if __name__ == "__main__":
8
+ main()