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,558 @@
|
|
|
1
|
+
# mypy: ignore-errors
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Literal, Optional
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
from pydantic_settings import BaseSettings, PydanticBaseSettingsSource, SettingsConfigDict, YamlConfigSettingsSource
|
|
9
|
+
|
|
10
|
+
from howler.common.logging.format import HWL_DATE_FORMAT, HWL_LOG_FORMAT
|
|
11
|
+
|
|
12
|
+
APP_NAME = os.environ.get("APP_NAME", "howler")
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger("howler.odm.models.config")
|
|
15
|
+
logger.setLevel(logging.INFO)
|
|
16
|
+
console = logging.StreamHandler()
|
|
17
|
+
console.setLevel(logging.INFO)
|
|
18
|
+
console.setFormatter(logging.Formatter(HWL_LOG_FORMAT, HWL_DATE_FORMAT))
|
|
19
|
+
logger.addHandler(console)
|
|
20
|
+
|
|
21
|
+
|
|
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
|
+
|
|
29
|
+
host: str = Field(description="Hostname of Redis instance")
|
|
30
|
+
port: int = Field(description="Port of Redis instance")
|
|
31
|
+
|
|
32
|
+
|
|
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
|
+
|
|
41
|
+
nonpersistent: RedisServer = Field(
|
|
42
|
+
default=RedisServer(host="127.0.0.1", port=6379), description="A volatile Redis instance"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
persistent: RedisServer = Field(
|
|
46
|
+
default=RedisServer(host="127.0.0.1", port=6380), description="A persistent Redis instance"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
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
|
+
|
|
59
|
+
name: str = Field(description="Name of the host")
|
|
60
|
+
username: Optional[str] = Field(description="Username to login with", default=None)
|
|
61
|
+
password: Optional[str] = Field(description="Password to login with", default=None)
|
|
62
|
+
apikey_id: Optional[str] = Field(description="ID of the API Key to use when connecting", default=None)
|
|
63
|
+
apikey_secret: Optional[str] = Field(description="Secret data of the API Key to use when connecting", default=None)
|
|
64
|
+
scheme: Optional[str] = Field(description="Scheme to use when connecting", default="http")
|
|
65
|
+
host: str = Field(description="URL to connect to")
|
|
66
|
+
|
|
67
|
+
def __repr__(self):
|
|
68
|
+
result = ""
|
|
69
|
+
|
|
70
|
+
if self.scheme:
|
|
71
|
+
result += f"{self.scheme}://"
|
|
72
|
+
|
|
73
|
+
username = os.getenv(f"{self.name.upper()}_HOST_USERNAME", self.username)
|
|
74
|
+
password = os.getenv(f"{self.name.upper()}_HOST_PASSWORD", self.password)
|
|
75
|
+
|
|
76
|
+
if username and password:
|
|
77
|
+
result += f"{username}:{password}@"
|
|
78
|
+
|
|
79
|
+
result += self.host
|
|
80
|
+
|
|
81
|
+
return result
|
|
82
|
+
|
|
83
|
+
def __str__(self):
|
|
84
|
+
return self.__repr__()
|
|
85
|
+
|
|
86
|
+
|
|
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
|
+
|
|
94
|
+
hosts: list[Host] = Field(
|
|
95
|
+
default=[Host(name="elastic", username="elastic", password="devpass", scheme="http", host="localhost:9200")], # noqa: S106
|
|
96
|
+
description="List of hosts used for the datastore",
|
|
97
|
+
)
|
|
98
|
+
type: Literal["elasticsearch"] = Field(
|
|
99
|
+
default="elasticsearch", description="Type of application used for the datastore"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
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
|
+
|
|
111
|
+
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL", "DISABLED"] = Field(
|
|
112
|
+
default="INFO",
|
|
113
|
+
description="What level of logging should we have?",
|
|
114
|
+
)
|
|
115
|
+
log_to_console: bool = Field(default=True, description="Should we log to console?")
|
|
116
|
+
log_to_file: bool = Field(default=False, description="Should we log to files on the server?")
|
|
117
|
+
log_directory: str = Field(
|
|
118
|
+
default=f"/var/log/{APP_NAME.replace('-dev', '')}/",
|
|
119
|
+
description="If `log_to_file: true`, what is the directory to store logs?",
|
|
120
|
+
)
|
|
121
|
+
log_to_syslog: bool = Field(default=False, description="Should logs be sent to a syslog server?")
|
|
122
|
+
syslog_host: str = Field(
|
|
123
|
+
default="localhost", description="If `log_to_syslog: true`, provide hostname/IP of the syslog server?"
|
|
124
|
+
)
|
|
125
|
+
syslog_port: int = Field(default=514, description="If `log_to_syslog: true`, provide port of the syslog server?")
|
|
126
|
+
export_interval: int = Field(default=5, description="How often, in seconds, should counters log their values?")
|
|
127
|
+
log_as_json: bool = Field(default=True, description="Log in JSON format?")
|
|
128
|
+
|
|
129
|
+
|
|
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
|
+
|
|
137
|
+
lower: bool = Field(default=False, description="Password must contain lowercase letters")
|
|
138
|
+
number: bool = Field(default=False, description="Password must contain numbers")
|
|
139
|
+
special: bool = Field(default=False, description="Password must contain special characters")
|
|
140
|
+
upper: bool = Field(default=False, description="Password must contain uppercase letters")
|
|
141
|
+
min_length: int = Field(default=12, description="Minimum password length")
|
|
142
|
+
|
|
143
|
+
|
|
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
|
+
|
|
152
|
+
enabled: bool = Field(default=True, description="Internal authentication allowed?")
|
|
153
|
+
failure_ttl: int = Field(
|
|
154
|
+
default=60, description="How long to wait after `max_failures` before re-attempting login?"
|
|
155
|
+
)
|
|
156
|
+
max_failures: int = Field(default=5, description="Maximum number of fails allowed before timeout")
|
|
157
|
+
password_requirements: PasswordRequirement = PasswordRequirement()
|
|
158
|
+
|
|
159
|
+
|
|
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
|
+
|
|
168
|
+
field: str = Field(description="Field to apply `pattern` to")
|
|
169
|
+
pattern: str = Field(description="Regex pattern for auto-prop assignment")
|
|
170
|
+
type: Literal["access", "classification", "role"] = Field(
|
|
171
|
+
description="Type of property assignment on pattern match",
|
|
172
|
+
)
|
|
173
|
+
value: str = Field(description="Assigned property value")
|
|
174
|
+
|
|
175
|
+
|
|
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
|
+
|
|
184
|
+
auto_create: bool = Field(default=True, description="Auto-create users if they are missing")
|
|
185
|
+
auto_sync: bool = Field(default=False, description="Should we automatically sync with OAuth provider?")
|
|
186
|
+
auto_properties: list[OAuthAutoProperty] = Field(
|
|
187
|
+
default=[],
|
|
188
|
+
description="Automatic role and classification assignments",
|
|
189
|
+
)
|
|
190
|
+
uid_randomize: bool = Field(
|
|
191
|
+
default=False,
|
|
192
|
+
description="Should we generate a random username for the authenticated user?",
|
|
193
|
+
)
|
|
194
|
+
uid_randomize_digits: int = Field(
|
|
195
|
+
default=0,
|
|
196
|
+
description="How many digits should we add at the end of the username?",
|
|
197
|
+
)
|
|
198
|
+
uid_randomize_delimiter: str = Field(
|
|
199
|
+
default="-",
|
|
200
|
+
description="What is the delimiter used by the random name generator?",
|
|
201
|
+
)
|
|
202
|
+
uid_regex: Optional[str] = Field(
|
|
203
|
+
default=None,
|
|
204
|
+
description="Regex used to parse an email address and capture parts to create a user ID out of it",
|
|
205
|
+
)
|
|
206
|
+
uid_format: Optional[str] = Field(
|
|
207
|
+
default=None,
|
|
208
|
+
description="Format of the user ID based on the captured parts from the regex",
|
|
209
|
+
)
|
|
210
|
+
client_id: Optional[str] = Field(
|
|
211
|
+
default=None,
|
|
212
|
+
description="ID of your application to authenticate to the OAuth provider",
|
|
213
|
+
)
|
|
214
|
+
client_secret: Optional[str] = Field(
|
|
215
|
+
default=None,
|
|
216
|
+
description="Password to your application to authenticate to the OAuth provider",
|
|
217
|
+
)
|
|
218
|
+
request_token_url: Optional[str] = Field(default=None, description="URL to request token")
|
|
219
|
+
request_token_params: Optional[str] = Field(default=None, description="Parameters to request token")
|
|
220
|
+
required_groups: list[str] = Field(
|
|
221
|
+
default=[],
|
|
222
|
+
description="The groups the JWT must contain in order to allow access",
|
|
223
|
+
)
|
|
224
|
+
role_map: dict[str, str] = Field(
|
|
225
|
+
default={},
|
|
226
|
+
description="A mapping of OAuth groups to howler roles",
|
|
227
|
+
)
|
|
228
|
+
access_token_url: Optional[str] = Field(default=None, description="URL to get access token")
|
|
229
|
+
access_token_params: Optional[str] = Field(default=None, description="Parameters to get access token")
|
|
230
|
+
authorize_url: Optional[str] = Field(default=None, description="URL used to authorize access to a resource")
|
|
231
|
+
authorize_params: Optional[str] = Field(
|
|
232
|
+
default=None, description="Parameters used to authorize access to a resource"
|
|
233
|
+
)
|
|
234
|
+
api_base_url: Optional[str] = Field(default=None, description="Base URL for downloading the user's and groups info")
|
|
235
|
+
audience: Optional[str] = Field(
|
|
236
|
+
default=None,
|
|
237
|
+
description="The audience to validate against. Only must be set if audience is different than the client id.",
|
|
238
|
+
)
|
|
239
|
+
scope: str = Field(description="The scope to validate against")
|
|
240
|
+
picture_url: Optional[str] = Field(default=None, description="URL for downloading the user's profile")
|
|
241
|
+
groups_url: Optional[str] = Field(
|
|
242
|
+
default=None,
|
|
243
|
+
description="URL for accessing additional data about the user's groups",
|
|
244
|
+
)
|
|
245
|
+
groups_key: Optional[str] = Field(
|
|
246
|
+
default=None,
|
|
247
|
+
description="Path to the list of groups in the response returned from groups_url",
|
|
248
|
+
)
|
|
249
|
+
iss: Optional[str] = Field(default=None, description="Optional issuer field for JWT validation")
|
|
250
|
+
jwks_uri: str = Field(description="URL used to verify if a returned JWKS token is valid")
|
|
251
|
+
user_get: Optional[str] = Field(default=None, description="Path from the base_url to fetch the user info")
|
|
252
|
+
|
|
253
|
+
|
|
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
|
+
|
|
262
|
+
enabled: bool = Field(default=False, description="Enable use of OAuth?")
|
|
263
|
+
gravatar_enabled: bool = Field(default=True, description="Enable gravatar?")
|
|
264
|
+
providers: dict[str, OAuthProvider] = Field(
|
|
265
|
+
default={},
|
|
266
|
+
description="OAuth provider configuration",
|
|
267
|
+
)
|
|
268
|
+
strict_apikeys: bool = Field(
|
|
269
|
+
description="Only allow apikeys that last as long as the access token used to log in",
|
|
270
|
+
default=False,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
|
|
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
|
+
|
|
282
|
+
allow_apikeys: bool = Field(default=True, description="Allow API keys?")
|
|
283
|
+
allow_extended_apikeys: bool = Field(default=True, description="Allow extended API keys?")
|
|
284
|
+
max_apikey_duration_amount: Optional[int] = Field(
|
|
285
|
+
default=None, description="Amount of unit of maximum duration for API keys"
|
|
286
|
+
)
|
|
287
|
+
max_apikey_duration_unit: Optional[Literal["seconds", "minutes", "hours", "days", "weeks"]] = Field(
|
|
288
|
+
default=None,
|
|
289
|
+
description="Unit of maximum duration for API keys",
|
|
290
|
+
)
|
|
291
|
+
internal: Internal = Internal()
|
|
292
|
+
oauth: OAuth = OAuth()
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
class APMServer(BaseModel):
|
|
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
|
+
"""
|
|
301
|
+
|
|
302
|
+
server_url: Optional[str] = Field(default=None, description="URL to API server")
|
|
303
|
+
token: Optional[str] = Field(default=None, description="Authentication token for server")
|
|
304
|
+
|
|
305
|
+
|
|
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
|
+
|
|
313
|
+
apm_server: APMServer = APMServer()
|
|
314
|
+
|
|
315
|
+
|
|
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
|
+
|
|
324
|
+
enabled: bool = Field(
|
|
325
|
+
default=True,
|
|
326
|
+
description=(
|
|
327
|
+
"Whether to enable the hit retention limit. If enabled, hits will "
|
|
328
|
+
"be purged after the specified duration."
|
|
329
|
+
),
|
|
330
|
+
)
|
|
331
|
+
limit_unit: Literal["days", "seconds", "microseconds", "milliseconds", "minutes", "hours", "weeks"] = Field(
|
|
332
|
+
description="The unit to use when computing the retention limit",
|
|
333
|
+
default="days",
|
|
334
|
+
)
|
|
335
|
+
limit_amount: int = Field(
|
|
336
|
+
default=350,
|
|
337
|
+
description="The number of limit_units to use when computing the retention limit",
|
|
338
|
+
)
|
|
339
|
+
crontab: str = Field(
|
|
340
|
+
default="0 0 * * *",
|
|
341
|
+
description="The crontab that denotes how often to run the retention job",
|
|
342
|
+
)
|
|
343
|
+
|
|
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
|
+
|
|
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
|
+
|
|
373
|
+
type: Literal["production", "staging", "development"] = Field(default="development", description="Type of system")
|
|
374
|
+
retention: Retention = Retention()
|
|
375
|
+
"Retention Configuration"
|
|
376
|
+
view_cleanup: ViewCleanup = ViewCleanup()
|
|
377
|
+
"View Cleanup Configuration"
|
|
378
|
+
|
|
379
|
+
|
|
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
|
+
|
|
388
|
+
audit: bool = Field(description="Should API calls be audited and saved to a separate log file?", default=True)
|
|
389
|
+
debug: bool = Field(default=False, description="Enable debugging?")
|
|
390
|
+
static_folder: Optional[str] = Field(
|
|
391
|
+
default=os.path.dirname(__file__) + "/../../../static",
|
|
392
|
+
description="The directory where static assets are stored.",
|
|
393
|
+
)
|
|
394
|
+
discover_url: Optional[str] = Field(default=None, description="Discovery URL")
|
|
395
|
+
enforce_quota: bool = Field(default=True, description="Enforce the user's quotas?")
|
|
396
|
+
secret_key: str = Field(
|
|
397
|
+
default=os.environ.get("FLASK_SECRET_KEY", "This is the default flask secret key... you should change this!"),
|
|
398
|
+
description="Flask secret key to store cookies, etc.",
|
|
399
|
+
)
|
|
400
|
+
validate_session_ip: bool = Field(
|
|
401
|
+
default=True, description="Validate if the session IP matches the IP the session was created from"
|
|
402
|
+
)
|
|
403
|
+
validate_session_useragent: bool = Field(
|
|
404
|
+
default=True, description="Validate if the session useragent matches the useragent the session was created with"
|
|
405
|
+
)
|
|
406
|
+
websocket_url: Optional[str] = Field(
|
|
407
|
+
default=None,
|
|
408
|
+
description="The url to hit when emitting websocket events on the cluster",
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
|
|
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?")
|
|
421
|
+
|
|
422
|
+
url: str = Field(
|
|
423
|
+
default="http://enrichment-rest.enrichment.svc.cluster.local:5000",
|
|
424
|
+
description="What url should Howler connect to to interact with Clue?",
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
status_checks: list[str] = Field(
|
|
428
|
+
default=[],
|
|
429
|
+
description="A list of clue fetchers that return status results given a Howler ID to show in the UI.",
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
|
|
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
|
+
|
|
441
|
+
enabled: bool = Field(default=False, description="Should nbgallery notebook integration be enabled?")
|
|
442
|
+
|
|
443
|
+
scope: Optional[str] = Field(default=None, description="The scope expected by nbgallery for JWTs")
|
|
444
|
+
|
|
445
|
+
url: str = Field(
|
|
446
|
+
default="http://nbgallery.nbgallery.svc.cluster.local:3000", description="The url to connect to nbgallery at"
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
|
|
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
|
+
|
|
458
|
+
plugins: set[str] = Field(description="A list of external plugins to load", default=set())
|
|
459
|
+
|
|
460
|
+
metrics: Metrics = Metrics()
|
|
461
|
+
"Configuration for Metrics Collection"
|
|
462
|
+
|
|
463
|
+
redis: Redis = Redis()
|
|
464
|
+
"Configuration for Redis instances"
|
|
465
|
+
|
|
466
|
+
clue: Clue = Clue()
|
|
467
|
+
"Configuration for Clue Integration"
|
|
468
|
+
|
|
469
|
+
notebook: Notebook = Notebook()
|
|
470
|
+
"Configuration for Notebook Integration"
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
root_path = Path("/etc") / APP_NAME.replace("-dev", "").replace("-stg", "")
|
|
474
|
+
|
|
475
|
+
config_locations = [
|
|
476
|
+
root_path / "conf" / "config.yml",
|
|
477
|
+
root_path / "conf" / "mappings.yml",
|
|
478
|
+
Path(os.environ.get("HWL_CONF_FOLDER", root_path)) / "config.yml",
|
|
479
|
+
Path(os.environ.get("HWL_CONF_FOLDER", root_path)) / "mappings.yml",
|
|
480
|
+
]
|
|
481
|
+
|
|
482
|
+
if os.getenv("AZURE_TEST_CONFIG", None) is not None:
|
|
483
|
+
import re
|
|
484
|
+
|
|
485
|
+
logger.info("Azure build environment detected, adding additional config path")
|
|
486
|
+
|
|
487
|
+
work_dir_parent = Path("/__w")
|
|
488
|
+
work_dir: Path | None = None
|
|
489
|
+
for sub_path in work_dir_parent.iterdir():
|
|
490
|
+
if not sub_path.is_dir():
|
|
491
|
+
continue
|
|
492
|
+
|
|
493
|
+
logger.info("Testing sub path %s", sub_path)
|
|
494
|
+
|
|
495
|
+
if re.match(r"\d+", str(sub_path.name)):
|
|
496
|
+
work_dir = work_dir_parent / sub_path
|
|
497
|
+
|
|
498
|
+
if work_dir is not None:
|
|
499
|
+
logger.info("Subpath %s exists, checking for test path", work_dir)
|
|
500
|
+
test_config_path = work_dir / "s" / "test" / "config" / "config.yml"
|
|
501
|
+
|
|
502
|
+
if test_config_path.exists():
|
|
503
|
+
config_locations.append(test_config_path)
|
|
504
|
+
logger.info("Path %s added as config path", test_config_path)
|
|
505
|
+
break
|
|
506
|
+
|
|
507
|
+
logger.error("Config path not found at path %s", test_config_path)
|
|
508
|
+
logger.info("Available files:\n%s", "\n".join(sorted(str(path) for path in (work_dir / "s").glob("**/*"))))
|
|
509
|
+
work_dir = None
|
|
510
|
+
|
|
511
|
+
logger.info("Fetching configuration files from %s", ":".join(str(c) for c in config_locations))
|
|
512
|
+
|
|
513
|
+
|
|
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
|
+
|
|
526
|
+
auth: Auth = Auth()
|
|
527
|
+
core: Core = Core()
|
|
528
|
+
datastore: Datastore = Datastore()
|
|
529
|
+
logging: Logging = Logging()
|
|
530
|
+
system: System = System()
|
|
531
|
+
ui: UI = UI()
|
|
532
|
+
mapping: dict[str, str] = Field(description="Mapping of alert keys to clue types", default={})
|
|
533
|
+
|
|
534
|
+
model_config = SettingsConfigDict(
|
|
535
|
+
yaml_file=config_locations,
|
|
536
|
+
yaml_file_encoding="utf-8",
|
|
537
|
+
strict=True,
|
|
538
|
+
env_nested_delimiter="__",
|
|
539
|
+
env_prefix="hwl_",
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
@classmethod
|
|
543
|
+
def settings_customise_sources(
|
|
544
|
+
cls, # noqa: ANN102
|
|
545
|
+
*args, # noqa: ANN002
|
|
546
|
+
**kwargs, # noqa: ANN002, ANN102
|
|
547
|
+
) -> tuple[PydanticBaseSettingsSource, ...]:
|
|
548
|
+
"Adds a YamlConfigSettingsSource object at the end of the settings_customize_sources response."
|
|
549
|
+
return (*super().settings_customise_sources(*args, **kwargs), YamlConfigSettingsSource(cls))
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
config = Config()
|
|
553
|
+
|
|
554
|
+
if __name__ == "__main__":
|
|
555
|
+
# When executed, the config model will print the default values of the configuration
|
|
556
|
+
import yaml
|
|
557
|
+
|
|
558
|
+
print(yaml.safe_dump(Config().model_dump(mode="json"))) # noqa: T201
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from typing import Literal, Optional, Union
|
|
2
|
+
|
|
3
|
+
from howler import odm
|
|
4
|
+
from howler.odm.models.lead import Lead
|
|
5
|
+
from howler.odm.models.pivot import Pivot
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@odm.model(
|
|
9
|
+
index=True,
|
|
10
|
+
store=True,
|
|
11
|
+
description="The dossier object stores individual tabs/fields for a given alert.",
|
|
12
|
+
)
|
|
13
|
+
class Dossier(odm.Model):
|
|
14
|
+
dossier_id: str = odm.UUID(description="A UUID for this dossier.")
|
|
15
|
+
leads: list[Lead] = odm.List(
|
|
16
|
+
odm.Compound(Lead),
|
|
17
|
+
default=[],
|
|
18
|
+
description="A list of the leads to show when the query matches the given alert.",
|
|
19
|
+
)
|
|
20
|
+
pivots: list[Pivot] = odm.List(
|
|
21
|
+
odm.Compound(Pivot),
|
|
22
|
+
default=[],
|
|
23
|
+
description="A list of the pivots to show when the query matches the given alert.",
|
|
24
|
+
)
|
|
25
|
+
title: str = odm.Keyword(description="The title of this dossier.")
|
|
26
|
+
owner: str = odm.Keyword(description="The person to whom this dossier belongs.")
|
|
27
|
+
query: Optional[str] = odm.Keyword(
|
|
28
|
+
description="The query that controls when this dossier should be shown in the UI.", optional=True, default=None
|
|
29
|
+
)
|
|
30
|
+
type: Union[Literal["personal"], Literal["global"]] = odm.Enum(
|
|
31
|
+
values=["personal", "global"],
|
|
32
|
+
description="The type of dossier - personal or global.",
|
|
33
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# mypy: ignore-errors
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from howler import odm
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@odm.model(
|
|
8
|
+
index=True,
|
|
9
|
+
store=True,
|
|
10
|
+
description="The agent fields contain the data about the software entity, "
|
|
11
|
+
"if any, that collects, detects, or observes events on a host, or takes measurements on a host.",
|
|
12
|
+
)
|
|
13
|
+
class Agent(odm.Model):
|
|
14
|
+
id: Optional[str] = odm.Optional(odm.Keyword(description="Unique identifier of this agent (if one exists)."))
|
|
15
|
+
name: Optional[str] = odm.Optional(odm.Keyword(description="Custom name of the agent."))
|
|
16
|
+
type: Optional[str] = odm.Optional(odm.Keyword(description="Type of the agent."))
|
|
17
|
+
version: Optional[str] = odm.Optional(odm.Keyword(description="Version of the agent."))
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from howler import odm
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@odm.model(
|
|
7
|
+
index=True,
|
|
8
|
+
store=True,
|
|
9
|
+
description=(
|
|
10
|
+
"Observer is defined as a special network, security, or application device used to detect, observe, "
|
|
11
|
+
"or create network, sercurity, or application event metrics"
|
|
12
|
+
),
|
|
13
|
+
)
|
|
14
|
+
class AS(odm.Model):
|
|
15
|
+
number: Optional[int] = odm.Optional(odm.Integer(description="Unique number allocated to the autonomous system"))
|
|
16
|
+
organization_name: Optional[str] = odm.Optional(odm.Keyword(description="Organization name"))
|