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.
- howler/__init__.py +0 -0
- howler/actions/__init__.py +168 -0
- howler/actions/add_label.py +111 -0
- howler/actions/add_to_bundle.py +159 -0
- howler/actions/change_field.py +76 -0
- howler/actions/demote.py +160 -0
- howler/actions/example_plugin.py +104 -0
- howler/actions/prioritization.py +93 -0
- howler/actions/promote.py +147 -0
- howler/actions/remove_from_bundle.py +133 -0
- howler/actions/remove_label.py +111 -0
- howler/actions/transition.py +200 -0
- howler/api/__init__.py +249 -0
- howler/api/base.py +88 -0
- howler/api/socket.py +114 -0
- howler/api/v1/__init__.py +97 -0
- howler/api/v1/action.py +372 -0
- howler/api/v1/analytic.py +748 -0
- howler/api/v1/auth.py +382 -0
- howler/api/v1/clue.py +99 -0
- howler/api/v1/configs.py +58 -0
- howler/api/v1/dossier.py +222 -0
- howler/api/v1/help.py +28 -0
- howler/api/v1/hit.py +1181 -0
- howler/api/v1/notebook.py +82 -0
- howler/api/v1/overview.py +191 -0
- howler/api/v1/search.py +788 -0
- howler/api/v1/template.py +206 -0
- howler/api/v1/tool.py +183 -0
- howler/api/v1/user.py +416 -0
- howler/api/v1/utils/__init__.py +0 -0
- howler/api/v1/utils/etag.py +84 -0
- howler/api/v1/view.py +288 -0
- howler/app.py +235 -0
- howler/common/README.md +125 -0
- howler/common/__init__.py +0 -0
- howler/common/classification.py +979 -0
- howler/common/classification.yml +107 -0
- howler/common/exceptions.py +167 -0
- howler/common/loader.py +154 -0
- howler/common/logging/__init__.py +241 -0
- howler/common/logging/audit.py +138 -0
- howler/common/logging/format.py +38 -0
- howler/common/net.py +79 -0
- howler/common/net_static.py +1494 -0
- howler/common/random_user.py +316 -0
- howler/common/swagger.py +117 -0
- howler/config.py +64 -0
- howler/cronjobs/__init__.py +29 -0
- howler/cronjobs/retention.py +61 -0
- howler/cronjobs/rules.py +274 -0
- howler/cronjobs/view_cleanup.py +88 -0
- howler/datastore/README.md +112 -0
- howler/datastore/__init__.py +0 -0
- howler/datastore/bulk.py +72 -0
- howler/datastore/collection.py +2342 -0
- howler/datastore/constants.py +119 -0
- howler/datastore/exceptions.py +41 -0
- howler/datastore/howler_store.py +105 -0
- howler/datastore/migrations/fix_process.py +41 -0
- howler/datastore/operations.py +130 -0
- howler/datastore/schemas.py +90 -0
- howler/datastore/store.py +231 -0
- howler/datastore/support/__init__.py +0 -0
- howler/datastore/support/build.py +215 -0
- howler/datastore/support/schemas.py +90 -0
- howler/datastore/types.py +22 -0
- howler/error.py +91 -0
- howler/external/__init__.py +0 -0
- howler/external/generate_mitre.py +96 -0
- howler/external/generate_sigma_rules.py +31 -0
- howler/external/generate_tlds.py +47 -0
- howler/external/reindex_data.py +66 -0
- howler/external/wipe_databases.py +58 -0
- howler/gunicorn_config.py +25 -0
- howler/healthz.py +47 -0
- howler/helper/__init__.py +0 -0
- howler/helper/azure.py +50 -0
- howler/helper/discover.py +59 -0
- howler/helper/hit.py +236 -0
- howler/helper/oauth.py +247 -0
- howler/helper/search.py +92 -0
- howler/helper/workflow.py +110 -0
- howler/helper/ws.py +378 -0
- howler/odm/README.md +102 -0
- howler/odm/__init__.py +1 -0
- howler/odm/base.py +1543 -0
- howler/odm/charter.txt +146 -0
- howler/odm/helper.py +416 -0
- howler/odm/howler_enum.py +25 -0
- howler/odm/models/__init__.py +0 -0
- howler/odm/models/action.py +33 -0
- howler/odm/models/analytic.py +90 -0
- howler/odm/models/assemblyline.py +48 -0
- howler/odm/models/aws.py +23 -0
- howler/odm/models/azure.py +16 -0
- howler/odm/models/cbs.py +44 -0
- howler/odm/models/config.py +558 -0
- howler/odm/models/dossier.py +33 -0
- howler/odm/models/ecs/__init__.py +0 -0
- howler/odm/models/ecs/agent.py +17 -0
- howler/odm/models/ecs/autonomous_system.py +16 -0
- howler/odm/models/ecs/client.py +149 -0
- howler/odm/models/ecs/cloud.py +141 -0
- howler/odm/models/ecs/code_signature.py +27 -0
- howler/odm/models/ecs/container.py +32 -0
- howler/odm/models/ecs/dns.py +62 -0
- howler/odm/models/ecs/egress.py +10 -0
- howler/odm/models/ecs/elf.py +74 -0
- howler/odm/models/ecs/email.py +122 -0
- howler/odm/models/ecs/error.py +14 -0
- howler/odm/models/ecs/event.py +140 -0
- howler/odm/models/ecs/faas.py +24 -0
- howler/odm/models/ecs/file.py +84 -0
- howler/odm/models/ecs/geo.py +30 -0
- howler/odm/models/ecs/group.py +18 -0
- howler/odm/models/ecs/hash.py +16 -0
- howler/odm/models/ecs/host.py +17 -0
- howler/odm/models/ecs/http.py +37 -0
- howler/odm/models/ecs/ingress.py +12 -0
- howler/odm/models/ecs/interface.py +21 -0
- howler/odm/models/ecs/network.py +30 -0
- howler/odm/models/ecs/observer.py +45 -0
- howler/odm/models/ecs/organization.py +12 -0
- howler/odm/models/ecs/os.py +21 -0
- howler/odm/models/ecs/pe.py +17 -0
- howler/odm/models/ecs/process.py +216 -0
- howler/odm/models/ecs/registry.py +26 -0
- howler/odm/models/ecs/related.py +45 -0
- howler/odm/models/ecs/rule.py +51 -0
- howler/odm/models/ecs/server.py +24 -0
- howler/odm/models/ecs/threat.py +247 -0
- howler/odm/models/ecs/tls.py +58 -0
- howler/odm/models/ecs/url.py +51 -0
- howler/odm/models/ecs/user.py +57 -0
- howler/odm/models/ecs/user_agent.py +20 -0
- howler/odm/models/ecs/vulnerability.py +41 -0
- howler/odm/models/gcp.py +16 -0
- howler/odm/models/hit.py +356 -0
- howler/odm/models/howler_data.py +328 -0
- howler/odm/models/lead.py +24 -0
- howler/odm/models/localized_label.py +13 -0
- howler/odm/models/overview.py +16 -0
- howler/odm/models/pivot.py +40 -0
- howler/odm/models/template.py +24 -0
- howler/odm/models/user.py +83 -0
- howler/odm/models/view.py +34 -0
- howler/odm/random_data.py +888 -0
- howler/odm/randomizer.py +609 -0
- howler/patched.py +5 -0
- howler/plugins/__init__.py +25 -0
- howler/plugins/config.py +123 -0
- howler/remote/__init__.py +0 -0
- howler/remote/datatypes/README.md +355 -0
- howler/remote/datatypes/__init__.py +98 -0
- howler/remote/datatypes/counters.py +63 -0
- howler/remote/datatypes/events.py +66 -0
- howler/remote/datatypes/hash.py +206 -0
- howler/remote/datatypes/lock.py +42 -0
- howler/remote/datatypes/queues/__init__.py +0 -0
- howler/remote/datatypes/queues/comms.py +59 -0
- howler/remote/datatypes/queues/multi.py +32 -0
- howler/remote/datatypes/queues/named.py +93 -0
- howler/remote/datatypes/queues/priority.py +215 -0
- howler/remote/datatypes/set.py +118 -0
- howler/remote/datatypes/user_quota_tracker.py +54 -0
- howler/security/__init__.py +253 -0
- howler/security/socket.py +108 -0
- howler/security/utils.py +185 -0
- howler/services/__init__.py +0 -0
- howler/services/action_service.py +111 -0
- howler/services/analytic_service.py +128 -0
- howler/services/auth_service.py +323 -0
- howler/services/config_service.py +128 -0
- howler/services/dossier_service.py +252 -0
- howler/services/event_service.py +93 -0
- howler/services/hit_service.py +893 -0
- howler/services/jwt_service.py +158 -0
- howler/services/lucene_service.py +286 -0
- howler/services/notebook_service.py +119 -0
- howler/services/overview_service.py +44 -0
- howler/services/template_service.py +45 -0
- howler/services/user_service.py +331 -0
- howler/utils/__init__.py +0 -0
- howler/utils/annotations.py +28 -0
- howler/utils/chunk.py +38 -0
- howler/utils/dict_utils.py +200 -0
- howler/utils/isotime.py +17 -0
- howler/utils/list_utils.py +11 -0
- howler/utils/lucene.py +77 -0
- howler/utils/path.py +27 -0
- howler/utils/socket_utils.py +61 -0
- howler/utils/str_utils.py +256 -0
- howler/utils/uid.py +47 -0
- howler_api-3.0.0.dev374.dist-info/METADATA +71 -0
- howler_api-3.0.0.dev374.dist-info/RECORD +198 -0
- howler_api-3.0.0.dev374.dist-info/WHEEL +4 -0
- 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
|
+

|
|
414
|
+
{{/if}}
|
|
415
|
+
{{#if (equals howler.status "in-progress")}}
|
|
416
|
+

|
|
417
|
+
{{/if}}
|
|
418
|
+
{{#if (and (equals howler.status "resolved") (equals howler.escalation "miss"))}}
|
|
419
|
+

|
|
420
|
+
{{/if}}
|
|
421
|
+
{{#if (and (equals howler.status "resolved") (equals howler.escalation "evidence"))}}
|
|
422
|
+

|
|
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.")
|