Files
deusdata--codebase-memory-mcp/scripts/ci/check-virustotal.sh
Martin Vogel 8eff872df5 fix(ci): teach the VirusTotal gate the withheld-executables manifest
The v0.10.6 release run failed deterministically at verify:

  BLOCKED: expected scan object is missing:
    objects/scan-3099e91c...--codebase-memory-mcp.exe

exclude-rescanned-selected-objects.sh (added after v0.10.5, first exercised
by this release) deliberately deletes the selected executables from the
surface-scan directory — their bytes were already scanned as candidates and
re-submitting identical bytes re-rolls a probabilistic classifier — and
writes binaries/virustotal-withheld.tsv. But check-virustotal.sh still
received the pre-withhold scan-set listing all sixteen objects and failed
closed on the first missing file. The rework's two halves never talked.

The gate now accepts an optional VT_WITHHELD manifest (strict parse: v1
marker, the stated reason required, sha256-keyed rows): an expected-set row
whose hash the manifest vouches for is exempt from the on-disk and
action-output contracts, while everything else keeps the strict path.
Fail-closed properties preserved and extended:

  - no VT_WITHHELD          -> byte-for-byte previous behavior (candidate
                               stage and dry-run call sites are unaffected;
                               verified against the original failure)
  - withheld object present -> blocked (inconsistent staging)
  - hash outside the set    -> blocked (spurious withhold)
  - everything withheld     -> blocked (scan would cover nothing)
  - mismatched object name  -> blocked

vt-results.tsv keeps its exact shape (scanned objects only) — the release
notes table already uses the candidate results, and the withheld manifest is
now preserved with the rest of the evidence artifacts. release.yml passes
VT_WITHHELD only in the verify stage, right after the withhold step.

Verified offline with a fixture reproducing the release failure verbatim
plus the four negative cases above; the positive case passes staging and
association validation and proceeds to VT polling.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-17 12:12:52 +02:00

880 lines
37 KiB
Bash
Executable File

