Skip to content

Commit 75b5dcb

Browse files
committed
Fix VirusTotal output parsing + use file report API
- Output is comma-separated, not newline — use tr ',' '\n' to split - URLs are /gui/file/<sha256> permalinks — extract SHA256 hash and query /api/v3/files/<sha256> for last_analysis_stats - Release notes table builder uses same comma-split fix - Gate uses /tmp flag file to propagate failure from subshell
1 parent 398ef68 commit 75b5dcb

File tree

1 file changed

+52
-37
lines changed

1 file changed

+52
-37
lines changed

.github/workflows/release.yml

Lines changed: 52 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)