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/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 * 6)
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.borealis.enabled:
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": "Borealis", "fr": "Borealis"},
265
- "format": "borealis",
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": "Borealis", "fr": "Borealis"},
277
- "format": "borealis",
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
  }
@@ -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 Borealis(BaseModel):
285
- enabled: bool = Field(default=False, description="Should borealis integration be enabled?")
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 Borealis?",
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 borealis fetchers that return status results given a Howler ID to show in the UI.",
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
- borealis: Borealis = Borealis()
318
- "Configuration for Borealis Integration"
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 borealis type", default={})
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,
@@ -118,7 +118,8 @@ class Link(odm.Model):
118
118
  description=(
119
119
  "The icon to show. Either an ID corresponding to an "
120
120
  "analytical platform application, or an external link."
121
- )
121
+ ),
122
+ optional=True,
122
123
  )
123
124
 
124
125
 
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.Enum(values=Formats, description="The format of the lead. ")
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
  )
@@ -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/borealis id to pivot on.")
44
- format: str = odm.Enum(values=Formats, description="The format of the pivot.")
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"] = "12"
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()
@@ -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/borealis"):
224
- logger.debug("Bypassing quota limits for borealis enrichment")
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
- "borealis": config.core.borealis.enabled,
120
+ "clue": config.core.clue.enabled,
121
121
  "notebook": config.core.notebook.enabled,
122
122
  **plugin_features,
123
123
  },
124
- "borealis": {"status_checks": config.core.borealis.status_checks},
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),