mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-10-28 07:03:20 +08:00
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
This commit is contained in:
@@ -483,73 +483,84 @@ jobs:
|
|||||||
assets/*.zip
|
assets/*.zip
|
||||||
|
|
||||||
# ── Wait for VirusTotal results and check for detections ──
|
# ── Wait for VirusTotal results and check for detections ──
|
||||||
|
# The action outputs comma-separated "file=URL" pairs.
|
||||||
|
# URLs are https://www.virustotal.com/gui/file/<sha256> permalinks.
|
||||||
|
# We extract the SHA256, query the API for scan stats, and gate on detections.
|
||||||
- name: Check VirusTotal scan results
|
- name: Check VirusTotal scan results
|
||||||
env:
|
env:
|
||||||
VT_API_KEY: ${{ secrets.VIRUS_TOTAL_SCANNER_API_KEY }}
|
VT_API_KEY: ${{ secrets.VIRUS_TOTAL_SCANNER_API_KEY }}
|
||||||
VT_ANALYSIS: ${{ steps.virustotal.outputs.analysis }}
|
VT_ANALYSIS: ${{ steps.virustotal.outputs.analysis }}
|
||||||
run: |
|
run: |
|
||||||
echo "=== Waiting for VirusTotal scan results ==="
|
echo "=== Checking VirusTotal scan results ==="
|
||||||
DETECTIONS=0
|
DETECTIONS=0
|
||||||
|
|
||||||
while IFS= read -r line; do
|
# Split comma-separated output into newlines
|
||||||
[ -z "$line" ] && continue
|
echo "$VT_ANALYSIS" | tr ',' '\n' | while IFS= read -r entry; do
|
||||||
URL=$(echo "$line" | cut -d'=' -f2-)
|
[ -z "$entry" ] && continue
|
||||||
|
FILE=$(echo "$entry" | cut -d'=' -f1)
|
||||||
|
URL=$(echo "$entry" | cut -d'=' -f2-)
|
||||||
|
BASENAME=$(basename "$FILE")
|
||||||
|
|
||||||
# Extract analysis ID from URL (last path segment)
|
# Extract SHA256 hash from URL (last path segment of /gui/file/<hash>)
|
||||||
ANALYSIS_ID=$(echo "$URL" | grep -oE '[a-zA-Z0-9_-]+$')
|
SHA256=$(echo "$URL" | grep -oE '[a-f0-9]{64}')
|
||||||
if [ -z "$ANALYSIS_ID" ]; then
|
if [ -z "$SHA256" ]; then
|
||||||
echo "WARNING: Could not extract analysis ID from $URL"
|
echo "WARNING: Could not extract SHA256 from $URL — skipping"
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
FILE=$(echo "$line" | cut -d'=' -f1)
|
# Poll the file report until last_analysis_results are available
|
||||||
BASENAME=$(basename "$FILE")
|
|
||||||
|
|
||||||
# Poll until analysis completes (max 5 minutes per file)
|
|
||||||
for attempt in $(seq 1 30); do
|
for attempt in $(seq 1 30); do
|
||||||
RESULT=$(curl -sf --max-time 10 \
|
RESULT=$(curl -sf --max-time 10 \
|
||||||
-H "x-apikey: $VT_API_KEY" \
|
-H "x-apikey: $VT_API_KEY" \
|
||||||
"https://www.virustotal.com/api/v3/analyses/$ANALYSIS_ID" 2>/dev/null || echo "{}")
|
"https://www.virustotal.com/api/v3/files/$SHA256" 2>/dev/null || echo "")
|
||||||
|
|
||||||
STATUS=$(echo "$RESULT" | python3 -c "
|
if [ -z "$RESULT" ]; then
|
||||||
|
echo " $BASENAME: waiting for scan (attempt $attempt/30)..."
|
||||||
|
sleep 10
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
MALICIOUS=$(echo "$RESULT" | python3 -c "
|
||||||
import json, sys
|
import json, sys
|
||||||
d = json.loads(sys.stdin.read())
|
d = json.loads(sys.stdin.read())
|
||||||
print(d.get('data', {}).get('attributes', {}).get('status', 'queued'))
|
stats = d.get('data', {}).get('attributes', {}).get('last_analysis_stats', {})
|
||||||
" 2>/dev/null || echo "queued")
|
|
||||||
|
|
||||||
if [ "$STATUS" = "completed" ]; then
|
|
||||||
MALICIOUS=$(echo "$RESULT" | python3 -c "
|
|
||||||
import json, sys
|
|
||||||
d = json.loads(sys.stdin.read())
|
|
||||||
stats = d.get('data', {}).get('attributes', {}).get('stats', {})
|
|
||||||
print(stats.get('malicious', 0))
|
print(stats.get('malicious', 0))
|
||||||
" 2>/dev/null || echo "0")
|
" 2>/dev/null || echo "0")
|
||||||
|
|
||||||
SUSPICIOUS=$(echo "$RESULT" | python3 -c "
|
SUSPICIOUS=$(echo "$RESULT" | python3 -c "
|
||||||
import json, sys
|
import json, sys
|
||||||
d = json.loads(sys.stdin.read())
|
d = json.loads(sys.stdin.read())
|
||||||
stats = d.get('data', {}).get('attributes', {}).get('stats', {})
|
stats = d.get('data', {}).get('attributes', {}).get('last_analysis_stats', {})
|
||||||
print(stats.get('suspicious', 0))
|
print(stats.get('suspicious', 0))
|
||||||
" 2>/dev/null || echo "0")
|
" 2>/dev/null || echo "0")
|
||||||
|
|
||||||
echo "$BASENAME: malicious=$MALICIOUS suspicious=$SUSPICIOUS"
|
TOTAL=$(echo "$RESULT" | python3 -c "
|
||||||
|
import json, sys
|
||||||
|
d = json.loads(sys.stdin.read())
|
||||||
|
stats = d.get('data', {}).get('attributes', {}).get('last_analysis_stats', {})
|
||||||
|
print(sum(stats.values()))
|
||||||
|
" 2>/dev/null || echo "0")
|
||||||
|
|
||||||
|
if [ "$TOTAL" -gt 0 ]; then
|
||||||
|
echo "$BASENAME: $MALICIOUS malicious, $SUSPICIOUS suspicious (of $TOTAL engines)"
|
||||||
|
|
||||||
if [ "$MALICIOUS" -gt 0 ] || [ "$SUSPICIOUS" -gt 0 ]; then
|
if [ "$MALICIOUS" -gt 0 ] || [ "$SUSPICIOUS" -gt 0 ]; then
|
||||||
echo "BLOCKED: $BASENAME flagged by $MALICIOUS malicious + $SUSPICIOUS suspicious engine(s)!"
|
echo "BLOCKED: $BASENAME flagged! See $URL"
|
||||||
DETECTIONS=$((DETECTIONS + 1))
|
echo "FAIL" >> /tmp/vt_gate_fail
|
||||||
fi
|
fi
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo " $BASENAME: scan $STATUS (attempt $attempt/30)..."
|
echo " $BASENAME: waiting for scan results (attempt $attempt/30)..."
|
||||||
sleep 10
|
sleep 10
|
||||||
done
|
done
|
||||||
done <<< "$VT_ANALYSIS"
|
done
|
||||||
|
|
||||||
if [ "$DETECTIONS" -gt 0 ]; then
|
if [ -f /tmp/vt_gate_fail ]; then
|
||||||
|
FAIL_COUNT=$(wc -l < /tmp/vt_gate_fail | tr -d ' ')
|
||||||
echo ""
|
echo ""
|
||||||
echo "=== VIRUSTOTAL GATE FAILED ==="
|
echo "=== VIRUSTOTAL GATE FAILED ==="
|
||||||
echo "$DETECTIONS binary(ies) flagged as malicious or suspicious."
|
echo "$FAIL_COUNT binary(ies) flagged as malicious or suspicious."
|
||||||
echo "Draft release will NOT be published. Investigate before retrying."
|
echo "Draft release will NOT be published. Investigate before retrying."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -596,16 +607,20 @@ jobs:
|
|||||||
REPORT=$'---\n\n### Security Verification\n\n'
|
REPORT=$'---\n\n### Security Verification\n\n'
|
||||||
REPORT+=$'All release binaries have been independently verified:\n\n'
|
REPORT+=$'All release binaries have been independently verified:\n\n'
|
||||||
|
|
||||||
# VirusTotal results
|
# VirusTotal results (comma-separated "file=URL" pairs)
|
||||||
REPORT+=$'**VirusTotal** — scanned by 70+ antivirus engines:\n\n'
|
REPORT+=$'**VirusTotal** — scanned by 70+ antivirus engines:\n\n'
|
||||||
REPORT+=$'| Binary | Scan |\n|--------|------|\n'
|
REPORT+=$'| Binary | Scan |\n|--------|------|\n'
|
||||||
while IFS= read -r line; do
|
echo "$VT_ANALYSIS" | tr ',' '\n' | while IFS= read -r entry; do
|
||||||
[ -z "$line" ] && continue
|
[ -z "$entry" ] && continue
|
||||||
FILE=$(echo "$line" | cut -d'=' -f1)
|
FILE=$(echo "$entry" | cut -d'=' -f1)
|
||||||
URL=$(echo "$line" | cut -d'=' -f2-)
|
URL=$(echo "$entry" | cut -d'=' -f2-)
|
||||||
BASENAME=$(basename "$FILE")
|
BASENAME=$(basename "$FILE")
|
||||||
REPORT+="| $BASENAME | [View Report]($URL) |"$'\n'
|
echo "| $BASENAME | [View Report]($URL) |"
|
||||||
done <<< "$VT_ANALYSIS"
|
done >> /tmp/vt_table
|
||||||
|
if [ -f /tmp/vt_table ]; then
|
||||||
|
REPORT+=$(cat /tmp/vt_table)$'\n'
|
||||||
|
rm -f /tmp/vt_table
|
||||||
|
fi
|
||||||
|
|
||||||
# OpenSSF Scorecard
|
# OpenSSF Scorecard
|
||||||
REPORT+=$'\n**OpenSSF Scorecard** — repository security health: **'"$SCORECARD_SCORE"$'/10**\n'
|
REPORT+=$'\n**OpenSSF Scorecard** — repository security health: **'"$SCORECARD_SCORE"$'/10**\n'
|
||||||
|
|||||||
Reference in New Issue
Block a user