howler-api 2.10.0.dev255__py3-none-any.whl → 2.13.0.dev344__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/api/__init__.py +1 -1
- howler/api/v1/auth.py +1 -1
- howler/api/v1/{borealis.py → clue.py} +24 -26
- howler/api/v1/dossier.py +4 -28
- howler/api/v1/hit.py +11 -7
- howler/api/v1/search.py +160 -17
- howler/api/v1/user.py +2 -2
- howler/api/v1/utils/etag.py +43 -5
- howler/api/v1/view.py +26 -34
- howler/app.py +4 -4
- howler/cronjobs/view_cleanup.py +88 -0
- howler/datastore/README.md +0 -2
- howler/datastore/collection.py +109 -132
- howler/datastore/howler_store.py +0 -45
- howler/datastore/store.py +25 -6
- howler/odm/base.py +1 -1
- howler/odm/helper.py +9 -6
- howler/odm/models/config.py +168 -8
- howler/odm/models/howler_data.py +2 -1
- howler/odm/models/lead.py +1 -10
- howler/odm/models/pivot.py +2 -11
- howler/odm/random_data.py +1 -1
- howler/security/__init__.py +2 -2
- howler/services/analytic_service.py +31 -0
- howler/services/config_service.py +2 -2
- howler/services/dossier_service.py +140 -7
- howler/services/hit_service.py +317 -72
- howler/services/lucene_service.py +14 -7
- howler/services/overview_service.py +44 -0
- howler/services/template_service.py +45 -0
- howler/utils/lucene.py +22 -2
- {howler_api-2.10.0.dev255.dist-info → howler_api-2.13.0.dev344.dist-info}/METADATA +5 -5
- {howler_api-2.10.0.dev255.dist-info → howler_api-2.13.0.dev344.dist-info}/RECORD +35 -32
- {howler_api-2.10.0.dev255.dist-info → howler_api-2.13.0.dev344.dist-info}/WHEEL +1 -1
- {howler_api-2.10.0.dev255.dist-info → howler_api-2.13.0.dev344.dist-info}/entry_points.txt +0 -0
howler/odm/helper.py
CHANGED
|
@@ -143,8 +143,9 @@ def generate_useful_hit(lookups: dict[str, dict[str, Any]], users: list[User], p
|
|
|
143
143
|
hit.howler.labels.mitigation = []
|
|
144
144
|
hit.howler.labels.operation = []
|
|
145
145
|
hit.howler.labels.threat = []
|
|
146
|
+
hit.howler.labels.tuning = []
|
|
146
147
|
|
|
147
|
-
label_type = ceil(rand_seed *
|
|
148
|
+
label_type = ceil(rand_seed * 7)
|
|
148
149
|
if label_type == 1:
|
|
149
150
|
hit.howler.labels.campaign = ["Bad event 2023-07"]
|
|
150
151
|
elif label_type == 2:
|
|
@@ -155,6 +156,8 @@ def generate_useful_hit(lookups: dict[str, dict[str, Any]], users: list[User], p
|
|
|
155
156
|
hit.howler.labels.mitigation = ["Blocked: google.com"]
|
|
156
157
|
elif label_type == 5:
|
|
157
158
|
hit.howler.labels.operation = ["OP_HOWLER"]
|
|
159
|
+
elif label_type == 6:
|
|
160
|
+
hit.howler.labels.tuning = ["Tune example"]
|
|
158
161
|
else:
|
|
159
162
|
hit.howler.labels.threat = ["Bad Mojo"]
|
|
160
163
|
|
|
@@ -256,13 +259,13 @@ def generate_useful_hit(lookups: dict[str, dict[str, Any]], users: list[User], p
|
|
|
256
259
|
),
|
|
257
260
|
]
|
|
258
261
|
|
|
259
|
-
if config.core.
|
|
262
|
+
if config.core.clue.enabled:
|
|
260
263
|
hit.howler.dossier.append(
|
|
261
264
|
Lead(
|
|
262
265
|
{
|
|
263
266
|
"icon": "material-symbols:image",
|
|
264
|
-
"label": {"en": "
|
|
265
|
-
"format": "
|
|
267
|
+
"label": {"en": "Clue", "fr": "Clue"},
|
|
268
|
+
"format": "clue",
|
|
266
269
|
"content": "test-plugin.image",
|
|
267
270
|
"metadata": {"type": "ip", "value": "127.0.01", "classification": "TLP:CLEAR"},
|
|
268
271
|
}
|
|
@@ -273,8 +276,8 @@ def generate_useful_hit(lookups: dict[str, dict[str, Any]], users: list[User], p
|
|
|
273
276
|
Lead(
|
|
274
277
|
{
|
|
275
278
|
"icon": "material-symbols:code-rounded",
|
|
276
|
-
"label": {"en": "
|
|
277
|
-
"format": "
|
|
279
|
+
"label": {"en": "Clue", "fr": "Clue"},
|
|
280
|
+
"format": "clue",
|
|
278
281
|
"content": "test-plugin.json",
|
|
279
282
|
"metadata": {"type": "ip", "value": "127.0.01", "classification": "TLP:CLEAR"},
|
|
280
283
|
}
|
howler/odm/models/config.py
CHANGED
|
@@ -20,11 +20,24 @@ logger.addHandler(console)
|
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
class RedisServer(BaseModel):
|
|
23
|
+
"""Configuration for a single Redis server instance.
|
|
24
|
+
|
|
25
|
+
Defines the connection parameters for a Redis server, including
|
|
26
|
+
the hostname and port number.
|
|
27
|
+
"""
|
|
28
|
+
|
|
23
29
|
host: str = Field(description="Hostname of Redis instance")
|
|
24
30
|
port: int = Field(description="Port of Redis instance")
|
|
25
31
|
|
|
26
32
|
|
|
27
33
|
class Redis(BaseModel):
|
|
34
|
+
"""Redis configuration for Howler.
|
|
35
|
+
|
|
36
|
+
Defines connections to both persistent and non-persistent Redis instances.
|
|
37
|
+
The non-persistent instance is used for volatile data like caches, while
|
|
38
|
+
the persistent instance is used for data that needs to survive restarts.
|
|
39
|
+
"""
|
|
40
|
+
|
|
28
41
|
nonpersistent: RedisServer = Field(
|
|
29
42
|
default=RedisServer(host="127.0.0.1", port=6379), description="A volatile Redis instance"
|
|
30
43
|
)
|
|
@@ -35,6 +48,14 @@ class Redis(BaseModel):
|
|
|
35
48
|
|
|
36
49
|
|
|
37
50
|
class Host(BaseModel):
|
|
51
|
+
"""Configuration for a remote host connection.
|
|
52
|
+
|
|
53
|
+
Defines connection parameters for external services, including authentication
|
|
54
|
+
credentials (username/password or API key) and connection details.
|
|
55
|
+
Environment variables can override username and password using the pattern
|
|
56
|
+
{NAME}_HOST_USERNAME and {NAME}_HOST_PASSWORD.
|
|
57
|
+
"""
|
|
58
|
+
|
|
38
59
|
name: str = Field(description="Name of the host")
|
|
39
60
|
username: Optional[str] = Field(description="Username to login with", default=None)
|
|
40
61
|
password: Optional[str] = Field(description="Password to login with", default=None)
|
|
@@ -64,6 +85,12 @@ class Host(BaseModel):
|
|
|
64
85
|
|
|
65
86
|
|
|
66
87
|
class Datastore(BaseModel):
|
|
88
|
+
"""Datastore configuration for Howler.
|
|
89
|
+
|
|
90
|
+
Defines the backend datastore used by Howler for storing hits and metadata.
|
|
91
|
+
Currently supports Elasticsearch as the datastore type.
|
|
92
|
+
"""
|
|
93
|
+
|
|
67
94
|
hosts: list[Host] = Field(
|
|
68
95
|
default=[Host(name="elastic", username="elastic", password="devpass", scheme="http", host="localhost:9200")], # noqa: S106
|
|
69
96
|
description="List of hosts used for the datastore",
|
|
@@ -74,6 +101,13 @@ class Datastore(BaseModel):
|
|
|
74
101
|
|
|
75
102
|
|
|
76
103
|
class Logging(BaseModel):
|
|
104
|
+
"""Logging configuration for Howler.
|
|
105
|
+
|
|
106
|
+
Defines how and where Howler logs should be output, including console,
|
|
107
|
+
file, and syslog destinations. Also controls log level, format, and
|
|
108
|
+
metric export intervals.
|
|
109
|
+
"""
|
|
110
|
+
|
|
77
111
|
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL", "DISABLED"] = Field(
|
|
78
112
|
default="INFO",
|
|
79
113
|
description="What level of logging should we have?",
|
|
@@ -94,6 +128,12 @@ class Logging(BaseModel):
|
|
|
94
128
|
|
|
95
129
|
|
|
96
130
|
class PasswordRequirement(BaseModel):
|
|
131
|
+
"""Password complexity requirements for internal authentication.
|
|
132
|
+
|
|
133
|
+
Defines the rules for password creation and validation, including
|
|
134
|
+
character type requirements and minimum length.
|
|
135
|
+
"""
|
|
136
|
+
|
|
97
137
|
lower: bool = Field(default=False, description="Password must contain lowercase letters")
|
|
98
138
|
number: bool = Field(default=False, description="Password must contain numbers")
|
|
99
139
|
special: bool = Field(default=False, description="Password must contain special characters")
|
|
@@ -102,6 +142,13 @@ class PasswordRequirement(BaseModel):
|
|
|
102
142
|
|
|
103
143
|
|
|
104
144
|
class Internal(BaseModel):
|
|
145
|
+
"""Internal authentication configuration.
|
|
146
|
+
|
|
147
|
+
Defines settings for Howler's built-in username/password authentication,
|
|
148
|
+
including password requirements and brute-force protection via login
|
|
149
|
+
failure tracking.
|
|
150
|
+
"""
|
|
151
|
+
|
|
105
152
|
enabled: bool = Field(default=True, description="Internal authentication allowed?")
|
|
106
153
|
failure_ttl: int = Field(
|
|
107
154
|
default=60, description="How long to wait after `max_failures` before re-attempting login?"
|
|
@@ -111,6 +158,13 @@ class Internal(BaseModel):
|
|
|
111
158
|
|
|
112
159
|
|
|
113
160
|
class OAuthAutoProperty(BaseModel):
|
|
161
|
+
"""Automatic property assignment based on OAuth attributes.
|
|
162
|
+
|
|
163
|
+
Defines rules for automatically assigning user properties (roles,
|
|
164
|
+
classifications, or access levels) based on pattern matching against
|
|
165
|
+
OAuth provider data.
|
|
166
|
+
"""
|
|
167
|
+
|
|
114
168
|
field: str = Field(description="Field to apply `pattern` to")
|
|
115
169
|
pattern: str = Field(description="Regex pattern for auto-prop assignment")
|
|
116
170
|
type: Literal["access", "classification", "role"] = Field(
|
|
@@ -120,6 +174,13 @@ class OAuthAutoProperty(BaseModel):
|
|
|
120
174
|
|
|
121
175
|
|
|
122
176
|
class OAuthProvider(BaseModel):
|
|
177
|
+
"""OAuth provider configuration.
|
|
178
|
+
|
|
179
|
+
Defines the connection and authentication settings for an OAuth 2.0 provider.
|
|
180
|
+
Includes user auto-creation, group mapping, JWT validation, and various
|
|
181
|
+
OAuth endpoints required for the authentication flow.
|
|
182
|
+
"""
|
|
183
|
+
|
|
123
184
|
auto_create: bool = Field(default=True, description="Auto-create users if they are missing")
|
|
124
185
|
auto_sync: bool = Field(default=False, description="Should we automatically sync with OAuth provider?")
|
|
125
186
|
auto_properties: list[OAuthAutoProperty] = Field(
|
|
@@ -191,6 +252,13 @@ class OAuthProvider(BaseModel):
|
|
|
191
252
|
|
|
192
253
|
|
|
193
254
|
class OAuth(BaseModel):
|
|
255
|
+
"""OAuth authentication configuration.
|
|
256
|
+
|
|
257
|
+
Top-level OAuth settings including enabling/disabling OAuth authentication,
|
|
258
|
+
Gravatar integration, and a dictionary of configured OAuth providers.
|
|
259
|
+
Also controls API key lifetime restrictions for OAuth-authenticated users.
|
|
260
|
+
"""
|
|
261
|
+
|
|
194
262
|
enabled: bool = Field(default=False, description="Enable use of OAuth?")
|
|
195
263
|
gravatar_enabled: bool = Field(default=True, description="Enable gravatar?")
|
|
196
264
|
providers: dict[str, OAuthProvider] = Field(
|
|
@@ -204,6 +272,13 @@ class OAuth(BaseModel):
|
|
|
204
272
|
|
|
205
273
|
|
|
206
274
|
class Auth(BaseModel):
|
|
275
|
+
"""Authentication configuration for Howler.
|
|
276
|
+
|
|
277
|
+
Configures all authentication methods supported by Howler, including
|
|
278
|
+
internal username/password authentication and OAuth providers. Also
|
|
279
|
+
controls API key settings and restrictions.
|
|
280
|
+
"""
|
|
281
|
+
|
|
207
282
|
allow_apikeys: bool = Field(default=True, description="Allow API keys?")
|
|
208
283
|
allow_extended_apikeys: bool = Field(default=True, description="Allow extended API keys?")
|
|
209
284
|
max_apikey_duration_amount: Optional[int] = Field(
|
|
@@ -218,17 +293,34 @@ class Auth(BaseModel):
|
|
|
218
293
|
|
|
219
294
|
|
|
220
295
|
class APMServer(BaseModel):
|
|
221
|
-
"APM server configuration
|
|
296
|
+
"""Application Performance Monitoring (APM) server configuration.
|
|
297
|
+
|
|
298
|
+
Defines the connection details for an external APM server used to
|
|
299
|
+
collect and analyze application performance metrics.
|
|
300
|
+
"""
|
|
222
301
|
|
|
223
302
|
server_url: Optional[str] = Field(default=None, description="URL to API server")
|
|
224
303
|
token: Optional[str] = Field(default=None, description="Authentication token for server")
|
|
225
304
|
|
|
226
305
|
|
|
227
306
|
class Metrics(BaseModel):
|
|
307
|
+
"""Metrics collection configuration.
|
|
308
|
+
|
|
309
|
+
Configures how Howler collects and exports application metrics,
|
|
310
|
+
including integration with external APM servers.
|
|
311
|
+
"""
|
|
312
|
+
|
|
228
313
|
apm_server: APMServer = APMServer()
|
|
229
314
|
|
|
230
315
|
|
|
231
316
|
class Retention(BaseModel):
|
|
317
|
+
"""Hit retention policy configuration.
|
|
318
|
+
|
|
319
|
+
Defines the automatic data retention policy for hits, including
|
|
320
|
+
the maximum age of hits before they are purged and the schedule
|
|
321
|
+
for running the retention cleanup job.
|
|
322
|
+
"""
|
|
323
|
+
|
|
232
324
|
enabled: bool = Field(
|
|
233
325
|
default=True,
|
|
234
326
|
description=(
|
|
@@ -250,13 +342,49 @@ class Retention(BaseModel):
|
|
|
250
342
|
)
|
|
251
343
|
|
|
252
344
|
|
|
345
|
+
class ViewCleanup(BaseModel):
|
|
346
|
+
"""View cleanup job configuration.
|
|
347
|
+
|
|
348
|
+
Defines the schedule and behavior for cleaning up stale dashboard views
|
|
349
|
+
that reference non-existent backend data.
|
|
350
|
+
"""
|
|
351
|
+
|
|
352
|
+
enabled: bool = Field(
|
|
353
|
+
default=True,
|
|
354
|
+
description=(
|
|
355
|
+
"Whether to enable the view cleanup. If enabled, views pinned "
|
|
356
|
+
"to the dashboard that no longer exist in the backend will be cleared."
|
|
357
|
+
),
|
|
358
|
+
)
|
|
359
|
+
crontab: str = Field(
|
|
360
|
+
default="0 0 * * *",
|
|
361
|
+
description="The crontab that denotes how often to run the view_cleanup job",
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
|
|
253
365
|
class System(BaseModel):
|
|
366
|
+
"""System-level configuration for Howler.
|
|
367
|
+
|
|
368
|
+
Defines global system settings including deployment type (production,
|
|
369
|
+
staging, or development) and configuration for automated maintenance
|
|
370
|
+
jobs like data retention and view cleanup.
|
|
371
|
+
"""
|
|
372
|
+
|
|
254
373
|
type: Literal["production", "staging", "development"] = Field(default="development", description="Type of system")
|
|
255
374
|
retention: Retention = Retention()
|
|
256
375
|
"Retention Configuration"
|
|
376
|
+
view_cleanup: ViewCleanup = ViewCleanup()
|
|
377
|
+
"View Cleanup Configuration"
|
|
257
378
|
|
|
258
379
|
|
|
259
380
|
class UI(BaseModel):
|
|
381
|
+
"""User interface and web server configuration.
|
|
382
|
+
|
|
383
|
+
Defines settings for the Howler web UI including Flask configuration,
|
|
384
|
+
session validation, API auditing, static file locations, and WebSocket
|
|
385
|
+
integration for real-time updates.
|
|
386
|
+
"""
|
|
387
|
+
|
|
260
388
|
audit: bool = Field(description="Should API calls be audited and saved to a separate log file?", default=True)
|
|
261
389
|
debug: bool = Field(default=False, description="Enable debugging?")
|
|
262
390
|
static_folder: Optional[str] = Field(
|
|
@@ -281,21 +409,35 @@ class UI(BaseModel):
|
|
|
281
409
|
)
|
|
282
410
|
|
|
283
411
|
|
|
284
|
-
class
|
|
285
|
-
|
|
412
|
+
class Clue(BaseModel):
|
|
413
|
+
"""Clue enrichment service integration configuration.
|
|
414
|
+
|
|
415
|
+
Defines settings for integrating with Clue, an external enrichment
|
|
416
|
+
service that can provide additional context and status information for
|
|
417
|
+
hits displayed in the Howler UI.
|
|
418
|
+
"""
|
|
419
|
+
|
|
420
|
+
enabled: bool = Field(default=False, description="Should clue integration be enabled?")
|
|
286
421
|
|
|
287
422
|
url: str = Field(
|
|
288
423
|
default="http://enrichment-rest.enrichment.svc.cluster.local:5000",
|
|
289
|
-
description="What url should Howler connect to to interact with
|
|
424
|
+
description="What url should Howler connect to to interact with Clue?",
|
|
290
425
|
)
|
|
291
426
|
|
|
292
427
|
status_checks: list[str] = Field(
|
|
293
428
|
default=[],
|
|
294
|
-
description="A list of
|
|
429
|
+
description="A list of clue fetchers that return status results given a Howler ID to show in the UI.",
|
|
295
430
|
)
|
|
296
431
|
|
|
297
432
|
|
|
298
433
|
class Notebook(BaseModel):
|
|
434
|
+
"""Jupyter notebook integration configuration.
|
|
435
|
+
|
|
436
|
+
Defines settings for integrating with nbgallery, a collaborative
|
|
437
|
+
Jupyter notebook platform, allowing users to access and share
|
|
438
|
+
notebooks related to their Howler analysis work.
|
|
439
|
+
"""
|
|
440
|
+
|
|
299
441
|
enabled: bool = Field(default=False, description="Should nbgallery notebook integration be enabled?")
|
|
300
442
|
|
|
301
443
|
scope: Optional[str] = Field(default=None, description="The scope expected by nbgallery for JWTs")
|
|
@@ -306,6 +448,13 @@ class Notebook(BaseModel):
|
|
|
306
448
|
|
|
307
449
|
|
|
308
450
|
class Core(BaseModel):
|
|
451
|
+
"""Core application configuration for Howler.
|
|
452
|
+
|
|
453
|
+
Aggregates all core service configurations including Redis, metrics,
|
|
454
|
+
and external integrations like Clue and nbgallery notebooks.
|
|
455
|
+
Also manages the loading of external plugins.
|
|
456
|
+
"""
|
|
457
|
+
|
|
309
458
|
plugins: set[str] = Field(description="A list of external plugins to load", default=set())
|
|
310
459
|
|
|
311
460
|
metrics: Metrics = Metrics()
|
|
@@ -314,8 +463,8 @@ class Core(BaseModel):
|
|
|
314
463
|
redis: Redis = Redis()
|
|
315
464
|
"Configuration for Redis instances"
|
|
316
465
|
|
|
317
|
-
|
|
318
|
-
"Configuration for
|
|
466
|
+
clue: Clue = Clue()
|
|
467
|
+
"Configuration for Clue Integration"
|
|
319
468
|
|
|
320
469
|
notebook: Notebook = Notebook()
|
|
321
470
|
"Configuration for Notebook Integration"
|
|
@@ -363,13 +512,24 @@ logger.info("Fetching configuration files from %s", ":".join(str(c) for c in con
|
|
|
363
512
|
|
|
364
513
|
|
|
365
514
|
class Config(BaseSettings):
|
|
515
|
+
"""Main Howler configuration model.
|
|
516
|
+
|
|
517
|
+
The root configuration object that aggregates all configuration sections
|
|
518
|
+
including authentication, datastore, logging, system settings, UI, and core
|
|
519
|
+
services. Configuration can be loaded from YAML files or environment variables
|
|
520
|
+
with the HWL_ prefix.
|
|
521
|
+
|
|
522
|
+
Environment variables use double underscores (__) for nested properties.
|
|
523
|
+
For example: HWL_DATASTORE__TYPE=elasticsearch
|
|
524
|
+
"""
|
|
525
|
+
|
|
366
526
|
auth: Auth = Auth()
|
|
367
527
|
core: Core = Core()
|
|
368
528
|
datastore: Datastore = Datastore()
|
|
369
529
|
logging: Logging = Logging()
|
|
370
530
|
system: System = System()
|
|
371
531
|
ui: UI = UI()
|
|
372
|
-
mapping: dict[str, str] = Field(description="Mapping of alert keys to
|
|
532
|
+
mapping: dict[str, str] = Field(description="Mapping of alert keys to clue types", default={})
|
|
373
533
|
|
|
374
534
|
model_config = SettingsConfigDict(
|
|
375
535
|
yaml_file=config_locations,
|
howler/odm/models/howler_data.py
CHANGED
howler/odm/models/lead.py
CHANGED
|
@@ -2,18 +2,9 @@
|
|
|
2
2
|
from typing import Optional
|
|
3
3
|
|
|
4
4
|
from howler import odm
|
|
5
|
-
from howler.odm.howler_enum import HowlerEnum
|
|
6
5
|
from howler.odm.models.localized_label import LocalizedLabel
|
|
7
6
|
|
|
8
7
|
|
|
9
|
-
class Formats(str, HowlerEnum):
|
|
10
|
-
BOREALIS = "borealis"
|
|
11
|
-
MARKDOWN = "markdown"
|
|
12
|
-
|
|
13
|
-
def __str__(self) -> str:
|
|
14
|
-
return self.value
|
|
15
|
-
|
|
16
|
-
|
|
17
8
|
@odm.model(
|
|
18
9
|
index=False,
|
|
19
10
|
store=True,
|
|
@@ -24,7 +15,7 @@ class Lead(odm.Model):
|
|
|
24
15
|
description="An optional icon to use in the tab display for this dossier.", optional=True
|
|
25
16
|
)
|
|
26
17
|
label: LocalizedLabel = odm.Compound(LocalizedLabel, description="Labels for the lead in the UI.")
|
|
27
|
-
format: str = odm.
|
|
18
|
+
format: str = odm.Keyword(description="The format of the lead.")
|
|
28
19
|
content: str = odm.Text(
|
|
29
20
|
description="The data for the content. Could be a link, raw markdown text, or other valid lead format.",
|
|
30
21
|
)
|
howler/odm/models/pivot.py
CHANGED
|
@@ -2,18 +2,9 @@
|
|
|
2
2
|
from typing import Optional
|
|
3
3
|
|
|
4
4
|
from howler import odm
|
|
5
|
-
from howler.odm.howler_enum import HowlerEnum
|
|
6
5
|
from howler.odm.models.localized_label import LocalizedLabel
|
|
7
6
|
|
|
8
7
|
|
|
9
|
-
class Formats(str, HowlerEnum):
|
|
10
|
-
BOREALIS = "borealis"
|
|
11
|
-
LINK = "link"
|
|
12
|
-
|
|
13
|
-
def __str__(self) -> str:
|
|
14
|
-
return self.value
|
|
15
|
-
|
|
16
|
-
|
|
17
8
|
@odm.model(
|
|
18
9
|
index=True,
|
|
19
10
|
store=True,
|
|
@@ -40,8 +31,8 @@ class Pivot(odm.Model):
|
|
|
40
31
|
description="An optional icon to use in the tab display for this dossier.", optional=True
|
|
41
32
|
)
|
|
42
33
|
label: LocalizedLabel = odm.Compound(LocalizedLabel, description="Labels for the pivot in the UI.")
|
|
43
|
-
value: str = odm.Keyword(description="The link/
|
|
44
|
-
format: str = odm.
|
|
34
|
+
value: str = odm.Keyword(description="The link/plugin information to pivot on.")
|
|
35
|
+
format: str = odm.Keyword(description="The format of the pivot.")
|
|
45
36
|
mappings: list[Mapping] = odm.List(
|
|
46
37
|
odm.Compound(Mapping),
|
|
47
38
|
default=[],
|
howler/odm/random_data.py
CHANGED
|
@@ -820,7 +820,7 @@ def wipe_dossiers(ds: HowlerDatastore):
|
|
|
820
820
|
|
|
821
821
|
def setup_hits(ds):
|
|
822
822
|
"Set up hits index"
|
|
823
|
-
os.environ["ELASTIC_HIT_SHARDS"] = "
|
|
823
|
+
os.environ["ELASTIC_HIT_SHARDS"] = "1"
|
|
824
824
|
os.environ["ELASTIC_HIT_REPLICAS"] = "1"
|
|
825
825
|
ds.hit.fix_shards()
|
|
826
826
|
ds.hit.fix_replicas()
|
howler/security/__init__.py
CHANGED
|
@@ -220,8 +220,8 @@ class api_login(object): # noqa: D101, N801
|
|
|
220
220
|
user_id=user.get("uname", None),
|
|
221
221
|
)
|
|
222
222
|
|
|
223
|
-
if request.path.startswith("/api/v1/
|
|
224
|
-
logger.debug("Bypassing quota limits for
|
|
223
|
+
if request.path.startswith("/api/v1/clue"):
|
|
224
|
+
logger.debug("Bypassing quota limits for clue enrichment")
|
|
225
225
|
elif self.enforce_quota:
|
|
226
226
|
# Check current user quota
|
|
227
227
|
flsk_session["quota_user"] = user["uname"]
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
from typing import Any, Union
|
|
2
|
+
|
|
1
3
|
from howler.common.loader import datastore
|
|
2
4
|
from howler.common.logging import get_logger
|
|
5
|
+
from howler.datastore.exceptions import SearchException
|
|
3
6
|
from howler.datastore.operations import OdmUpdateOperation
|
|
4
7
|
from howler.odm.models.analytic import Analytic
|
|
5
8
|
from howler.odm.models.hit import Hit
|
|
@@ -36,6 +39,34 @@ def update_analytic(
|
|
|
36
39
|
return result
|
|
37
40
|
|
|
38
41
|
|
|
42
|
+
def get_matching_analytics(hits: Union[list[Hit], list[dict[str, Any]]]) -> list[Analytic]:
|
|
43
|
+
"""Get a list of matching analytics for the given list of hits.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
hits (Union[list[Hit], list[dict[str, Any]]]): A list of Hit objects or dictionaries representing hits.
|
|
47
|
+
Returns:
|
|
48
|
+
list[Analytic]: A list of Analytic objects that match the analytics referenced in the hits.
|
|
49
|
+
"""
|
|
50
|
+
if len(hits) < 1:
|
|
51
|
+
return []
|
|
52
|
+
|
|
53
|
+
storage = datastore()
|
|
54
|
+
|
|
55
|
+
analytic_names: set[str] = set()
|
|
56
|
+
for hit in hits:
|
|
57
|
+
analytic_names.add(f'"{sanitize_lucene_query(hit["howler"]["analytic"])}"')
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
existing_analytics: list[Analytic] = storage.analytic.search(
|
|
61
|
+
f'name:({" OR ".join(analytic_names)})', as_obj=True
|
|
62
|
+
)["items"]
|
|
63
|
+
|
|
64
|
+
return existing_analytics
|
|
65
|
+
except SearchException:
|
|
66
|
+
logger.exception("Exception on analytic matching")
|
|
67
|
+
return []
|
|
68
|
+
|
|
69
|
+
|
|
39
70
|
def save_from_hit(hit: Hit, user: User):
|
|
40
71
|
"""Save updates to an analytic based on a new hit that has been created
|
|
41
72
|
|
|
@@ -117,11 +117,11 @@ def get_configuration(user: User, **kwargs):
|
|
|
117
117
|
},
|
|
118
118
|
"mapping": config.mapping,
|
|
119
119
|
"features": {
|
|
120
|
-
"
|
|
120
|
+
"clue": config.core.clue.enabled,
|
|
121
121
|
"notebook": config.core.notebook.enabled,
|
|
122
122
|
**plugin_features,
|
|
123
123
|
},
|
|
124
|
-
"
|
|
124
|
+
"clue": {"status_checks": config.core.clue.status_checks},
|
|
125
125
|
},
|
|
126
126
|
"c12nDef": classification_definition,
|
|
127
127
|
"indexes": list_all_fields("admin" in user["type"] if user is not None else False),
|