Skip to content

Commit 4730fe9

Browse files
authored
Merge pull request DeusData#1340 from DeusData/ci/release-skip-tests
ci(release): double-verify ML antivirus false positives instead of re-rolling builds
2 parents e9ce367 + 48fc942 commit 4730fe9

5 files changed

Lines changed: 474 additions & 31 deletions

File tree

‎.github/workflows/release.yml‎

Lines changed: 24 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ on:
3131
required: false
3232
type: boolean
3333
default: false
34+
skip_tests:
35+
description: "Skip the test phase entirely (re-release of an already test-green tree; build/smoke/soak/verify still run)"
36+
required: false
37+
type: boolean
38+
default: false
3439

3540
permissions:
3641
contents: read
@@ -46,7 +51,12 @@ jobs:
4651
uses: ./.github/workflows/_lint.yml
4752

4853
# ── 2. Tests (all platforms, full suite for release) ────────────
54+
# skip_tests exists for RE-releases of a tree whose tests are already green:
55+
# when only packaging/gating changed and the previous run proved the suites,
56+
# re-running ~2h of tests adds no information. It skips ONLY this phase —
57+
# build, smoke, soak and verify always run against the fresh artifacts.
4958
test:
59+
if: ${{ !inputs.skip_tests }}
5060
needs: [lint]
5161
uses: ./.github/workflows/_test.yml
5262
with:
@@ -55,7 +65,10 @@ jobs:
5565
shard_suites: true
5666

