@@ -222,5 +222,98 @@ def test_file_is_valid_json_with_meta(self):
222222 os .remove (path )
223223
224224
225+ class TestMultiTargetTaskId (_CollectorCase ):
226+ """
227+ Regression coverage for issue #6123: a '-m' bulk-file run shares ONE process (and thus one
228+ report collector) across many targets. _storeData()'s COMPLETE-status branch deletes any
229+ existing row for a (taskid, content_type) key before inserting the new one, so if every
230+ target reused the same fixed REPORT_TASKID, a later target's TARGET/TECHNIQUES write would
231+ silently delete an earlier target's row of the same content_type. The fix keys each target's
232+ writes by kb.reportTaskId instead.
233+ """
234+
235+ def setUp (self ):
236+ super (TestMultiTargetTaskId , self ).setUp ()
237+ from lib .core .dump import Dump
238+ self ._saved_reportTaskId = kb .get ("reportTaskId" )
239+ self ._saved_dumper = conf .get ("dumper" )
240+ self ._saved_reportCollector = conf .get ("reportCollector" )
241+ conf .dumper = Dump ()
242+ conf .reportCollector = self .c
243+
244+ def tearDown (self ):
245+ kb .reportTaskId = self ._saved_reportTaskId
246+ conf .dumper = self ._saved_dumper
247+ conf .reportCollector = self ._saved_reportCollector
248+ super (TestMultiTargetTaskId , self ).tearDown ()
249+
250+ def test_second_target_does_not_overwrite_first (self ):
251+ kb .reportTaskId = 1
252+ conf .dumper ._reportData ({"url" : "http://host1/?id=1" }, CONTENT_TYPE .TARGET )
253+
254+ kb .reportTaskId = 2
255+ conf .dumper ._reportData ({"url" : "http://host2/?id=1" }, CONTENT_TYPE .TARGET )
256+
257+ first = api ._assembleData (self .c , 1 )["data" ]
258+ second = api ._assembleData (self .c , 2 )["data" ]
259+ self .assertEqual (first [0 ]["value" ]["url" ], "http://host1/?id=1" ) # not clobbered by target #2
260+ self .assertEqual (second [0 ]["value" ]["url" ], "http://host2/?id=1" )
261+
262+ def test_write_report_json_wraps_multiple_targets (self ):
263+ kb .reportTaskId = 1
264+ conf .dumper ._reportData ({"url" : "http://host1/?id=1" }, CONTENT_TYPE .TARGET )
265+ kb .reportTaskId = 2
266+ conf .dumper ._reportData ({"url" : "http://host2/?id=1" }, CONTENT_TYPE .TARGET )
267+
268+ fd , path = tempfile .mkstemp (suffix = ".json" )
269+ os .close (fd )
270+ try :
271+ api .writeReportJson (self .c , path )
272+ with io .open (path , encoding = "utf-8" ) as f :
273+ loaded = json .load (f )
274+ self .assertIn ("targets" , loaded )
275+ self .assertEqual (len (loaded ["targets" ]), 2 )
276+ urls = [t ["data" ][0 ]["value" ]["url" ] for t in loaded ["targets" ]]
277+ self .assertEqual (urls , ["http://host1/?id=1" , "http://host2/?id=1" ])
278+ finally :
279+ os .remove (path )
280+
281+ def test_single_target_report_keeps_flat_shape (self ):
282+ # backward compatibility: exactly one taskid -> no 'targets' wrapper, same shape as before
283+ kb .reportTaskId = 1
284+ conf .dumper ._reportData ({"url" : "http://host1/?id=1" }, CONTENT_TYPE .TARGET )
285+
286+ fd , path = tempfile .mkstemp (suffix = ".json" )
287+ os .close (fd )
288+ try :
289+ api .writeReportJson (self .c , path )
290+ with io .open (path , encoding = "utf-8" ) as f :
291+ loaded = json .load (f )
292+ self .assertNotIn ("targets" , loaded )
293+ self .assertEqual (loaded ["data" ][0 ]["value" ]["url" ], "http://host1/?id=1" )
294+ finally :
295+ os .remove (path )
296+
297+ def test_error_recorded_under_active_target (self ):
298+ import logging
299+ from lib .core .data import logger
300+
301+ saved_level = logger .level
302+ logger .setLevel (logging .ERROR )
303+ # mute pre-existing handlers (e.g. console) but not the ReportErrorRecorder added by setUp
304+ muted = [(handler , handler .level ) for handler in logger .handlers if not isinstance (handler , api .ReportErrorRecorder )]
305+ for handler , _ in muted :
306+ handler .setLevel (logging .CRITICAL + 1 )
307+ try :
308+ kb .reportTaskId = 2
309+ logger .error ("boom for target 2" )
310+ self .assertTrue (any ("boom for target 2" in _ for _ in api ._assembleData (self .c , 2 )["error" ]))
311+ self .assertEqual (api ._assembleData (self .c , 1 )["error" ], []) # not attributed to target #1
312+ finally :
313+ logger .setLevel (saved_level )
314+ for handler , level in muted :
315+ handler .setLevel (level )
316+
317+
225318if __name__ == "__main__" :
226319 unittest .main (verbosity = 2 )
0 commit comments