#!/usr/bin/env bash
# Verify that the VirusTotal action submitted and completed exactly the
# content-bound scan set produced by extract-release-archives.sh.
#
# Required: VT_API_KEY, VT_ANALYSIS, VT_EXPECTED_SCAN_SET, VT_ASSOCIATIONS
# Workflow policy: MIN_ENGINES=50, clean or exactly one disclosed Microsoft
# machine-learning (`!ml`) verdict; every other detection blocks.
# Optional: VT_RESULTS_PATH, VT_REQUEST_INTERVAL_SECONDS,
# VT_POLL_TIMEOUT_SECONDS, VT_CURL_TIMEOUT_SECONDS.
set -euo pipefail
command -v python3 >/dev/null 2>&1 || {
echo "BLOCKED: python3 is required for the VirusTotal gate" >&2
exit 1
}
command -v curl >/dev/null 2>&1 || {
echo "BLOCKED: curl is required for the VirusTotal gate" >&2
exit 1
}
python3 - "$BASH" <<'PY'
from __future__ import annotations
import csv
import hashlib
import json
import os
import pathlib
import re
import stat
import subprocess
import sys
import tempfile
import time
import urllib.parse
from collections import deque
from dataclasses import dataclass
from typing import Deque, Dict, List, Optional, Sequence, Tuple
bash_executable = pathlib.Path(sys.argv[1])
class GateError(Exception):
pass
SCAN_FIELDS = (
"scan_path",
"sha256",
"size",
"association_count",
"association_kinds",
)
ASSOCIATION_FIELDS = (
"association_type",
"archive",
"archive_sha256",
"variant",
"kind",
"member",
"asset_path",
"mime",
"scan_path",
"object_sha256",
"size",
)
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",
)
SHA256_RE = re.compile(r"\A[0-9a-f]{64}\Z")
ANALYSIS_ID_RE = re.compile(r"\A[A-Za-z0-9_+/-]+={0,2}\Z")
ALLOWED_STATS = {
"confirmed-timeout",
"timeout",
"failure",
"harmless",
"undetected",
"suspicious",
"malicious",
"type-unsupported",
}
DECISIVE_STATS = ("malicious", "suspicious", "undetected", "harmless")
ALLOWED_STATUS = {"queued", "in-progress", "completed"}
@dataclass(frozen=True)
class ExpectedObject:
scan_path: str
local_path: pathlib.Path
sha256: str
size: int
association_count: int
association_kinds: frozenset[str]
withheld: bool
@property
def requires_microsoft(self) -> bool:
return "binary" in self.association_kinds
@dataclass(frozen=True)
class Submission:
expected: ExpectedObject
analysis_id: str
action_url: str
@dataclass(frozen=True)
class CompletedResult:
submission: Submission
completed_engines: int
total_engines: int
malicious: int
suspicious: int
microsoft_category: str
microsoft_result: str
microsoft_engine_version: str
microsoft_engine_update: str
detections: Tuple[Tuple[str, str, str, str, str], ...]
def required_env(name: str) -> str:
value = os.environ.get(name)
if value is None or value == "":
raise GateError(f"required environment value is empty: {name}")
return value
def parse_int_env(name: str, default: str, *, minimum: int) -> int:
raw = os.environ.get(name, default)
if not raw.isdigit() or int(raw) < minimum:
raise GateError(f"{name} must be an integer >= {minimum}")
return int(raw)
def sha256_file(path: pathlib.Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def parse_versioned_tsv(
path: pathlib.Path,
*,
marker: str,
fields: Sequence[str],
) -> Tuple[Dict[str, int], List[Dict[str, str]]]:
if path.is_symlink() or not path.is_file():
raise GateError(f"expected evidence manifest is not a regular file: {path}")
if path.stat().st_size > 16 * 1024 * 1024:
raise GateError(f"expected evidence manifest is unexpectedly large: {path}")
lines = path.read_text(encoding="utf-8").splitlines()
if not lines or lines[0] != f"# {marker}":
raise GateError(f"expected evidence manifest marker is missing: {marker}")
metadata: Dict[str, int] = {}
cursor = 1
while cursor < len(lines) and lines[cursor].startswith("# "):
raw = lines[cursor][2:]
key, separator, value = raw.partition("=")
if not separator or not key or key in metadata or not value.isdigit():
raise GateError(f"malformed or duplicate evidence metadata: {raw}")
metadata[key] = int(value)
cursor += 1
reader = csv.DictReader(lines[cursor:], delimiter="\t")
if tuple(reader.fieldnames or ()) != tuple(fields):
raise GateError("evidence TSV header is malformed or unexpected")
rows = list(reader)
if any(None in row or any(row.get(field) is None for field in fields) for row in rows):
raise GateError("evidence TSV row has missing or surplus cells")
if not rows:
raise GateError("expected scan set is empty")
return metadata, rows
def load_withheld(raw_path: str) -> Dict[str, str]:
"""Objects deliberately removed from the surface scan (VT_WITHHELD).
exclude-rescanned-selected-objects.sh deletes the SELECTED executables from
the objects directory before the upload — their bytes were already scanned
as release candidates, identity is settled by sha256, and re-submitting
identical bytes re-rolls a probabilistic classifier (measured on v0.10.5:
verdicts flipped in both directions within the hour). The gate accepts a
withheld object only when this manifest vouches for it BY HASH; everything
else keeps the strict on-disk contract. An absent manifest keeps the
original behavior everywhere else this gate runs (candidate stage,
dry-run), where nothing is ever withheld."""
path = pathlib.Path(raw_path).absolute()
if path.is_symlink() or not path.is_file():
raise GateError(f"withheld manifest is not a regular file: {path}")
if path.stat().st_size > 1024 * 1024:
raise GateError(f"withheld manifest is unexpectedly large: {path}")
lines = path.read_text(encoding="utf-8").splitlines()
if not lines or lines[0] != "# cbm-virustotal-withheld-v1":
raise GateError("withheld manifest marker is missing")
cursor = 1
metadata: Dict[str, str] = {}
while cursor < len(lines) and lines[cursor].startswith("# "):
key, separator, value = lines[cursor][2:].partition("=")
if not separator or not key or key in metadata or not value:
raise GateError(f"malformed withheld metadata: {lines[cursor]}")
metadata[key] = value
cursor += 1
if metadata.get("reason") != "already-scanned-as-candidate":
raise GateError("withheld manifest does not state the accepted reason")
if cursor >= len(lines) or lines[cursor] != "sha256\tobject":
raise GateError("withheld manifest header is malformed")
withheld: Dict[str, str] = {}
for line in lines[cursor + 1 :]:
sha256, separator, name = line.partition("\t")
if not separator or SHA256_RE.fullmatch(sha256) is None or not name or "/" in name:
raise GateError(f"malformed withheld row: {line}")
if sha256 in withheld:
raise GateError(f"duplicate withheld sha256: {sha256}")
withheld[sha256] = name
if not withheld:
raise GateError("withheld manifest names no objects")
return withheld
def load_expected(
path: pathlib.Path, withheld: Dict[str, str]
) -> Tuple[List[ExpectedObject], int]:
metadata, rows = parse_versioned_tsv(
path,
marker="cbm-release-scan-set-v2",
fields=SCAN_FIELDS,
)
expected_count = metadata.get("scan_objects")
associations = metadata.get("associations")
if expected_count != len(rows) or associations is None or associations < len(rows):
raise GateError("scan-set metadata does not match its rows")
objects: List[ExpectedObject] = []
seen_paths: set[str] = set()
association_total = 0
for row in rows:
scan_path = row["scan_path"]
pure = pathlib.PurePosixPath(scan_path)
if (
pure.is_absolute()
or len(pure.parts) != 2
or pure.parts[0] != "objects"
or pure.parts[1] in ("", ".", "..")
or scan_path != pure.as_posix()
or scan_path in seen_paths
):
raise GateError(f"invalid or duplicate scan path in expected set: {scan_path}")
seen_paths.add(scan_path)
sha256 = row["sha256"]
if SHA256_RE.fullmatch(sha256) is None:
raise GateError(f"invalid SHA-256 in expected set: {scan_path}")
if not row["size"].isdigit() or not row["association_count"].isdigit():
raise GateError(f"invalid size/count in expected set: {scan_path}")
size = int(row["size"])
association_count = int(row["association_count"])
kinds = row["association_kinds"].split(",")
allowed_kinds = {"binary", "runtime"}
if (
association_count < 1
or any(not kind or kind not in allowed_kinds for kind in kinds)
or kinds != sorted(set(kinds))
):
raise GateError(f"unassociated object in expected set: {scan_path}")
local_path = path.parent.joinpath(*pure.parts)
is_withheld = sha256 in withheld
if is_withheld:
if withheld[sha256] != pure.parts[1]:
raise GateError(
f"withheld manifest names a different object for this hash: {scan_path}"
)
if os.path.lexists(local_path):
raise GateError(
f"withheld object is still present in the scan directory: {scan_path}"
)
else:
try:
mode = local_path.lstat().st_mode
except FileNotFoundError as error:
raise GateError(f"expected scan object is missing: {scan_path}") from error
if not stat.S_ISREG(mode):
raise GateError(f"expected scan object is not a regular file: {scan_path}")
actual_size = local_path.stat().st_size
if actual_size != size or sha256_file(local_path) != sha256:
raise GateError(f"expected scan object changed after extraction: {scan_path}")
association_total += association_count
objects.append(
ExpectedObject(
scan_path=scan_path,
local_path=local_path.absolute(),
sha256=sha256,
size=size,
association_count=association_count,
association_kinds=frozenset(kinds),
withheld=is_withheld,
)
)
if association_total != associations:
raise GateError("scan-set association counts do not match manifest metadata")
expected_hashes = {item.sha256 for item in objects}
spurious = sorted(set(withheld) - expected_hashes)
if spurious:
raise GateError(f"withheld manifest names hashes outside the expected scan set: {spurious}")
if all(item.withheld for item in objects):
raise GateError("every expected object is withheld; the surface scan would cover nothing")
return objects, associations
def validate_associations(
path: pathlib.Path,
*,
objects: Sequence[ExpectedObject],
expected_associations: int,
) -> None:
metadata, rows = parse_versioned_tsv(
path,
marker="cbm-release-scan-associations-v3",
fields=ASSOCIATION_FIELDS,
)
if (
metadata.get("associations") != expected_associations
or metadata.get("scan_objects") != len(objects)
or len(rows) != expected_associations
):
raise GateError("association manifest metadata does not match the expected scan set")
expected_by_path = {item.scan_path: item for item in objects}
counts: Dict[str, int] = {}
kinds: Dict[str, set[str]] = {}
seen: set[Tuple[str, str, str, str]] = set()
archive_hashes: Dict[str, str] = {}
for row in rows:
archive = row["archive"]
archive_sha256 = row["archive_sha256"]
if not archive or SHA256_RE.fullmatch(archive_sha256) is None:
raise GateError(f"malformed archive provenance: {archive}")
previous = archive_hashes.setdefault(archive, archive_sha256)
if previous != archive_sha256:
raise GateError(f"conflicting archive provenance: {archive}")
if metadata.get("archives") != len(archive_hashes):
raise GateError("archive provenance count does not match the association manifest")
for row in rows:
association_type = row["association_type"]
key = (association_type, row["archive"], row["member"], row["asset_path"])
if key in seen:
raise GateError(f"duplicate release association: {key}")
seen.add(key)
if association_type != "member" or not row["archive"]:
raise GateError(f"malformed release association: {key}")
if archive_hashes.get(row["archive"]) != row["archive_sha256"]:
raise GateError(f"association is not bound to its archive provenance: {key}")
semantic_valid = bool(row["member"]) and not row["asset_path"]
if not semantic_valid:
raise GateError(f"malformed release association semantics: {key}")
expected = expected_by_path.get(row["scan_path"])
if expected is None:
raise GateError(f"association maps outside expected scan set: {key}")
if (
row["object_sha256"] != expected.sha256
or not row["size"].isdigit()
or int(row["size"]) != expected.size
or SHA256_RE.fullmatch(row["archive_sha256"]) is None
):
raise GateError(f"association hash/size is not content-bound: {key}")
counts[expected.scan_path] = counts.get(expected.scan_path, 0) + 1
kinds.setdefault(expected.scan_path, set()).add(row["kind"])
if set(counts) != set(expected_by_path):
raise GateError("association manifest does not map every expected scan object")
for scan_path, expected in expected_by_path.items():
if counts[scan_path] != expected.association_count:
raise GateError(f"association count differs from expected scan set: {scan_path}")
if frozenset(kinds[scan_path]) != expected.association_kinds:
raise GateError(f"association kinds differ from expected scan set: {scan_path}")
def output_aliases(objects: Sequence[ExpectedObject], manifest: pathlib.Path) -> Dict[str, ExpectedObject]:
aliases: Dict[str, ExpectedObject] = {}
cwd = pathlib.Path.cwd().absolute()
for item in objects:
relative_from_cwd = os.path.relpath(item.local_path, cwd)
candidates = {
item.scan_path,
item.local_path.name,
str(item.local_path),
pathlib.PurePosixPath(relative_from_cwd).as_posix(),
pathlib.PurePosixPath(manifest.parent.name, item.scan_path).as_posix(),
f"./{pathlib.PurePosixPath(relative_from_cwd).as_posix()}",
}
for alias in candidates:
previous = aliases.get(alias)
if previous is not None and previous.scan_path != item.scan_path:
raise GateError(f"ambiguous expected action-output alias: {alias}")
aliases[alias] = item
return aliases
def parse_analysis_id(url: str) -> str:
parsed = urllib.parse.urlsplit(url)
if parsed.scheme != "https" or parsed.hostname not in {"www.virustotal.com", "virustotal.com"}:
raise GateError(f"malformed VirusTotal analysis URL: {url}")
parts = parsed.path.split("/")
try:
index = parts.index("file-analysis")
analysis_id = urllib.parse.unquote(parts[index + 1])
except (ValueError, IndexError):
raise GateError(f"malformed VirusTotal analysis URL: {url}") from None
if ANALYSIS_ID_RE.fullmatch(analysis_id) is None:
raise GateError(f"malformed VirusTotal analysis id in URL: {url}")
return analysis_id
def parse_action_output(
raw: str,
*,
objects: Sequence[ExpectedObject],
manifest: pathlib.Path,
) -> List[Submission]:
objects = [item for item in objects if not item.withheld]
if not raw:
raise GateError("VirusTotal action output is empty")
aliases = output_aliases(objects, manifest)
submissions: Dict[str, Submission] = {}
used_ids: set[str] = set()
entries = raw.split(",")
if any(entry == "" or entry != entry.strip() for entry in entries):
raise GateError("VirusTotal action output has an empty or whitespace-padded entry")
for entry in entries:
filename, separator, url = entry.partition("=")
if not separator or not filename or not url:
raise GateError(f"malformed VirusTotal action output entry: {entry}")
expected = aliases.get(filename)
if expected is None:
raise GateError(f"VirusTotal action returned an unexpected path: {filename}")
if expected.scan_path in submissions:
raise GateError(f"VirusTotal action returned a duplicate path: {filename}")
analysis_id = parse_analysis_id(url)
if analysis_id in used_ids:
raise GateError("VirusTotal action reused one analysis for multiple scan objects")
used_ids.add(analysis_id)
submissions[expected.scan_path] = Submission(expected, analysis_id, url)
expected_paths = {item.scan_path for item in objects}
actual_paths = set(submissions)
if actual_paths != expected_paths:
missing = sorted(expected_paths - actual_paths)
unexpected = sorted(actual_paths - expected_paths)
raise GateError(
f"VirusTotal action output is partial or unexpected: missing={missing}, unexpected={unexpected}"
)
return [submissions[path] for path in sorted(submissions)]
def nonnegative_int(value: object, label: str) -> int:
if isinstance(value, bool) or not isinstance(value, int) or value < 0:
raise GateError(f"malformed VirusTotal statistic: {label}")
return value
def one_line(value: object) -> str:
return " ".join(str(value).split())
def required_one_line_string(value: object, label: str) -> str:
if not isinstance(value, str):
raise GateError(f"VirusTotal field is not a string: {label}")
normalized = " ".join(value.split())
if not normalized:
raise GateError(f"VirusTotal field is empty: {label}")
return normalized
def parse_completed(document: object, submission: Submission) -> Tuple[str, Optional[CompletedResult], List[str]]:
if not isinstance(document, dict):
raise GateError("VirusTotal response root is not an object")
data = document.get("data")
if not isinstance(data, dict):
raise GateError("VirusTotal response has no analysis data object")
if data.get("type") != "analysis":
raise GateError(f"VirusTotal response is not an analysis for {submission.expected.scan_path}")
response_id = data.get("id")
if not isinstance(response_id, str) or ANALYSIS_ID_RE.fullmatch(response_id) is None:
raise GateError(f"VirusTotal response has an invalid analysis id: {submission.expected.scan_path}")
# The id is recorded as evidence, NOT required to equal the submitted one.
#
# VirusTotal is content-addressed, and it recognising our bytes is the
# behaviour we WANT, not a problem to defend against. The evidence this
# pipeline publishes is the hash-keyed file report — append-vt-notes.sh
# asserts the URL is exactly .../gui/file/<sha256>/detection — so what we
# gate on and what a reader sees when they look that SHA-256 up themselves
# are the same report. Demanding a freshly minted analysis id would have
# contradicted the evidence we publish alongside it.
#
# It also does not work in practice: requiring equality blocked a real
# dry-run on the 282 MB linux-arm64 candidate, exactly the kind of large,
# previously seen artifact VirusTotal answers for from its own record, so it
# would have recurred on most releases.
#
# What must hold is that the verdict describes THESE bytes, and the
# completed branch below enforces that against file_info.sha256 and size —
# a strictly stronger binding than an id. It is also what keeps a tuple's
# two candidates apart: stripped and unstripped differ in hash by
# construction, so neither can be read as the other whatever ids VirusTotal
# hands out (the wrong-hash and wrong-size contract cases pin this).
attributes = data.get("attributes")
if not isinstance(attributes, dict):
raise GateError("VirusTotal response has no analysis attributes")
status = attributes.get("status")
if status not in ALLOWED_STATUS:
raise GateError(f"VirusTotal response has invalid status: {status!r}")
if status != "completed":
return str(status), None, []
meta = document.get("meta")
file_info = meta.get("file_info") if isinstance(meta, dict) else None
if not isinstance(file_info, dict):
raise GateError(f"completed analysis has no file_info: {submission.expected.scan_path}")
actual_sha = file_info.get("sha256")
actual_size = file_info.get("size")
if actual_sha != submission.expected.sha256 or nonnegative_int(actual_size, "file_info.size") != submission.expected.size:
raise GateError(
f"completed analysis hash/size does not match submitted object: {submission.expected.scan_path}"
)
stats = attributes.get("stats")
if not isinstance(stats, dict) or not stats:
raise GateError(f"completed analysis has no stats: {submission.expected.scan_path}")
missing = sorted(ALLOWED_STATS - set(stats))
unknown = sorted(set(stats) - ALLOWED_STATS)
if missing or unknown:
raise GateError(
f"completed analysis has missing/unknown stats categories: "
f"missing={missing}, unknown={unknown}"
)
normalized = {key: nonnegative_int(value, f"stats.{key}") for key, value in stats.items()}
completed = sum(normalized[key] for key in DECISIVE_STATS)
total = sum(normalized.values())
malicious = normalized["malicious"]
suspicious = normalized["suspicious"]
detections: List[Tuple[str, str, str, str, str]] = []
results = attributes.get("results")
microsoft_category = ""
microsoft_result = ""
microsoft_engine_version = ""
microsoft_engine_update = ""
detail_detections = {"malicious": 0, "suspicious": 0}
if results is not None:
if not isinstance(results, dict):
raise GateError("VirusTotal per-engine results are malformed")
for engine, result in sorted(results.items()):
if not isinstance(engine, str) or not isinstance(result, dict):
raise GateError("VirusTotal per-engine result entry is malformed")
category = result.get("category")
if category not in ALLOWED_STATS:
raise GateError(f"VirusTotal engine {engine} has invalid category: {category!r}")
if category in {"malicious", "suspicious"}:
detail_detections[category] += 1
label = one_line(result.get("result") or category)
version = one_line(result.get("engine_version") or "?")
updated = one_line(result.get("engine_update") or "?")
detections.append((one_line(engine), label, str(category), version, updated))
if engine == "Microsoft":
microsoft_category = str(category)
microsoft_result = one_line(result.get("result") or "")
microsoft_engine_version = required_one_line_string(
result.get("engine_version"), "Microsoft.engine_version"
)
microsoft_engine_update = required_one_line_string(
result.get("engine_update"), "Microsoft.engine_update"
)
for category, count in detail_detections.items():
if count > normalized[category]:
raise GateError(
f"per-engine {category} results contradict aggregate stats: "
f"{submission.expected.scan_path}"
)
if submission.expected.requires_microsoft:
if microsoft_category not in DECISIVE_STATS:
raise GateError(
f"completed executable analysis has no decisive Microsoft result: "
f"{submission.expected.scan_path}"
)
if normalized[microsoft_category] < 1:
raise GateError(
f"Microsoft category is inconsistent with aggregate stats: "
f"{submission.expected.scan_path}"
)
if not microsoft_engine_version or not microsoft_engine_update:
raise GateError(
f"completed executable analysis has incomplete Microsoft version evidence: "
f"{submission.expected.scan_path}"
)
return (
"completed",
CompletedResult(
submission,
completed,
total,
malicious,
suspicious,
microsoft_category,
microsoft_result,
microsoft_engine_version,
microsoft_engine_update,
tuple(detections),
),
detections,
)
def fetch_analysis(api_key: str, analysis_id: str, curl_timeout: int) -> Optional[object]:
encoded_id = urllib.parse.quote(analysis_id, safe="")
response = subprocess.run(
[
str(bash_executable),
"-c",
'exec curl "$@"',
"curl",
"-sf",
"--max-time",
str(curl_timeout),
"-H",
f"x-apikey: {api_key}",
f"https://www.virustotal.com/api/v3/analyses/{encoded_id}",
],
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
check=False,
)
if response.returncode != 0 or not response.stdout:
return None
try:
return json.loads(response.stdout)
except (UnicodeDecodeError, json.JSONDecodeError) as error:
raise GateError(f"VirusTotal returned malformed JSON for analysis {analysis_id}") from error
def write_results(
path: pathlib.Path,
*,
results: Sequence[CompletedResult],
associations: int,
min_engines: int,
) -> None:
if os.path.lexists(path) and path.is_symlink():
raise GateError(f"results path is a symlink: {path}")
path.parent.mkdir(parents=True, exist_ok=True)
fd, temporary_name = tempfile.mkstemp(prefix=f".{path.name}.", dir=str(path.parent))
temporary = pathlib.Path(temporary_name)
try:
with os.fdopen(fd, "w", encoding="utf-8", newline="") as handle:
handle.write("# cbm-virustotal-results-v2\n")
handle.write(f"# scan_objects={len(results)}\n")
handle.write(f"# associations={associations}\n")
handle.write(f"# min_engines_policy={min_engines}\n")
handle.write(f"# min_completed_engines={min(item.completed_engines for item in results)}\n")
handle.write(f"# max_completed_engines={max(item.completed_engines for item in results)}\n")
writer = csv.DictWriter(
handle,
fieldnames=RESULT_FIELDS,
delimiter="\t",
lineterminator="\n",
)
writer.writeheader()
for item in sorted(results, key=lambda result: result.submission.expected.scan_path):
expected = item.submission.expected
writer.writerow(
{
"scan_path": expected.scan_path,
"sha256": expected.sha256,
"size": expected.size,
"association_count": expected.association_count,
"completed_engines": item.completed_engines,
"total_engines": item.total_engines,
"malicious": item.malicious,
"suspicious": item.suspicious,
"analysis_id": item.submission.analysis_id,
"microsoft_category": item.microsoft_category,
"microsoft_result": item.microsoft_result,
"policy_classification": classify_result(item, min_engines),
"microsoft_engine_version": item.microsoft_engine_version,
"microsoft_engine_update": item.microsoft_engine_update,
"virustotal_url": f"https://www.virustotal.com/gui/file/{expected.sha256}/detection",
}
)
handle.flush()
os.fsync(handle.fileno())
os.chmod(temporary, 0o600)
os.replace(temporary, path)
finally:
if os.path.lexists(temporary):
temporary.unlink()
# Release policy: exactly ONE Microsoft machine-learning verdict is tolerated.
#
# Microsoft's `!ml` suffix marks a heuristic/ML classification rather than a
# signature match, and it is an endemic false positive on large unsigned native
# binaries -- llama.cpp, GitHub's own `gh`, Microsoft's own Go toolchain and
# Anthropic's Claude installer have all carried the same family. Our own
# evidence is that the verdict is not a property of our bytes: it inverts across
# architectures and link modes, moves between sibling artifacts of one build,
# and lands in different variant buckets for the same source.
#
# Everything else still fails the release: two or more engines, any label that
# is not `!ml` (a signature hit is a real finding), any non-Microsoft engine,
# any suspicious verdict, and every infrastructure error.
def is_tolerated_detection(result: CompletedResult) -> bool:
if result.suspicious or result.malicious != 1 or len(result.detections) != 1:
return False
engine, label, category, _version, _updated = result.detections[0]
return engine == "Microsoft" and category == "malicious" and label.endswith("!ml")
# Classification depends ONLY on what engines found, never on how many answered.
#
# "hard" means an engine actually found something we will not ship: two or more
# engines, any non-Microsoft engine, any label that is not `!ml`, or anything
# suspicious.
#
# How many engines returned a decisive result is NOT a policy input. It is a
# property of VirusTotal's fleet on the day, which we cannot influence: these
# binaries are ~300 MB and many engines skip or time out at that size, so the
# count varies run to run (observed on one run: one object at 48, the other
# fifteen spread 59-68). A 50-engine floor turned that variance into a release
# blocker — it failed an 8-target release on a windows-arm64 candidate with
# ZERO detections whose own sibling scanned clean at 66. Gating on it makes
# shipping a lottery decided by someone else's infrastructure, so the count is
# recorded as evidence and nothing more.
def classify_result(result: CompletedResult, min_engines: int) -> str:
del min_engines # retained for signature stability; not a policy input
if result.malicious or result.suspicious:
return "microsoft-ml" if is_tolerated_detection(result) else "hard"
return "clean"
def main() -> None:
api_key = required_env("VT_API_KEY")
raw_analysis = required_env("VT_ANALYSIS")
expected_manifest = pathlib.Path(required_env("VT_EXPECTED_SCAN_SET")).absolute()
associations_manifest = pathlib.Path(required_env("VT_ASSOCIATIONS")).absolute()
min_engines = parse_int_env("MIN_ENGINES", "50", minimum=1)
request_interval = parse_int_env("VT_REQUEST_INTERVAL_SECONDS", "15", minimum=0)
poll_timeout = parse_int_env("VT_POLL_TIMEOUT_SECONDS", "7200", minimum=1)
curl_timeout = parse_int_env("VT_CURL_TIMEOUT_SECONDS", "15", minimum=1)
results_path = pathlib.Path(
os.environ.get("VT_RESULTS_PATH", str(expected_manifest.with_name("vt-results.tsv")))
).absolute()
if os.path.lexists(results_path):
if results_path.is_symlink() or not results_path.is_file():
raise GateError(f"unsafe pre-existing VT results path: {results_path}")
results_path.unlink()
withheld_path = os.environ.get("VT_WITHHELD", "")
withheld = load_withheld(withheld_path) if withheld_path else {}
expected, associations = load_expected(expected_manifest, withheld)
validate_associations(
associations_manifest,
objects=expected,
expected_associations=associations,
)
submissions = parse_action_output(
raw_analysis,
objects=expected,
manifest=expected_manifest,
)
withheld_count = sum(1 for item in expected if item.withheld)
if withheld_count:
print(
f"=== {withheld_count} expected object(s) withheld from the surface scan "
f"(already scanned as candidates; identity settled by sha256, manifest: "
f"{withheld_path}) ==="
)
print(
f"=== VirusTotal exact-set gate: {len(submissions)} distinct objects, "
f"{associations} associations, >= {min_engines} decisive engines each ==="
)
pending: Deque[Submission] = deque(submissions)
completed: List[CompletedResult] = []
failures: List[str] = []
attempts: Dict[str, int] = {item.expected.scan_path: 0 for item in submissions}
started = time.monotonic()
next_request_at = started
while pending:
now = time.monotonic()
if now - started >= poll_timeout:
names = [item.expected.scan_path for item in pending]
raise GateError(f"VirusTotal analyses did not complete within {poll_timeout}s: {names}")
if now < next_request_at:
time.sleep(min(next_request_at - now, 1.0))
continue
submission = pending.popleft()
attempts[submission.expected.scan_path] += 1
document = fetch_analysis(api_key, submission.analysis_id, curl_timeout)
next_request_at = time.monotonic() + request_interval
if document is None:
pending.append(submission)
if attempts[submission.expected.scan_path] == 1 or attempts[submission.expected.scan_path] % 4 == 0:
print(f" {submission.expected.scan_path}: API unavailable; will retry round-robin")
continue
status, result, detections = parse_completed(document, submission)
if status != "completed" or result is None:
pending.append(submission)
if attempts[submission.expected.scan_path] == 1 or attempts[submission.expected.scan_path] % 4 == 0:
print(f" {submission.expected.scan_path}: {status}; will retry round-robin")
continue
completed.append(result)
tolerated = is_tolerated_detection(result)
if result.completed_engines < min_engines:
# Reported, never fatal. See classify_result: engine count is
# VirusTotal's fleet availability, not a property of our binary.
print(
f"NOTE: {submission.expected.scan_path} was judged by "
f"{result.completed_engines}/{result.total_engines} decisive engines "
f"(below the {min_engines} reference); verdict still applies"
)
if result.malicious or result.suspicious:
message = (
f"{submission.expected.scan_path} flagged "
f"({result.malicious} malicious, {result.suspicious} suspicious / "
f"{result.completed_engines} decisive engines)"
)
if tolerated:
print(f"TOLERATED: {message}")
else:
failures.append(message)
print(f"BLOCKED: {message}")
if detections:
for engine, label, _category, version, updated in detections:
print(f" detected by: {engine} = {label} (engine {version}, defs {updated})")
else:
print(" detected by: <engine names unavailable in the analysis response>")
print(
f" https://www.virustotal.com/gui/file/"
f"{submission.expected.sha256}/detection"
)
else:
print(
f"OK: {submission.expected.scan_path} clean "
f"({result.completed_engines}/{result.total_engines} decisive/total engines)"
)
completed_paths = {item.submission.expected.scan_path for item in completed}
expected_paths = {item.expected.scan_path for item in submissions}
if completed_paths != expected_paths or len(completed) != len(submissions):
raise GateError("completed VirusTotal set differs from the exact submitted set")
write_results(
results_path,
results=completed,
associations=associations,
min_engines=min_engines,
)
if failures:
print(f"VirusTotal completed-set evidence: {results_path}")
raise GateError(f"{len(failures)} VirusTotal object(s) failed release policy")
print(f"=== All {len(completed)} exact VirusTotal objects passed; results: {results_path} ===")
try:
main()
except (GateError, OSError, subprocess.SubprocessError) as error:
print(f"BLOCKED: {error}", file=sys.stderr)
raise SystemExit(1)
PY