@@ -530,90 +530,98 @@ jobs:
530530 assets/*.tar.gz
531531 assets/*.zip
532532
533- # ── Wait for VirusTotal results and check for detections ──
533+ # ── Wait for ALL VirusTotal engines to complete, then check ──
534534 # The action outputs comma-separated "file=URL" pairs.
535- # URLs are https://www.virustotal.com/gui/file/<sha256> permalinks.
536- # We extract the SHA256, query the API for scan stats, and gate on detections.
537- - name : Check VirusTotal scan results
535+ # We wait until every engine has reported (not just the first few),
536+ # then check for any malicious or suspicious detection.
537+ # Minimum 60 engines required — below that we consider the scan incomplete.
538+ - name : Check VirusTotal scan results (wait for 100% completion)
538539 env :
539540 VT_API_KEY : ${{ secrets.VIRUS_TOTAL_SCANNER_API_KEY }}
540541 VT_ANALYSIS : ${{ steps.virustotal.outputs.analysis }}
541542 run : |
542- echo "=== Checking VirusTotal scan results ==="
543- DETECTIONS=0
543+ echo "=== Waiting for VirusTotal scans to fully complete ==="
544+ MIN_ENGINES=60
544545
545- # Split comma-separated output into newlines
546546 echo "$VT_ANALYSIS" | tr ',' '\n' | while IFS= read -r entry; do
547547 [ -z "$entry" ] && continue
548548 FILE=$(echo "$entry" | cut -d'=' -f1)
549549 URL=$(echo "$entry" | cut -d'=' -f2-)
550550 BASENAME=$(basename "$FILE")
551551
552- # Extract SHA256 hash from URL (last path segment of /gui/file/<hash>)
553552 SHA256=$(echo "$URL" | grep -oE '[a-f0-9]{64}')
554553 if [ -z "$SHA256" ]; then
555554 echo "WARNING: Could not extract SHA256 from $URL — skipping"
556555 continue
557556 fi
558557
559- # Poll the file report until last_analysis_results are available
560- for attempt in $(seq 1 30); do
558+ # Poll until ALL engines have completed (max 20 minutes per binary)
559+ SCAN_COMPLETE=false
560+ for attempt in $(seq 1 120); do
561561 RESULT=$(curl -sf --max-time 10 \
562562 -H "x-apikey: $VT_API_KEY" \
563563 "https://www.virustotal.com/api/v3/files/$SHA256" 2>/dev/null || echo "")
564564
565565 if [ -z "$RESULT" ]; then
566- echo " $BASENAME: waiting for scan (attempt $attempt/30 )..."
566+ echo " $BASENAME: waiting for file to be processed (attempt $attempt)..."
567567 sleep 10
568568 continue
569569 fi
570570
571- MALICIOUS=$(echo "$RESULT" | python3 -c "
571+ # Parse all stats in one python call
572+ STATS=$(echo "$RESULT" | python3 -c "
572573 import json, sys
573574 d = json.loads(sys.stdin.read())
574575 stats = d.get('data', {}).get('attributes', {}).get('last_analysis_stats', {})
575- print(stats.get('malicious', 0))
576- " 2>/dev/null || echo "0")
577-
578- SUSPICIOUS=$(echo "$RESULT" | python3 -c "
579- import json, sys
580- d = json.loads(sys.stdin.read())
581- stats = d.get('data', {}).get('attributes', {}).get('last_analysis_stats', {})
582- print(stats.get('suspicious', 0))
583- " 2>/dev/null || echo "0")
584-
585- TOTAL=$(echo "$RESULT" | python3 -c "
586- import json, sys
587- d = json.loads(sys.stdin.read())
588- stats = d.get('data', {}).get('attributes', {}).get('last_analysis_stats', {})
589- print(sum(stats.values()))
590- " 2>/dev/null || echo "0")
591-
592- if [ "$TOTAL" -gt 0 ]; then
593- echo "$BASENAME: $MALICIOUS malicious, $SUSPICIOUS suspicious (of $TOTAL engines)"
576+ malicious = stats.get('malicious', 0)
577+ suspicious = stats.get('suspicious', 0)
578+ undetected = stats.get('undetected', 0)
579+ harmless = stats.get('harmless', 0)
580+ failure = stats.get('failure', 0)
581+ timeout = stats.get('timeout', 0)
582+ unsupported = stats.get('type-unsupported', 0)
583+ total = sum(stats.values())
584+ # 'confirmed-timeout' may also exist
585+ completed = malicious + suspicious + undetected + harmless
586+ print(f'{malicious},{suspicious},{completed},{total}')
587+ " 2>/dev/null || echo "0,0,0,0")
588+
589+ MALICIOUS=$(echo "$STATS" | cut -d',' -f1)
590+ SUSPICIOUS=$(echo "$STATS" | cut -d',' -f2)
591+ COMPLETED=$(echo "$STATS" | cut -d',' -f3)
592+ TOTAL=$(echo "$STATS" | cut -d',' -f4)
593+
594+ if [ "$TOTAL" -ge "$MIN_ENGINES" ] && [ "$COMPLETED" -ge "$MIN_ENGINES" ]; then
595+ echo "$BASENAME: $MALICIOUS malicious, $SUSPICIOUS suspicious ($COMPLETED completed, $TOTAL total engines)"
594596
595597 if [ "$MALICIOUS" -gt 0 ] || [ "$SUSPICIOUS" -gt 0 ]; then
596598 echo "BLOCKED: $BASENAME flagged! See $URL"
597599 echo "FAIL" >> /tmp/vt_gate_fail
598600 fi
601+ SCAN_COMPLETE=true
599602 break
600603 fi
601604
602- echo " $BASENAME: waiting for scan results (attempt $attempt/30 )..."
605+ echo " $BASENAME: $COMPLETED/$TOTAL engines completed (attempt $attempt, need $MIN_ENGINES )..."
603606 sleep 10
604607 done
608+
609+ if [ "$SCAN_COMPLETE" != "true" ]; then
610+ echo "BLOCKED: $BASENAME scan did not complete within 20 minutes!"
611+ echo "FAIL" >> /tmp/vt_gate_fail
612+ fi
605613 done
606614
607615 if [ -f /tmp/vt_gate_fail ]; then
608616 FAIL_COUNT=$(wc -l < /tmp/vt_gate_fail | tr -d ' ')
609617 echo ""
610618 echo "=== VIRUSTOTAL GATE FAILED ==="
611- echo "$FAIL_COUNT binary(ies) flagged as malicious or suspicious ."
619+ echo "$FAIL_COUNT binary(ies) flagged or scan incomplete ."
612620 echo "Draft release will NOT be published. Investigate before retrying."
613621 exit 1
614622 fi
615623
616- echo "=== All binaries clean ==="
624+ echo "=== All binaries clean (all engines completed) ==="
617625
618626 # ── OpenSSF Scorecard ────────────────────────────────────
619627 - name : Run OpenSSF Scorecard
0 commit comments