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
@@ -0,0 +1,888 @@
1
+ import os
2
+ import sys
3
+ from pathlib import Path
4
+
5
+ from dotenv import load_dotenv
6
+
7
+ from howler.plugins import get_plugins
8
+
9
+ load_dotenv()
10
+
11
+ # We append the plugin directory for howler to the python part
12
+ PLUGIN_PATH = Path(os.environ.get("HWL_PLUGIN_DIRECTORY", "/etc/howler/plugins"))
13
+ sys.path.insert(0, str(PLUGIN_PATH))
14
+ sys.path.append(str(PLUGIN_PATH / f".venv/lib/python3.{sys.version_info.minor}/site-packages"))
15
+
16
+ import importlib
17
+ import json
18
+ import random
19
+ import textwrap
20
+ from datetime import datetime
21
+ from random import choice, randint, sample
22
+ from typing import Any, Callable, cast
23
+
24
+ import yaml
25
+
26
+ from howler.common import loader
27
+ from howler.common.logging import get_logger
28
+ from howler.config import config
29
+ from howler.datastore.howler_store import HowlerDatastore
30
+ from howler.datastore.operations import OdmHelper
31
+ from howler.helper.hit import assess_hit
32
+ from howler.helper.oauth import VALID_CHARS
33
+ from howler.odm.base import Keyword
34
+ from howler.odm.helper import generate_useful_dossier, generate_useful_hit
35
+ from howler.odm.models.action import Action
36
+ from howler.odm.models.analytic import Analytic, Comment, Notebook, TriageOptions
37
+ from howler.odm.models.ecs.event import EVENT_CATEGORIES
38
+ from howler.odm.models.hit import Hit
39
+ from howler.odm.models.howler_data import Assessment, Escalation, HitStatus, Scrutiny
40
+ from howler.odm.models.overview import Overview
41
+ from howler.odm.models.template import Template
42
+ from howler.odm.models.user import User
43
+ from howler.odm.models.view import View
44
+ from howler.odm.randomizer import get_random_string, get_random_user, get_random_word, random_model_obj
45
+ from howler.security.utils import get_password_hash
46
+ from howler.services import analytic_service
47
+
48
+ classification = loader.get_classification()
49
+
50
+ logger = get_logger(__file__)
51
+
52
+ hit_helper = OdmHelper(Hit)
53
+
54
+
55
+ def run_modifications(odm: str, data: Any, log: bool = False):
56
+ "Running modifications"
57
+ new_keys: list[str] = []
58
+ for plugin in get_plugins(): # pragma: no cover
59
+ if generate := plugin.modules.odm.generation.get(odm, None):
60
+ _new_keys, data = generate(data)
61
+ new_keys += _new_keys
62
+
63
+ if len(new_keys) > 0 and log:
64
+ logger.debug("%s new top-level fields configured for %s", len(new_keys), odm)
65
+
66
+ return data
67
+
68
+
69
+ def create_users(ds):
70
+ """Create number of user accounts"""
71
+ admin_pass = os.getenv("DEV_ADMIN_PASS", "admin") or "admin"
72
+ user_pass = os.getenv("DEV_USER_PASS", "user") or "user"
73
+ shawnh_pass = "shawn-h"
74
+ goose_pass = "goose"
75
+ huey_pass = "huey"
76
+
77
+ admin_hash = get_password_hash(admin_pass)
78
+
79
+ admin_view = View(
80
+ {
81
+ "title": "view.assigned_to_me",
82
+ "query": "howler.assignment:admin",
83
+ "type": "readonly",
84
+ "owner": "admin",
85
+ }
86
+ )
87
+
88
+ admin_view = run_modifications("view", admin_view)
89
+
90
+ user_data = User(
91
+ {
92
+ "apikeys": {
93
+ "devkey": {"acl": ["R", "W", "E"], "password": admin_hash},
94
+ "readonly": {"acl": ["R"], "password": admin_hash},
95
+ "readonly1": {"acl": ["R"], "password": admin_hash},
96
+ "impersonate": {"acl": ["R", "I"], "password": admin_hash},
97
+ "readonly2": {"acl": ["R"], "password": admin_hash},
98
+ "readonly3": {"acl": ["R"], "password": admin_hash},
99
+ "write1": {"acl": ["W"], "password": admin_hash},
100
+ "write2": {"acl": ["W"], "password": admin_hash},
101
+ "both": {"acl": ["R", "W"], "password": admin_hash},
102
+ "read_extended": {"acl": ["R", "E"], "password": admin_hash},
103
+ "write_extended": {"acl": ["W", "E"], "password": admin_hash},
104
+ "expired": {
105
+ "acl": ["R", "W", "E"],
106
+ "password": admin_hash,
107
+ "expiry_date": "2023-05-30T05:12:28.566Z",
108
+ },
109
+ "not_expired": {
110
+ "acl": ["R", "W", "E"],
111
+ "password": admin_hash,
112
+ "expiry_date": datetime.now().replace(year=3000).isoformat(),
113
+ },
114
+ },
115
+ "classification": classification.RESTRICTED,
116
+ "name": "Michael Scott",
117
+ "email": "admin@howler.cyber.gc.ca",
118
+ "password": admin_hash,
119
+ "uname": "admin",
120
+ "type": ["admin", "user", "automation_basic", "automation_advanced"],
121
+ "groups": [
122
+ "group1",
123
+ "group2",
124
+ ],
125
+ "favourite_views": [admin_view.view_id],
126
+ }
127
+ )
128
+
129
+ user_data = run_modifications("view", user_data, True)
130
+
131
+ ds.user.save("admin", user_data)
132
+ ds.user_avatar.save(
133
+ "admin",
134
+ "https://static.wikia.nocookie.net/theoffice/images/b/be/Character_-_MichaelScott.PNG",
135
+ )
136
+ ds.view.save(admin_view.view_id, admin_view)
137
+
138
+ if "pytest" not in sys.modules:
139
+ logger.info(f"\t{user_data.uname}:{admin_pass}")
140
+
141
+ user_hash = get_password_hash(user_pass)
142
+
143
+ user_view = View(
144
+ {
145
+ "title": "view.assigned_to_me",
146
+ "query": "howler.assignment:user",
147
+ "type": "readonly",
148
+ "owner": "user",
149
+ }
150
+ )
151
+
152
+ user_data = User(
153
+ {
154
+ "name": "Dwight Schrute",
155
+ "email": "user@howler.cyber.gc.ca",
156
+ "apikeys": {
157
+ "devkey": {"acl": ["R", "W"], "password": user_hash},
158
+ "impersonate_admin": {
159
+ "acl": ["R", "W", "I"],
160
+ "agents": ["admin", "goose"],
161
+ "password": user_hash,
162
+ },
163
+ "impersonate_potato": {
164
+ "acl": ["R", "W", "I"],
165
+ "agents": ["potato"],
166
+ "password": user_hash,
167
+ },
168
+ },
169
+ "password": user_hash,
170
+ "uname": "user",
171
+ "favourite_views": [user_view.view_id],
172
+ }
173
+ )
174
+
175
+ user_view = run_modifications("view", user_view)
176
+ user_data = run_modifications("user", user_data)
177
+
178
+ ds.user.save("user", user_data)
179
+ ds.user_avatar.save(
180
+ "user",
181
+ "https://static.wikia.nocookie.net/theoffice/images/c/c5/Dwight_.jpg",
182
+ )
183
+ ds.view.save(user_view.view_id, user_view)
184
+
185
+ if "pytest" not in sys.modules:
186
+ logger.info(f"\t{user_data.uname}:{user_pass}")
187
+
188
+ huey_hash = get_password_hash(huey_pass)
189
+
190
+ huey_view = View(
191
+ {
192
+ "title": "view.assigned_to_me",
193
+ "query": "howler.assignment:huey",
194
+ "type": "readonly",
195
+ "owner": "huey",
196
+ }
197
+ )
198
+
199
+ huey_data = User(
200
+ {
201
+ "name": "Huey Guy",
202
+ "email": "huey@howler.cyber.gc.ca",
203
+ "apikeys": {
204
+ "devkey": {"acl": ["R", "W"], "password": huey_hash},
205
+ "impersonate_admin": {
206
+ "acl": ["R", "W", "I"],
207
+ "agents": ["admin", "goose"],
208
+ "password": huey_hash,
209
+ },
210
+ "impersonate_potato": {
211
+ "acl": ["R", "W", "I"],
212
+ "agents": ["potato"],
213
+ "password": huey_hash,
214
+ },
215
+ },
216
+ "password": huey_hash,
217
+ "uname": "huey",
218
+ "favourite_views": [huey_view.view_id],
219
+ }
220
+ )
221
+
222
+ huey_view = run_modifications("view", huey_view)
223
+ huey_data = run_modifications("user", huey_data)
224
+
225
+ ds.user.save("huey", huey_data)
226
+ ds.user_avatar.save(
227
+ "huey",
228
+ "https://static.wikia.nocookie.net/theoffice/images/c/c5/Dwight_.jpg",
229
+ )
230
+ ds.view.save(huey_view.view_id, huey_view)
231
+
232
+ if "pytest" not in sys.modules:
233
+ logger.info(f"\t{huey_data.uname}:{huey_pass}")
234
+
235
+ shawnh_view = View(
236
+ {
237
+ "title": "view.assigned_to_me",
238
+ "query": "howler.assignment:shawnh",
239
+ "type": "readonly",
240
+ "owner": "shawn-h",
241
+ }
242
+ )
243
+ shawn_data = User(
244
+ {
245
+ "name": "Shawn Hannigans",
246
+ "email": "shawn.hannigans@howler.com",
247
+ "apikeys": {},
248
+ "type": ["admin", "user"],
249
+ "groups": ["group1", "group2"],
250
+ "password": get_password_hash(shawnh_pass),
251
+ "uname": "shawn-h",
252
+ "favourite_views": [shawnh_view.view_id],
253
+ }
254
+ )
255
+
256
+ shawnh_view = run_modifications("view", shawnh_view)
257
+ shawn_data = run_modifications("user", shawn_data)
258
+
259
+ ds.user.save("shawn-h", shawn_data)
260
+ ds.view.save(shawnh_view.view_id, shawnh_view)
261
+
262
+ if "pytest" not in sys.modules:
263
+ logger.info(f"\t{shawn_data.uname}:{shawnh_pass}")
264
+
265
+ goose_view = View(
266
+ {
267
+ "title": "view.assigned_to_me",
268
+ "query": "howler.assignment:goose",
269
+ "type": "readonly",
270
+ "owner": "goose",
271
+ }
272
+ )
273
+ goose_data = User(
274
+ {
275
+ "name": "Mister Goose",
276
+ "email": "goose@howler.cyber.gc.ca",
277
+ "apikeys": {},
278
+ "type": ["admin", "user"],
279
+ "groups": ["group1", "group2"],
280
+ "password": get_password_hash(goose_pass),
281
+ "uname": "goose",
282
+ "favourite_views": [goose_view.view_id],
283
+ }
284
+ )
285
+
286
+ goose_view = run_modifications("view", goose_view)
287
+ goose_data = run_modifications("user", goose_data)
288
+
289
+ ds.user.save("goose", goose_data)
290
+ ds.view.save(goose_view.view_id, goose_view)
291
+
292
+ if "pytest" not in sys.modules:
293
+ logger.info(f"\t{goose_data.uname}:{goose_pass}")
294
+
295
+ ds.user.commit()
296
+ ds.user_avatar.commit()
297
+ ds.view.commit()
298
+
299
+
300
+ def wipe_users(ds):
301
+ """Wipe the users index"""
302
+ ds.user.wipe()
303
+ ds.user_avatar.wipe()
304
+
305
+
306
+ def create_templates(ds: HowlerDatastore):
307
+ """Create some random templates"""
308
+ for i in range(2):
309
+ keys = sample(list(Hit.flat_fields().keys()), 5)
310
+
311
+ for detection in ["Detection 1", "Detection 2"]:
312
+ template = Template(
313
+ {
314
+ "analytic": choice(["Password Checker", "Bad Guy Finder", "SecretAnalytic"]),
315
+ "detection": detection,
316
+ "type": "global",
317
+ "keys": keys,
318
+ }
319
+ )
320
+
321
+ template = run_modifications("template", template, i == 0)
322
+
323
+ ds.template.save(
324
+ template.template_id,
325
+ template,
326
+ )
327
+
328
+ for analytic in ["Password Checker", "Bad Guy Finder"]:
329
+ template = Template(
330
+ {
331
+ "analytic": analytic,
332
+ "type": "global",
333
+ "keys": ["howler.id", "howler.hash"],
334
+ }
335
+ )
336
+
337
+ template = run_modifications("template", template)
338
+
339
+ ds.template.save(
340
+ template.template_id,
341
+ template,
342
+ )
343
+
344
+ template = Template(
345
+ {
346
+ "analytic": analytic,
347
+ "owner": "admin",
348
+ "type": "personal",
349
+ "keys": ["howler.id", "howler.hash", "howler.analytic", "agent.id"],
350
+ }
351
+ )
352
+
353
+ template = run_modifications("template", template)
354
+
355
+ ds.template.save(
356
+ template.template_id,
357
+ template,
358
+ )
359
+
360
+ template = Template(
361
+ {
362
+ "analytic": analytic,
363
+ "owner": "goose",
364
+ "type": "personal",
365
+ "keys": ["agent.id", "agent.type", "container.id"],
366
+ }
367
+ )
368
+
369
+ ds.template.save(
370
+ template.template_id,
371
+ template,
372
+ )
373
+
374
+ ds.template.commit()
375
+
376
+
377
+ def wipe_templates(ds):
378
+ """Wipe the templates index"""
379
+ ds.template.wipe()
380
+
381
+
382
+ def create_overviews(ds: HowlerDatastore):
383
+ """Create some random overviews"""
384
+ for i in range(2):
385
+ keys = sample(list(Hit.flat_fields().keys()), 5)
386
+
387
+ for detection in ["Detection 1", "Detection 2"]:
388
+ content = "\n\n".join(f"{{{key}}}" for key in keys)
389
+ overview = Overview(
390
+ {
391
+ "analytic": choice(["Password Checker", "Bad Guy Finder", "SecretAnalytic"]),
392
+ "owner": "admin",
393
+ "detection": detection,
394
+ "content": f"# Hello, World!\n\n{content}",
395
+ }
396
+ )
397
+
398
+ overview = run_modifications("overview", overview, i == 0)
399
+
400
+ ds.overview.save(
401
+ overview.overview_id,
402
+ overview,
403
+ )
404
+
405
+ for analytic in ["Password Checker", "Bad Guy Finder"]:
406
+ overview = Overview(
407
+ {
408
+ "analytic": analytic,
409
+ "owner": "admin",
410
+ "content": textwrap.dedent("""
411
+ # {{howler.analytic}} Alert
412
+ {{#if (equals howler.status "open")}}
413
+ ![](https://img.shields.io/badge/open-blue)
414
+ {{/if}}
415
+ {{#if (equals howler.status "in-progress")}}
416
+ ![](https://img.shields.io/badge/in%20progress-yellow)
417
+ {{/if}}
418
+ {{#if (and (equals howler.status "resolved") (equals howler.escalation "miss"))}}
419
+ ![](https://img.shields.io/badge/safe-green)
420
+ {{/if}}
421
+ {{#if (and (equals howler.status "resolved") (equals howler.escalation "evidence"))}}
422
+ ![](https://img.shields.io/badge/malicious-red)
423
+ {{/if}}
424
+
425
+ `{{fetch "/api/v1/configs" "api_response.c12nDef.UNRESTRICTED"}}`
426
+
427
+ {{#if (and (equals howler.status "resolved") (equals howler.escalation "evidence"))}}
428
+ {{howler.rationale}}
429
+ {{/if}}
430
+
431
+ ## Summary
432
+
433
+ > {{howler.outline.summary}}
434
+
435
+ {{#if howler.assignment}}
436
+ <div style="display: grid; align-items: center; grid-template-columns: auto auto; width: fit-content; border: 1px solid grey; padding: 0.25rem; border-radius: 5px; margin-bottom: 1rem">
437
+ {{img src=(fetch (join "/api/v1/user/avatar/" howler.assignment ) "api_response") style="width: 32px; border-radius: 100px"}} {{howler.assignment}}
438
+ </div>
439
+ {{/if}}
440
+ """), # noqa: E501
441
+ }
442
+ )
443
+
444
+ overview = run_modifications("overview", overview)
445
+
446
+ ds.overview.save(
447
+ overview.overview_id,
448
+ overview,
449
+ )
450
+
451
+ ds.overview.commit()
452
+
453
+
454
+ def wipe_overviews(ds):
455
+ """Wipe the overviews index"""
456
+ ds.overview.wipe()
457
+
458
+
459
+ def create_views(ds: HowlerDatastore):
460
+ """Create some random views"""
461
+ view = View(
462
+ {
463
+ "title": "CMT Hits",
464
+ "query": "howler.analytic:cmt.*",
465
+ "type": "global",
466
+ "owner": "admin",
467
+ }
468
+ )
469
+
470
+ view = run_modifications("view", view)
471
+
472
+ ds.view.save(
473
+ view.view_id,
474
+ view,
475
+ )
476
+
477
+ view = View(
478
+ {
479
+ "title": "Howler Bundles",
480
+ "query": "howler.is_bundle:true",
481
+ "type": "readonly",
482
+ "owner": "none",
483
+ }
484
+ )
485
+
486
+ view = run_modifications("view", view)
487
+
488
+ ds.view.save(
489
+ view.view_id,
490
+ view,
491
+ )
492
+
493
+ fields = Hit.flat_fields()
494
+ key_list = [key for key in fields.keys() if isinstance(fields[key], Keyword)]
495
+ for _ in range(10):
496
+ query = f"{choice(key_list)}:*{choice(VALID_CHARS)}* OR {choice(key_list)}:*{choice(VALID_CHARS)}*"
497
+ view = View(
498
+ {
499
+ "title": get_random_word(),
500
+ "query": query,
501
+ "type": "global",
502
+ "owner": get_random_user(),
503
+ }
504
+ )
505
+
506
+ view = run_modifications("view", view)
507
+
508
+ ds.view.save(
509
+ view.view_id,
510
+ view,
511
+ )
512
+
513
+ ds.view.commit()
514
+
515
+
516
+ def wipe_views(ds):
517
+ """Wipe the views index"""
518
+ ds.view.wipe()
519
+
520
+
521
+ def create_hits(ds: HowlerDatastore, hit_count: int = 200):
522
+ """Create some random hits"""
523
+ lookups = loader.get_lookups()
524
+ users = ds.user.search("*:*")["items"]
525
+ for hit_idx in range(hit_count):
526
+ hit = generate_useful_hit(lookups, [user["uname"] for user in users], prune_hit=False)
527
+
528
+ if hit_idx + 1 == hit_count:
529
+ hit.howler.analytic = "SecretAnalytic"
530
+ hit.howler.detection = None
531
+
532
+ ds.hit.save(hit.howler.id, hit)
533
+ analytic_service.save_from_hit(hit, random.choice(users))
534
+ ds.analytic.commit()
535
+
536
+ if choice([True, False, False, False]):
537
+ user = choice(users)
538
+ ds.hit.update(
539
+ hit.howler.id,
540
+ [
541
+ *assess_hit(
542
+ assessment=choice(Assessment.list()),
543
+ rationale=get_random_string(),
544
+ hit=hit,
545
+ ),
546
+ hit_helper.update(
547
+ "howler.assignment",
548
+ user.get("uname", user.get("username", None)),
549
+ ),
550
+ hit_helper.update("howler.status", HitStatus.RESOLVED),
551
+ ],
552
+ )
553
+
554
+ ds.hit.commit()
555
+
556
+ if hit_idx % 25 == 0 and "pytest" not in sys.modules:
557
+ logger.info("\tCreated %s/%s", hit_idx, hit_count)
558
+
559
+ if "pytest" not in sys.modules:
560
+ logger.info("\tCreated %s/%s", hit_idx + 1, hit_count)
561
+
562
+ logger.info(
563
+ "%s total hits in datastore", ds.hit.search(query="howler.id:*", track_total_hits=True, rows=0)["total"]
564
+ )
565
+
566
+
567
+ def create_bundles(ds: HowlerDatastore):
568
+ """Create some random bundles"""
569
+ lookups = loader.get_lookups()
570
+ users = [user.uname for user in ds.user.search("*:*")["items"]]
571
+
572
+ hits = {}
573
+
574
+ for i in range(3):
575
+ bundle_hit: Hit = generate_useful_hit(lookups, users)
576
+ bundle_hit.howler.is_bundle = True
577
+
578
+ for hit in ds.hit.search("howler.is_bundle:false", rows=randint(3, 10), offset=(i * 2))["items"]:
579
+ if hit.howler.id not in hits:
580
+ hits[hit.howler.id] = hit
581
+
582
+ bundle_hit.howler.hits.append(hit.howler.id)
583
+ hits[hit.howler.id].howler.bundles.append(bundle_hit.howler.id)
584
+
585
+ analytic_service.save_from_hit(bundle_hit, random.choice(ds.user.search("*:*")["items"]))
586
+ bundle_hit.howler.bundle_size = len(bundle_hit.howler.hits)
587
+ ds.hit.save(bundle_hit.howler.id, bundle_hit)
588
+
589
+ for hit in hits.values():
590
+ ds.hit.save(hit.howler.id, hit)
591
+
592
+ ds.hit.commit()
593
+
594
+
595
+ def wipe_hits(ds):
596
+ """Wipe the hits index"""
597
+ ds.hit.wipe()
598
+
599
+
600
+ def random_escalations() -> list[Escalation]:
601
+ """Return a list of random escalations"""
602
+ return random.sample(Escalation.list(), k=random.randint(1, len(Escalation.list())))
603
+
604
+
605
+ def random_scrutinies() -> list[Scrutiny]:
606
+ """Return a list of random scrutinies"""
607
+ return random.sample(Scrutiny.list(), k=random.randint(1, len(Scrutiny.list())))
608
+
609
+
610
+ def random_event_categories():
611
+ """Return a list of random event categories"""
612
+ return random.choice(EVENT_CATEGORIES)
613
+
614
+
615
+ def create_analytics(ds: HowlerDatastore, num_analytics: int = 10):
616
+ """Create some random analytics"""
617
+ users = [user.uname for user in ds.user.search("*:*")["items"]]
618
+
619
+ for analytic in ds.analytic.search("*:*")["items"]:
620
+ for detection in analytic.detections:
621
+ analytic.comment.append(
622
+ Comment(
623
+ {
624
+ "value": f"Placeholder Comment - {detection}",
625
+ "user": random.choice(users),
626
+ "detection": detection,
627
+ }
628
+ )
629
+ )
630
+
631
+ analytic.comment.append(
632
+ Comment(
633
+ {
634
+ "value": "Placeholder Comment - Analytic",
635
+ "user": random.choice(users),
636
+ }
637
+ )
638
+ )
639
+
640
+ if config.core.notebook.enabled:
641
+ analytic.notebooks.append(
642
+ Notebook(
643
+ {
644
+ "value": "Link to super notebook",
645
+ "name": "Super notebook",
646
+ "user": random.choice(users),
647
+ }
648
+ )
649
+ )
650
+
651
+ analytic = run_modifications("analytic", analytic)
652
+
653
+ ds.analytic.save(analytic.analytic_id, analytic)
654
+
655
+ fields = Hit.flat_fields()
656
+ key_list = [key for key in fields.keys() if isinstance(fields[key], Keyword)]
657
+ for _ in range(num_analytics):
658
+ a: Analytic = random_model_obj(cast(Any, Analytic))
659
+ a.name = " ".join([get_random_word().capitalize() for _ in range(random.randint(1, 3))])
660
+ a.detections = list(set(a.detections))
661
+ a.owner = random.choice(users)
662
+ a.contributors = list(set(random.sample(users, k=random.randint(1, 3))))
663
+ a.rule = None
664
+ a.rule_crontab = None
665
+ a.rule_type = None
666
+
667
+ assessments = Assessment.list()
668
+
669
+ cast(TriageOptions, a.triage_settings).valid_assessments = list(
670
+ set(random.sample(assessments, counts=([3] * len(assessments)), k=random.randint(1, len(assessments) * 3)))
671
+ )
672
+
673
+ a = run_modifications("analytic", a)
674
+
675
+ ds.analytic.save(a.analytic_id, a)
676
+
677
+ for rule_type in ["lucene", "eql", "sigma"]:
678
+ a: Analytic = random_model_obj(cast(Any, Analytic))
679
+ a.rule_type = rule_type
680
+ a.name = " ".join([get_random_word().capitalize() for _ in range(random.randint(1, 3))])
681
+ a.detections = ["Rule"]
682
+ a.owner = random.choice(users)
683
+ a.contributors = list(set(random.sample(users, k=random.randint(1, 3))))
684
+ a.rule_crontab = (
685
+ f"{','.join([str(k) for k in sorted(random.sample(list(range(60)), k=random.randint(2, 5)))])} * * * *"
686
+ )
687
+
688
+ cast(TriageOptions, a.triage_settings).valid_assessments = list(
689
+ set(random.sample(assessments, counts=([3] * len(assessments)), k=random.randint(1, len(assessments) * 3)))
690
+ )
691
+
692
+ if a.rule_type == "lucene":
693
+ a.rule = (
694
+ f"{choice(key_list)}:*{choice(VALID_CHARS)}*\n#example "
695
+ f"comment\nOR\n{choice(key_list)}:*{choice(VALID_CHARS)}*"
696
+ )
697
+ elif a.rule_type == "eql":
698
+ category1 = random_event_categories()
699
+ category2 = random_event_categories()
700
+
701
+ a.rule = textwrap.dedent(
702
+ f"""
703
+ sequence
704
+ [ {category1} where howler.escalation in ({", ".join([f'"{item}"' for item in random_escalations()])}) ]
705
+ [ {category2} where howler.scrutiny in ({", ".join([f'"{item}"' for item in random_scrutinies()])}) ]
706
+ """
707
+ ).strip()
708
+ elif a.rule_type == "sigma":
709
+ files = []
710
+
711
+ sigma_dir = Path(__file__).parent / "sigma"
712
+ if sigma_dir.exists():
713
+ files = list(sigma_dir.glob("*.yml"))
714
+
715
+ if len(files) > 0:
716
+ file_name = random.choice(files)
717
+ file_data = file_name.read_text("utf-8")
718
+ data = yaml.safe_load(file_data)
719
+ a.name = data["title"]
720
+ a.description = data["description"]
721
+ a.rule = file_data
722
+ else:
723
+ logger.warning(
724
+ "For better test data using sigma rules, execute howler/external/generate_sigma_rules.py."
725
+ )
726
+
727
+ a = run_modifications("analytic", a)
728
+
729
+ ds.analytic.save(a.analytic_id, a)
730
+
731
+ ds.analytic.commit()
732
+ ds.hit.commit()
733
+
734
+
735
+ def wipe_analytics(ds):
736
+ """Wipe the analytics index"""
737
+ ds.analytic.wipe()
738
+
739
+
740
+ def create_actions(ds: HowlerDatastore, num_actions: int = 30):
741
+ """Create random actions"""
742
+ fields = Hit.flat_fields()
743
+ key_list = [key for key in fields.keys() if isinstance(fields[key], Keyword)]
744
+ users = ds.user.search("*:*")["items"]
745
+
746
+ module_path = Path(__file__).parents[1] / "actions"
747
+ available_operations = {
748
+ operation.OPERATION_ID: operation
749
+ for operation in (
750
+ importlib.import_module(f"howler.actions.{module.stem}")
751
+ for module in module_path.iterdir()
752
+ if module.suffix == ".py" and module.name != "__init__.py"
753
+ )
754
+ }
755
+
756
+ operation_options = list(available_operations.keys())
757
+ if "transition" in operation_options:
758
+ operation_options.remove("transition")
759
+
760
+ for _ in range(num_actions):
761
+ operations: list[dict[str, str]] = []
762
+ operation_ids = sample(operation_options, k=randint(1, len(operation_options)))
763
+ for operation_id in operation_ids:
764
+ action_data = {}
765
+
766
+ for step in available_operations[operation_id].specification()["steps"]:
767
+ for key in step["args"].keys():
768
+ potential_values = step["options"].get(key, None)
769
+ if potential_values:
770
+ if isinstance(potential_values, dict):
771
+ try:
772
+ action_data[key] = choice(potential_values[choice(list(potential_values.keys()))])
773
+ except IndexError:
774
+ continue
775
+ else:
776
+ action_data[key] = choice(potential_values)
777
+ else:
778
+ action_data[key] = get_random_word()
779
+
780
+ if operation_id == "prioritization":
781
+ action_data["value"] = float(random.randint(0, 10000)) / 10
782
+
783
+ operations.append({"operation_id": operation_id, "data_json": json.dumps((action_data))})
784
+
785
+ action = Action(
786
+ {
787
+ "name": get_random_word(),
788
+ "owner_id": choice([user["uname"] for user in users]),
789
+ "query": f"{choice(key_list)}:*{choice(VALID_CHARS)}* OR {choice(key_list)}:*{choice(VALID_CHARS)}*",
790
+ "operations": operations,
791
+ }
792
+ )
793
+
794
+ action = run_modifications("action", action)
795
+
796
+ ds.action.save(action.action_id, action)
797
+
798
+ ds.action.commit()
799
+
800
+
801
+ def wipe_actions(ds: HowlerDatastore):
802
+ """Wipe the actions index"""
803
+ ds.action.wipe()
804
+
805
+
806
+ def create_dossiers(ds: HowlerDatastore, num_dossiers: int = 5):
807
+ "Create random dossiers"
808
+ users = ds.user.search("*:*")["items"]
809
+ for _ in range(num_dossiers):
810
+ dossier = generate_useful_dossier(users)
811
+ ds.dossier.save(dossier.dossier_id, dossier)
812
+
813
+ ds.dossier.commit()
814
+
815
+
816
+ def wipe_dossiers(ds: HowlerDatastore):
817
+ """Wipe the dossiers index"""
818
+ ds.dossier.wipe()
819
+
820
+
821
+ def setup_hits(ds):
822
+ "Set up hits index"
823
+ os.environ["ELASTIC_HIT_SHARDS"] = "1"
824
+ os.environ["ELASTIC_HIT_REPLICAS"] = "1"
825
+ ds.hit.fix_shards()
826
+ ds.hit.fix_replicas()
827
+
828
+
829
+ def setup_users(ds):
830
+ "Set up users index"
831
+ os.environ["ELASTIC_USER_REPLICAS"] = "1"
832
+ os.environ["ELASTIC_USER_AVATAR_REPLICAS"] = "1"
833
+ ds.user.fix_replicas()
834
+ ds.user_avatar.fix_replicas()
835
+
836
+
837
+ INDEXES: dict[str, tuple[Callable, list[Callable]]] = {
838
+ "users": (wipe_users, [create_users]),
839
+ "templates": (wipe_templates, [create_templates]),
840
+ "overviews": (wipe_overviews, [create_overviews]),
841
+ "views": (wipe_views, [create_views]),
842
+ "hits": (wipe_hits, [create_hits, create_bundles]),
843
+ "analytics": (wipe_analytics, [create_analytics]),
844
+ "actions": (wipe_actions, [create_actions]),
845
+ "dossiers": (wipe_dossiers, [create_dossiers]),
846
+ }
847
+
848
+
849
+ if __name__ == "__main__":
850
+ # TODO: Implement a solid command line interface for running this
851
+
852
+ args = [*sys.argv]
853
+
854
+ # Remove the file path
855
+ args.pop(0)
856
+
857
+ if "all" in args or len(args) < 1:
858
+ logger.info("Adding test data to all indexes.")
859
+ args = list(INDEXES.keys())
860
+ else:
861
+ logger.info("Adding test data to indexes: (%s).", ", ".join(args))
862
+
863
+ ds = loader.datastore(archive_access=False)
864
+
865
+ if "--no-wipe" not in args:
866
+ logger.info("Wiping existing data.")
867
+
868
+ for index, operations in INDEXES.items():
869
+ if index in args:
870
+ # Wipe function
871
+ operations[0](ds)
872
+
873
+ logger.info("Running setup steps.")
874
+ if "hits" in args:
875
+ setup_hits(ds)
876
+
877
+ if "users" in args:
878
+ setup_users(ds)
879
+
880
+ for index, operations in INDEXES.items():
881
+ if index in args:
882
+ logger.info(f"Creating {index}...")
883
+
884
+ # Create functions
885
+ for create_fn in operations[1]:
886
+ create_fn(ds)
887
+
888
+ logger.info("Done.")