66 paths :
77 - " docker/**"
88 - " templates/docker-compose/**"
9+ - " src/**"
910 - " .github/workflows/docker-security-scan.yml"
1011
1112 pull_request :
1213 paths :
1314 - " docker/**"
1415 - " templates/docker-compose/**"
16+ - " src/**"
1517 - " .github/workflows/docker-security-scan.yml"
1618
1719 # Scheduled scans are important because new CVEs appear
18- # even if the code or images didn’ t change
20+ # even if the code or images didn' t change
1921 schedule :
2022 - cron : " 0 6 * * *" # Daily at 6 AM UTC
2123
6062 ${{ matrix.image.context }}
6163
6264 # Human-readable output in logs
63- # This NEVER fails the job; it’ s only for visibility
65+ # This NEVER fails the job; it' s only for visibility
6466 - name : Display vulnerabilities (table format)
6567 uses : aquasecurity/trivy-action@0.35.0
6668 with :
9395 path : trivy-${{ matrix.image.name }}.sarif
9496 retention-days : 30
9597
98+ extract-images :
99+ name : Extract Third-Party Docker Images from Source
100+ runs-on : ubuntu-latest
101+ timeout-minutes : 10
102+ outputs :
103+ # JSON array of Docker image references for use in scan matrix
104+ # Example: ["torrust/tracker:develop","mysql:8.4","prom/prometheus:v3.5.0","grafana/grafana:12.3.1","caddy:2.10"]
105+ images : ${{ steps.extract.outputs.images }}
106+
107+ steps :
108+ - name : Checkout code
109+ uses : actions/checkout@v5
110+
111+ - name : Install Rust toolchain
112+ uses : actions-rust-lang/setup-rust-toolchain@v1
113+
114+ - name : Build deployer CLI
115+ run : cargo build --release
116+
117+ # Creates a minimal environment config with all optional services
118+ # enabled so that all third-party Docker images appear in the output.
119+ # - MySQL: enables mysql image in docker_images output
120+ # - Prometheus: enables prometheus image in docker_images output
121+ # - Grafana: enables grafana image in docker_images output
122+ # Uses fixture SSH keys (already committed to the repository).
123+ - name : Create environment config for image extraction
124+ run : |
125+ cat > /tmp/ci-images-env.json <<EOF
126+ {
127+ "environment": { "name": "ci-images" },
128+ "ssh_credentials": {
129+ "private_key_path": "$GITHUB_WORKSPACE/fixtures/testing_rsa",
130+ "public_key_path": "$GITHUB_WORKSPACE/fixtures/testing_rsa.pub"
131+ },
132+ "provider": {
133+ "provider": "lxd",
134+ "profile_name": "ci-profile"
135+ },
136+ "tracker": {
137+ "core": {
138+ "database": {
139+ "driver": "mysql",
140+ "host": "mysql",
141+ "port": 3306,
142+ "database_name": "torrust_tracker",
143+ "username": "tracker_user",
144+ "password": "tracker_password"
145+ },
146+ "private": false
147+ },
148+ "udp_trackers": [{ "bind_address": "0.0.0.0:6969" }],
149+ "http_trackers": [{ "bind_address": "0.0.0.0:7070" }],
150+ "http_api": { "bind_address": "0.0.0.0:1212", "admin_token": "ci-token" },
151+ "health_check_api": { "bind_address": "127.0.0.1:1313" }
152+ },
153+ "prometheus": { "scrape_interval_in_secs": 15 },
154+ "grafana": { "admin_user": "admin", "admin_password": "admin" }
155+ }
156+ EOF
157+
158+ - name : Create minimal environment (no infrastructure provisioned)
159+ run : |
160+ ./target/release/torrust-tracker-deployer \
161+ --working-dir /tmp/ci-workspace \
162+ create environment \
163+ --env-file /tmp/ci-images-env.json
164+
165+ # Extract Docker images from show command JSON output.
166+ # The show command lists all configured service images in docker_images.
167+ # Caddy is always in the docker-compose stack but is not tracked as
168+ # a domain service, so it is appended to the list manually.
169+ - name : Extract Docker images
170+ id : extract
171+ run : |
172+ show_output=$(./target/release/torrust-tracker-deployer \
173+ --working-dir /tmp/ci-workspace \
174+ show ci-images)
175+
176+ images=$(echo "$show_output" | \
177+ jq -c '[
178+ .docker_images.tracker,
179+ .docker_images.mysql,
180+ .docker_images.prometheus,
181+ .docker_images.grafana
182+ ] | map(select(. != null)) + ["caddy:2.10"]')
183+
184+ echo "Detected images: $images"
185+ echo "images=$images" >> "$GITHUB_OUTPUT"
186+
96187 scan-third-party-images :
97188 name : Scan Third-Party Docker Images
189+ needs : extract-images
98190 runs-on : ubuntu-latest
99191 timeout-minutes : 15
100192 permissions :
@@ -103,14 +195,9 @@ jobs:
103195 strategy :
104196 fail-fast : false
105197 matrix :
106- # These must match docker-compose templates
107- # in templates/docker-compose/docker-compose.yml.tera
108- image :
109- - torrust/tracker:develop
110- - mysql:8.0
111- - grafana/grafana:11.4.0
112- - prom/prometheus:v3.0.1
113- - caddy:2.10
198+ # Dynamic image list extracted from the deployer CLI at build time.
199+ # Images come from domain config constants — no manual maintenance needed.
200+ image : ${{ fromJson(needs.extract-images.outputs.images) }}
114201
115202 steps :
116203 - name : Display vulnerabilities (table format)
@@ -154,7 +241,7 @@ jobs:
154241 - scan-project-images
155242 - scan-third-party-images
156243
157- # Always run so we don’ t lose security visibility
244+ # Always run so we don' t lose security visibility
158245 if : always()
159246
160247 permissions :
@@ -168,7 +255,6 @@ jobs:
168255
169256 # Upload each SARIF file with CodeQL Action using unique categories.
170257 # The category parameter enables proper alert tracking per image.
171- # Must use CodeQL Action (not gh API) - API doesn't support category field.
172258 #
173259 # VIEWING RESULTS:
174260 # - For pull requests: /security/code-scanning?query=pr:NUMBER+is:open
@@ -192,42 +278,41 @@ jobs:
192278 category : docker-project-ssh-server
193279 continue-on-error : true
194280
195- - name : Upload third-party mysql SARIF
196- if : always()
197- uses : github/codeql-action/upload-sarif@v4
198- with :
199- sarif_file : sarif-third-party-mysql-8.0-${{ github.run_id }}/trivy.sarif
200- category : docker-third-party-mysql-8.0
201- continue-on-error : true
202-
203- - name : Upload third-party tracker SARIF
281+ # Dynamic upload of all third-party image SARIF results.
282+ # Iterates over every sarif-third-party-* artifact directory so
283+ # no manual step additions are needed when images change version.
284+ # The category is derived from the artifact directory name so
285+ # GitHub Code Scanning properly tracks alerts per image.
286+ - name : Upload all third-party SARIF results
204287 if : always()
205- uses : github/codeql-action/upload-sarif@v4
206- with :
207- sarif_file : sarif-third-party-torrust-tracker-develop-${{ github.run_id }}/trivy.sarif
208- category : docker-third-party-torrust-tracker-develop
209- continue-on-error : true
288+ env :
289+ GH_TOKEN : ${{ github.token }}
290+ shell : bash
291+ run : |
292+ for sarif_dir in sarif-third-party-*; do
293+ if [[ ! -d "$sarif_dir" ]]; then
294+ continue
295+ fi
296+ sarif_file="$sarif_dir/trivy.sarif"
297+ if [[ ! -f "$sarif_file" ]]; then
298+ echo "No SARIF file in $sarif_dir, skipping"
299+ continue
300+ fi
210301
211- - name : Upload third-party grafana SARIF
212- if : always()
213- uses : github/codeql-action/upload-sarif@v4
214- with :
215- sarif_file : sarif-third-party-grafana-grafana-11.4.0-${{ github.run_id }}/trivy.sarif
216- category : docker-third-party-grafana-grafana-11.4.0
217- continue-on-error : true
302+ # Derive unique Code Scanning category from the artifact directory name.
303+ # Example: sarif-third-party-mysql-8.4-12345 -> docker-third-party-mysql-8.4
304+ artifact_name="${sarif_dir%-${{ github.run_id }}}"
305+ category="docker-${artifact_name#sarif-}"
218306
219- - name : Upload third-party prometheus SARIF
220- if : always()
221- uses : github/codeql-action/upload-sarif@v4
222- with :
223- sarif_file : sarif-third-party-prom-prometheus-v3.0.1-${{ github.run_id }}/trivy.sarif
224- category : docker-third-party-prom-prometheus-v3.0.1
225- continue-on-error : true
307+ echo "Uploading $sarif_file with category: $category"
226308
227- - name : Upload third-party caddy SARIF
228- if : always()
229- uses : github/codeql-action/upload-sarif@v4
230- with :
231- sarif_file : sarif-third-party-caddy-2.10-${{ github.run_id }}/trivy.sarif
232- category : docker-third-party-caddy-2.10
233- continue-on-error : true
309+ gh api \
310+ --method POST \
311+ -H "Accept: application/vnd.github+json" \
312+ "/repos/${{ github.repository }}/code-scanning/sarifs" \
313+ -f "commit_sha=${{ github.sha }}" \
314+ -f "ref=${{ github.ref }}" \
315+ -f "sarif=$(gzip -c "$sarif_file" | base64 -w 0)" \
316+ -f "category=$category" \
317+ || echo "Warning: Upload failed for $sarif_file (category: $category)"
318+ done
0 commit comments