@@ -426,3 +426,66 @@ async def test_applet_custom_attributes(bbot_server, bbot_events):
426426 # count with custom attribute filter
427427 count = await bbot_server .count_assets (query = {"custom_tag" : "important" })
428428 assert count == 2
429+
430+
431+ # Verifies the MongoDB predicates the frontend's isEmpty / isNotEmpty operators emit
432+ # (`$in: [null, []]` and `$nin: [null, []]`) correctly distinguish missing-field, explicit-null,
433+ # empty-list, and populated documents on an array field like `open_ports`.
434+ async def test_applet_assets_empty_query_open_ports (bbot_server , bbot_events ):
435+ bbot_server = await bbot_server (needs_worker = True )
436+
437+ # skip testing of the http interface (since direct collection mutation isn't supported)
438+ if not bbot_server .is_native :
439+ return
440+
441+ # ingest BBOT events to create some assets
442+ scan1_events , _scan2_events = bbot_events
443+ for e in scan1_events :
444+ await bbot_server .insert_event (e )
445+ await asyncio .sleep (INGEST_PROCESSING_DELAY )
446+
447+ # pick four distinct hosts and force each into a different open_ports shape
448+ host_absent = "evilcorp.com" # field removed entirely
449+ host_null = "www.evilcorp.com" # explicit null
450+ host_empty = "api.evilcorp.com" # empty list
451+ host_populated = "cname.evilcorp.com" # populated
452+
453+ collection = bbot_server .assets .collection
454+ await collection .update_one ({"host" : host_absent , "type" : "Asset" }, {"$unset" : {"open_ports" : "" }})
455+ await collection .update_one ({"host" : host_null , "type" : "Asset" }, {"$set" : {"open_ports" : None }})
456+ await collection .update_one ({"host" : host_empty , "type" : "Asset" }, {"$set" : {"open_ports" : []}})
457+ await collection .update_one ({"host" : host_populated , "type" : "Asset" }, {"$set" : {"open_ports" : [80 ]}})
458+
459+ # sanity: confirm the four documents are in the expected shapes
460+ docs = {
461+ d ["host" ]: d
462+ async for d in collection .find (
463+ {"host" : {"$in" : [host_absent , host_null , host_empty , host_populated ]}, "type" : "Asset" }
464+ )
465+ }
466+ assert "open_ports" not in docs [host_absent ]
467+ assert docs [host_null ]["open_ports" ] is None
468+ assert docs [host_empty ]["open_ports" ] == []
469+ assert docs [host_populated ]["open_ports" ] == [80 ]
470+
471+ target_hosts = {host_absent , host_null , host_empty , host_populated }
472+
473+ # isEmpty contract: $in: [null, []] matches missing-field, explicit-null, and empty-list documents.
474+ is_empty_results = [
475+ a
476+ async for a in bbot_server .query_assets (
477+ type = "Asset" ,
478+ query = {"host" : {"$in" : list (target_hosts )}, "open_ports" : {"$in" : [None , []]}},
479+ )
480+ ]
481+ assert {a ["host" ] for a in is_empty_results } == {host_absent , host_null , host_empty }
482+
483+ # isNotEmpty contract: $nin: [null, []] matches only the populated document.
484+ is_not_empty_results = [
485+ a
486+ async for a in bbot_server .query_assets (
487+ type = "Asset" ,
488+ query = {"host" : {"$in" : list (target_hosts )}, "open_ports" : {"$nin" : [None , []]}},
489+ )
490+ ]
491+ assert {a ["host" ] for a in is_not_empty_results } == {host_populated }
0 commit comments