@@ -392,6 +392,111 @@ async def api_reputation_all():
392392 return JSONResponse (content = get_all_reputations (conn ))
393393
394394
395+ # --- Diff/Delta API ---
396+
397+ @app .get ("/api/diff/{domain}" )
398+ async def api_diff_domain (domain : str ):
399+ """Compare the last two scans for a domain, showing what changed."""
400+ from app .learning .fingerprint import fingerprint_diff
401+ conn = storage .get_conn ()
402+ rows = conn .execute (
403+ "SELECT * FROM component_fingerprints WHERE domain = ? ORDER BY created_at DESC LIMIT 2" ,
404+ (domain ,),
405+ ).fetchall ()
406+ if len (rows ) < 2 :
407+ return JSONResponse (content = {"error" : "Need at least 2 scans to compare" }, status_code = 404 )
408+
409+ new_fp = _fp_from_row (rows [0 ])
410+ old_fp = _fp_from_row (rows [1 ])
411+ changes = fingerprint_diff (old_fp , new_fp )
412+
413+ # Also compare findings count from scan_history
414+ history = conn .execute (
415+ "SELECT score, findings_count, scanned_at FROM scan_history WHERE domain = ? ORDER BY scanned_at DESC LIMIT 2" ,
416+ (domain ,),
417+ ).fetchall ()
418+ score_delta = None
419+ if len (history ) >= 2 :
420+ score_delta = {
421+ "old_score" : history [1 ]["score" ],
422+ "new_score" : history [0 ]["score" ],
423+ "old_findings" : history [1 ]["findings_count" ],
424+ "new_findings" : history [0 ]["findings_count" ],
425+ "old_date" : history [1 ]["scanned_at" ],
426+ "new_date" : history [0 ]["scanned_at" ],
427+ }
428+
429+ return JSONResponse (content = {
430+ "domain" : domain ,
431+ "fingerprint_changes" : changes ,
432+ "score_delta" : score_delta ,
433+ "old_hash" : old_fp .get ("fingerprint_hash" , "" ),
434+ "new_hash" : new_fp .get ("fingerprint_hash" , "" ),
435+ })
436+
437+
438+ @app .get ("/api/diff" )
439+ async def api_diff_all ():
440+ """List all domains/repos that have fingerprint changes between last 2 scans."""
441+ conn = storage .get_conn ()
442+ domains = conn .execute (
443+ "SELECT DISTINCT domain FROM component_fingerprints WHERE domain != '' ORDER BY domain"
444+ ).fetchall ()
445+ repos = conn .execute (
446+ "SELECT DISTINCT repo_url FROM component_fingerprints WHERE domain = '' AND repo_url != '' ORDER BY repo_url"
447+ ).fetchall ()
448+
449+ changed = []
450+ from app .learning .fingerprint import fingerprint_diff
451+
452+ for row in domains :
453+ d = row ["domain" ]
454+ fps = conn .execute (
455+ "SELECT * FROM component_fingerprints WHERE domain = ? ORDER BY created_at DESC LIMIT 2" ,
456+ (d ,),
457+ ).fetchall ()
458+ if len (fps ) < 2 :
459+ continue
460+ new_fp = _fp_from_row (fps [0 ])
461+ old_fp = _fp_from_row (fps [1 ])
462+ if new_fp .get ("fingerprint_hash" ) != old_fp .get ("fingerprint_hash" ):
463+ changes = fingerprint_diff (old_fp , new_fp )
464+ changed .append ({"domain" : d , "changes" : changes })
465+
466+ for row in repos :
467+ url = row ["repo_url" ]
468+ fps = conn .execute (
469+ "SELECT * FROM component_fingerprints WHERE repo_url = ? AND domain = '' ORDER BY created_at DESC LIMIT 2" ,
470+ (url ,),
471+ ).fetchall ()
472+ if len (fps ) < 2 :
473+ continue
474+ new_fp = _fp_from_row (fps [0 ])
475+ old_fp = _fp_from_row (fps [1 ])
476+ if new_fp .get ("fingerprint_hash" ) != old_fp .get ("fingerprint_hash" ):
477+ changes = fingerprint_diff (old_fp , new_fp )
478+ changed .append ({"repo_url" : url , "changes" : changes })
479+
480+ return JSONResponse (content = {"changed" : changed , "total_tracked" : len (domains ) + len (repos )})
481+
482+
483+ def _fp_from_row (row ) -> dict :
484+ """Convert a SQLite row to a fingerprint dict."""
485+ import json
486+ return {
487+ "domain" : row ["domain" ],
488+ "repo_url" : row ["repo_url" ],
489+ "fingerprint_hash" : row ["fingerprint_hash" ],
490+ "imports" : json .loads (row ["imports" ]),
491+ "ha_apis" : json .loads (row ["ha_apis" ]),
492+ "network_domains" : json .loads (row ["network_domains" ]),
493+ "file_types" : json .loads (row ["file_types" ]),
494+ "py_files" : row ["py_files" ],
495+ "js_files" : row ["js_files" ],
496+ "total_lines" : row ["total_lines" ],
497+ }
498+
499+
395500# --- Scheduler API ---
396501
397502@app .get ("/api/scheduler" )
0 commit comments