Skip to content

Commit 7bd2955

Browse files
committed
fix: export first_seen, last_seen in ISO format, drop null attributes
1 parent b523131 commit 7bd2955

3 files changed

Lines changed: 44 additions & 1 deletion

File tree

api/app/repositories/exports.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,14 +283,41 @@ def _strip_ids(obj: dict) -> None:
283283
obj.pop("uuid", None)
284284
obj.pop("id", None)
285285

286+
# first_seen/last_seen are stored as epoch seconds; MISP serializes them as
287+
# microsecond-precision ISO-8601 strings (e.g. "2026-06-19T00:00:00.000000+00:00").
288+
def _format_seen(obj: dict) -> None:
289+
for field in ("first_seen", "last_seen"):
290+
value = obj.get(field)
291+
if value is None:
292+
continue
293+
try:
294+
obj[field] = datetime.fromtimestamp(
295+
int(value), tz=timezone.utc
296+
).isoformat(timespec="microseconds")
297+
except (ValueError, TypeError, OverflowError):
298+
pass
299+
286300
event = payload.get("Event", {})
287301
_strip_ids(event)
288302
for attribute in event.get("Attribute", []):
289303
_strip_ids(attribute)
304+
_format_seen(attribute)
290305
for misp_object in event.get("Object", []):
291306
_strip_ids(misp_object)
307+
_format_seen(misp_object)
292308
for attribute in misp_object.get("Attribute", []):
293309
_strip_ids(attribute)
310+
_format_seen(attribute)
311+
312+
# Drop keys with null values so the exported MISP JSON stays compact.
313+
def _drop_nulls(obj):
314+
if isinstance(obj, dict):
315+
return {k: _drop_nulls(v) for k, v in obj.items() if v is not None}
316+
if isinstance(obj, list):
317+
return [_drop_nulls(v) for v in obj]
318+
return obj
319+
320+
payload = _drop_nulls(payload)
294321

295322
content = json.dumps(payload, default=str, indent=2).encode("utf-8")
296323
return content, "json", "application/json", len(attributes)

api/app/tests/api/test_exports.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,13 +351,15 @@ def test_run_export_misp_format(
351351

352352
# Two attributes from two different misp-workbench events — they should
353353
# be merged into a single MISP event named after the export.
354+
first_seen_epoch = int(datetime(2026, 6, 19, tzinfo=timezone.utc).timestamp())
354355
hits = [
355356
{
356357
"uuid": "11111111-1111-4111-8111-111111111111",
357358
"type": "ip-dst",
358359
"value": "1.2.3.4",
359360
"category": "Network activity",
360361
"event_uuid": "aaaaaaaa-1111-4111-8111-111111111111",
362+
"first_seen": first_seen_epoch,
361363
},
362364
{
363365
"uuid": "22222222-2222-4222-8222-222222222222",
@@ -396,6 +398,17 @@ def test_run_export_misp_format(
396398
assert "uuid" not in event and "id" not in event
397399
for attr in event["Attribute"]:
398400
assert "uuid" not in attr and "id" not in attr
401+
# null-valued fields are dropped from the MISP output.
402+
assert None not in attr.values()
403+
# first_seen is emitted as a microsecond ISO-8601 string.
404+
seen = {
405+
a["value"]: a.get("first_seen") for a in event["Attribute"]
406+
}
407+
assert seen["1.2.3.4"] == "2026-06-19T00:00:00.000000+00:00"
408+
# the attribute without first_seen has it dropped entirely.
409+
assert "first_seen" not in next(
410+
a for a in event["Attribute"] if a["value"] == "5.6.7.8"
411+
)
399412
finally:
400413
db.delete(export)
401414
db.commit()

docs/features/exports.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,10 @@ can always fetch the same URL for the latest results.
3636
different misp-workbench events. It requires a **distribution** level and
3737
keeps correlation enabled. The `uuid` and `id` fields are stripped from the
3838
event and its attributes, so importing the file always creates fresh
39-
records rather than colliding with existing ones.
39+
records rather than colliding with existing ones. `first_seen`/`last_seen`
40+
are emitted as microsecond ISO-8601 strings
41+
(e.g. `2026-06-19T00:00:00.000000+00:00`), and any field with a `null`
42+
value is omitted to keep the output compact.
4043

4144
!!! note "STIX limits"
4245
STIX 2.1 conversion is CPU-intensive, so STIX exports are capped at

0 commit comments

Comments
 (0)