@@ -75,15 +75,130 @@ jobs:
7575 path : ${{ matrix.asset_name }}
7676 if-no-files-found : error
7777
78- release :
79- name : Create GitHub Release
78+ # Collect all matrix binaries and compute hashes for SLSA provenance.
79+ hashes :
8080 needs : build
8181 runs-on : ubuntu-latest
82+ permissions : {}
83+ outputs :
84+ hashes : ${{ steps.hash.outputs.hashes }}
85+ steps :
86+ - uses : actions/download-artifact@v4
87+ with :
88+ path : artifacts
89+ merge-multiple : true
90+
91+ - name : Compute SHA-256 hashes
92+ id : hash
93+ run : |
94+ echo "hashes=$(sha256sum artifacts/* | base64 -w0)" >> "$GITHUB_OUTPUT"
95+
96+ # SLSA Level 3 provenance covers all release binaries.
97+ provenance :
98+ needs : hashes
8299 permissions :
100+ actions : read
101+ id-token : write
83102 contents : write
103+ uses : slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0
104+ with :
105+ base64-subjects : ${{ needs.hashes.outputs.hashes }}
106+ upload-assets : false
107+
108+ # Scans all four binaries concurrently to minimise wall-clock time.
109+ virustotal :
110+ needs : build
111+ runs-on : ubuntu-latest
112+ permissions : {}
84113 outputs :
85- version : ${{ steps.meta.outputs.version }}
86- release_url : ${{ steps.publish.outputs.url }}
114+ arm64_report : ${{ steps.scan.outputs.arm64_report }}
115+ arm64_status : ${{ steps.scan.outputs.arm64_status }}
116+ x86_64_mac_report : ${{ steps.scan.outputs.x86_64_mac_report }}
117+ x86_64_mac_status : ${{ steps.scan.outputs.x86_64_mac_status }}
118+ linux_report : ${{ steps.scan.outputs.linux_report }}
119+ linux_status : ${{ steps.scan.outputs.linux_status }}
120+ windows_report : ${{ steps.scan.outputs.windows_report }}
121+ windows_status : ${{ steps.scan.outputs.windows_status }}
122+ steps :
123+ - uses : actions/download-artifact@v4
124+ with :
125+ path : artifacts
126+ merge-multiple : true
127+
128+ - name : Scan with VirusTotal
129+ id : scan
130+ env :
131+ VT_API_KEY : ${{ secrets.VIRUSTOTAL_API_KEY }}
132+ run : |
133+ upload_and_poll() {
134+ local file="$1" outprefix="$2"
135+ local sha256 size_bytes upload_url analysis_id last_response poll_status
136+ local malicious suspicious undetected harmless total detected result_status
137+ sha256=$(sha256sum "$file" | awk '{print $1}')
138+ size_bytes=$(stat -c%s "$file")
139+ upload_url="https://www.virustotal.com/api/v3/files"
140+ if [ "$size_bytes" -gt 33554432 ]; then
141+ upload_url=$(curl -sS --request GET \
142+ --url https://www.virustotal.com/api/v3/files/upload_url \
143+ --header "x-apikey: $VT_API_KEY" | jq -r '.data')
144+ if [ -z "$upload_url" ] || [ "$upload_url" = "null" ]; then
145+ printf '%s' "https://www.virustotal.com/gui/file/$sha256/detection" > "${outprefix}.url"
146+ printf '%s' "⬜ N/A" > "${outprefix}.status"
147+ return
148+ fi
149+ fi
150+ analysis_id=$(curl -sS --request POST \
151+ --url "$upload_url" \
152+ --header "x-apikey: $VT_API_KEY" \
153+ --form "file=@$file" | jq -r '.data.id')
154+ if [ -z "$analysis_id" ] || [ "$analysis_id" = "null" ]; then
155+ printf '%s' "https://www.virustotal.com/gui/file/$sha256/detection" > "${outprefix}.url"
156+ printf '%s' "⬜ N/A" > "${outprefix}.status"
157+ return
158+ fi
159+ last_response=""
160+ for i in $(seq 1 30); do
161+ sleep 20
162+ last_response=$(curl -sS \
163+ --url "https://www.virustotal.com/api/v3/analyses/$analysis_id" \
164+ --header "x-apikey: $VT_API_KEY")
165+ poll_status=$(echo "$last_response" | jq -r '.data.attributes.status')
166+ [ "$poll_status" = "completed" ] && break
167+ done
168+ malicious=$(echo "$last_response" | jq -r '.data.attributes.stats.malicious // 0')
169+ suspicious=$(echo "$last_response" | jq -r '.data.attributes.stats.suspicious // 0')
170+ undetected=$(echo "$last_response" | jq -r '.data.attributes.stats.undetected // 0')
171+ harmless=$(echo "$last_response" | jq -r '.data.attributes.stats.harmless // 0')
172+ total=$((malicious + suspicious + undetected + harmless))
173+ detected=$((malicious + suspicious))
174+ [ "$detected" -eq 0 ] \
175+ && result_status="✅ ${detected}/${total} Clean" \
176+ || result_status="⚠️ ${detected}/${total} Detected"
177+ printf '%s' "https://www.virustotal.com/gui/file/$sha256/detection" > "${outprefix}.url"
178+ printf '%s' "$result_status" > "${outprefix}.status"
179+ }
180+ upload_and_poll "artifacts/plainapp-cli-macos-arm64" /tmp/vt_arm64 &
181+ upload_and_poll "artifacts/plainapp-cli-macos-x86_64" /tmp/vt_x86_64_mac &
182+ upload_and_poll "artifacts/plainapp-cli-linux-x86_64" /tmp/vt_linux &
183+ upload_and_poll "artifacts/plainapp-cli-windows-x86_64.exe" /tmp/vt_windows &
184+ wait
185+ {
186+ echo "arm64_report=$(cat /tmp/vt_arm64.url)"
187+ echo "arm64_status=$(cat /tmp/vt_arm64.status)"
188+ echo "x86_64_mac_report=$(cat /tmp/vt_x86_64_mac.url)"
189+ echo "x86_64_mac_status=$(cat /tmp/vt_x86_64_mac.status)"
190+ echo "linux_report=$(cat /tmp/vt_linux.url)"
191+ echo "linux_status=$(cat /tmp/vt_linux.status)"
192+ echo "windows_report=$(cat /tmp/vt_windows.url)"
193+ echo "windows_status=$(cat /tmp/vt_windows.status)"
194+ } >> $GITHUB_OUTPUT
195+
196+ release :
197+ name : Create GitHub Release
198+ needs : [provenance, virustotal]
199+ runs-on : ubuntu-latest
200+ permissions :
201+ contents : write
87202 steps :
88203 - uses : actions/checkout@v4
89204
@@ -93,22 +208,24 @@ jobs:
93208 VER=$(grep '^version' Cargo.toml | head -1 | awk -F '"' '{print $2}')
94209 echo "version=$VER" >> "$GITHUB_OUTPUT"
95210
96- - name : Download all artifacts
97- uses : actions/download-artifact@v4
211+ - uses : actions/download-artifact@v4
98212 with :
99213 path : artifacts
214+ merge-multiple : true
215+
216+ - uses : actions/download-artifact@v4
217+ with :
218+ name : ${{ needs.provenance.outputs.provenance-name }}
100219
101220 - name : Compute SHA-256 checksums
102221 run : |
103- cd artifacts
104- find . -type f | sort | xargs sha256sum > ../checksums.txt
105- cat ../checksums.txt
222+ sha256sum artifacts/* ${{ needs.provenance.outputs.provenance-name }} > checksums.txt
223+ cat checksums.txt
106224
107225 - name : Publish GitHub Release (draft)
108- id : publish
109- uses : softprops/action-gh-release@v2
226+ uses : ncipollo/release-action@v1
110227 with :
111- tag_name : v${{ steps.meta.outputs.version }}
228+ tag : v${{ steps.meta.outputs.version }}
112229 name : plainapp-cli v${{ steps.meta.outputs.version }}
113230 body : |
114231 ## Install
@@ -124,10 +241,27 @@ jobs:
124241 sudo mv plainapp-cli-* /usr/local/bin/plainapp-cli
125242 ```
126243
244+ ## Security
245+
246+ ### VirusTotal Scan
247+ | File | Status | Scan Report |
248+ |------|--------|-------------|
249+ | `plainapp-cli-macos-arm64` | ${{ needs.virustotal.outputs.arm64_status }} | [View Report](${{ needs.virustotal.outputs.arm64_report }}) |
250+ | `plainapp-cli-macos-x86_64` | ${{ needs.virustotal.outputs.x86_64_mac_status }} | [View Report](${{ needs.virustotal.outputs.x86_64_mac_report }}) |
251+ | `plainapp-cli-linux-x86_64` | ${{ needs.virustotal.outputs.linux_status }} | [View Report](${{ needs.virustotal.outputs.linux_report }}) |
252+ | `plainapp-cli-windows-x86_64.exe` | ${{ needs.virustotal.outputs.windows_status }} | [View Report](${{ needs.virustotal.outputs.windows_report }}) |
253+
254+ ### SLSA Provenance (Level 3)
255+ The `.intoto.jsonl` file is a signed SLSA provenance document covering all release binaries.
256+ Verify with [slsa-verifier](https://github.com/slsa-framework/slsa-verifier):
257+ ```sh
258+ slsa-verifier verify-artifact plainapp-cli-linux-x86_64 \
259+ --provenance-path ${{ needs.provenance.outputs.provenance-name }} \
260+ --source-uri github.com/${{ github.repository }}
261+ ```
262+
127263 ## Checksums (SHA-256)
128264 See `checksums.txt` attached below.
129- files : |
130- artifacts/**/*
131- checksums.txt
132265 draft : true
133266 prerelease : false
267+ artifacts : " artifacts/*,${{ needs.provenance.outputs.provenance-name }},checksums.txt"
0 commit comments