future-framework 1.1.0__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.
- future/__init__.py +3 -0
- future/application.py +656 -0
- future/authentication/Auth0Authentication.py +9 -0
- future/authentication/Authentication.py +11 -0
- future/authentication/AzureADAuthentication.py +9 -0
- future/authentication/BasicAuthentication.py +9 -0
- future/authentication/KerberosAuthentication.py +9 -0
- future/authentication/KeycloakAuthentication.py +9 -0
- future/authentication/OAuth2Authentication.py +9 -0
- future/authentication/OpenIdConnectAuthentication.py +9 -0
- future/authentication/SAMLAuthentication.py +9 -0
- future/cli/__init__.py +1 -0
- future/cli/main.py +608 -0
- future/cli/stubs.py +114 -0
- future/controllers/__init__.py +4 -0
- future/controllers/base.py +8 -0
- future/controllers/builtins.py +41 -0
- future/controllers/graphql.py +23 -0
- future/controllers/openapi.py +224 -0
- future/databases/Clickhouse.py +176 -0
- future/databases/Connections.py +20 -0
- future/databases/Database.py +66 -0
- future/databases/Elasticsearch.py +146 -0
- future/databases/MongoDB.py +177 -0
- future/databases/MySQL.py +215 -0
- future/databases/Postgres.py +214 -0
- future/databases/Redis.py +146 -0
- future/databases/SQLite.py +219 -0
- future/exceptions.py +27 -0
- future/graphql/__init__.py +1 -0
- future/graphql/schema.py +148 -0
- future/lifespan.py +70 -0
- future/logger.py +53 -0
- future/middleware/Middleware.py +201 -0
- future/middleware/SessionMiddleware.py +77 -0
- future/middleware/__init__.py +21 -0
- future/migrations/Blueprint.py +54 -0
- future/migrations/Column.py +33 -0
- future/migrations/Migration.py +11 -0
- future/migrations/MigrationGenerator.py +137 -0
- future/migrations/Migrator.py +72 -0
- future/migrations/Schema.py +21 -0
- future/models/__init__.py +1 -0
- future/models/model.py +224 -0
- future/openapi.py +233 -0
- future/plugins/ElasticsearchPlugin.py +496 -0
- future/plugins/__init__.py +9 -0
- future/request.py +143 -0
- future/response.py +201 -0
- future/routing.py +342 -0
- future/seeds/SeedGenerator.py +95 -0
- future/seeds/SeedRunner.py +42 -0
- future/seeds/Seeder.py +9 -0
- future/settings.py +94 -0
- future/tasks/__init__.py +14 -0
- future/tasks/scheduler.py +267 -0
- future/testing/__init__.py +1 -0
- future/testing/client.py +135 -0
- future/types.py +47 -0
- future_framework-1.1.0.dist-info/METADATA +68 -0
- future_framework-1.1.0.dist-info/RECORD +64 -0
- future_framework-1.1.0.dist-info/WHEEL +4 -0
- future_framework-1.1.0.dist-info/entry_points.txt +3 -0
- future_framework-1.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
from future.plugins import Plugin
|
|
2
|
+
from elasticsearch import Elasticsearch, helpers
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ElasticsearchPlugin(Plugin):
|
|
6
|
+
# __type__ = "database"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
"""Plugin/Database class for Elasticsearch operations - provides ORM-like interface"""
|
|
10
|
+
|
|
11
|
+
def __init__(self, host, username, password):
|
|
12
|
+
self.es = Elasticsearch(host, basic_auth=(username, password))
|
|
13
|
+
self.host = host
|
|
14
|
+
self._username = username
|
|
15
|
+
self._password = password
|
|
16
|
+
#self.auth = ES_AUTH
|
|
17
|
+
|
|
18
|
+
def create_index(self, index_name, mapping=None, settings=None):
|
|
19
|
+
"""Create a new index with optional mapping and settings"""
|
|
20
|
+
try:
|
|
21
|
+
payload = {}
|
|
22
|
+
if mapping:
|
|
23
|
+
payload["mappings"] = mapping
|
|
24
|
+
if settings:
|
|
25
|
+
payload["settings"] = settings
|
|
26
|
+
|
|
27
|
+
result = self.es.indices.create(index=index_name, body=payload)
|
|
28
|
+
return {"status": "Index created", "index": index_name, "result": result}
|
|
29
|
+
except Exception as e:
|
|
30
|
+
return {"error": str(e)}
|
|
31
|
+
|
|
32
|
+
def delete_index(self, index_name):
|
|
33
|
+
"""Delete an index"""
|
|
34
|
+
try:
|
|
35
|
+
result = self.es.indices.delete(index=index_name)
|
|
36
|
+
return {"status": f"Index {index_name} deleted", "result": result}
|
|
37
|
+
except Exception as e:
|
|
38
|
+
return {"error": str(e)}
|
|
39
|
+
|
|
40
|
+
def index_exists(self, index_name):
|
|
41
|
+
"""Check if an index exists"""
|
|
42
|
+
try:
|
|
43
|
+
return self.es.indices.exists(index=index_name)
|
|
44
|
+
except Exception:
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
def get_index_info(self, index_name=None):
|
|
48
|
+
"""Get information about an index or all indices"""
|
|
49
|
+
try:
|
|
50
|
+
if index_name:
|
|
51
|
+
result = self.es.indices.get(index=index_name)
|
|
52
|
+
else:
|
|
53
|
+
result = self.es.cat.indices(format="json")
|
|
54
|
+
return result
|
|
55
|
+
except Exception as e:
|
|
56
|
+
return {"error": str(e)}
|
|
57
|
+
|
|
58
|
+
def create_alias(self, index_name, alias_name):
|
|
59
|
+
"""Create an alias for an index"""
|
|
60
|
+
try:
|
|
61
|
+
result = self.es.indices.put_alias(index=index_name, name=alias_name)
|
|
62
|
+
return {"status": f"Alias {alias_name} created for index {index_name}", "result": result}
|
|
63
|
+
except Exception as e:
|
|
64
|
+
return {"error": str(e)}
|
|
65
|
+
|
|
66
|
+
def delete_alias(self, index_name, alias_name):
|
|
67
|
+
"""Delete an alias"""
|
|
68
|
+
try:
|
|
69
|
+
result = self.es.indices.delete_alias(index=index_name, name=alias_name)
|
|
70
|
+
return {"status": f"Alias {alias_name} removed from index {index_name}", "result": result}
|
|
71
|
+
except Exception as e:
|
|
72
|
+
return {"error": str(e)}
|
|
73
|
+
|
|
74
|
+
def index_document(self, index_name, document, document_id=None):
|
|
75
|
+
"""Index a single document"""
|
|
76
|
+
try:
|
|
77
|
+
if document_id:
|
|
78
|
+
result = self.es.index(index=index_name, id=document_id, body=document, refresh=True)
|
|
79
|
+
else:
|
|
80
|
+
result = self.es.index(index=index_name, body=document, refresh=True)
|
|
81
|
+
|
|
82
|
+
return {"status": "Document indexed", "result": result}
|
|
83
|
+
except Exception as e:
|
|
84
|
+
return {"error": str(e)}
|
|
85
|
+
|
|
86
|
+
# FIXME: this should definitely not be the way bulk is used. Why queue up a python list and then send it to the server?
|
|
87
|
+
def bulk_index(self, index_name, documents, bulk_options=None):
|
|
88
|
+
"""Bulk index multiple documents with chunking (prevents timeouts on large payloads)"""
|
|
89
|
+
try:
|
|
90
|
+
# Prepare bulk request body
|
|
91
|
+
#bulk_body = []
|
|
92
|
+
#for doc in documents:
|
|
93
|
+
# bulk_body.append({"index": {"_index": index_name}})
|
|
94
|
+
# bulk_body.append(doc)
|
|
95
|
+
|
|
96
|
+
#result = self.es.bulk(body=bulk_body)
|
|
97
|
+
result = helpers.bulk(self.es, index=index_name, actions=documents)
|
|
98
|
+
return result
|
|
99
|
+
except Exception as e:
|
|
100
|
+
return {"error": str(e)}
|
|
101
|
+
|
|
102
|
+
def bulk_actions(self, actions, raise_on_error=False):
|
|
103
|
+
"""Bulk index/update/delete using elasticsearch.helpers.bulk action dicts."""
|
|
104
|
+
try:
|
|
105
|
+
success, errors = helpers.bulk(self.es, actions, raise_on_error=raise_on_error)
|
|
106
|
+
error_count = len(errors) if errors else 0
|
|
107
|
+
return success, error_count
|
|
108
|
+
except Exception:
|
|
109
|
+
return 0, 1 if raise_on_error else 0
|
|
110
|
+
|
|
111
|
+
def scroll_document_ids(self, index_name, query=None, page_size=1000, scroll_ttl="2m"):
|
|
112
|
+
"""Scroll an index and return all document IDs."""
|
|
113
|
+
if not self.index_exists(index_name):
|
|
114
|
+
return set()
|
|
115
|
+
|
|
116
|
+
if query is None:
|
|
117
|
+
query = {"match_all": {}}
|
|
118
|
+
|
|
119
|
+
known: set[str] = set()
|
|
120
|
+
response = self.es.search(
|
|
121
|
+
index=index_name,
|
|
122
|
+
scroll=scroll_ttl,
|
|
123
|
+
size=page_size,
|
|
124
|
+
_source=False,
|
|
125
|
+
query=query,
|
|
126
|
+
)
|
|
127
|
+
scroll_id = response.get("_scroll_id")
|
|
128
|
+
hits = response.get("hits", {}).get("hits", [])
|
|
129
|
+
known.update(hit["_id"] for hit in hits)
|
|
130
|
+
|
|
131
|
+
while hits:
|
|
132
|
+
response = self.es.scroll(scroll_id=scroll_id, scroll=scroll_ttl)
|
|
133
|
+
scroll_id = response.get("_scroll_id")
|
|
134
|
+
hits = response.get("hits", {}).get("hits", [])
|
|
135
|
+
known.update(hit["_id"] for hit in hits)
|
|
136
|
+
|
|
137
|
+
if scroll_id:
|
|
138
|
+
self.es.clear_scroll(scroll_id=scroll_id)
|
|
139
|
+
|
|
140
|
+
return known
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def search_documents(self, index_name, query, search_options=None):
|
|
144
|
+
"""Search documents using Elasticsearch query DSL with configurable options"""
|
|
145
|
+
try:
|
|
146
|
+
# Apply search options to the query
|
|
147
|
+
if search_options:
|
|
148
|
+
query.update(search_options)
|
|
149
|
+
|
|
150
|
+
result = self.es.search(index=index_name, body=query)
|
|
151
|
+
return result
|
|
152
|
+
except Exception as e:
|
|
153
|
+
return {"error": str(e)}
|
|
154
|
+
|
|
155
|
+
def search(self, index_name, **kwargs):
|
|
156
|
+
"""Pass-through search for full Elasticsearch query DSL (including aggregations)."""
|
|
157
|
+
try:
|
|
158
|
+
return self.es.search(index=index_name, **kwargs)
|
|
159
|
+
except Exception as e:
|
|
160
|
+
return {"error": str(e)}
|
|
161
|
+
|
|
162
|
+
def get_document(self, index_name, document_id):
|
|
163
|
+
"""Get a specific document by ID"""
|
|
164
|
+
try:
|
|
165
|
+
result = self.es.get(index=index_name, id=document_id)
|
|
166
|
+
return result
|
|
167
|
+
except Exception as e:
|
|
168
|
+
return {"error": str(e)}
|
|
169
|
+
|
|
170
|
+
def update_document(self, index_name, document_id, update_data):
|
|
171
|
+
"""Update a document"""
|
|
172
|
+
try:
|
|
173
|
+
result = self.es.update(index=index_name, id=document_id, body={"doc": update_data})
|
|
174
|
+
return {"status": "Document updated", "result": result}
|
|
175
|
+
except Exception as e:
|
|
176
|
+
return {"error": str(e)}
|
|
177
|
+
|
|
178
|
+
def delete_document(self, index_name, document_id):
|
|
179
|
+
"""Delete a document"""
|
|
180
|
+
try:
|
|
181
|
+
result = self.es.delete(index=index_name, id=document_id)
|
|
182
|
+
return {"status": f"Document {document_id} deleted", "result": result}
|
|
183
|
+
except Exception as e:
|
|
184
|
+
return {"error": str(e)}
|
|
185
|
+
|
|
186
|
+
def get_index_settings(self, index_name):
|
|
187
|
+
"""Get index settings"""
|
|
188
|
+
try:
|
|
189
|
+
result = self.es.indices.get_settings(index=index_name)
|
|
190
|
+
return result
|
|
191
|
+
except Exception as e:
|
|
192
|
+
return {"error": str(e)}
|
|
193
|
+
|
|
194
|
+
def update_index_settings(self, index_name, settings):
|
|
195
|
+
"""Update index settings"""
|
|
196
|
+
try:
|
|
197
|
+
result = self.es.indices.put_settings(index=index_name, body=settings)
|
|
198
|
+
return {"status": "Settings updated", "result": result}
|
|
199
|
+
except Exception as e:
|
|
200
|
+
return {"error": str(e)}
|
|
201
|
+
|
|
202
|
+
def get_index_mapping(self, index_name):
|
|
203
|
+
"""Get index mapping"""
|
|
204
|
+
try:
|
|
205
|
+
result = self.es.indices.get_mapping(index=index_name)
|
|
206
|
+
return result
|
|
207
|
+
except Exception as e:
|
|
208
|
+
return {"error": str(e)}
|
|
209
|
+
|
|
210
|
+
def update_index_mapping(self, index_name, mapping):
|
|
211
|
+
"""Update index mapping"""
|
|
212
|
+
try:
|
|
213
|
+
result = self.es.indices.put_mapping(index=index_name, body=mapping)
|
|
214
|
+
return {"status": "Mapping updated", "result": result}
|
|
215
|
+
except Exception as e:
|
|
216
|
+
return {"error": str(e)}
|
|
217
|
+
|
|
218
|
+
def reindex(self, source_index, dest_index, query=None):
|
|
219
|
+
"""Reindex from source to destination index"""
|
|
220
|
+
try:
|
|
221
|
+
if query is None:
|
|
222
|
+
query = {"match_all": {}}
|
|
223
|
+
|
|
224
|
+
body = {
|
|
225
|
+
"source": {
|
|
226
|
+
"index": source_index,
|
|
227
|
+
"query": query
|
|
228
|
+
},
|
|
229
|
+
"dest": {
|
|
230
|
+
"index": dest_index
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
result = self.es.reindex(body=body)
|
|
235
|
+
return {"status": "Reindexing started", "result": result}
|
|
236
|
+
except Exception as e:
|
|
237
|
+
return {"error": str(e)}
|
|
238
|
+
|
|
239
|
+
def refresh_index(self, index_name):
|
|
240
|
+
"""Refresh an index to make recent changes visible"""
|
|
241
|
+
try:
|
|
242
|
+
result = self.es.indices.refresh(index=index_name)
|
|
243
|
+
return {"status": f"Index {index_name} refreshed", "result": result}
|
|
244
|
+
except Exception as e:
|
|
245
|
+
return {"error": str(e)}
|
|
246
|
+
|
|
247
|
+
def flush_index(self, index_name):
|
|
248
|
+
"""Flush an index to disk"""
|
|
249
|
+
try:
|
|
250
|
+
result = self.es.indices.flush(index=index_name)
|
|
251
|
+
return {"status": f"Index {index_name} flushed", "result": result}
|
|
252
|
+
except Exception as e:
|
|
253
|
+
return {"error": str(e)}
|
|
254
|
+
|
|
255
|
+
def rename_index(self, old_index_name, new_index_name, delete_old=True):
|
|
256
|
+
"""Rename an index by reindexing to a new name (Elasticsearch doesn't support direct renaming)"""
|
|
257
|
+
try:
|
|
258
|
+
# Step 1: Check if old index exists
|
|
259
|
+
if not self.index_exists(old_index_name):
|
|
260
|
+
return {"error": f"Source index {old_index_name} does not exist"}
|
|
261
|
+
|
|
262
|
+
# Step 2: Check if new index already exists
|
|
263
|
+
if self.index_exists(new_index_name):
|
|
264
|
+
return {"error": f"Target index {new_index_name} already exists"}
|
|
265
|
+
|
|
266
|
+
# Step 3: Get the mapping and settings from old index
|
|
267
|
+
old_mapping = self.es.indices.get_mapping(index=old_index_name)
|
|
268
|
+
old_settings = self.es.indices.get_settings(index=old_index_name)
|
|
269
|
+
|
|
270
|
+
# Step 4: Create new index with same mapping and settings
|
|
271
|
+
new_index_body = {
|
|
272
|
+
"mappings": old_mapping[old_index_name]["mappings"],
|
|
273
|
+
"settings": old_settings[old_index_name]["settings"]["index"]
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
# Remove settings that can't be copied
|
|
277
|
+
if "creation_date" in new_index_body["settings"]:
|
|
278
|
+
del new_index_body["settings"]["creation_date"]
|
|
279
|
+
if "version" in new_index_body["settings"]:
|
|
280
|
+
del new_index_body["settings"]["version"]
|
|
281
|
+
if "uuid" in new_index_body["settings"]:
|
|
282
|
+
del new_index_body["settings"]["uuid"]
|
|
283
|
+
|
|
284
|
+
self.es.indices.create(index=new_index_name, body=new_index_body)
|
|
285
|
+
|
|
286
|
+
# Step 5: Reindex all data
|
|
287
|
+
reindex_body = {
|
|
288
|
+
"source": {"index": old_index_name},
|
|
289
|
+
"dest": {"index": new_index_name}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
reindex_result = self.es.reindex(body=reindex_body)
|
|
293
|
+
|
|
294
|
+
# Step 6: Delete old index if requested
|
|
295
|
+
if delete_old:
|
|
296
|
+
self.es.indices.delete(index=old_index_name)
|
|
297
|
+
return {
|
|
298
|
+
"status": f"Index renamed from {old_index_name} to {new_index_name}",
|
|
299
|
+
"old_index_deleted": True,
|
|
300
|
+
"reindex_result": reindex_result
|
|
301
|
+
}
|
|
302
|
+
else:
|
|
303
|
+
return {
|
|
304
|
+
"status": f"Index copied from {old_index_name} to {new_index_name}",
|
|
305
|
+
"old_index_deleted": False,
|
|
306
|
+
"reindex_result": reindex_result
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
except Exception as e:
|
|
310
|
+
return {"error": str(e)}
|
|
311
|
+
|
|
312
|
+
def copy_index(self, source_index_name, target_index_name):
|
|
313
|
+
"""Copy an index to a new name (keeps original)"""
|
|
314
|
+
return self.rename_index(source_index_name, target_index_name, delete_old=False)
|
|
315
|
+
|
|
316
|
+
def get_updatable_settings(self, index_name):
|
|
317
|
+
"""Get a list of index settings that can be updated"""
|
|
318
|
+
try:
|
|
319
|
+
# Get current settings
|
|
320
|
+
current_settings = self.es.indices.get_settings(index=index_name)
|
|
321
|
+
index_settings = current_settings[index_name]["settings"]["index"]
|
|
322
|
+
|
|
323
|
+
# Filter out settings that cannot be updated
|
|
324
|
+
updatable_settings = {}
|
|
325
|
+
for key, value in index_settings.items():
|
|
326
|
+
if key not in ["creation_date", "version", "uuid", "number_of_shards"]:
|
|
327
|
+
updatable_settings[key] = value
|
|
328
|
+
|
|
329
|
+
return {
|
|
330
|
+
"updatable_settings": updatable_settings,
|
|
331
|
+
"all_settings": index_settings,
|
|
332
|
+
"note": "Only updatable settings are shown above. Core settings like shards cannot be changed."
|
|
333
|
+
}
|
|
334
|
+
except Exception as e:
|
|
335
|
+
return {"error": str(e)}
|
|
336
|
+
|
|
337
|
+
def update_index_metadata(self, index_name, metadata_updates):
|
|
338
|
+
"""Update index metadata (settings that can be modified)"""
|
|
339
|
+
try:
|
|
340
|
+
# Validate that we're not trying to update immutable settings
|
|
341
|
+
immutable_settings = ["number_of_shards", "creation_date", "version", "uuid"]
|
|
342
|
+
for setting in immutable_settings:
|
|
343
|
+
if setting in metadata_updates:
|
|
344
|
+
return {"error": f"Cannot update immutable setting: {setting}"}
|
|
345
|
+
|
|
346
|
+
# Update the settings
|
|
347
|
+
result = self.es.indices.put_settings(index=index_name, body=metadata_updates)
|
|
348
|
+
return {
|
|
349
|
+
"status": "Index metadata updated",
|
|
350
|
+
"updated_settings": metadata_updates,
|
|
351
|
+
"result": result
|
|
352
|
+
}
|
|
353
|
+
except Exception as e:
|
|
354
|
+
return {"error": str(e)}
|
|
355
|
+
|
|
356
|
+
def get_index_stats(self, index_name):
|
|
357
|
+
"""Get comprehensive index statistics"""
|
|
358
|
+
try:
|
|
359
|
+
stats = self.es.indices.stats(index=index_name)
|
|
360
|
+
return stats
|
|
361
|
+
except Exception as e:
|
|
362
|
+
return {"error": str(e)}
|
|
363
|
+
|
|
364
|
+
def force_merge_index(self, index_name, max_num_segments=1):
|
|
365
|
+
"""Force merge index segments for optimization"""
|
|
366
|
+
try:
|
|
367
|
+
result = self.es.indices.forcemerge(index=index_name, max_num_segments=max_num_segments)
|
|
368
|
+
return {"status": f"Index {index_name} force merged", "result": result}
|
|
369
|
+
except Exception as e:
|
|
370
|
+
return {"error": str(e)}
|
|
371
|
+
|
|
372
|
+
def close_index(self, index_name):
|
|
373
|
+
"""Close an index (makes it read-only and frees up memory)"""
|
|
374
|
+
try:
|
|
375
|
+
result = self.es.indices.close(index=index_name)
|
|
376
|
+
return {"status": f"Index {index_name} closed", "result": result}
|
|
377
|
+
except Exception as e:
|
|
378
|
+
return {"error": str(e)}
|
|
379
|
+
|
|
380
|
+
def open_index(self, index_name):
|
|
381
|
+
"""Open a closed index"""
|
|
382
|
+
try:
|
|
383
|
+
result = self.es.indices.open(index=index_name)
|
|
384
|
+
return {"status": f"Index {index_name} opened", "result": result}
|
|
385
|
+
except Exception as e:
|
|
386
|
+
return {"error": str(e)}
|
|
387
|
+
|
|
388
|
+
def create_api_key(self, index_name, alias="logs", elastic_user=None, elastic_pass=None, privileges=None, role_name="writer", expiration=None, metadata=None):
|
|
389
|
+
"""Create API key for a specific index (for SIEM use case) with configurable options"""
|
|
390
|
+
try:
|
|
391
|
+
# Default to configured credentials if none provided
|
|
392
|
+
if elastic_user is None:
|
|
393
|
+
elastic_user = self._username
|
|
394
|
+
if elastic_pass is None:
|
|
395
|
+
elastic_pass = self._password
|
|
396
|
+
|
|
397
|
+
# Default privileges if none specified
|
|
398
|
+
if privileges is None:
|
|
399
|
+
privileges = ["write", "create_index", "read"]
|
|
400
|
+
|
|
401
|
+
# Step 1: Create index with best compression (ZSTD in ES 8.x)
|
|
402
|
+
index_body = {
|
|
403
|
+
"settings": {
|
|
404
|
+
"index.codec": "best_compression"
|
|
405
|
+
},
|
|
406
|
+
"mappings": {
|
|
407
|
+
"_source": { "enabled": True },
|
|
408
|
+
"properties": {
|
|
409
|
+
"timestamp": { "type": "date" },
|
|
410
|
+
"level": { "type": "keyword" },
|
|
411
|
+
"message": { "type": "text" },
|
|
412
|
+
"user": { "type": "keyword" },
|
|
413
|
+
"command": { "type": "keyword" },
|
|
414
|
+
"source": { "type": "keyword" },
|
|
415
|
+
"target": { "type": "keyword" }
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
# Use the ES client for index creation
|
|
421
|
+
if not self.index_exists(index_name):
|
|
422
|
+
self.es.indices.create(index=index_name, body=index_body)
|
|
423
|
+
|
|
424
|
+
# Step 2: Assign alias
|
|
425
|
+
try:
|
|
426
|
+
# Prefer setting write index when creating an alias
|
|
427
|
+
self.es.indices.put_alias(index=index_name, name=alias, body={"is_write_index": True})
|
|
428
|
+
except Exception:
|
|
429
|
+
# Fallback to simple alias if cluster/version does not support body param
|
|
430
|
+
self.es.indices.put_alias(index=index_name, name=alias)
|
|
431
|
+
|
|
432
|
+
# Step 3: Create API key - this still needs HTTP request as ES client doesn't have security API
|
|
433
|
+
import requests
|
|
434
|
+
api_key_payload = {
|
|
435
|
+
"name": f"{index_name}-token",
|
|
436
|
+
"role_descriptors": {
|
|
437
|
+
role_name: {
|
|
438
|
+
"index": [{
|
|
439
|
+
"names": [alias],
|
|
440
|
+
"privileges": privileges
|
|
441
|
+
}]
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
# Add optional parameters if provided
|
|
447
|
+
if expiration:
|
|
448
|
+
api_key_payload["expiration"] = expiration
|
|
449
|
+
if metadata:
|
|
450
|
+
api_key_payload["metadata"] = metadata
|
|
451
|
+
|
|
452
|
+
resp = requests.post(
|
|
453
|
+
f"{self.host}/_security/api_key",
|
|
454
|
+
auth=(elastic_user, elastic_pass),
|
|
455
|
+
json=api_key_payload
|
|
456
|
+
)
|
|
457
|
+
resp.raise_for_status()
|
|
458
|
+
key = resp.json()["api_key"]
|
|
459
|
+
return {
|
|
460
|
+
"status": "API key created",
|
|
461
|
+
"index": index_name,
|
|
462
|
+
"alias": alias,
|
|
463
|
+
"api_key": key,
|
|
464
|
+
"role_name": role_name,
|
|
465
|
+
"privileges": privileges,
|
|
466
|
+
"expiration": expiration,
|
|
467
|
+
"metadata": metadata,
|
|
468
|
+
"config": {
|
|
469
|
+
"auth.user": "token",
|
|
470
|
+
"auth.password": key
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
except Exception as e:
|
|
474
|
+
return {"error": str(e)}
|
|
475
|
+
|
|
476
|
+
def bulk_insert(self, index_name, docs, bulk_options=None):
|
|
477
|
+
"""Bulk insert documents (for SIEM use case) with configurable options"""
|
|
478
|
+
try:
|
|
479
|
+
# Use the ES client bulk method
|
|
480
|
+
bulk_body = []
|
|
481
|
+
for doc in docs:
|
|
482
|
+
bulk_body.append({"index": {"_index": index_name}})
|
|
483
|
+
bulk_body.append(doc)
|
|
484
|
+
|
|
485
|
+
# Apply bulk options if provided
|
|
486
|
+
if bulk_options:
|
|
487
|
+
result = self.es.bulk(body=bulk_body, **bulk_options) # compare to: es.bulk_actions(actions, raise_on_error=False)
|
|
488
|
+
else:
|
|
489
|
+
result = self.es.bulk(body=bulk_body)
|
|
490
|
+
|
|
491
|
+
return {"status": f"Inserted {len(docs)} documents", "result": result}
|
|
492
|
+
except Exception as e:
|
|
493
|
+
return {"error": str(e)}
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
# Legacy methods removed (used undefined 'es')
|
future/request.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
from future.types import ASGIReceive, ASGIScope
|
|
2
|
+
from urllib.parse import parse_qs
|
|
3
|
+
from typing import Any
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class UploadedFile:
|
|
8
|
+
def __init__(self, filename: str, content_type: str, content: bytes):
|
|
9
|
+
self.filename = filename
|
|
10
|
+
self.content_type = content_type
|
|
11
|
+
self.content = content
|
|
12
|
+
|
|
13
|
+
def __repr__(self):
|
|
14
|
+
return f"UploadedFile(filename={self.filename!r}, content_type={self.content_type!r}, size={len(self.content)})"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Request:
|
|
18
|
+
def __init__(self, scope: ASGIScope, receive: ASGIReceive):
|
|
19
|
+
self.scope = scope
|
|
20
|
+
self.receive = receive
|
|
21
|
+
self.method = scope.get("method") or ("WEBSOCKET" if scope.get("type") == "websocket" else "")
|
|
22
|
+
self.path = scope["path"]
|
|
23
|
+
self.route = None
|
|
24
|
+
# ASGI headers are byte pairs; decode once for app code
|
|
25
|
+
self.headers = dict([(key.decode("utf-8"), value.decode("utf-8")) for key, value in scope["headers"]])
|
|
26
|
+
self.host = self.headers.get("host", "")
|
|
27
|
+
# self.host = dict(scope['headers']).get(b'host', b'').decode()
|
|
28
|
+
self.context: dict[str, Any] = {} # for custom data we inject into the request
|
|
29
|
+
self.scheme = scope["scheme"]
|
|
30
|
+
self.session = {}
|
|
31
|
+
self.cookies: dict[str, str] = {}
|
|
32
|
+
self._body: bytes | None = None
|
|
33
|
+
self._form: dict[str, Any] | None = None
|
|
34
|
+
self._files: dict[str, Any] | None = None
|
|
35
|
+
cookie_header = self.headers.get("cookie", "")
|
|
36
|
+
if cookie_header:
|
|
37
|
+
for part in cookie_header.split(";"):
|
|
38
|
+
part = part.strip()
|
|
39
|
+
if not part or "=" not in part:
|
|
40
|
+
continue
|
|
41
|
+
name, value = part.split("=", 1)
|
|
42
|
+
self.cookies[name.strip()] = value.strip()
|
|
43
|
+
# Query: single value as str, repeated keys as list[str]
|
|
44
|
+
self.query: dict[str, Any] = {}
|
|
45
|
+
raw_qs = scope.get("query_string", b"") or b""
|
|
46
|
+
if isinstance(raw_qs, bytes):
|
|
47
|
+
raw_qs = raw_qs.decode("utf-8")
|
|
48
|
+
for key, values in parse_qs(raw_qs, keep_blank_values=True).items():
|
|
49
|
+
self.query[key] = values[0] if len(values) == 1 else values
|
|
50
|
+
|
|
51
|
+
async def body(self) -> bytes:
|
|
52
|
+
if self._body is not None:
|
|
53
|
+
return self._body
|
|
54
|
+
more_body = True
|
|
55
|
+
body: bytes = b""
|
|
56
|
+
while more_body:
|
|
57
|
+
message = await self.receive()
|
|
58
|
+
body += message.get("body", b"")
|
|
59
|
+
more_body = message.get("more_body", False)
|
|
60
|
+
self._body = body
|
|
61
|
+
return body
|
|
62
|
+
|
|
63
|
+
async def json(self) -> dict[str, Any]:
|
|
64
|
+
body = await self.body()
|
|
65
|
+
return json.loads(body)
|
|
66
|
+
|
|
67
|
+
async def form(self) -> dict[str, Any]:
|
|
68
|
+
if self._form is not None:
|
|
69
|
+
return self._form
|
|
70
|
+
content_type = self.headers.get("content-type", "")
|
|
71
|
+
body = await self.body()
|
|
72
|
+
if "multipart/form-data" in content_type.lower():
|
|
73
|
+
fields, files = self._parse_multipart(content_type, body)
|
|
74
|
+
self._form = fields
|
|
75
|
+
self._files = files
|
|
76
|
+
return self._form
|
|
77
|
+
parsed = parse_qs(body.decode("utf-8"), keep_blank_values=True)
|
|
78
|
+
self._form = {key: (values[0] if len(values) == 1 else values) for key, values in parsed.items()}
|
|
79
|
+
self._files = {}
|
|
80
|
+
return self._form
|
|
81
|
+
|
|
82
|
+
async def files(self) -> dict[str, Any]:
|
|
83
|
+
if self._files is None:
|
|
84
|
+
await self.form()
|
|
85
|
+
return self._files or {}
|
|
86
|
+
|
|
87
|
+
def _parse_multipart(self, content_type: str, body: bytes):
|
|
88
|
+
boundary = None
|
|
89
|
+
for part in content_type.split(";"):
|
|
90
|
+
part = part.strip()
|
|
91
|
+
if part.lower().startswith("boundary="):
|
|
92
|
+
boundary = part.split("=", 1)[1].strip().strip('"')
|
|
93
|
+
break
|
|
94
|
+
if not boundary:
|
|
95
|
+
raise ValueError("multipart/form-data missing boundary")
|
|
96
|
+
delimiter = b"--" + boundary.encode("utf-8")
|
|
97
|
+
fields: dict[str, Any] = {}
|
|
98
|
+
files: dict[str, Any] = {}
|
|
99
|
+
for raw_part in body.split(delimiter):
|
|
100
|
+
if not raw_part or raw_part in (b"--", b"--\r\n", b"\r\n"):
|
|
101
|
+
continue
|
|
102
|
+
if raw_part.startswith(b"--"):
|
|
103
|
+
break
|
|
104
|
+
if raw_part.startswith(b"\r\n"):
|
|
105
|
+
raw_part = raw_part[2:]
|
|
106
|
+
if raw_part.endswith(b"\r\n"):
|
|
107
|
+
raw_part = raw_part[:-2]
|
|
108
|
+
header_blob, _, content = raw_part.partition(b"\r\n\r\n")
|
|
109
|
+
if not header_blob:
|
|
110
|
+
continue
|
|
111
|
+
disposition = ""
|
|
112
|
+
part_type = "text/plain"
|
|
113
|
+
for header_line in header_blob.decode("utf-8", errors="replace").split("\r\n"):
|
|
114
|
+
lower = header_line.lower()
|
|
115
|
+
if lower.startswith("content-disposition:"):
|
|
116
|
+
disposition = header_line.split(":", 1)[1].strip()
|
|
117
|
+
elif lower.startswith("content-type:"):
|
|
118
|
+
part_type = header_line.split(":", 1)[1].strip()
|
|
119
|
+
name = None
|
|
120
|
+
filename = None
|
|
121
|
+
for item in disposition.split(";"):
|
|
122
|
+
item = item.strip()
|
|
123
|
+
if item.startswith("name="):
|
|
124
|
+
name = item.split("=", 1)[1].strip().strip('"')
|
|
125
|
+
elif item.startswith("filename="):
|
|
126
|
+
filename = item.split("=", 1)[1].strip().strip('"')
|
|
127
|
+
if not name:
|
|
128
|
+
continue
|
|
129
|
+
if filename is not None:
|
|
130
|
+
uploaded = UploadedFile(filename=filename, content_type=part_type, content=content)
|
|
131
|
+
if name in files:
|
|
132
|
+
existing = files[name]
|
|
133
|
+
files[name] = existing + [uploaded] if isinstance(existing, list) else [existing, uploaded]
|
|
134
|
+
else:
|
|
135
|
+
files[name] = uploaded
|
|
136
|
+
else:
|
|
137
|
+
text = content.decode("utf-8", errors="replace")
|
|
138
|
+
if name in fields:
|
|
139
|
+
existing = fields[name]
|
|
140
|
+
fields[name] = existing + [text] if isinstance(existing, list) else [existing, text]
|
|
141
|
+
else:
|
|
142
|
+
fields[name] = text
|
|
143
|
+
return fields, files
|