@@ -159,6 +159,20 @@ def update_export_schedule(
159159 return db_export
160160
161161
162+ def requeue_export (
163+ db : Session , export_id : int , user_id : int
164+ ) -> export_models .Export :
165+ """Reset an existing export to ``queued`` so it can be re-run in place."""
166+ db_export = get_export_by_id (db , export_id , user_id )
167+ if db_export is None :
168+ return None
169+ db_export .status = "queued"
170+ db_export .error = None
171+ db .commit ()
172+ db .refresh (db_export )
173+ return db_export
174+
175+
162176def set_celery_task_id (db : Session , export_id : int , task_id : str ) -> None :
163177 db_export = (
164178 db .query (export_models .Export )
@@ -211,7 +225,13 @@ def _fetch_hits(index: str, query: str) -> list[dict]:
211225 return hits
212226
213227
214- def _to_misp_json (db : Session , hits : list [dict ], index_target : str , name : str ):
228+ def _to_misp_json (
229+ db : Session ,
230+ hits : list [dict ],
231+ index_target : str ,
232+ name : str ,
233+ distribution : int = None ,
234+ ):
215235 """Serialize all matching attributes into a single MISP event.
216236
217237 Every matching attribute is collected into one synthetic MISP event named
@@ -221,7 +241,6 @@ def _to_misp_json(db: Session, hits: list[dict], index_target: str, name: str):
221241 output matches MISP's event API shape (``{"Event": {...}}``).
222242 """
223243 import json
224- import uuid as uuid_module
225244
226245 from app .schemas import attribute as attribute_schemas
227246 from app .schemas import event as event_schemas
@@ -248,19 +267,31 @@ def _to_misp_json(db: Session, hits: list[dict], index_target: str, name: str):
248267 except Exception as e : # skip malformed rows
249268 logger .warning ("Skipping attribute in MISP export: %s" , e )
250269
251- # Stable UUID per export so re-runs (e.g. scheduled) update the same event
252- # on re-import rather than creating a new one each time.
253- event_uuid = uuid_module .uuid5 (
254- uuid_module .NAMESPACE_URL , f"misp-workbench:export:{ name } "
255- )
256270 misp_event = event_schemas .Event (
257271 info = name ,
258- uuid = event_uuid ,
259272 timestamp = int (datetime .now (timezone .utc ).timestamp ()),
273+ distribution = distribution ,
274+ disable_correlation = False ,
260275 attributes = attributes ,
261276 )
262277
263278 payload = misp_event .to_misp_format ()
279+
280+ # Strip identifiers so importing the file always creates fresh records
281+ # rather than colliding with existing event/attribute uuids.
282+ def _strip_ids (obj : dict ) -> None :
283+ obj .pop ("uuid" , None )
284+ obj .pop ("id" , None )
285+
286+ event = payload .get ("Event" , {})
287+ _strip_ids (event )
288+ for attribute in event .get ("Attribute" , []):
289+ _strip_ids (attribute )
290+ for misp_object in event .get ("Object" , []):
291+ _strip_ids (misp_object )
292+ for attribute in misp_object .get ("Attribute" , []):
293+ _strip_ids (attribute )
294+
264295 content = json .dumps (payload , default = str , indent = 2 ).encode ("utf-8" )
265296 return content , "json" , "application/json" , len (attributes )
266297
@@ -293,7 +324,11 @@ def run_export(db: Session, export_id: int) -> None:
293324 # MISP format merges all matches into one event via the server-push
294325 # serializer; record_count reflects the attributes in that event.
295326 content , extension , _content_type , record_count = _to_misp_json (
296- db , hits , db_export .index_target , db_export .name
327+ db ,
328+ hits ,
329+ db_export .index_target ,
330+ db_export .name ,
331+ db_export .distribution ,
297332 )
298333 else :
299334 content , extension , _content_type = converters .convert (
0 commit comments