@@ -483,73 +483,84 @@ jobs:
483483 assets/*.zip
484484
485485 # ── Wait for VirusTotal results and check for detections ──
486+ # The action outputs comma-separated "file=URL" pairs.
487+ # URLs are https://www.virustotal.com/gui/file/<sha256> permalinks.
488+ # We extract the SHA256, query the API for scan stats, and gate on detections.
486489 - name : Check VirusTotal scan results
487490 env :
488491 VT_API_KEY : ${{ secrets.VIRUS_TOTAL_SCANNER_API_KEY }}
489492 VT_ANALYSIS : ${{ steps.virustotal.outputs.analysis }}
490493 run : |
491- echo "=== Waiting for VirusTotal scan results ==="
494+ echo "=== Checking VirusTotal scan results ==="
492495 DETECTIONS=0
493496
494- while IFS= read -r line; do
495- [ -z "$line" ] && continue
496- URL=$(echo "$line" | cut -d'=' -f2-)
497+ # Split comma-separated output into newlines
498+ echo "$VT_ANALYSIS" | tr ',' '\n' | while IFS= read -r entry; do
499+ [ -z "$entry" ] && continue
500+ FILE=$(echo "$entry" | cut -d'=' -f1)
501+ URL=$(echo "$entry" | cut -d'=' -f2-)
502+ BASENAME=$(basename "$FILE")
497503
498- # Extract analysis ID from URL (last path segment)
499- ANALYSIS_ID =$(echo "$URL" | grep -oE '[a-zA-Z0-9_-]+$ ')
500- if [ -z "$ANALYSIS_ID " ]; then
501- echo "WARNING: Could not extract analysis ID from $URL"
504+ # Extract SHA256 hash from URL (last path segment of /gui/file/<hash> )
505+ SHA256 =$(echo "$URL" | grep -oE '[a-f0-9]{64} ')
506+ if [ -z "$SHA256 " ]; then
507+ echo "WARNING: Could not extract SHA256 from $URL — skipping "
502508 continue
503509 fi
504510
505- FILE=$(echo "$line" | cut -d'=' -f1)
506- BASENAME=$(basename "$FILE")
507-
508- # Poll until analysis completes (max 5 minutes per file)
511+ # Poll the file report until last_analysis_results are available
509512 for attempt in $(seq 1 30); do
510513 RESULT=$(curl -sf --max-time 10 \
511514 -H "x-apikey: $VT_API_KEY" \
512- "https://www.virustotal.com/api/v3/analyses/$ANALYSIS_ID " 2>/dev/null || echo "{} ")
515+ "https://www.virustotal.com/api/v3/files/$SHA256 " 2>/dev/null || echo "")
513516
514- STATUS=$(echo "$RESULT" | python3 -c "
515- import json, sys
516- d = json.loads(sys.stdin.read())
517- print(d.get('data', {}).get('attributes', {}).get('status', 'queued'))
518- " 2>/dev/null || echo "queued")
517+ if [ -z "$RESULT" ]; then
518+ echo " $BASENAME: waiting for scan (attempt $attempt/30)..."
519+ sleep 10
520+ continue
521+ fi
519522
520- if [ "$STATUS" = "completed" ]; then
521- MALICIOUS=$(echo "$RESULT" | python3 -c "
523+ MALICIOUS=$(echo "$RESULT" | python3 -c "
522524 import json, sys
523525 d = json.loads(sys.stdin.read())
524- stats = d.get('data', {}).get('attributes', {}).get('stats ', {})
526+ stats = d.get('data', {}).get('attributes', {}).get('last_analysis_stats ', {})
525527 print(stats.get('malicious', 0))
526528 " 2>/dev/null || echo "0")
527529
528- SUSPICIOUS=$(echo "$RESULT" | python3 -c "
530+ SUSPICIOUS=$(echo "$RESULT" | python3 -c "
529531 import json, sys
530532 d = json.loads(sys.stdin.read())
531- stats = d.get('data', {}).get('attributes', {}).get('stats ', {})
533+ stats = d.get('data', {}).get('attributes', {}).get('last_analysis_stats ', {})
532534 print(stats.get('suspicious', 0))
533535 " 2>/dev/null || echo "0")
534536
535- echo "$BASENAME: malicious=$MALICIOUS suspicious=$SUSPICIOUS"
537+ TOTAL=$(echo "$RESULT" | python3 -c "
538+ import json, sys
539+ d = json.loads(sys.stdin.read())
540+ stats = d.get('data', {}).get('attributes', {}).get('last_analysis_stats', {})
541+ print(sum(stats.values()))
542+ " 2>/dev/null || echo "0")
543+
544+ if [ "$TOTAL" -gt 0 ]; then
545+ echo "$BASENAME: $MALICIOUS malicious, $SUSPICIOUS suspicious (of $TOTAL engines)"
536546
537547 if [ "$MALICIOUS" -gt 0 ] || [ "$SUSPICIOUS" -gt 0 ]; then
538- echo "BLOCKED: $BASENAME flagged by $MALICIOUS malicious + $SUSPICIOUS suspicious engine(s)! "
539- DETECTIONS=$((DETECTIONS + 1))
548+ echo "BLOCKED: $BASENAME flagged! See $URL "
549+ echo "FAIL" >> /tmp/vt_gate_fail
540550 fi
541551 break
542552 fi
543553
544- echo " $BASENAME: scan $STATUS (attempt $attempt/30)..."
554+ echo " $BASENAME: waiting for scan results (attempt $attempt/30)..."
545555 sleep 10
546556 done
547- done <<< "$VT_ANALYSIS"
557+ done
548558
549- if [ "$DETECTIONS" -gt 0 ]; then
559+ if [ -f /tmp/vt_gate_fail ]; then
560+ FAIL_COUNT=$(wc -l < /tmp/vt_gate_fail | tr -d ' ')
550561 echo ""
551562 echo "=== VIRUSTOTAL GATE FAILED ==="
552- echo "$DETECTIONS binary(ies) flagged as malicious or suspicious."
563+ echo "$FAIL_COUNT binary(ies) flagged as malicious or suspicious."
553564 echo "Draft release will NOT be published. Investigate before retrying."
554565 exit 1
555566 fi
@@ -596,16 +607,20 @@ jobs:
596607 REPORT=$'---\n\n### Security Verification\n\n'
597608 REPORT+=$'All release binaries have been independently verified:\n\n'
598609
599- # VirusTotal results
610+ # VirusTotal results (comma-separated "file=URL" pairs)
600611 REPORT+=$'**VirusTotal** — scanned by 70+ antivirus engines:\n\n'
601612 REPORT+=$'| Binary | Scan |\n|--------|------|\n'
602- while IFS= read -r line ; do
603- [ -z "$line " ] && continue
604- FILE=$(echo "$line " | cut -d'=' -f1)
605- URL=$(echo "$line " | cut -d'=' -f2-)
613+ echo "$VT_ANALYSIS" | tr ',' '\n' | while IFS= read -r entry ; do
614+ [ -z "$entry " ] && continue
615+ FILE=$(echo "$entry " | cut -d'=' -f1)
616+ URL=$(echo "$entry " | cut -d'=' -f2-)
606617 BASENAME=$(basename "$FILE")
607- REPORT+="| $BASENAME | [View Report]($URL) |"$'\n'
608- done <<< "$VT_ANALYSIS"
618+ echo "| $BASENAME | [View Report]($URL) |"
619+ done >> /tmp/vt_table
620+ if [ -f /tmp/vt_table ]; then
621+ REPORT+=$(cat /tmp/vt_table)$'\n'
622+ rm -f /tmp/vt_table
623+ fi
609624
610625 # OpenSSF Scorecard
611626 REPORT+=$'\n**OpenSSF Scorecard** — repository security health: **'"$SCORECARD_SCORE"$'/10**\n'
0 commit comments