Files
codebase-memory-mcp/tests/test_vt_release_notes_contract.sh
T
Martin Vogel 7b4533b67d fix(ci): finish the three-candidate wiring and restore full-surface VT scanning
Two things, both found by the dry-run and by sweeping what it exposed.

1. The three-candidate change was incomplete. The dry-run failed at
   stage-release-candidates.py, which carries its OWN copy of the transform
   validation that the previous commit only fixed in the selector:

     stage-release-candidates: candidate linux-amd64/debug-stripped has
     invalid transform: 'strip-debug'

   Sweeping for that assumption found it in six places, not two:
   stage-release-candidates.py, select-release-candidates.py,
   verify-release-selection.py, append-vt-notes.sh and two contract-test
   fixtures — as hardcoded 16s, `len(TARGETS) * 2`, two-entry VARIANTS tuples
   and two-key truth tables. All are now derived from len(VARIANTS).

   Field prefixes needed care: the variant NAME keeps its hyphen because it is
   the on-disk directory, while the evidence columns use underscores, so
   `debug-stripped` reads `debug_stripped_sha256`. Every lookup now goes through
   an explicit FIELD_KEY map instead of interpolating the variant directly.

   The selection contract test now covers the truth table EXHAUSTIVELY: three
   variants x two tolerated classifications is exactly eight combinations, and
   there are exactly eight targets, so every case is exercised once.

