howler-api 3.0.0.dev374__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.

Potentially problematic release.


This version of howler-api might be problematic. Click here for more details.

Files changed (198) hide show
  1. howler/__init__.py +0 -0
  2. howler/actions/__init__.py +168 -0
  3. howler/actions/add_label.py +111 -0
  4. howler/actions/add_to_bundle.py +159 -0
  5. howler/actions/change_field.py +76 -0
  6. howler/actions/demote.py +160 -0
  7. howler/actions/example_plugin.py +104 -0
  8. howler/actions/prioritization.py +93 -0
  9. howler/actions/promote.py +147 -0
  10. howler/actions/remove_from_bundle.py +133 -0
  11. howler/actions/remove_label.py +111 -0
  12. howler/actions/transition.py +200 -0
  13. howler/api/__init__.py +249 -0
  14. howler/api/base.py +88 -0
  15. howler/api/socket.py +114 -0
  16. howler/api/v1/__init__.py +97 -0
  17. howler/api/v1/action.py +372 -0
  18. howler/api/v1/analytic.py +748 -0
  19. howler/api/v1/auth.py +382 -0
  20. howler/api/v1/clue.py +99 -0
  21. howler/api/v1/configs.py +58 -0
  22. howler/api/v1/dossier.py +222 -0
  23. howler/api/v1/help.py +28 -0
  24. howler/api/v1/hit.py +1181 -0
  25. howler/api/v1/notebook.py +82 -0
  26. howler/api/v1/overview.py +191 -0
  27. howler/api/v1/search.py +788 -0
  28. howler/api/v1/template.py +206 -0
  29. howler/api/v1/tool.py +183 -0
  30. howler/api/v1/user.py +416 -0
  31. howler/api/v1/utils/__init__.py +0 -0
  32. howler/api/v1/utils/etag.py +84 -0
  33. howler/api/v1/view.py +288 -0
  34. howler/app.py +235 -0
  35. howler/common/README.md +125 -0
  36. howler/common/__init__.py +0 -0
  37. howler/common/classification.py +979 -0
  38. howler/common/classification.yml +107 -0
  39. howler/common/exceptions.py +167 -0
  40. howler/common/loader.py +154 -0
  41. howler/common/logging/__init__.py +241 -0
  42. howler/common/logging/audit.py +138 -0
  43. howler/common/logging/format.py +38 -0
  44. howler/common/net.py +79 -0
  45. howler/common/net_static.py +1494 -0
  46. howler/common/random_user.py +316 -0
  47. howler/common/swagger.py +117 -0
  48. howler/config.py +64 -0
  49. howler/cronjobs/__init__.py +29 -0
  50. howler/cronjobs/retention.py +61 -0
  51. howler/cronjobs/rules.py +274 -0
  52. howler/cronjobs/view_cleanup.py +88 -0
  53. howler/datastore/README.md +112 -0
  54. howler/datastore/__init__.py +0 -0
  55. howler/datastore/bulk.py +72 -0
  56. howler/datastore/collection.py +2342 -0
  57. howler/datastore/constants.py +119 -0
  58. howler/datastore/exceptions.py +41 -0
  59. howler/datastore/howler_store.py +105 -0
  60. howler/datastore/migrations/fix_process.py +41 -0
  61. howler/datastore/operations.py +130 -0
  62. howler/datastore/schemas.py +90 -0
  63. howler/datastore/store.py +231 -0
  64. howler/datastore/support/__init__.py +0 -0
  65. howler/datastore/support/build.py +215 -0
  66. howler/datastore/support/schemas.py +90 -0
  67. howler/datastore/types.py +22 -0
  68. howler/error.py +91 -0
  69. howler/external/__init__.py +0 -0
  70. howler/external/generate_mitre.py +96 -0
  71. howler/external/generate_sigma_rules.py +31 -0
  72. howler/external/generate_tlds.py +47 -0
  73. howler/external/reindex_data.py +66 -0
  74. howler/external/wipe_databases.py +58 -0
  75. howler/gunicorn_config.py +25 -0
  76. howler/healthz.py +47 -0
  77. howler/helper/__init__.py +0 -0
  78. howler/helper/azure.py +50 -0
  79. howler/helper/discover.py +59 -0
  80. howler/helper/hit.py +236 -0
  81. howler/helper/oauth.py +247 -0
  82. howler/helper/search.py +92 -0
  83. howler/helper/workflow.py +110 -0
  84. howler/helper/ws.py +378 -0
  85. howler/odm/README.md +102 -0
  86. howler/odm/__init__.py +1 -0
  87. howler/odm/base.py +1543 -0
  88. howler/odm/charter.txt +146 -0
  89. howler/odm/helper.py +416 -0
  90. howler/odm/howler_enum.py +25 -0
  91. howler/odm/models/__init__.py +0 -0
  92. howler/odm/models/action.py +33 -0
  93. howler/odm/models/analytic.py +90 -0
  94. howler/odm/models/assemblyline.py +48 -0
  95. howler/odm/models/aws.py +23 -0
  96. howler/odm/models/azure.py +16 -0
  97. howler/odm/models/cbs.py +44 -0
  98. howler/odm/models/config.py +558 -0
  99. howler/odm/models/dossier.py +33 -0
  100. howler/odm/models/ecs/__init__.py +0 -0
  101. howler/odm/models/ecs/agent.py +17 -0
  102. howler/odm/models/ecs/autonomous_system.py +16 -0
  103. howler/odm/models/ecs/client.py +149 -0
  104. howler/odm/models/ecs/cloud.py +141 -0
  105. howler/odm/models/ecs/code_signature.py +27 -0
  106. howler/odm/models/ecs/container.py +32 -0
  107. howler/odm/models/ecs/dns.py +62 -0
  108. howler/odm/models/ecs/egress.py +10 -0
  109. howler/odm/models/ecs/elf.py +74 -0
  110. howler/odm/models/ecs/email.py +122 -0
  111. howler/odm/models/ecs/error.py +14 -0
  112. howler/odm/models/ecs/event.py +140 -0
  113. howler/odm/models/ecs/faas.py +24 -0
  114. howler/odm/models/ecs/file.py +84 -0
  115. howler/odm/models/ecs/geo.py +30 -0
  116. howler/odm/models/ecs/group.py +18 -0
  117. howler/odm/models/ecs/hash.py +16 -0
  118. howler/odm/models/ecs/host.py +17 -0
  119. howler/odm/models/ecs/http.py +37 -0
  120. howler/odm/models/ecs/ingress.py +12 -0
  121. howler/odm/models/ecs/interface.py +21 -0
  122. howler/odm/models/ecs/network.py +30 -0
  123. howler/odm/models/ecs/observer.py +45 -0
  124. howler/odm/models/ecs/organization.py +12 -0
  125. howler/odm/models/ecs/os.py +21 -0
  126. howler/odm/models/ecs/pe.py +17 -0
  127. howler/odm/models/ecs/process.py +216 -0
  128. howler/odm/models/ecs/registry.py +26 -0
  129. howler/odm/models/ecs/related.py +45 -0
  130. howler/odm/models/ecs/rule.py +51 -0
  131. howler/odm/models/ecs/server.py +24 -0
  132. howler/odm/models/ecs/threat.py +247 -0
  133. howler/odm/models/ecs/tls.py +58 -0
  134. howler/odm/models/ecs/url.py +51 -0
  135. howler/odm/models/ecs/user.py +57 -0
  136. howler/odm/models/ecs/user_agent.py +20 -0
  137. howler/odm/models/ecs/vulnerability.py +41 -0
  138. howler/odm/models/gcp.py +16 -0
  139. howler/odm/models/hit.py +356 -0
  140. howler/odm/models/howler_data.py +328 -0
  141. howler/odm/models/lead.py +24 -0
  142. howler/odm/models/localized_label.py +13 -0
  143. howler/odm/models/overview.py +16 -0
  144. howler/odm/models/pivot.py +40 -0
  145. howler/odm/models/template.py +24 -0
  146. howler/odm/models/user.py +83 -0
  147. howler/odm/models/view.py +34 -0
  148. howler/odm/random_data.py +888 -0
  149. howler/odm/randomizer.py +609 -0
  150. howler/patched.py +5 -0
  151. howler/plugins/__init__.py +25 -0
  152. howler/plugins/config.py +123 -0
  153. howler/remote/__init__.py +0 -0
  154. howler/remote/datatypes/README.md +355 -0
  155. howler/remote/datatypes/__init__.py +98 -0
  156. howler/remote/datatypes/counters.py +63 -0
  157. howler/remote/datatypes/events.py +66 -0
  158. howler/remote/datatypes/hash.py +206 -0
  159. howler/remote/datatypes/lock.py +42 -0
  160. howler/remote/datatypes/queues/__init__.py +0 -0
  161. howler/remote/datatypes/queues/comms.py +59 -0
  162. howler/remote/datatypes/queues/multi.py +32 -0
  163. howler/remote/datatypes/queues/named.py +93 -0
  164. howler/remote/datatypes/queues/priority.py +215 -0
  165. howler/remote/datatypes/set.py +118 -0
  166. howler/remote/datatypes/user_quota_tracker.py +54 -0
  167. howler/security/__init__.py +253 -0
  168. howler/security/socket.py +108 -0
  169. howler/security/utils.py +185 -0
  170. howler/services/__init__.py +0 -0
  171. howler/services/action_service.py +111 -0
  172. howler/services/analytic_service.py +128 -0
  173. howler/services/auth_service.py +323 -0
  174. howler/services/config_service.py +128 -0
  175. howler/services/dossier_service.py +252 -0
  176. howler/services/event_service.py +93 -0
  177. howler/services/hit_service.py +893 -0
  178. howler/services/jwt_service.py +158 -0
  179. howler/services/lucene_service.py +286 -0
  180. howler/services/notebook_service.py +119 -0
  181. howler/services/overview_service.py +44 -0
  182. howler/services/template_service.py +45 -0
  183. howler/services/user_service.py +331 -0
  184. howler/utils/__init__.py +0 -0
  185. howler/utils/annotations.py +28 -0
  186. howler/utils/chunk.py +38 -0
  187. howler/utils/dict_utils.py +200 -0
  188. howler/utils/isotime.py +17 -0
  189. howler/utils/list_utils.py +11 -0
  190. howler/utils/lucene.py +77 -0
  191. howler/utils/path.py +27 -0
  192. howler/utils/socket_utils.py +61 -0
  193. howler/utils/str_utils.py +256 -0
  194. howler/utils/uid.py +47 -0
  195. howler_api-3.0.0.dev374.dist-info/METADATA +71 -0
  196. howler_api-3.0.0.dev374.dist-info/RECORD +198 -0
  197. howler_api-3.0.0.dev374.dist-info/WHEEL +4 -0
  198. howler_api-3.0.0.dev374.dist-info/entry_points.txt +8 -0
