Skip to content

Commit 06bccdf

Browse files
dimsmchmarny
andauthored
feat(ci): add ClamAV malware scanning GitHub Action (#171)
Co-authored-by: Mark Chmarny <mchmarny@users.noreply.github.com>
1 parent 4e15a0e commit 06bccdf

File tree

3 files changed

+225
-0
lines changed

3 files changed

+225
-0
lines changed
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
name: 'ClamAV Malware Scan'
16+
description: 'Run ClamAV malware scan on release artifacts and upload SARIF results to GitHub Security'
17+
18+
inputs:
19+
scan_path:
20+
description: 'Path to scan for malware (e.g. dist/ for release binaries)'
21+
required: false
22+
default: '.'
23+
output_file:
24+
description: 'Output SARIF file name'
25+
required: false
26+
default: 'clamav-results.sarif'
27+
category:
28+
description: 'GitHub Security category for SARIF upload'
29+
required: false
30+
default: 'clamav'
31+
fail_on_finding:
32+
description: 'Fail the step if malware is detected (true/false)'
33+
required: false
34+
default: 'true'
35+
36+
outputs:
37+
infected_count:
38+
description: 'Number of infected files found'
39+
value: ${{ steps.scan.outputs.infected_count }}
40+
scanned_count:
41+
description: 'Number of files scanned'
42+
value: ${{ steps.scan.outputs.scanned_count }}
43+
outcome:
44+
description: 'Scan outcome (clean, infected, error)'
45+
value: ${{ steps.scan.outputs.outcome }}
46+
47+
runs:
48+
using: 'composite'
49+
steps:
50+
- name: Install ClamAV
51+
shell: bash
52+
run: |
53+
set -euo pipefail
54+
sudo apt-get update -qq
55+
sudo apt-get install -y -qq clamav > /dev/null 2>&1
56+
echo "Installed: $(clamscan --version)"
57+
58+
- name: Update virus definitions
59+
shell: bash
60+
run: |
61+
set -euo pipefail
62+
# Stop freshclam daemon if running (it holds the lock)
63+
sudo systemctl stop clamav-freshclam 2>/dev/null || true
64+
sudo freshclam --quiet
65+
echo "Virus definitions updated"
66+
67+
- name: Run ClamAV scan
68+
id: scan
69+
shell: bash
70+
run: |
71+
set -euo pipefail
72+
73+
SCAN_PATH="${{ inputs.scan_path }}"
74+
RAW_OUTPUT="clamav-raw.log"
75+
76+
echo "::group::ClamAV scan of ${SCAN_PATH}"
77+
78+
# clamscan exit codes: 0=clean, 1=infected found, 2=error
79+
SCAN_EXIT=0
80+
clamscan \
81+
--recursive \
82+
--infected \
83+
--suppress-ok-results \
84+
--max-filesize=100M \
85+
--max-scansize=400M \
86+
"${SCAN_PATH}" > "${RAW_OUTPUT}" 2>&1 || SCAN_EXIT=$?
87+
88+
cat "${RAW_OUTPUT}"
89+
echo "::endgroup::"
90+
91+
# Parse summary line: "Infected files: N"
92+
INFECTED=$(grep -oP 'Infected files:\s*\K\d+' "${RAW_OUTPUT}" || echo "0")
93+
SCANNED=$(grep -oP 'Scanned files:\s*\K\d+' "${RAW_OUTPUT}" || echo "0")
94+
95+
echo "infected_count=${INFECTED}" >> "$GITHUB_OUTPUT"
96+
echo "scanned_count=${SCANNED}" >> "$GITHUB_OUTPUT"
97+
98+
if [[ "${SCAN_EXIT}" -eq 2 ]]; then
99+
echo "outcome=error" >> "$GITHUB_OUTPUT"
100+
echo "::error::ClamAV encountered an error during scanning"
101+
exit 1
102+
elif [[ "${INFECTED}" -gt 0 ]]; then
103+
echo "outcome=infected" >> "$GITHUB_OUTPUT"
104+
echo "::error::ClamAV detected ${INFECTED} infected file(s)"
105+
else
106+
echo "outcome=clean" >> "$GITHUB_OUTPUT"
107+
echo "Scan clean: ${SCANNED} files scanned, 0 infected"
108+
fi
109+
110+
- name: Convert results to SARIF
111+
id: convert
112+
shell: bash
113+
run: |
114+
set -euo pipefail
115+
116+
RAW_OUTPUT="clamav-raw.log"
117+
SARIF_FILE="${{ inputs.output_file }}"
118+
INFECTED="${{ steps.scan.outputs.infected_count }}"
119+
120+
# Build SARIF results array from infected lines
121+
# ClamAV infected output format: "/path/to/file: SignatureName FOUND"
122+
RESULTS="[]"
123+
RULES="[]"
124+
125+
if [[ "${INFECTED}" -gt 0 ]]; then
126+
while IFS= read -r line; do
127+
# Parse: /path/to/file: MalwareName FOUND
128+
FILE_PATH=$(echo "${line}" | sed 's/: .* FOUND$//')
129+
SIGNATURE=$(echo "${line}" | sed 's/^.*: //; s/ FOUND$//')
130+
131+
# Check if rule already exists
132+
RULE_ID="clamav/${SIGNATURE}"
133+
RULE_EXISTS=$(echo "${RULES}" | jq --arg id "${RULE_ID}" 'map(.id) | index($id)')
134+
135+
if [[ "${RULE_EXISTS}" == "null" ]]; then
136+
RULES=$(echo "${RULES}" | jq \
137+
--arg id "${RULE_ID}" \
138+
--arg name "${SIGNATURE}" \
139+
'. += [{"id": $id, "name": $name, "shortDescription": {"text": ("Malware detected: " + $name)}, "properties": {"tags": ["security", "malware"]}}]')
140+
fi
141+
142+
RESULTS=$(echo "${RESULTS}" | jq \
143+
--arg rule_id "${RULE_ID}" \
144+
--arg uri "${FILE_PATH}" \
145+
--arg msg "ClamAV detected malware signature: ${SIGNATURE}" \
146+
'. += [{"ruleId": $rule_id, "level": "error", "message": {"text": $msg}, "locations": [{"physicalLocation": {"artifactLocation": {"uri": $uri}}}]}]')
147+
148+
done < <(grep "FOUND$" "${RAW_OUTPUT}" || true)
149+
fi
150+
151+
# Emit SARIF v2.1.0
152+
jq -n \
153+
--argjson results "${RESULTS}" \
154+
--argjson rules "${RULES}" \
155+
--arg version "$(clamscan --version)" \
156+
'{
157+
"$schema": "https://json.schemastore.org/sarif-2.1.0.json",
158+
"version": "2.1.0",
159+
"runs": [{
160+
"tool": {
161+
"driver": {
162+
"name": "ClamAV",
163+
"informationUri": "https://www.clamav.net",
164+
"semanticVersion": $version,
165+
"rules": $rules
166+
}
167+
},
168+
"results": $results
169+
}]
170+
}' > "${SARIF_FILE}"
171+
172+
echo "SARIF written to ${SARIF_FILE}"
173+
echo "exists=true" >> "$GITHUB_OUTPUT"
174+
175+
- name: Upload SARIF to GitHub Security
176+
if: steps.convert.outputs.exists == 'true'
177+
id: upload_sarif
178+
uses: github/codeql-action/upload-sarif@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
179+
with:
180+
sarif_file: ${{ inputs.output_file }}
181+
category: ${{ inputs.category }}
182+
continue-on-error: true
183+
184+
- name: SARIF upload status
185+
if: steps.convert.outputs.exists == 'true'
186+
shell: bash
187+
run: |
188+
if [[ "${{ steps.upload_sarif.outcome }}" == "failure" ]]; then
189+
echo "::notice::SARIF upload skipped - GitHub Advanced Security may not be enabled for this repository"
190+
fi
191+
192+
- name: Enforce scan result
193+
if: inputs.fail_on_finding == 'true' && steps.scan.outputs.outcome == 'infected'
194+
shell: bash
195+
run: |
196+
echo "::error::Malware scan failed: ${{ steps.scan.outputs.infected_count }} infected file(s) detected"
197+
exit 1

