repomatic.release.virustotal module¶
Upload release binaries to VirusTotal and record detection snapshots.
Submits compiled binaries (.bin, .exe) to the VirusTotal API for malware
scanning. This seeds antivirus vendor databases with the signatures of freshly
built binaries, which keeps false-positive rates in check for downstream
distributors.
Detection statistics polled after an upload are appended to a CSV history, one
record per binary per scan date. The sync-binaries command renders that
history into the binaries catalog page (docs/binaries.md).
Note
Scan results are deliberately kept out of GitHub release notes: a raw
flagged / total count next to a download link reads as a malware verdict to
visitors, when it is almost always Nuitka onefile false positives. See
kdeldycke/meta-package-manager#1911
for the confusion this caused. The catalog page provides the context release
notes cannot.
Note
The free-tier API allows 4 requests per minute. All API calls (uploads and polls) are rate-limited with a sleep between each request.
- repomatic.release.virustotal.FREE_TIER_RATE_LIMIT = 4¶
VirusTotal free-tier request budget, in API calls per minute.
The single source for the upload and polling pace: the
scan-virustotalCLI default and both client functions below derive from it.
- repomatic.release.virustotal.SCAN_HEADERS = ('tag', 'filename', 'sha256', 'scanned', 'malicious', 'suspicious', 'undetected', 'harmless')¶
Columns of the committed scan history, in file order.
The release and the file it identifies first, then the four verdict counts, so the table reads left to right from what was scanned to what came back. Rows are ordered by release version rather than alphabetically, which a plain sort of the tag strings would get wrong past
v9.
- repomatic.release.virustotal.VIRUSTOTAL_GUI_URL = 'https://www.virustotal.com/gui/file/{sha256}'¶
URL template for the VirusTotal file analysis page.
- class repomatic.release.virustotal.DetectionStats(malicious, suspicious, undetected, harmless)[source]¶
Bases:
objectDetection statistics from a completed VirusTotal analysis.
Stores only the four categories that constitute a definitive verdict.
type-unsupported,timeout, andfailurefrom the API response are excluded from the total.
- class repomatic.release.virustotal.ScanResult(filename, sha256, analysis_url, detection_stats=None)[source]¶
Bases:
objectResult of uploading a single file to VirusTotal.
- detection_stats: DetectionStats | None = None¶
Detection statistics, or
Noneif analysis is still pending.
- class repomatic.release.virustotal.ScanRecord(tag, filename, sha256, scanned, stats)[source]¶
Bases:
objectA detection snapshot for one binary, taken on a given date.
Records accumulate in a CSV history (see
upsert_scan_records()) committed to the repository. Each record freezes theflagged / totalverdict counts at scan time, so the history supports trend analysis across releases even after VirusTotal re-analyzes the files or vendors process false-positive reports.- stats: DetectionStats¶
Detection statistics at scan time.
- repomatic.release.virustotal.scan_files(api_key, file_paths, rate_limit=4)[source]¶
Upload files to VirusTotal and return scan results.
Uses the synchronous
vt.ClientAPI. Sleeps between uploads to respect the free-tier rate limit.
- repomatic.release.virustotal.poll_detection_stats(api_key, results, rate_limit=4, timeout=600)[source]¶
Poll VirusTotal for detection statistics of previously uploaded files.
Queries
GET /files/{sha256}for each file until analysis completes or the timeout is reached. Respects the free-tier rate limit for all API calls.- Parameters:
api_key (
str) – VirusTotal API key.results (
list[ScanResult]) – Scan results from a previous upload.rate_limit (
int) – Maximum API requests per minute (shared with uploads).timeout (
int) – Maximum seconds to wait for all analyses to complete.
- Return type:
- Returns:
Results with
detection_statspopulated (orNonefor files whose analysis did not complete before the timeout).
- repomatic.release.virustotal.records_from_results(results, tag, scanned=None)[source]¶
Build history records from scan results whose analysis completed.
Results still pending (no detection statistics) are skipped: a record without verdict counts carries no information the release assets don’t already provide.
- Parameters:
results (
list[ScanResult]) – Scan results, typically frompoll_detection_stats().tag (
str) – Git tag of the release the binaries belong to.scanned (
str|None) – Snapshot date inYYYY-MM-DDformat. Today (UTC) whenNone.
- Return type:
- Returns:
One record per result with detection statistics.
- repomatic.release.virustotal.records_from_release_notes(body, tag, scanned)[source]¶
Recover detection snapshots from a legacy release-notes table.
Before the scan history file existed, the release pipeline appended a VirusTotal table to GitHub release notes, with a
flagged / totalDetections cell frozen minutes after publication. Those cells are genuine at-release snapshots, sosync-binaries --backfill-recordsharvests them to seed the history for releases that predate the file.Note
The legacy table only recorded the flagged and total aggregates, not the malicious/suspicious/undetected/harmless split. The split is rebuilt as flagged = malicious and the remainder = undetected, which is lossless for everything the catalog consumes (
flaggedandtotal).- Parameters:
- Return type:
- Returns:
One record per table row carrying a numeric Detections cell.
- repomatic.release.virustotal.load_scan_records(path)[source]¶
Load scan records from the CSV history file.
Note
A repository whose history predates the CSV store carries the same records in a sibling
.json, and is read from there when the CSV is absent. The nextupsert_scan_records()write lands as CSV, so a repository migrates on its first release after upgrading without anyone converting anything. The stale.jsonis then inert and can be deleted.- Parameters:
path (
Path) – Path to the CSV file.- Return type:
- Returns:
The records, or an empty list when neither file exists.
- Raises:
ValueError – When a file exists but cannot be parsed. Loud on purpose: a corrupt history must never be silently clobbered by the next
upsert_scan_records()write.
- repomatic.release.virustotal.upsert_scan_records(path, new_records)[source]¶
Merge new records into the CSV history at path.
Records sharing the same
(sha256, scanned)identity are replaced, so re-running a scan the same day is idempotent. The file is created (with its parent directories) when missing, and always rewritten in normalized form, sorted by version, filename and scan date.CSV also keeps the store out of the autofix lane: nothing there rewrites a
.csv, where a committed JSON file has to match whatever layout Biome is configured for orformat-jsonreformats it right back. Seeclaude.md§ Naming conventions rule 8 for the rest of that reasoning.- Parameters:
path (
Path) – Path to the CSV history.new_records (
list[ScanRecord]) – Records to merge in.
- Return type:
- Returns:
Truewhen the file content changed.