2121 AlertsResponse ,
2222 Analysis ,
2323 AnalysisListResponse ,
24+ BulkTriageUpdate ,
2425 CorpusOverview ,
2526 CVECorpusEntry ,
2627 DriverSummary ,
3435 TriageSummary ,
3536 TriageUpdate ,
3637 VariantAlertEntry ,
38+ VariantMatch ,
3739)
3840from .storage import FileStorage , MWDBStorage
3941from .triage import TriageStore
@@ -435,6 +437,12 @@ async def get_triage_states(analysis_id: str):
435437 return triage_store .get_for_analysis (analysis_id )
436438
437439
440+ @app .put ("/api/triage/{analysis_id}/bulk" , response_model = list [TriageEntry ])
441+ async def bulk_triage (analysis_id : str , body : BulkTriageUpdate , _auth : None = Depends (require_api_key )):
442+ """Bulk update triage state for multiple findings."""
443+ return triage_store .set_bulk (analysis_id , body .functions , body .state , body .note )
444+
445+
438446@app .get ("/api/triage/{analysis_id}/{function}" , response_model = TriageEntry )
439447async def get_triage_state (analysis_id : str , function : str ):
440448 """Get triage state for a specific finding."""
@@ -444,7 +452,49 @@ async def get_triage_state(analysis_id: str, function: str):
444452@app .put ("/api/triage/{analysis_id}/{function}" , response_model = TriageEntry )
445453async def set_triage_state (analysis_id : str , function : str , body : TriageUpdate , _auth : None = Depends (require_api_key )):
446454 """Update triage state for a specific finding."""
447- return triage_store .set (analysis_id , function , body .state , body .note )
455+ return triage_store .set (analysis_id , function , body .state , body .note , body .exploit_stage )
456+
457+
458+ # ==========================================================================
459+ # Variant Search (cross-driver)
460+ # ==========================================================================
461+
462+
463+ @app .get ("/api/variants/{analysis_id}/{function}" , response_model = list [VariantMatch ])
464+ async def find_variants (analysis_id : str , function : str ):
465+ """Find similar findings (same rule_id or category) in other analyses."""
466+ source = file_storage .get_analysis (analysis_id )
467+ if source is None :
468+ raise HTTPException (status_code = 404 , detail = "Analysis not found" )
469+ source_finding = next ((f for f in source .findings if f .function == function ), None )
470+ if source_finding is None :
471+ raise HTTPException (status_code = 404 , detail = "Finding not found" )
472+
473+ matches : list [VariantMatch ] = []
474+ for item in file_storage .list_analyses ():
475+ if item .id == analysis_id :
476+ continue
477+ other = file_storage .get_analysis (item .id )
478+ if other is None :
479+ continue
480+ for f in other .findings :
481+ if f .rule_id == source_finding .rule_id or (
482+ f .category == source_finding .category
483+ and abs (f .final_score - source_finding .final_score ) < 3.0
484+ ):
485+ matches .append (VariantMatch (
486+ analysis_id = item .id ,
487+ driver_name = item .driver_name or item .id ,
488+ function = f .function ,
489+ rule_id = f .rule_id ,
490+ category = f .category .value if hasattr (f .category , 'value' ) else str (f .category ),
491+ final_score = f .final_score ,
492+ confidence = f .confidence ,
493+ reachability_class = f .reachability_class .value if hasattr (f .reachability_class , 'value' ) else str (f .reachability_class ),
494+ created_at = item .created_at .isoformat () if hasattr (item .created_at , 'isoformat' ) else str (item .created_at ),
495+ ))
496+ matches .sort (key = lambda m : m .final_score , reverse = True )
497+ return matches [:30 ]
448498
449499
450500# ==========================================================================
0 commit comments