#005: Virus scanning pre-built binaries of my tools
A user opened an issue on hours last week informing me that Windows Defender had flagged the latest release as malware. One of the vendors on VirusTotal — a malware analysis platform — also detected it as malicious, which was concerning.
The detection went away after I reanalysed the file on VirusTotal. I also ran analyses on several older commits and they all came back clean, so my hunch is that the detection was a false positive. But it got me thinking — what if this happens again, and I don’t find out about it for weeks?
This motivated me (nerdsniped, more like) to get ahead of these detections by setting up proactive virus scanning for my tools.
What I needed
I wanted a “set it and forget it” kind of solution for this, which meant I needed the following:
- Scheduled jobs on GitHub Actions that build binaries for all of my tools (for all platforms and architectures supported by each tool), and then scan them using VirusTotal
- To help keep an eye on the reports for over 25 tools, a centralised dashboard for the results
Here’s how I set it up.
Building a composite action
In order to reduce duplication of code, I decided to write a composite action for this. The action does three things:
First, it installs the VirusTotal CLI. Then it scans the files provided to it
(which can be a glob pattern). For each file, it runs vt scan file <FILE>
and waits for the analysis to complete.
vtallows scanning multiple files concurrently, but since I’m on the free plan, I found that running sequential scans reduces the chances of hitting rate limits.
Once analysis is complete, results come back in this format:
last_analysis_stats:
confirmed-timeout: 0
failure: 0
harmless: 0
malicious: 0
suspicious: 0
timeout: 0
type-unsupported: 12
undetected: 64If there’s anything detected as malicious or suspicious (or there are other failures), the job fails. It also provides a link to the web-based VirusTotal report for further investigation.

Setting this up for Go tools
Setting this up for pure Go tools was straightforward. I use goreleaser for generating release binaries, so all I needed was a workflow that generates binaries without actually releasing them:
jobs:
virus-total:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: 'go.mod'
- name: Build Binaries
uses: goreleaser/goreleaser-action@v6
with:
version: 'v2.9.0'
args: build --snapshot
- uses: dhth/composite-actions/.github/actions/scan-files@main
with:
files: './dist/hours_*/hours'
vt-api-key: ${{ secrets.VT_API_KEY }}Cross-compiling Go projects with C dependencies is generally more complicated, but since I’m already using goreleaser-cross for such projects, a similar workflow worked for these as well.
Rust tools were trickier
Unlike Go (pure Go, to be precise), where goreleaser can build for all targets from a single runner (I find this magical), generating binaries for several platforms for Rust projects is a bit more involved, often working best when compiling natively on the target OS.
I use cargo-dist for this, which generates a GitHub Actions workflow file from its own config. This workflow compiles binaries for each OS on a runner of that same OS, and then gathers them at the end for publishing. It also handles installing any system dependencies needed at compile time.
For the scanning, I replicated the release workflow file and stripped out steps related to attestations, publishing, or announcing the release. After the binaries are generated, I simply run my composite action on them.
The dashboard
Next, I needed a dashboard to display the status of each scan run. Luckily, I already had the foundation for this. I track the status of important GitHub Actions workflow runs for my tools at https://dhth.github.io/act3-runner. All I needed was another page for the scanning workflows.
This dashboard is built using act3. A while back I added a command to it to fetch workflows where the name matches a pattern. This command has turned out to be quite handy for scenarios like this. All I needed to do was to add the scan workflows to the act3-runner repo:
act3 config gen \
-r "$(cat repos.txt | xargs | tr ' ' ',')" \
-n '^scan$' \
>scan.ymlAnd then:
act3 \
-g \
-c ./scan.yml \
--html-title "scans" \
--html-template-path templates/report.html \
-f html > dist/reports/scan.htmlThis generates the scan report dashboard, which gives me a simple overview of which tools have been scanned recently and whether any issues were detected.

Summary
Even though I don’t anticipate many of my tools getting flagged on VirusTotal and similar platforms, it’s reassuring to have this automated checking in place. I’ve only set this up for 10 tools so far — there’s still scope for optimizing the scanning workflow I’ve created. Once it looks solid, I’ll extend it to the rest of my tools.