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,130 @@
|
|
|
1
|
+
# This the default classification engine provided with Clue,
|
|
2
|
+
# it showcases all the different features of the classification engine
|
|
3
|
+
# while providing a useful configuration
|
|
4
|
+
|
|
5
|
+
# Turn on/off classification enforcement. When this flag is off, this
|
|
6
|
+
# completely disables the classification engine, any documents added while
|
|
7
|
+
# the classification engine is off gets the default unrestricted value
|
|
8
|
+
enforce: false
|
|
9
|
+
|
|
10
|
+
# Turn on/off dynamic group creation. This feature allow you to dynamically create classification groups based on
|
|
11
|
+
# features from the user.
|
|
12
|
+
dynamic_groups: false
|
|
13
|
+
|
|
14
|
+
# Set the type of dynamic groups to be used
|
|
15
|
+
# email: groups will be based of the user's email domain
|
|
16
|
+
# group: groups will be created out the the user's group values
|
|
17
|
+
# all: groups will be created out of both the email domain and the group values
|
|
18
|
+
dynamic_groups_type: email
|
|
19
|
+
|
|
20
|
+
# List of Classification level:
|
|
21
|
+
# Graded list were a smaller number is less restricted then an higher number.
|
|
22
|
+
levels:
|
|
23
|
+
# List of alternate names for the current marking
|
|
24
|
+
- aliases:
|
|
25
|
+
- UNRESTRICTED
|
|
26
|
+
- UNCLASSIFIED
|
|
27
|
+
- U
|
|
28
|
+
- TLP:W
|
|
29
|
+
- TLP:WHITE
|
|
30
|
+
# Stylesheet applied in the UI for the different levels
|
|
31
|
+
css:
|
|
32
|
+
# Name of the color scheme used for display (default, primary, secondary, success, info, warning, error)
|
|
33
|
+
color: default
|
|
34
|
+
# Description of the classification level
|
|
35
|
+
description: Subject to standard copyright rules, TLP:CLEAR information may be distributed without restriction.
|
|
36
|
+
# Interger value of the Classification level (higher is more classified)
|
|
37
|
+
lvl: 100
|
|
38
|
+
# Long name of the classification item
|
|
39
|
+
name: TLP:CLEAR
|
|
40
|
+
# Short name of the classification item
|
|
41
|
+
short_name: TLP:C
|
|
42
|
+
- aliases: []
|
|
43
|
+
css:
|
|
44
|
+
color: success
|
|
45
|
+
description:
|
|
46
|
+
Recipients may share TLP:GREEN information with peers and partner organizations
|
|
47
|
+
within their sector or community, but not via publicly accessible channels. Information
|
|
48
|
+
in this category can be circulated widely within a particular community. TLP:GREEN
|
|
49
|
+
information may not be released outside of the community.
|
|
50
|
+
lvl: 110
|
|
51
|
+
name: TLP:GREEN
|
|
52
|
+
short_name: TLP:G
|
|
53
|
+
- aliases: []
|
|
54
|
+
css:
|
|
55
|
+
color: warning
|
|
56
|
+
description:
|
|
57
|
+
Recipients may only share TLP:AMBER information with members of their
|
|
58
|
+
own organization and with clients or customers who need to know the information
|
|
59
|
+
to protect themselves or prevent further harm.
|
|
60
|
+
lvl: 120
|
|
61
|
+
name: TLP:AMBER
|
|
62
|
+
short_name: TLP:A
|
|
63
|
+
- aliases:
|
|
64
|
+
- RESTRICTED
|
|
65
|
+
css:
|
|
66
|
+
color: warning
|
|
67
|
+
description:
|
|
68
|
+
Recipients may only share TLP:AMBER+STRICT information with members of their
|
|
69
|
+
own organization.
|
|
70
|
+
lvl: 125
|
|
71
|
+
name: TLP:AMBER+STRICT
|
|
72
|
+
short_name: TLP:A+S
|
|
73
|
+
|
|
74
|
+
# List of required tokens:
|
|
75
|
+
# A user requesting access to an item must have all the
|
|
76
|
+
# required tokens the item has to gain access to it
|
|
77
|
+
required:
|
|
78
|
+
- aliases: []
|
|
79
|
+
description: Produced using a commercial tool with limited distribution
|
|
80
|
+
name: COMMERCIAL
|
|
81
|
+
short_name: CMR
|
|
82
|
+
# The minimum classification level an item must have
|
|
83
|
+
# for this token to be valid. (optional)
|
|
84
|
+
# require_lvl: 100
|
|
85
|
+
# This is a token that is required but will display in the groups part
|
|
86
|
+
# of the classification string. (optional)
|
|
87
|
+
# is_required_group: true
|
|
88
|
+
|
|
89
|
+
# List of groups:
|
|
90
|
+
# A user requesting access to an item must be part of a least
|
|
91
|
+
# of one the group the item is part of to gain access
|
|
92
|
+
groups:
|
|
93
|
+
- aliases: []
|
|
94
|
+
# This is a special flag that when set to true, if any groups are selected
|
|
95
|
+
# in a classification. This group will automatically be selected too. (optional)
|
|
96
|
+
auto_select: true
|
|
97
|
+
description: Employees of CSE
|
|
98
|
+
name: CSE
|
|
99
|
+
short_name: CSE
|
|
100
|
+
# Assuming that this groups is the only group selected, this is the display name
|
|
101
|
+
# that will be used in the classification (that values has to be in the aliases
|
|
102
|
+
# of this group and only this group) (optional)
|
|
103
|
+
# solitary_display_name: ANY
|
|
104
|
+
|
|
105
|
+
# List of subgroups:
|
|
106
|
+
# A user requesting access to an item must be part of a least
|
|
107
|
+
# of one the subgroup the item is part of to gain access
|
|
108
|
+
subgroups:
|
|
109
|
+
- aliases: []
|
|
110
|
+
description: Member of Incident Response team
|
|
111
|
+
name: IR TEAM
|
|
112
|
+
short_name: IR
|
|
113
|
+
- aliases: []
|
|
114
|
+
description: Member of the Canadian Centre for Cyber Security
|
|
115
|
+
# This is a special flag that auto-select the corresponding group
|
|
116
|
+
# when this subgroup is selected (optional)
|
|
117
|
+
require_group: CSE
|
|
118
|
+
name: CCCS
|
|
119
|
+
short_name: CCCS
|
|
120
|
+
# This is a special flag that makes sure that none other then the
|
|
121
|
+
# corresponding group is selected when this subgroup is selected (optional)
|
|
122
|
+
# limited_to_group: CSE
|
|
123
|
+
|
|
124
|
+
# Default restricted classification
|
|
125
|
+
restricted: TLP:A+S//CMR
|
|
126
|
+
|
|
127
|
+
# Default unrestricted classification:
|
|
128
|
+
# When no classification are provided or that the classification engine is
|
|
129
|
+
# disabled, this is the classification value each items will get
|
|
130
|
+
unrestricted: TLP:C
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
from typing import Any, AnyStr, Optional, cast
|
|
3
|
+
from typing import Mapping as _Mapping
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def strip_nulls(d: Any):
|
|
7
|
+
"""Remove null values from a dict"""
|
|
8
|
+
if isinstance(d, dict):
|
|
9
|
+
return {k: strip_nulls(v) for k, v in d.items() if v is not None}
|
|
10
|
+
else:
|
|
11
|
+
return d
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def recursive_update(
|
|
15
|
+
d: Optional[dict[str, Any]],
|
|
16
|
+
u: Optional[_Mapping[str, Any]],
|
|
17
|
+
stop_keys: list[AnyStr] = [],
|
|
18
|
+
allow_recursion: bool = True,
|
|
19
|
+
) -> dict[str, Any]:
|
|
20
|
+
"Recursively update a dict with another value"
|
|
21
|
+
if d is None:
|
|
22
|
+
return cast(dict, u or {})
|
|
23
|
+
|
|
24
|
+
if u is None:
|
|
25
|
+
return d
|
|
26
|
+
|
|
27
|
+
for k, v in u.items():
|
|
28
|
+
if isinstance(v, Mapping) and allow_recursion:
|
|
29
|
+
d[k] = recursive_update(d.get(k, {}), v, stop_keys=stop_keys, allow_recursion=k not in stop_keys)
|
|
30
|
+
else:
|
|
31
|
+
d[k] = v
|
|
32
|
+
|
|
33
|
+
return d
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_recursive_delta(
|
|
37
|
+
d1: Optional[_Mapping[str, Any]],
|
|
38
|
+
d2: Optional[_Mapping[str, Any]],
|
|
39
|
+
stop_keys: list[AnyStr] = [],
|
|
40
|
+
allow_recursion: bool = True,
|
|
41
|
+
) -> Optional[dict[str, Any]]:
|
|
42
|
+
"Get the recursive difference between two objects"
|
|
43
|
+
if d1 is None:
|
|
44
|
+
return cast(dict, d2)
|
|
45
|
+
|
|
46
|
+
if d2 is None:
|
|
47
|
+
return cast(dict, d1)
|
|
48
|
+
|
|
49
|
+
out = {}
|
|
50
|
+
for k1, v1 in d1.items():
|
|
51
|
+
if isinstance(v1, Mapping) and allow_recursion:
|
|
52
|
+
internal = get_recursive_delta(
|
|
53
|
+
v1,
|
|
54
|
+
d2.get(k1, {}),
|
|
55
|
+
stop_keys=stop_keys,
|
|
56
|
+
allow_recursion=k1 not in stop_keys,
|
|
57
|
+
)
|
|
58
|
+
if internal:
|
|
59
|
+
out[k1] = internal
|
|
60
|
+
else:
|
|
61
|
+
if k1 in d2:
|
|
62
|
+
v2 = d2[k1]
|
|
63
|
+
if v1 != v2:
|
|
64
|
+
out[k1] = v2
|
|
65
|
+
|
|
66
|
+
for k2, v2 in d2.items():
|
|
67
|
+
if k2 not in d1:
|
|
68
|
+
out[k2] = v2
|
|
69
|
+
|
|
70
|
+
return out
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def flatten(data: _Mapping, parent_key: Optional[str] = None) -> dict[str, Any]:
|
|
74
|
+
"Flatten a nested dict"
|
|
75
|
+
items: list[tuple[str, Any]] = []
|
|
76
|
+
for k, v in data.items():
|
|
77
|
+
cur_key = f"{parent_key}.{k}" if parent_key is not None else k
|
|
78
|
+
if isinstance(v, dict):
|
|
79
|
+
items.extend(flatten(v, cur_key).items())
|
|
80
|
+
else:
|
|
81
|
+
items.append((cur_key, v))
|
|
82
|
+
|
|
83
|
+
return dict(items)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def unflatten(data: _Mapping) -> _Mapping:
|
|
87
|
+
"Unflatted a nested dict"
|
|
88
|
+
out: dict[str, Any] = dict()
|
|
89
|
+
for k, v in data.items():
|
|
90
|
+
parts = k.split(".")
|
|
91
|
+
d = out
|
|
92
|
+
for p in parts[:-1]:
|
|
93
|
+
if p not in d:
|
|
94
|
+
d[p] = dict()
|
|
95
|
+
d = d[p]
|
|
96
|
+
d[parts[-1]] = v
|
|
97
|
+
return out
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def prune(data: _Mapping, keys: list[str], parent_key: Optional[str] = None) -> dict[str, Any]:
|
|
101
|
+
"Remove all keys in the given list from the dict if they exist"
|
|
102
|
+
pruned_items: list[tuple[str, Any]] = []
|
|
103
|
+
|
|
104
|
+
for key, val in data.items():
|
|
105
|
+
cur_key = f"{parent_key}.{key}" if parent_key else key
|
|
106
|
+
|
|
107
|
+
if isinstance(val, dict):
|
|
108
|
+
child_keys = [_key for _key in keys if _key.startswith(cur_key)]
|
|
109
|
+
|
|
110
|
+
if len(child_keys) > 0:
|
|
111
|
+
pruned_items.append((key, prune(val, child_keys, cur_key)))
|
|
112
|
+
elif isinstance(val, list):
|
|
113
|
+
if cur_key not in keys and not any(_key.startswith(cur_key) for _key in keys):
|
|
114
|
+
continue
|
|
115
|
+
|
|
116
|
+
list_result = []
|
|
117
|
+
for entry in val:
|
|
118
|
+
if isinstance(val, dict):
|
|
119
|
+
child_keys = [_key for _key in keys if _key.startswith(cur_key)]
|
|
120
|
+
|
|
121
|
+
if len(child_keys) > 0:
|
|
122
|
+
pruned_items.append((key, prune(val, child_keys, cur_key)))
|
|
123
|
+
else:
|
|
124
|
+
list_result.append(entry)
|
|
125
|
+
|
|
126
|
+
pruned_items.append((key, list_result))
|
|
127
|
+
elif cur_key in keys:
|
|
128
|
+
pruned_items.append((key, val))
|
|
129
|
+
|
|
130
|
+
return {k: v for k, v in pruned_items}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
from inspect import getmembers, isfunction
|
|
2
|
+
from sys import exc_info
|
|
3
|
+
from traceback import format_tb
|
|
4
|
+
from typing import Optional, Self
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ClueException(Exception):
|
|
8
|
+
"""Wrapper for all exceptions thrown in Clue' code"""
|
|
9
|
+
|
|
10
|
+
message: str
|
|
11
|
+
cause: Exception | None
|
|
12
|
+
status_code: int | None
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self: Self,
|
|
16
|
+
message: str = "Something went wrong",
|
|
17
|
+
cause: Exception | None = None,
|
|
18
|
+
status_code: int | None = None,
|
|
19
|
+
) -> None:
|
|
20
|
+
super().__init__(message)
|
|
21
|
+
self.message = message
|
|
22
|
+
self.cause = cause
|
|
23
|
+
self.status_code = status_code
|
|
24
|
+
|
|
25
|
+
def __repr__(self: Self) -> str:
|
|
26
|
+
"""String reproduction of the Clue exception. Pass the message on"""
|
|
27
|
+
return self.message
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class InvalidClassification(ClueException):
|
|
31
|
+
"""Exception for Invalid Classification"""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class InvalidDefinition(ClueException):
|
|
35
|
+
"""Exception for Invalid Definition"""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class InvalidRangeException(ClueException):
|
|
39
|
+
"""Exception for Invalid Range"""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class NonRecoverableError(ClueException):
|
|
43
|
+
"""Exception for an unrecoverable error"""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class RecoverableError(ClueException):
|
|
47
|
+
"""Exception for a recoverable error"""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ConfigException(ClueException):
|
|
51
|
+
"""Exception thrown due to invalid configuration"""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ResourceExists(ClueException):
|
|
55
|
+
"""Exception thrown due to a pre-existing resource"""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class VersionConflict(ClueException):
|
|
59
|
+
"""Exception thrown due to a version conflict"""
|
|
60
|
+
|
|
61
|
+
def __init__(self: Self, message: str = "Something went wrong", cause: Optional[Exception] = None) -> None:
|
|
62
|
+
ClueException.__init__(self, message, cause)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class ClueTypeError(ClueException, TypeError):
|
|
66
|
+
"""TypeError child specifically for exceptions thrown by us"""
|
|
67
|
+
|
|
68
|
+
def __init__(
|
|
69
|
+
self: Self,
|
|
70
|
+
message: str = "Something went wrong",
|
|
71
|
+
cause: Optional[Exception] = None,
|
|
72
|
+
status_code: int | None = None,
|
|
73
|
+
) -> None:
|
|
74
|
+
ClueException.__init__(
|
|
75
|
+
self, message, cause if cause is not None else TypeError(message), status_code=status_code
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class ClueAttributeError(ClueException, AttributeError):
|
|
80
|
+
"""AttributeError child specifically for exceptions thrown by us"""
|
|
81
|
+
|
|
82
|
+
def __init__(
|
|
83
|
+
self: Self,
|
|
84
|
+
message: str = "Something went wrong",
|
|
85
|
+
cause: Optional[Exception] = None,
|
|
86
|
+
status_code: int | None = None,
|
|
87
|
+
) -> None:
|
|
88
|
+
ClueException.__init__(
|
|
89
|
+
self, message, cause if cause is not None else AttributeError(message), status_code=status_code
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class ClueValueError(ClueException, ValueError):
|
|
94
|
+
"""ValueError child specifically for exceptions thrown by us"""
|
|
95
|
+
|
|
96
|
+
def __init__(
|
|
97
|
+
self: Self,
|
|
98
|
+
message: str = "Something went wrong",
|
|
99
|
+
cause: Optional[Exception] = None,
|
|
100
|
+
status_code: int | None = None,
|
|
101
|
+
) -> None:
|
|
102
|
+
ClueException.__init__(
|
|
103
|
+
self, message, cause if cause is not None else ValueError(message), status_code=status_code
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class ClueNotImplementedError(ClueException, NotImplementedError):
|
|
108
|
+
"""NotImplementedError child specifically for exceptions thrown by us"""
|
|
109
|
+
|
|
110
|
+
def __init__(self: Self, message: str = "Something went wrong", cause: Optional[Exception] = None) -> None:
|
|
111
|
+
ClueException.__init__(self, message, cause if cause is not None else NotImplementedError(message))
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class ClueKeyError(ClueException, KeyError):
|
|
115
|
+
"""KeyError child specifically for exceptions thrown by us"""
|
|
116
|
+
|
|
117
|
+
def __init__(self: Self, message: str = "Something went wrong", cause: Optional[Exception] = None) -> None:
|
|
118
|
+
ClueException.__init__(self, message, cause if cause is not None else KeyError(message))
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class ClueRuntimeError(ClueException, RuntimeError):
|
|
122
|
+
"""RuntimeError child specifically for exceptions thrown by us"""
|
|
123
|
+
|
|
124
|
+
def __init__(self: Self, message: str = "Something went wrong", cause: Optional[Exception] = None) -> None:
|
|
125
|
+
ClueException.__init__(self, message, cause if cause is not None else RuntimeError(message))
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class NotFoundException(ClueException):
|
|
129
|
+
"""Exception thrown when a resource cannot be found"""
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class AccessDeniedException(ClueException):
|
|
133
|
+
"""Exception thrown when a resource cannot be accessed by a user"""
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class InvalidDataException(ClueException):
|
|
137
|
+
"""Exception thrown when user-provided data is invalid"""
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class AuthenticationException(ClueException):
|
|
141
|
+
"""Exception thrown when a user cannot be authenticated"""
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class TimeoutException(ClueException):
|
|
145
|
+
"""Exception for Timeout"""
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class UnprocessableException(ClueException):
|
|
149
|
+
"""Exception for Unprocessable"""
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class Chain(object):
|
|
153
|
+
"""This class can be used as a decorator to override the type of exceptions returned by a function"""
|
|
154
|
+
|
|
155
|
+
def __init__(self: Self, exception: type[Exception]):
|
|
156
|
+
self.exception = exception
|
|
157
|
+
|
|
158
|
+
def __call__(self, original):
|
|
159
|
+
"""Execute a function and wrap any resulting exceptions"""
|
|
160
|
+
|
|
161
|
+
def wrapper(*args, **kwargs):
|
|
162
|
+
try:
|
|
163
|
+
return original(*args, **kwargs)
|
|
164
|
+
except Exception as e:
|
|
165
|
+
wrapped = self.exception(str(e), e)
|
|
166
|
+
raise wrapped.with_traceback(exc_info()[2])
|
|
167
|
+
|
|
168
|
+
wrapper.__name__ = original.__name__
|
|
169
|
+
wrapper.__doc__ = original.__doc__
|
|
170
|
+
wrapper.__dict__.update(original.__dict__)
|
|
171
|
+
|
|
172
|
+
return wrapper
|
|
173
|
+
|
|
174
|
+
def execute(self, func, *args, **kwargs):
|
|
175
|
+
"""Execute a function and wrap any resulting exceptions"""
|
|
176
|
+
try:
|
|
177
|
+
return func(*args, **kwargs)
|
|
178
|
+
except Exception as e:
|
|
179
|
+
wrapped = self.exception(str(e), e)
|
|
180
|
+
raise wrapped.with_traceback(exc_info()[2])
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class ChainAll:
|
|
184
|
+
"""This class can be used as a decorator to override the type of exceptions returned by every method of a class"""
|
|
185
|
+
|
|
186
|
+
def __init__(self: Self, exception: type[Exception]):
|
|
187
|
+
self.exception = Chain(exception)
|
|
188
|
+
|
|
189
|
+
def __call__(self, cls):
|
|
190
|
+
"""We can use an instance of this class as a decorator."""
|
|
191
|
+
for method in getmembers(cls, predicate=isfunction):
|
|
192
|
+
setattr(cls, method[0], self.exception(method[1]))
|
|
193
|
+
|
|
194
|
+
return cls
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def get_stacktrace_info(ex: Exception) -> str:
|
|
198
|
+
"""Get and format traceback information from a given exception"""
|
|
199
|
+
return "".join(format_tb(exc_info()[2]) + [": ".join((ex.__class__.__name__, str(ex)))])
|
clue/common/forge.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# This file contains the loaders for the different components of the system
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from string import Template
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from flask_caching import Cache
|
|
11
|
+
|
|
12
|
+
from clue.common.dict_utils import recursive_update
|
|
13
|
+
from clue.common.logging.format import BRL_DATE_FORMAT, BRL_LOG_FORMAT
|
|
14
|
+
from clue.common.str_utils import default_string_value
|
|
15
|
+
|
|
16
|
+
APP_NAME: str = default_string_value(env_name="APP_NAME", default="clue") # type: ignore[assignment]
|
|
17
|
+
APP_PREFIX = os.environ.get("APP_PREFIX", "brl")
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from clue.common.classification import Classification
|
|
21
|
+
|
|
22
|
+
cache = Cache(config={"CACHE_TYPE": "SimpleCache"})
|
|
23
|
+
|
|
24
|
+
classification_engines: dict[Path, Classification] = {}
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(f"{APP_NAME}.common.forge")
|
|
27
|
+
logger.setLevel(logging.INFO)
|
|
28
|
+
console = logging.StreamHandler()
|
|
29
|
+
console.setLevel(logging.INFO)
|
|
30
|
+
console.setFormatter(logging.Formatter(BRL_LOG_FORMAT, BRL_DATE_FORMAT))
|
|
31
|
+
logger.addHandler(console)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def __get_yml_path(yml_config: str | None = None) -> Path | None: # noqa: C901
|
|
35
|
+
if yml_config is not None:
|
|
36
|
+
return Path(yml_config)
|
|
37
|
+
|
|
38
|
+
if (_yml_path := Path(f"/etc/{APP_NAME}/classification.yml")).exists():
|
|
39
|
+
return _yml_path
|
|
40
|
+
|
|
41
|
+
if (_yml_path := Path(f"/etc/{APP_NAME}/conf/classification.yml")).exists():
|
|
42
|
+
return _yml_path
|
|
43
|
+
|
|
44
|
+
if os.getenv("AZURE_TEST_CONFIG", None) is not None:
|
|
45
|
+
import re
|
|
46
|
+
|
|
47
|
+
logger.info("Azure build environment detected, checking additional classification path")
|
|
48
|
+
|
|
49
|
+
work_dir_parent = Path("/__w")
|
|
50
|
+
work_dir: Path | None = None
|
|
51
|
+
for sub_path in work_dir_parent.iterdir():
|
|
52
|
+
if not sub_path.is_dir():
|
|
53
|
+
continue
|
|
54
|
+
|
|
55
|
+
logger.info("Testing sub path %s", sub_path)
|
|
56
|
+
|
|
57
|
+
if re.match(r"\d+", sub_path.name):
|
|
58
|
+
work_dir = work_dir_parent / sub_path
|
|
59
|
+
|
|
60
|
+
if work_dir is not None:
|
|
61
|
+
logger.info("Subpath %s exists, checking for test path", work_dir)
|
|
62
|
+
test_classification_path = work_dir / "s" / "test" / "config" / "classification.yml"
|
|
63
|
+
|
|
64
|
+
if test_classification_path.exists():
|
|
65
|
+
logger.info("Path %s detected", test_classification_path)
|
|
66
|
+
return test_classification_path
|
|
67
|
+
|
|
68
|
+
logger.error("No classification path found at path %s", test_classification_path)
|
|
69
|
+
logger.info(
|
|
70
|
+
"Available files:\n%s", "\n".join(sorted(str(path) for path in (work_dir / "s").glob("**/*")))
|
|
71
|
+
)
|
|
72
|
+
work_dir = None
|
|
73
|
+
|
|
74
|
+
custom_path = os.environ.get("CLUE_CONF_FOLDER", None)
|
|
75
|
+
if custom_path is None:
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
if (_yml_path := (Path(custom_path) / "classification.yml")).exists():
|
|
79
|
+
return _yml_path
|
|
80
|
+
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def get_classification(yml_config: str | None = None): # noqa: C901
|
|
85
|
+
"""Creates and registers a Classification engine.
|
|
86
|
+
|
|
87
|
+
If a yaml config is not provided, it will search in /etc/clue and /etc/clue/conf for a classification.yml
|
|
88
|
+
file instead.
|
|
89
|
+
|
|
90
|
+
Arguments:
|
|
91
|
+
yml_config: An optional yaml config to load.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
The created Classification engine.
|
|
95
|
+
"""
|
|
96
|
+
import yaml
|
|
97
|
+
|
|
98
|
+
from clue.common.classification import Classification, InvalidDefinition
|
|
99
|
+
|
|
100
|
+
_yml_path = __get_yml_path(yml_config)
|
|
101
|
+
|
|
102
|
+
if _yml_path:
|
|
103
|
+
logger.debug("Classification file found at %s", _yml_path)
|
|
104
|
+
else:
|
|
105
|
+
logger.warning("Missing classification.yml file!")
|
|
106
|
+
|
|
107
|
+
if _yml_path in classification_engines:
|
|
108
|
+
return classification_engines[_yml_path]
|
|
109
|
+
|
|
110
|
+
classification_definition = {}
|
|
111
|
+
default_file = Path(__file__).parent / "classification.yml"
|
|
112
|
+
if default_file.exists():
|
|
113
|
+
with default_file.open() as default_fh:
|
|
114
|
+
default_yml_data = yaml.safe_load(default_fh.read())
|
|
115
|
+
if default_yml_data:
|
|
116
|
+
classification_definition.update(default_yml_data)
|
|
117
|
+
|
|
118
|
+
# Load modifiers from the yaml config
|
|
119
|
+
if _yml_path is not None and _yml_path.exists():
|
|
120
|
+
with _yml_path.open() as yml_fh:
|
|
121
|
+
yml_data = yaml.safe_load(yml_fh.read())
|
|
122
|
+
if yml_data:
|
|
123
|
+
classification_definition = recursive_update(classification_definition, yml_data)
|
|
124
|
+
|
|
125
|
+
if not classification_definition:
|
|
126
|
+
raise InvalidDefinition("Could not find any classification definition to load.")
|
|
127
|
+
|
|
128
|
+
classification_engine = Classification(classification_definition)
|
|
129
|
+
|
|
130
|
+
if _yml_path:
|
|
131
|
+
classification_engines[_yml_path] = classification_engine
|
|
132
|
+
|
|
133
|
+
return classification_engine
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def env_substitute(buffer):
|
|
137
|
+
"""Replace environment variables in the buffer with their value.
|
|
138
|
+
|
|
139
|
+
Use the built in template expansion tool that expands environment variable style strings ${}
|
|
140
|
+
We set the idpattern to none so that $abc doesn't get replaced but ${abc} does.
|
|
141
|
+
|
|
142
|
+
Case insensitive.
|
|
143
|
+
Variables that are found in the buffer, but are not defined as environment variables are ignored.
|
|
144
|
+
"""
|
|
145
|
+
return Template(buffer).safe_substitute(os.environ, idpattern=None, bracedidpattern="(?a:[_a-z][_a-z0-9]*)")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def get_metrics_sink(redis=None):
|
|
149
|
+
"""Creates a clue_metrics CommsQueue on redis for metrics."""
|
|
150
|
+
from clue.remote.datatypes.queues.comms import CommsQueue
|
|
151
|
+
|
|
152
|
+
return CommsQueue("clue_metrics", host=redis)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def try_parse_json(json_str: str, return_raw: bool = False) -> dict[str, Any] | str | None:
|
|
6
|
+
"Try and parse JSON, optionally returning the raw string if json loading fails."
|
|
7
|
+
try:
|
|
8
|
+
return json.loads(json_str)
|
|
9
|
+
except json.JSONDecodeError:
|
|
10
|
+
return json_str if return_raw else None
|