howler/utils/lucene.py ADDED
@@ -0,0 +1,77 @@
1
+ import ipaddress
2
+ import math
3
+ import re
4
+ import sys
5
+ from datetime import datetime
6
+ from typing import Any, Callable, Literal, Optional
7
+
8
+ from howler.api import Union
9
+
10
+
11
+ def try_parse_date(date: str) -> Optional[datetime]:
12
+ "Try and parse an ISO-formatted string into a date. Returns None if string is invalid."
13
+ try:
14
+ # Check if the value is a ISO-formatted date
15
+ if sys.version_info.major < 11:
16
+ return datetime.strptime(date, "%Y-%m-%dT%H:%M:%S.%f%z")
17
+ else:
18
+ return datetime.fromisoformat(date)
19
+ except (ValueError, TypeError):
20
+ return None
21
+
22
+
23
+ def try_parse_number(number: Union[str, int, float]) -> Optional[Union[int, float]]:
24
+ "Try and parse a number string into an integer or float type, or infinity. Returns None if string is invalid."
25
+ if isinstance(number, (int, float)):
26
+ return number
27
+
28
+ if number.lower() == "infinity":
29
+ return math.inf
30
+
31
+ try:
32
+ # Check if the value is an integer
33
+ return int(number)
34
+ except ValueError:
35
+ try:
36
+ # Check if the value is a float
37
+ return float(number)
38
+ except ValueError:
39
+ return None
40
+
41
+
42
+ def try_parse_ip(ip: str) -> Optional[Union[ipaddress.IPv4Address, ipaddress.IPv6Address]]:
43
+ "Try and parse an ipv4/ipv6 string into a date. Returns None if string is invalid."
44
+ try:
45
+ # Check if the value is an IP address
46
+ return ipaddress.ip_address(ip)
47
+ except ValueError:
48
+ return None
49
+
50
+
51
+ def coerce(value: Union[list[str], str], fn: Callable[[str], Any]) -> Any:
52
+ """Coerce a value of list of values using a given function or class.
53
+
54
+ Will return an empty list if all results are None.
55
+ """
56
+ if isinstance(value, list):
57
+ result: list = []
58
+ for _value in value:
59
+ if fn_result := fn(_value):
60
+ result.append(fn_result)
61
+ return result
62
+ else:
63
+ return fn(value)
64
+
65
+
66
+ def normalize_phrase(value: str, type: Union[Literal["phrase"], Literal["word"]]) -> list[str]:
67
+ "Normalize a phrase or word for validation"
68
+ if re.match(r"^[a-zA-Z0-9]+$", value):
69
+ # probably an ID, no normalization
70
+ return [value, value.lower()]
71
+
72
+ if type == "word":
73
+ value = re.sub(r"[^a-z0-9.,@_:/;()\-]", "", value.lower())
74
+ else:
75
+ value = re.sub(r"[^a-z0-9.,@_:/;()\- ]", "", value, flags=re.IGNORECASE)
76
+
77
+ return [value]
howler/utils/path.py ADDED
@@ -0,0 +1,27 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import sys
5
+ from typing import Optional
6
+
7
+
8
+ def modulepath(modulename: str) -> str:
9
+ "Get the path to a given mdule"
10
+ m = sys.modules[modulename]
11
+ f = getattr(m, "__file__", None)
12
+
13
+ if not f:
14
+ return os.path.abspath(os.getcwd())
15
+
16
+ return os.path.dirname(os.path.abspath(f))
17
+
18
+
19
+ def splitpath(path: str, sep: Optional[str] = None) -> list:
20
+ """Split the path into a list of items"""
21
+ return list(filter(len, path.split(sep or os.path.sep)))
22
+
23
+
24
+ ASCII_NUMBERS = list(range(48, 58))
25
+ ASCII_UPPER_CASE_LETTERS = list(range(65, 91))
26
+ ASCII_LOWER_CASE_LETTERS = list(range(97, 123))
27
+ ASCII_OTHER = [45, 46, 92] # "-", ".", and "\"
@@ -0,0 +1,61 @@
1
+ from howler.common.logging import get_logger
2
+ from howler.datastore.operations import OdmHelper
3
+ from howler.odm.models.hit import Hit
4
+ from howler.services import event_service, hit_service
5
+
6
+ logger = get_logger(__file__)
7
+
8
+ hit_helper = OdmHelper(Hit)
9
+
10
+
11
+ def check_action(
12
+ id: str, action: str, broadcast: bool, outstanding_actions: list[tuple[str, str, bool]] = [], **kwargs
13
+ ) -> list[tuple[str, str, bool]]:
14
+ """Emit an event based on the specified action for use by websocket clients
15
+
16
+ Args:
17
+ id (str): The id of the item the action is being run on
18
+ action (str): The action we are running
19
+ broadcast (bool): Whether to advertise this action to other users
20
+ outstanding_actions (list[tuple[str, str, bool]], optional): A list of actions that must be run after the
21
+ user is disconnected. Defaults to [].
22
+
23
+ Returns:
24
+ list[tuple[str, str, bool]]: The new list of outstanding actions
25
+ """
26
+ if broadcast:
27
+ event_service.emit(
28
+ "broadcast",
29
+ {"id": id, "action": action, "username": kwargs["username"]},
30
+ )
31
+
32
+ if action == "typing":
33
+ outstanding_actions.append((id, "stop_typing", True))
34
+ elif action == "stop_typing":
35
+ outstanding_actions = [a for a in outstanding_actions if a[1] != "stop_typing"]
36
+
37
+ elif action == "viewing":
38
+ if hit_service.exists(id):
39
+ outstanding_actions.append((id, "stop_viewing", False))
40
+ hit_service.update_hit(
41
+ id,
42
+ [
43
+ hit_helper.list_add(
44
+ "howler.viewers",
45
+ kwargs["username"],
46
+ silent=True,
47
+ if_missing=True,
48
+ )
49
+ ],
50
+ user=kwargs["username"],
51
+ )
52
+ elif action == "stop_viewing":
53
+ if hit_service.exists(id):
54
+ hit_service.update_hit(
55
+ id,
56
+ [hit_helper.list_remove("howler.viewers", kwargs["username"], silent=True)],
57
+ user=kwargs["username"],
58
+ )
59
+ outstanding_actions = [a for a in outstanding_actions if a[1] != "stop_viewing"]
60
+
61
+ return outstanding_actions
@@ -0,0 +1,256 @@
1
+ import os
2
+ import re
3
+ from copy import copy
4
+ from typing import Any, Optional, Union
5
+
6
+ import chardet
7
+
8
+ from howler.common.exceptions import HowlerAttributeError, HowlerTypeError
9
+
10
+
11
+ def remove_bidir_unicode_controls(in_str):
12
+ """Remove UBA characters"""
13
+ try:
14
+ no_controls_str = "".join(
15
+ c
16
+ for c in in_str
17
+ if c
18
+ not in [
19
+ "\u202e",
20
+ "\u202b",
21
+ "\u202d",
22
+ "\u202a",
23
+ "\u200e",
24
+ "\u200f",
25
+ ]
26
+ )
27
+ except Exception:
28
+ no_controls_str = in_str
29
+
30
+ return no_controls_str
31
+
32
+
33
+ def wrap_bidir_unicode_string(uni_str):
34
+ """Wraps str in a LRE (Left-to-Right Embed) unicode control.
35
+
36
+ Guarantees that str can be concatenated to other strings without
37
+ affecting their left-to-right direction
38
+ """
39
+ if len(uni_str) == 0 or isinstance(uni_str, bytes): # Not str, return it unchanged
40
+ return uni_str
41
+
42
+ re_obj = re.search(r"[\u202E\u202B\u202D\u202A\u200E\u200F]", uni_str)
43
+ if re_obj is None or len(re_obj.group()) == 0: # No unicode bidir controls found, return string unchanged
44
+ return uni_str
45
+
46
+ # Parse str for unclosed bidir blocks
47
+ count = 0
48
+ for letter in uni_str:
49
+ if letter in ["\u202a", "\u202b", "\u202d", "\u202e"]: # bidir block open?
50
+ count += 1
51
+ elif letter == "\u202c":
52
+ if count > 0:
53
+ count -= 1
54
+
55
+ # close all bidir blocks
56
+ if count > 0:
57
+ uni_str += "\u202c" * count
58
+
59
+ # Final wrapper (LTR block) to neutralize any Marks (u+200E and u+200F)
60
+ uni_str = "\u202a" + uni_str + "\u202c"
61
+
62
+ return uni_str
63
+
64
+
65
+ # According to wikipedia, RFC 3629 restricted UTF-8 to end at U+10FFFF.
66
+ # This removed the 6, 5 and (irritatingly) half of the 4 byte sequences.
67
+ #
68
+ # The start byte for 2-byte sequences should be a value between 0xc0 and
69
+ # 0xdf but the values 0xc0 and 0xc1 are invalid as they could only be
70
+ # the result of an overlong encoding of basic ASCII characters. There
71
+ # are similar restrictions on the valid values for 3 and 4-byte sequences.
72
+ _valid_utf8 = re.compile(
73
+ rb"""((?:
74
+ [\x09\x0a\x20-\x7e]| # 1-byte (ASCII excluding control chars).
75
+ [\xc2-\xdf][\x80-\xbf]| # 2-bytes (excluding overlong sequences).
76
+ [\xe0][\xa0-\xbf][\x80-\xbf]| # 3-bytes (excluding overlong sequences).
77
+
78
+ [\xe1-\xec][\x80-\xbf]{2}| # 3-bytes.
79
+ [\xed][\x80-\x9f][\x80-\xbf]| # 3-bytes (up to invalid code points).
80
+ [\xee-\xef][\x80-\xbf]{2}| # 3-bytes (after invalid code points).
81
+
82
+ [\xf0][\x90-\xbf][\x80-\xbf]{2}| # 4-bytes (excluding overlong sequences).
83
+ [\xf1-\xf3][\x80-\xbf]{3}| # 4-bytes.
84
+ [\xf4][\x80-\x8f][\x80-\xbf]{2} # 4-bytes (up to U+10FFFF).
85
+ )+)""",
86
+ re.VERBOSE,
87
+ )
88
+
89
+
90
+ def _escape(t, reversible=True):
91
+ if t[0] % 2:
92
+ return t[1].replace(b"\\", b"\\\\") if reversible else t[1]
93
+ else:
94
+ return b"".join((b"\\x%02x" % x) for x in t[1])
95
+
96
+
97
+ def dotdump(s: Union[bytes, str, list[int]]):
98
+ """Remove any non-ascii characters and replace them with periods
99
+
100
+ https://www.cs.cmu.edu/~pattis/15-1XX/common/handouts/ascii.html
101
+ """
102
+ if isinstance(s, str):
103
+ s = s.encode()
104
+
105
+ return "".join(["." if x < 32 or x > 126 else chr(x) for x in s])
106
+
107
+
108
+ def escape_str(s, reversible=True, force_str=False):
109
+ """Escape a string"""
110
+ if isinstance(s, bytes):
111
+ return escape_str_strict(s, reversible)
112
+ elif not isinstance(s, str):
113
+ if force_str:
114
+ return str(s)
115
+ return s
116
+
117
+ try:
118
+ return escape_str_strict(
119
+ s.encode("utf-16", "surrogatepass").decode("utf-16").encode("utf-8"),
120
+ reversible,
121
+ )
122
+ except Exception:
123
+ return escape_str_strict(s.encode("utf-8", errors="backslashreplace"), reversible)
124
+
125
+
126
+ # Returns a string (str) with only valid UTF-8 byte sequences.
127
+ def escape_str_strict(s: bytes, reversible: bool = True) -> str:
128
+ """Strictly escape a string"""
129
+ escaped = b"".join([_escape(t, reversible) for t in enumerate(_valid_utf8.split(s))])
130
+ return escaped.decode("utf-8")
131
+
132
+
133
+ def safe_str(s, force_str=False):
134
+ """Create a safe, escaped string"""
135
+ return escape_str(s, reversible=False, force_str=force_str)
136
+
137
+
138
+ def is_safe_str(s: str) -> bool:
139
+ """Check if a given string is safe"""
140
+ return escape_str(copy(s), reversible=False) == s
141
+
142
+
143
+ def translate_str(s: Union[str, bytes], min_confidence: float = 0.7) -> dict:
144
+ """Translate a string from an arbitrary encoding to a python str"""
145
+ if not isinstance(s, (str, bytes)):
146
+ raise HowlerTypeError(f"Expected str or bytes got {type(s)}")
147
+
148
+ if isinstance(s, str):
149
+ s = s.encode("utf-8")
150
+
151
+ try:
152
+ r: Any = chardet.detect(s)
153
+ except Exception:
154
+ r = {"confidence": 0.0, "encoding": None, "language": None}
155
+
156
+ if r["confidence"] > 0 and r["confidence"] >= min_confidence:
157
+ try:
158
+ t: Union[str, bytes] = s.decode(r["encoding"] or "utf-8")
159
+ except Exception:
160
+ t = s
161
+ else:
162
+ t = s
163
+
164
+ r["converted"] = safe_str(t)
165
+ r["encoding"] = r["encoding"] or "unknown"
166
+ r["language"] = r["language"] or "unknown"
167
+
168
+ return r # type: ignore
169
+
170
+
171
+ # This method not really necessary. More to stop people from rolling their own.
172
+ def unescape_str(s):
173
+ """unescape a string"""
174
+ return s.decode("string_escape")
175
+
176
+
177
+ def truncate(data: Union[bytes, str], length: int = 100) -> str:
178
+ """This method is a helper used to avoid cluttering output
179
+
180
+ :param data: The buffer that will be determined if it needs to be sliced
181
+ :param length: The limit of characters to the buffer
182
+ :return str: The potentially truncated buffer
183
+ """
184
+ string = safe_str(data)
185
+ if len(string) > length:
186
+ return string[:length] + "..."
187
+ return string
188
+
189
+
190
+ def default_string_value(
191
+ *values: Optional[str], env_name: Optional[str] = None, default: Optional[str] = None
192
+ ) -> Optional[str]:
193
+ """Return a string value based on a list of potential values, an environmnet variable, or a default string"""
194
+ return next(
195
+ (val for val in values if val), (os.getenv(env_name, default or "") or default) if env_name else default
196
+ )
197
+
198
+
199
+ def get_parent_key(key: str) -> str:
200
+ """Get a parent key of a key in the format a.b.c"""
201
+ return re.sub(r"^(.+)\..+?$", r"\1", key)
202
+
203
+
204
+ def sanitize_lucene_query(query: str):
205
+ """Take in a given string, and escape it to ensure it is safe to search on via lucene"""
206
+ query = re.sub(r'([\^"~*?:\\/()[\]{}\-!])', r"\\\1", query)
207
+
208
+ return query.replace("&&", "\\&&").replace("||", "\\||")
209
+
210
+
211
+ class NamedConstants(object):
212
+ """A class containing a list of named constants, as well as a reverse map for those constants to their name"""
213
+
214
+ def __init__(self, name: str, string_value_list: list[tuple[str, Union[str, int]]]):
215
+ self._name = name
216
+ self._value_map = dict(string_value_list)
217
+ self._reverse_map = dict([(s[1], s[0]) for s in string_value_list])
218
+
219
+ # we also import the list as attributes so things like
220
+ # tab completion and introspection still work.
221
+ for s, v in self._value_map.items():
222
+ setattr(self, s, v)
223
+
224
+ def name_for_value(self, v):
225
+ """Get the name of a given value"""
226
+ return self._reverse_map[v]
227
+
228
+ def contains_value(self, v):
229
+ """Chgeck if this instance contains the given value"""
230
+ return v in self._reverse_map
231
+
232
+ def __getitem__(self, s):
233
+ return self._value_map[s]
234
+
235
+ def __getattr__(self, s):
236
+ # We implement our own getattr mainly to provide the better exception.
237
+ return self._value_map[s]
238
+
239
+
240
+ class StringTable(NamedConstants):
241
+ """A subclass of NamedConstants that throws an attribute error if the value does not exist in the table"""
242
+
243
+ def contains_string(self, s):
244
+ """Chgeck if this instance contains the given value"""
245
+ return s in self._reverse_map
246
+
247
+ def __getitem__(self, s):
248
+ if s in self._value_map:
249
+ return s
250
+ raise HowlerAttributeError("Invalid value for %s (%s)" % (self._name, s))
251
+
252
+ def __getattr__(self, s):
253
+ # We implement our own getattr mainly to provide the better exception.
254
+ if s in self._value_map:
255
+ return s
256
+ raise HowlerAttributeError("Invalid value for %s (%s)" % (self._name, s))
howler/utils/uid.py ADDED
@@ -0,0 +1,47 @@
1
+ import hashlib
2
+ import uuid
3
+ from typing import Any, Optional
4
+
5
+ import baseconv
6
+
7
+ from howler.common.exceptions import HowlerValueError
8
+
9
+ TINY = 8
10
+ SHORT = 16
11
+ MEDIUM = NORMAL = 32
12
+ LONG = 64
13
+
14
+
15
+ def get_random_id() -> str:
16
+ """Get a random id in base 62
17
+
18
+ Returns:
19
+ str: A random base62 id
20
+ """
21
+ return baseconv.base62.encode(uuid.uuid4().int)
22
+
23
+
24
+ def get_id_from_data(data: Any, prefix: Optional[str] = None, length: int = MEDIUM) -> str:
25
+ """Get an ID of a particular length, with an optional prefix, based on the provided data.
26
+
27
+ Args:
28
+ data (Any): The data to generate an ID from
29
+ prefix (str, optional): The prefix to append to the generated ID. Defaults to None.
30
+ length (int, optional): The length of the random ID. Defaults to MEDIUM.
31
+
32
+ Raises:
33
+ HowlerValueError: An invalid length was provided.
34
+
35
+ Returns:
36
+ str: A unique hash generated from the provided data
37
+ """
38
+ possible_len = [TINY, SHORT, MEDIUM, LONG]
39
+ if length not in possible_len:
40
+ raise HowlerValueError(f"Invalid hash length of {length}. Possible values are: {str(possible_len)}.")
41
+ sha256_hash = hashlib.sha256(str(data).encode()).hexdigest()[:length]
42
+ _hash = baseconv.base62.encode(int(sha256_hash, 16))
43
+
44
+ if isinstance(prefix, str):
45
+ _hash = f"{prefix}_{_hash}"
46
+
47
+ return _hash
@@ -0,0 +1,71 @@
1
+ Metadata-Version: 2.4
2
+ Name: howler-api
3
+ Version: 3.0.0.dev374
4
+ Summary: Howler - API server
5
+ License: MIT
6
+ Keywords: howler,alerting,gc,canada,cse-cst,cse,cst,cyber,cccs
7
+ Author: Canadian Centre for Cyber Security
8
+ Author-email: howler@cyber.gc.ca
9
+ Maintainer: Matthew Rafuse
10
+ Maintainer-email: matthew.rafuse@cyber.gc.ca
11
+ Requires-Python: >=3.9.17,<4.0.0
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Topic :: Software Development :: Libraries
22
+ Requires-Dist: apscheduler (==3.10.4)
23
+ Requires-Dist: authlib (>=1.6.0,<2.0.0)
24
+ Requires-Dist: azure-identity (==1.16.1)
25
+ Requires-Dist: azure-storage-blob (==12.14.1)
26
+ Requires-Dist: chardet (==5.1.0)
27
+ Requires-Dist: chevron (==0.14.0)
28
+ Requires-Dist: elastic-apm[flask] (>=6.22.0,<7.0.0)
29
+ Requires-Dist: elasticsearch (==8.6.1)
30
+ Requires-Dist: flasgger (>=0.9.7.1,<0.10.0.0)
31
+ Requires-Dist: flask (==2.2.5)
32
+ Requires-Dist: flask-caching (==2.0.2)
33
+ Requires-Dist: gevent (==23.9.1)
34
+ Requires-Dist: gunicorn (==23.0.0)
35
+ Requires-Dist: luqum (>=1.0.0,<2.0.0)
36
+ Requires-Dist: mergedeep (>=1.3.4,<2.0.0)
37
+ Requires-Dist: packaging (<25.0)
38
+ Requires-Dist: passlib (==1.7.4)
39
+ Requires-Dist: prometheus-client (==0.17.1)
40
+ Requires-Dist: pydantic (>=2.11.4,<3.0.0)
41
+ Requires-Dist: pydantic-settings[yaml] (>=2.9.1,<3.0.0)
42
+ Requires-Dist: pydash (>=8.0.5,<9.0.0)
43
+ Requires-Dist: pyjwt (==2.6.0)
44
+ Requires-Dist: pyroute2-core (==0.6.13)
45
+ Requires-Dist: pysftp (==0.2.9)
46
+ Requires-Dist: pysigma (==0.11.17)
47
+ Requires-Dist: pysigma-backend-elasticsearch (>=1.1.2,<2.0.0)
48
+ Requires-Dist: python-baseconv (==1.2.2)
49
+ Requires-Dist: python-datemath (==3.0.3)
50
+ Requires-Dist: python-dotenv (>=1.1.0,<2.0.0)
51
+ Requires-Dist: pyyaml (==6.0.2)
52
+ Requires-Dist: redis (==4.5.4)
53
+ Requires-Dist: requests (==2.32.4)
54
+ Requires-Dist: typing-extensions (>=4.12.2,<5.0.0)
55
+ Requires-Dist: validators (>=0.34.0,<0.35.0)
56
+ Requires-Dist: wsproto (==1.2.0)
57
+ Project-URL: Documentation, https://cybercentrecanada.github.io/howler/developer/backend/
58
+ Project-URL: Homepage, https://cybercentrecanada.github.io/howler/
59
+ Project-URL: Repository, https://github.com/CybercentreCanada/howler-api
60
+ Description-Content-Type: text/markdown
61
+
62
+ # Howler API
63
+
64
+ ## Introduction
65
+
66
+ Howler is an application that allows analysts to triage hits and alerts. It provides a way for analysts to efficiently review and analyze alerts generated by different analytics and detections.
67
+
68
+ ## Contributing
69
+
70
+ See [CONTRIBUTING.md](doc/CONTRIBUTING.md).
71
+