5767
# ── 3. Build all platforms ──────────────────────────────────────
68+
# !cancelled() && !failure(): run when `test` is deliberately skipped, but
69+
# never when lint or test actually failed.
5870
build:
71+
if: ${{ !cancelled() && !failure() }}
5972
needs: [test]
6073
permissions:
6174
contents: read
@@ -220,44 +233,27 @@ jobs:
220233
vt_api_key: ${{ secrets.VIRUS_TOTAL_SCANNER_API_KEY }}
221234
files: binaries/*
222235

236+
# VERSION + GH_TOKEN enable the pre-release ML false-positive tolerance:
237+
# the gate may downgrade a single-engine Microsoft "!ml" verdict to
238+
# TOLERATED, but only for -rc./-pre versions and only when hash-pinned
239+
# Defender ENDPOINT evidence is attached to the draft release
240+
# (defender-endpoint-verification.txt, from scripts/ci/av-endpoint-verify.sh).
223241
- name: Wait for VirusTotal results
224242
env:
225243
VT_API_KEY: ${{ secrets.VIRUS_TOTAL_SCANNER_API_KEY }}
226244
VT_ANALYSIS: ${{ steps.virustotal.outputs.analysis }}
245+
VERSION: ${{ inputs.version }}
246+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
227247
run: scripts/ci/check-virustotal.sh
228248

249+
# Renders per-binary sha256 + VT links into the notes; when the gate
250+
# tolerated an ML false positive it states that honestly instead of
251+
# claiming "0 detections".
229252
- name: Append VirusTotal scan links to release notes
230253
env:
231254
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
232255
VERSION: ${{ inputs.version }}
233-
run: |
234-
# Hash the extracted binaries (not the archives — VT indexes binary hashes)
235-
TABLE="\n\n## Security Verification\n\n"
236-
TABLE+="All release binaries scanned with 70+ antivirus engines — **0 detections**.\n\n"
237-
TABLE+="| Binary | SHA-256 | VirusTotal |\n"
238-
TABLE+="|--------|---------|------------|\n"
239-
240-
for bin in binaries/codebase-memory-mcp-*; do
241-
[ -f "$bin" ] || continue
242-
name=$(basename "$bin")
243-
# Skip UI variants and non-binary files
244-
echo "$name" | grep -qE \
245-
'^codebase-memory-mcp-(linux|darwin|windows)-(amd64|arm64)(\.exe)?$' || continue
246-
sha256=$(sha256sum "$bin" 2>/dev/null | awk '{print $1}' \
247-
|| shasum -a 256 "$bin" | awk '{print $1}')
248-
label=$(echo "$name" \
249-
| sed 's/^codebase-memory-mcp-//' \
250-
| sed 's/\.exe$//')
251-
short="${sha256:0:20}..."
252-
vt_url="https://www.virustotal.com/gui/file/${sha256}/detection"
253-
TABLE+="| \`${label}\` | \`${short}\` | [0/72 ✅](${vt_url}) |\n"
254-
done
255-
256-
CURRENT=$(gh release view "$VERSION" \
257-
--json body --jq '.body // ""' --repo "$GITHUB_REPOSITORY")
258-
printf '%s%b' "$CURRENT" "$TABLE" > /tmp/release_notes.md
259-
gh release edit "$VERSION" \
260-
--notes-file /tmp/release_notes.md --repo "$GITHUB_REPOSITORY"
256+
run: scripts/ci/append-vt-notes.sh
261257

262258
# ── 8. Publish package wrappers (npm + PyPI) ──────────────────
263259
# Wrappers in pkg/npm and pkg/pypi download the released binary at

‎scripts/ci/append-vt-notes.sh‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#!/usr/bin/env bash
2+
# Append the Security Verification section to the release notes: per-binary
3+
# sha256 + VirusTotal links, and — when the pre-release ML false-positive
4+
# tolerance fired (scripts/ci/check-virustotal.sh wrote /tmp/vt_tolerated.tsv)
5+
# — an honest annotation instead of a blanket "0 detections" claim.
6+
# Expects: GH_TOKEN, VERSION; run from the verify job workspace (binaries/).
7+
set -euo pipefail
8+
9+
TOL=/tmp/vt_tolerated.tsv
10+
11+
TABLE="\n\n## Security Verification\n\n"
12+
if [ -s "$TOL" ]; then
13+
N=$(wc -l < "$TOL" | tr -d ' ')
14+
TABLE+="All release binaries were scanned with 70+ antivirus engines. "
15+
TABLE+="**${N} of them currently show a single machine-learning heuristic detection** "
16+
TABLE+="(Microsoft \`Wacatac!ml\`) that we have verified to be a false positive: "
17+
TABLE+="an up-to-date Microsoft Defender endpoint — signatures updated at scan time, "
18+
TABLE+="real-time protection on — scans the identical bytes clean. The hash-pinned "
19+
TABLE+="evidence is attached to this release as \`defender-endpoint-verification.txt\`, "
20+
TABLE+="and a false-positive report for these exact hashes has been submitted to "
21+
TABLE+="Microsoft. See **Antivirus false positives on release binaries** in the notes "
22+
TABLE+="above for the full story and what we are doing about it.\n\n"
23+
else
24+
TABLE+="All release binaries scanned with 70+ antivirus engines — **0 detections**.\n\n"
25+
fi
26+
TABLE+="| Binary | SHA-256 | VirusTotal |\n"
27+
TABLE+="|--------|---------|------------|\n"
28+
29+
for bin in binaries/codebase-memory-mcp-*; do
30+
[ -f "$bin" ] || continue
31+
name=$(basename "$bin")
32+
sha256=$(sha256sum "$bin" 2>/dev/null | awk '{print $1}' \
33+
|| shasum -a 256 "$bin" | awk '{print $1}')
34+
label=$(echo "$name" | sed 's/^codebase-memory-mcp-//' | sed 's/\.exe$//')
35+
short="${sha256:0:20}..."
36+
vt_url="https://www.virustotal.com/gui/file/${sha256}/detection"
37+
if [ -s "$TOL" ] && grep -q "^${name} " "$TOL"; then
38+
completed=$(grep "^${name} " "$TOL" | cut -f6)
39+
cell="[1/${completed} ⚠️ ML false positive, endpoint-verified clean](${vt_url})"
40+
else
41+
cell="[0 detections ✅](${vt_url})"
42+
fi
43+
TABLE+="| \`${label}\` | \`${short}\` | ${cell} |\n"
44+
done
45+
46+
CURRENT=$(gh release view "$VERSION" \
47+
--json body --jq '.body // ""' --repo "$GITHUB_REPOSITORY")
48+
printf '%s%b' "$CURRENT" "$TABLE" > /tmp/release_notes.md
49+
gh release edit "$VERSION" \
50+
--notes-file /tmp/release_notes.md --repo "$GITHUB_REPOSITORY"

‎scripts/ci/av-endpoint-verify.sh‎

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
#!/usr/bin/env bash
2+
# av-endpoint-verify.sh — verify release binaries on a REAL Microsoft Defender
3+
# endpoint (the local Windows test VM) and produce the hash-pinned evidence
4+
# file that scripts/ci/check-virustotal.sh accepts for pre-release ML false
5+
# positives.
6+
#
7+
# VirusTotal's "Microsoft" engine and the shipping Defender product can
8+
# disagree: VT runs a differently-configured engine instance, and for our
9+
# large binaries it sits at an unstable ML decision boundary (Wacatac!ml came
10+
# and went across four release cycles with no code-side cause). This script
11+
# asks the authoritative source — Defender itself, signatures updated minutes
12+
# before the scan, real-time protection on — about the exact bytes VT flagged,
13+
# and records the answer:
14+
#
15+
# defender-endpoint-verification.txt
16+
# # release/engine/signature header lines
17+
# <sha256> <asset-name> CLEAN|DETECTED
18+
#
19+
# The release gate downgrades a pre-release BLOCK to TOLERATED only when every
20+
# flagged binary's sha256 appears here as CLEAN.
21+
#
22+
# Usage: scripts/ci/av-endpoint-verify.sh <version> <asset-name...> [--upload]
23+
# version release tag holding the draft assets (e.g. v0.9.1-rc.1)
24+
# asset-name archive basename without extension, as printed by the gate
25+
# (e.g. codebase-memory-mcp-ui-linux-amd64)
26+
# --upload attach/replace the evidence file on the (draft) release
27+
#
28+
# Runs on the dev machine: needs gh (authenticated) and the local Windows VM
29+
# (test-infrastructure/vm/win.sh), plus ~300 MB VM disk per binary.
30+
set -euo pipefail
31+
32+
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
33+
cd "$ROOT"
34+
WIN=test-infrastructure/vm/win.sh
35+
[ -x "$WIN" ] || { echo "av-endpoint-verify: $WIN not found — this runs on the dev machine" >&2; exit 2; }
36+
37+
VERSION="${1:-}"
38+
[ -n "$VERSION" ] || { echo "usage: scripts/ci/av-endpoint-verify.sh <version> <asset-name...> [--upload]" >&2; exit 2; }
39+
shift
40+
UPLOAD=false
41+
NAMES=()
42+
for arg in "$@"; do
43+
case "$arg" in
44+
--upload) UPLOAD=true ;;
45+
*) NAMES+=("$arg") ;;
46+
esac
47+
done
48+
[ "${#NAMES[@]}" -ge 1 ] || { echo "av-endpoint-verify: no asset names given" >&2; exit 2; }
49+
50+
TMP="$(mktemp -d "${TMPDIR:-/tmp}/av-endpoint.XXXXXX")"
51+
trap 'rm -rf "$TMP"' EXIT
52+
53+
# ── Fetch + extract the exact release bytes ─────────────────────────────────
54+
declare -a SHAS
55+
for name in "${NAMES[@]}"; do
56+
if gh release download "$VERSION" --pattern "$name.tar.gz" --dir "$TMP" 2>/dev/null; then
57+
mkdir -p "$TMP/x-$name"
58+
tar -xzf "$TMP/$name.tar.gz" -C "$TMP/x-$name"
59+
BIN="$TMP/x-$name/codebase-memory-mcp"
60+
elif gh release download "$VERSION" --pattern "$name.zip" --dir "$TMP" 2>/dev/null; then
61+
mkdir -p "$TMP/x-$name"
62+
unzip -oq "$TMP/$name.zip" -d "$TMP/x-$name"
63+
BIN="$TMP/x-$name/codebase-memory-mcp.exe"
64+
else
65+
echo "av-endpoint-verify: no $name.tar.gz or $name.zip on release $VERSION" >&2
66+
exit 2
67+
fi
68+
[ -f "$BIN" ] || { echo "av-endpoint-verify: archive $name held no binary" >&2; exit 2; }
69+
mv "$BIN" "$TMP/probe-$name"
70+
SHAS+=("$(shasum -a 256 "$TMP/probe-$name" | awk '{print $1}')")
71+
echo "=== fetched $name (${SHAS[${#SHAS[@]}-1]}) ==="
72+
done
73+
74+
# ── Build the scan driver (pure ASCII: PowerShell 5.1 decodes BOM-less .ps1
75+
# as ANSI, and a stray non-ASCII byte corrupts the parse) ────────────────────
76+
PS1="$TMP/av-verify.ps1"
77+
{
78+
echo '$mp = Join-Path $env:ProgramFiles "Windows Defender\MpCmdRun.exe"'
79+
echo '& $mp -SignatureUpdate | Out-Null'
80+
echo '$s = Get-MpComputerStatus'
81+
echo 'Write-Output ("ENGINE=" + $s.AMEngineVersion)'
82+
echo 'Write-Output ("SIGNATURES=" + $s.AntivirusSignatureVersion)'
83+
echo 'Write-Output ("SIG_UPDATED=" + $s.AntivirusSignatureLastUpdated.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ"))'
84+
echo 'Write-Output ("RTP=" + $s.RealTimeProtectionEnabled)'
85+
for name in "${NAMES[@]}"; do
86+
echo '$f = "C:\Users\test\av-verify-'"$name"'"'
87+
echo 'if (-not (Test-Path $f)) { Write-Output ("RESULT '"$name"' MISSING") } else {'
88+
echo ' & $mp -Scan -ScanType 3 -File $f | Out-Null'
89+
echo ' $code = $LASTEXITCODE'
90+
# RTP quarantining the file between push and scan is itself a detection.
91+
echo ' if ($code -eq 0 -and (Test-Path $f)) { Write-Output ("RESULT '"$name"' CLEAN") }'
92+
echo ' else { Write-Output ("RESULT '"$name"' DETECTED code=" + $code) }'
93+
echo ' Remove-Item $f -Force -ErrorAction SilentlyContinue'
94+
echo '}'
95+
done
96+
} > "$PS1"
97+
LC_ALL=C grep -q '[^ -~]' "$PS1" && { echo "av-endpoint-verify: driver is not pure ASCII" >&2; exit 2; }
98+
99+
# ── Push and scan ───────────────────────────────────────────────────────────
100+
for name in "${NAMES[@]}"; do
101+
echo "=== pushing $name to the VM ==="
102+
"$WIN" push-file "$TMP/probe-$name" "C:/Users/test/av-verify-$name"
103+
done
104+
"$WIN" push-file "$PS1" "C:/Users/test/av-verify.ps1"
105+
echo "=== updating signatures + scanning on the endpoint ==="
106+
SCAN_OUT="$TMP/scan-out.txt"
107+
"$WIN" sh "powershell -NoProfile -ExecutionPolicy Bypass -File C:/Users/test/av-verify.ps1" | tee "$SCAN_OUT"
108+
109+
ENGINE=$(sed -n 's/^ENGINE=//p' "$SCAN_OUT" | tr -d '\r' | tail -1)
110+
SIGS=$(sed -n 's/^SIGNATURES=//p' "$SCAN_OUT" | tr -d '\r' | tail -1)
111+
SIG_UPDATED=$(sed -n 's/^SIG_UPDATED=//p' "$SCAN_OUT" | tr -d '\r' | tail -1)
112+
RTP=$(sed -n 's/^RTP=//p' "$SCAN_OUT" | tr -d '\r' | tail -1)
113+
[ -n "$ENGINE" ] && [ -n "$SIGS" ] || { echo "av-endpoint-verify: endpoint did not report engine/signature versions" >&2; exit 1; }
114+
[ "$RTP" = "True" ] || { echo "av-endpoint-verify: real-time protection is OFF on the VM — evidence would be meaningless" >&2; exit 1; }
115+
116+
# ── Write the evidence file ─────────────────────────────────────────────────
117+
OUT="defender-endpoint-verification.txt"
118+
{
119+
echo "# defender-endpoint-verification"
120+
echo "# release: $VERSION"
121+
echo "# generated: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
122+
echo "# venue: local Windows 11 ARM64 VM, Microsoft Defender endpoint, real-time protection on"
123+
echo "# engine: $ENGINE"
124+
echo "# signatures: $SIGS (updated $SIG_UPDATED)"
125+
} > "$OUT"
126+
127+
FAILED=false
128+
for i in "${!NAMES[@]}"; do
129+
name="${NAMES[$i]}"
130+
verdict=$(sed -n "s/^RESULT $name //p" "$SCAN_OUT" | tr -d '\r' | tail -1)
131+
case "$verdict" in
132+
CLEAN) printf '%s %s CLEAN\n' "${SHAS[$i]}" "$name" >> "$OUT" ;;
133+
*)
134+
printf '%s %s DETECTED\n' "${SHAS[$i]}" "$name" >> "$OUT"
135+
echo "av-endpoint-verify: $name verdict '$verdict' — NOT clean" >&2
136+
FAILED=true
137+
;;
138+
esac
139+
done
140+
141+
echo "=== evidence written: $OUT ==="
142+
cat "$OUT"
143+
144+
if [ "$FAILED" = "true" ]; then
145+
echo "av-endpoint-verify: Defender itself reports a detection — this is NOT" >&2
146+
echo "the false-positive pattern; do not release these binaries." >&2
147+
exit 1
148+
fi
149+
150+
if [ "$UPLOAD" = "true" ]; then
151+
gh release upload "$VERSION" "$OUT" --clobber
152+
echo "=== uploaded $OUT to release $VERSION ==="
153+
fi

0 commit comments

Comments
 (0)