2. Full-surface VirusTotal scanning is restored. This PR had moved scanning
   upstream to the candidates and deleted the post-package pass, which silently
   narrowed coverage from everything we ship to executables only. The 42 runtime
   files across the 14 containers — install.sh, install.ps1, LICENSE,
   THIRD_PARTY_NOTICES.md, the MCPB manifest.json and the unpacked UI assets —
   were still extracted, structurally verified and strings-audited, but no
   longer scanned at all. install.sh and install.ps1 are the highest-consequence
   non-executable bytes we publish; users pipe them straight into a shell.

   The verify job scans every extracted object again, under the same policy.
   Re-submitting the selected executables alongside them is close to free
   because VirusTotal is content-addressed and answers for identical bytes from
   its own record — the same property that made the analysis-id equality check
   untenable two commits ago.

   The gate-chain contract asserted the opposite ("duplicate post-package
   VirusTotal path remains"). That assertion is inverted: the pass is required,
   and it is not a duplicate, since it covers a strictly larger set. README and
   SECURITY.md updated from "archive containers are checksummed rather than
   redundantly rescanned" to state the full covered surface.

All five release/VT contract tests pass.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-14 00:21:31 +02:00

232 lines
10 KiB
Bash

#!/usr/bin/env bash
# Contract: release notes reuse the exact pre-smoke candidate verdicts.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
SCRIPT="$ROOT/scripts/ci/append-vt-notes.sh"
FIX="$(mktemp -d "${TMPDIR:-/tmp}/vt-notes-contract.XXXXXX")"
trap 'rm -rf "$FIX"' EXIT
fail() { echo "FAIL: $*" >&2; exit 1; }
mkdir -p "$FIX/bin" "$FIX/evidence"
python3 - "$FIX/evidence" <<'PY'
import csv
import hashlib
import pathlib
import sys
root = pathlib.Path(sys.argv[1])
targets = (
"linux-amd64", "linux-arm64", "linux-amd64-portable",
"linux-arm64-portable", "darwin-amd64", "darwin-arm64",
"windows-amd64", "windows-arm64",
)
variants = ("unstripped", "debug-stripped", "stripped")
field_key = {v: v.replace("-", "_") for v in variants}
candidate_fields = (
"target", "variant", "relative_path", "source_sha256", "pre_sign_sha256",
"sha256", "size", "format", "architecture", "linkage", "transform",
"signature", "strip_tool", "strip_version", "pair_verification", "scan_path",
)
result_fields = (
"scan_path", "sha256", "size", "association_count", "completed_engines",
"total_engines", "malicious", "suspicious", "analysis_id",
"microsoft_category", "microsoft_result", "policy_classification",
"microsoft_engine_version", "microsoft_engine_update", "virustotal_url",
)
selection_fields = (
"target", "selected_variant", "selected_path", "selected_sha256",
"selected_size", "decision", "unstripped_sha256", "unstripped_scan_path",
"unstripped_classification", "unstripped_analysis_id",
"unstripped_virustotal_url",
"debug_stripped_sha256", "debug_stripped_scan_path",
"debug_stripped_classification", "debug_stripped_analysis_id",
"debug_stripped_virustotal_url",
"stripped_sha256", "stripped_scan_path",
"stripped_classification", "stripped_analysis_id", "stripped_virustotal_url",
)
def digest(value: str) -> str:
return hashlib.sha256(value.encode()).hexdigest()
def write(path, marker, metadata, fields, rows):
with path.open("w", encoding="utf-8", newline="") as handle:
handle.write(f"# {marker}\n")
for key, value in metadata.items():
handle.write(f"# {key}={value}\n")
writer = csv.DictWriter(handle, fieldnames=fields, delimiter="\t", lineterminator="\n")
writer.writeheader()
writer.writerows(rows)
classes = {(target, variant): "clean" for target in targets for variant in variants}
classes["linux-amd64", "stripped"] = "microsoft-ml"
classes["darwin-arm64", "stripped"] = "microsoft-ml"
classes["darwin-arm64", "unstripped"] = "microsoft-ml"
candidates = []
results = []
by_pair = {}
for target in targets:
source_sha = digest(f"{target}:linker-output")
for variant in variants:
sha = digest(f"{target}:{variant}")
name = "codebase-memory-mcp.exe" if target.startswith("windows-") else "codebase-memory-mcp"
row = {
"target": target, "variant": variant, "relative_path": f"{variant}/{name}",
"source_sha256": source_sha, "pre_sign_sha256": sha, "sha256": sha,
"size": "1234", "format": "pe" if target.startswith("windows-") else "macho" if target.startswith("darwin-") else "elf",
"architecture": target.split("-")[1],
"linkage": "portable" if target.endswith("-portable") else "dynamic" if target.startswith("linux-") else "native",
"transform": {"unstripped": "copy", "debug-stripped": "strip-debug", "stripped": "strip"}[variant],
"signature": "adhoc-verified" if target.startswith("darwin-") else "not-applicable",
"strip_tool": "strip", "strip_version": "contract",
"pair_verification": "same-linker-output-v1", "scan_path": f"objects/{sha}",
}
candidates.append(row)
by_pair[target, variant] = row
classification = classes[target, variant]
clean = classification == "clean"
results.append({
"scan_path": row["scan_path"], "sha256": sha, "size": row["size"],
"association_count": "1", "completed_engines": "61", "total_engines": "64",
"malicious": "0" if clean else "1", "suspicious": "0",
"analysis_id": f"analysis-{sha[:20]}",
"microsoft_category": "undetected" if clean else "malicious",
"microsoft_result": "" if clean else "Trojan:Win32/Wacatac.B!ml",
"policy_classification": classification,
"microsoft_engine_version": "1.1.1", "microsoft_engine_update": "20260813",
"virustotal_url": f"https://www.virustotal.com/gui/file/{sha}/detection",
})
result_by_pair = {(row["target"], row["variant"]): result for row, result in zip(candidates, results)}
selections = []
for target in targets:
order = ("stripped", "debug-stripped", "unstripped")
clean = [v for v in order if classes[target, v] == "clean"]
chosen = clean[0] if clean else "stripped"
selected = by_pair[target, chosen]
if not clean:
decision = "stripped-all-candidates-microsoft-ml"
elif chosen == "stripped":
decision = "stripped-preferred"
else:
others = "-".join(f"{v}:{classes[target, v]}" for v in order if v != chosen)
decision = f"{chosen}-clean-after-{others}"
row = {
"target": target, "selected_variant": chosen,
"selected_path": f"selected/{target}/" + ("codebase-memory-mcp.exe" if target.startswith("windows-") else "codebase-memory-mcp"),
"selected_sha256": selected["sha256"], "selected_size": selected["size"],
"decision": decision,
}
for variant in variants:
candidate = by_pair[target, variant]
result = result_by_pair[target, variant]
row.update({
f"{field_key[variant]}_sha256": candidate["sha256"],
f"{field_key[variant]}_scan_path": candidate["scan_path"],
f"{field_key[variant]}_classification": result["policy_classification"],
f"{field_key[variant]}_analysis_id": result["analysis_id"],
f"{field_key[variant]}_virustotal_url": result["virustotal_url"],
})
selections.append(row)
write(root / "release-candidates.tsv", "cbm-release-candidates-v1",
{"targets": 8, "candidates": 24}, candidate_fields, candidates)
write(root / "virustotal-candidate-results.tsv", "cbm-virustotal-results-v2",
{"scan_objects": 24, "associations": 24, "min_engines_policy": 50,
"min_completed_engines": 61, "max_completed_engines": 61}, result_fields, results)
write(root / "release-selection.tsv", "cbm-release-selection-v1",
{"policy": "virustotal-v2", "targets": 8, "candidates": 24},
selection_fields, selections)
PY
cat > "$FIX/current.md" <<'EOF'
Intro text.
<!-- cbm-security-verification:start -->
stale data
<!-- cbm-security-verification:end -->
Outro text.
EOF
cat > "$FIX/bin/gh" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
if [ "$1" = release ] && [ "$2" = view ]; then
cat "$STUB_RELEASE_BODY"
exit 0
fi
if [ "$1" = release ] && [ "$2" = edit ]; then
while [ "$#" -gt 0 ]; do
if [ "$1" = --notes-file ]; then
cp "$2" "$STUB_CAPTURE"
exit 0
fi
shift
done
fi
exit 2
EOF
chmod +x "$FIX/bin/gh"
run_notes() {
(cd "$FIX" &&
PATH="$FIX/bin:$PATH" \
GH_TOKEN=stub VERSION=v1.0.0 \
GITHUB_REPOSITORY=DeusData/codebase-memory-mcp \
VT_CANDIDATES=evidence/release-candidates.tsv \
VT_RESULTS_PATH=evidence/virustotal-candidate-results.tsv \
RELEASE_SELECTION=evidence/release-selection.tsv \
STUB_RELEASE_BODY="$1" STUB_CAPTURE="$2" \
bash "$SCRIPT")
}
run_notes "$FIX/current.md" "$FIX/first.md"
[ "$(grep -c 'cbm-security-verification:start' "$FIX/first.md")" = 1 ] || fail "start marker duplicated"
[ "$(grep -c 'cbm-security-verification:end' "$FIX/first.md")" = 1 ] || fail "end marker duplicated"
grep -q 'Intro text.' "$FIX/first.md" || fail "content before section was lost"
grep -q 'Outro text.' "$FIX/first.md" || fail "content after section was lost"
! grep -q 'stale data' "$FIX/first.md" || fail "stale section was appended"
grep -q '24 executable scans' "$FIX/first.md" || fail "candidate scan scope missing"
grep -q '8 release products' "$FIX/first.md" || fail "product scope missing"
grep -q '3 candidate(s).*Microsoft' "$FIX/first.md" || fail "tolerated findings not disclosed"
grep -q 'no other decisive engine reported malicious or suspicious' "$FIX/first.md" || fail "non-decisive engines are overstated as clean"
! grep -q 'every other engine was clean' "$FIX/first.md" || fail "non-decisive engines are incorrectly described as clean"
grep -q '| Product | Stripped candidate | Unstripped candidate | Shipped |' "$FIX/first.md" || fail "candidate table missing"
grep -q '| `linux-amd64` |' "$FIX/first.md" || fail "Linux product row missing"
grep -q '| `windows-arm64` |' "$FIX/first.md" || fail "Windows product row missing"
grep -q 'archive containers were not redundantly submitted' "$FIX/first.md" || fail "no-rescan boundary missing"
for asset in release-candidates.tsv virustotal-candidate-results.tsv release-selection.tsv; do
grep -q "releases/download/v1.0.0/$asset" "$FIX/first.md" || fail "evidence link missing: $asset"
done
run_notes "$FIX/first.md" "$FIX/second.md"
python3 -c 'import sys;sys.exit(open(sys.argv[1],"rb").read()!=open(sys.argv[2],"rb").read())' \
"$FIX/first.md" "$FIX/second.md" || fail "release-note update is not idempotent"
cp "$FIX/evidence/release-selection.tsv" "$FIX/evidence/release-selection.clean.tsv"
python3 - "$FIX/evidence/release-selection.tsv" <<'PY'
import pathlib, sys
p = pathlib.Path(sys.argv[1])
text = p.read_text()
p.write_text(text.replace("\t1234\tstripped-preferred", "\t9999\tstripped-preferred", 1))
PY
if run_notes "$FIX/current.md" "$FIX/tampered.md"; then
fail "selection hash/size mismatch must fail closed"
fi
mv "$FIX/evidence/release-selection.clean.tsv" "$FIX/evidence/release-selection.tsv"
cat > "$FIX/reversed.md" <<'EOF'
<!-- cbm-security-verification:end -->
stale reversed data
<!-- cbm-security-verification:start -->
EOF
if run_notes "$FIX/reversed.md" "$FIX/reversed-capture.md"; then
fail "reversed verification markers must fail closed"
fi
echo 'PASS: release notes reuse all 24 candidate verdicts, disclose selection, and avoid a duplicate scan'