clue-api 1.7.0.dev458__tar.gz → 1.7.0.dev463__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.
Files changed (107) hide show
  1. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/PKG-INFO +1 -1
  2. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/api/__init__.py +17 -10
  3. clue_api-1.7.0.dev458/clue/api/v1/__init__.py → clue_api-1.7.0.dev463/clue/api/documentation.py +8 -11
  4. clue_api-1.7.0.dev463/clue/api/v1/__init__.py +8 -0
  5. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/api/v1/lookup.py +1 -1
  6. clue_api-1.7.0.dev463/clue/api/v2/__init__.py +8 -0
  7. clue_api-1.7.0.dev463/clue/api/v2/lookup.py +41 -0
  8. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/app.py +4 -0
  9. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/common/regex.py +12 -9
  10. clue_api-1.7.0.dev463/clue/constants/supported_types.py +108 -0
  11. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/services/type_service.py +15 -7
  12. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/pyproject.toml +1 -1
  13. clue_api-1.7.0.dev458/clue/constants/supported_types.py +0 -85
  14. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/LICENSE +0 -0
  15. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/README.md +0 -0
  16. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/.gitignore +0 -0
  17. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/__init__.py +0 -0
  18. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/api/base.py +0 -0
  19. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/api/v1/actions.py +0 -0
  20. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/api/v1/auth.py +0 -0
  21. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/api/v1/configs.py +0 -0
  22. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/api/v1/fetchers.py +0 -0
  23. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/api/v1/registration.py +0 -0
  24. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/api/v1/static.py +0 -0
  25. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/api/v1/sync.py +0 -0
  26. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/cache/__init__.py +0 -0
  27. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/common/__init__.py +0 -0
  28. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/common/bytes_utils.py +0 -0
  29. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/common/classification.py +0 -0
  30. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/common/classification.yml +0 -0
  31. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/common/dict_utils.py +0 -0
  32. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/common/exceptions.py +0 -0
  33. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/common/forge.py +0 -0
  34. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/common/json_utils.py +0 -0
  35. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/common/list_utils.py +0 -0
  36. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/common/logging/__init__.py +0 -0
  37. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/common/logging/audit.py +0 -0
  38. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/common/logging/format.py +0 -0
  39. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/common/str_utils.py +0 -0
  40. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/common/swagger.py +0 -0
  41. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/common/uid.py +0 -0
  42. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/config.py +0 -0
  43. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/constants/__init__.py +0 -0
  44. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/constants/env.py +0 -0
  45. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/cronjobs/__init__.py +0 -0
  46. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/cronjobs/plugins.py +0 -0
  47. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/error.py +0 -0
  48. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/extensions/__init__.py +0 -0
  49. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/extensions/config.py +0 -0
  50. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/gunicorn_config.py +0 -0
  51. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/healthz.py +0 -0
  52. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/helper/discover.py +0 -0
  53. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/helper/headers.py +0 -0
  54. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/helper/oauth.py +0 -0
  55. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/helper/obo.py +0 -0
  56. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/helper/plugin_requests.py +0 -0
  57. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/models/__init__.py +0 -0
  58. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/models/actions.py +0 -0
  59. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/models/auth_user.py +0 -0
  60. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/models/config.py +0 -0
  61. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/models/fetchers.py +0 -0
  62. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/models/graph.py +0 -0
  63. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/models/jwks.py +0 -0
  64. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/models/model_list.py +0 -0
  65. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/models/network.py +0 -0
  66. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/models/results/__init__.py +0 -0
  67. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/models/results/base.py +0 -0
  68. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/models/results/file.py +0 -0
  69. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/models/results/graph.py +0 -0
  70. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/models/results/image.py +0 -0
  71. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/models/results/status.py +0 -0
  72. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/models/results/validation.py +0 -0
  73. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/models/schema.py +0 -0
  74. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/models/selector.py +0 -0
  75. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/models/sync.py +0 -0
  76. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/models/validators.py +0 -0
  77. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/patched.py +0 -0
  78. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/plugin/__init__.py +0 -0
  79. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/plugin/celery_app.py +0 -0
  80. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/plugin/helpers/__init__.py +0 -0
  81. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/plugin/helpers/central_server.py +0 -0
  82. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/plugin/helpers/email_render.py +0 -0
  83. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/plugin/helpers/token.py +0 -0
  84. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/plugin/helpers/trino.py +0 -0
  85. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/plugin/models.py +0 -0
  86. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/plugin/utils.py +0 -0
  87. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/py.typed +0 -0
  88. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/remote/__init__.py +0 -0
  89. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/remote/datatypes/__init__.py +0 -0
  90. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/remote/datatypes/cache.py +0 -0
  91. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/remote/datatypes/events.py +0 -0
  92. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/remote/datatypes/hash.py +0 -0
  93. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/remote/datatypes/queues/__init__.py +0 -0
  94. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/remote/datatypes/queues/comms.py +0 -0
  95. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/remote/datatypes/set.py +0 -0
  96. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/remote/datatypes/user_quota_tracker.py +0 -0
  97. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/security/__init__.py +0 -0
  98. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/security/obo.py +0 -0
  99. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/security/utils.py +0 -0
  100. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/services/action_service.py +0 -0
  101. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/services/auth_service.py +0 -0
  102. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/services/config_service.py +0 -0
  103. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/services/fetcher_service.py +0 -0
  104. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/services/jwt_service.py +0 -0
  105. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/services/lookup_service.py +0 -0
  106. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/clue/services/mongo_service.py +0 -0
  107. {clue_api-1.7.0.dev458 → clue_api-1.7.0.dev463}/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.dev458
3
+ Version: 1.7.0.dev463
4
4
  Summary: Clue distributed enrichment service
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -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, err: Union[str, Exception] = "", warnings: list[str] = [], status_code: int = 200, cookies: Any = None
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
- ClueResponse(response=data, error_message=err, warning=warnings, status_code=status_code).model_dump(
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):
@@ -1,19 +1,14 @@
1
1
  from textwrap import dedent
2
2
 
3
- from flask import Blueprint, current_app, request
3
+ from flask import current_app, request
4
4
 
5
5
  from clue.api import ok
6
- from clue.api.base import api_login
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(request.path):
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 == "apiv1":
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("apiv1.", "").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.v1.{rule.endpoint}",
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
- EMAIL_REGEX = f"^[a-zA-Z0-9!#$%&'*+/=?^_‘{{|}}~-]+(?:\\.[a-zA-Z0-9!#$%&'*+/=?^_‘{{|}}~-]+)*@({DOMAIN_REGEX})$"
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}|:)|fe80:(?::[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|"
16
- r"::(?:ffff(?::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]|"
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
- # Case-insensitive version of DOMAIN_REGEX for URIs
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 regular expression to detect the different types"""
28
- access_token = request.headers.get("Authorization", type=str)
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.dev458"
144
+ version = "1.7.0.dev463"
145
145
  description = "Clue distributed enrichment service"
146
146
  authors = ["Canadian Centre for Cyber Security <contact@cyber.gc.ca>"]
147
147
  license = "MIT"
@@ -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