.github/workflows/on-tag.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,12 @@ jobs:
7272
outputs:
7373
release_outcome: ${{ steps.release.outputs.release_outcome }}
7474
permissions:
75+
actions: read
7576
contents: write
7677
packages: write
7778
id-token: write
7879
attestations: write
80+
security-events: write
7981
steps:
8082
- name: Checkout Code
8183
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -99,6 +101,13 @@ jobs:
99101
with:
100102
ko_version: ${{ steps.versions.outputs.ko }}
101103

104+
- name: Malware Scan Release Binaries
105+
if: steps.release.outcome == 'success'
106+
uses: ./.github/actions/malware-scan
107+
with:
108+
scan_path: dist/
109+
category: 'clamav-release-binaries'
110+
102111
# =============================================================================
103112
# Docker Jobs: Native per-arch validator builds (parallel with GoReleaser)
104113
# =============================================================================

.github/workflows/vuln-scan.yaml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,22 @@ jobs:
8585
uses: github/codeql-action/upload-sarif@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
8686
with:
8787
sarif_file: ${{ env.SARIF_OUTPUT }}
88+
89+
malware-scan:
90+
runs-on: ubuntu-latest
91+
timeout-minutes: 30
92+
permissions:
93+
actions: read
94+
contents: read
95+
security-events: write
96+
steps:
97+
- name: Checkout
98+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
99+
with:
100+
persist-credentials: false
101+
102+
- name: Malware Scan
103+
uses: ./.github/actions/malware-scan
104+
with:
105+
scan_path: '.'
106+
category: 'clamav'

0 commit comments

Comments
 (0)