Skip to content

Commit af3fc7b

Browse files
committed
feat: [#252] implement dynamic image detection for vulnerability scanning
1 parent 725b473 commit af3fc7b

22 files changed

Lines changed: 559 additions & 60 deletions

File tree

.github/workflows/docker-security-scan.yml

Lines changed: 132 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,18 @@ on:
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 didnt change
20+
# even if the code or images didn't change
1921
schedule:
2022
- cron: "0 6 * * *" # Daily at 6 AM UTC
2123

@@ -60,7 +62,7 @@ jobs:
6062
${{ matrix.image.context }}
6163
6264
# Human-readable output in logs
63-
# This NEVER fails the job; its 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:
@@ -93,8 +95,98 @@ jobs:
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 dont 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

project-words.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,7 @@ rustup
426426
rwxrwx
427427
sandboxed
428428
sarif
429+
sarifs
429430
scannability
430431
schemafile
431432
schemars

src/application/command_handlers/show/handler.rs

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,15 @@ use std::sync::Arc;
2828
use tracing::instrument;
2929

3030
use super::errors::ShowCommandHandlerError;
31-
use super::info::{EnvironmentInfo, GrafanaInfo, InfrastructureInfo, PrometheusInfo, ServiceInfo};
31+
use super::info::{
32+
DockerImagesInfo, EnvironmentInfo, GrafanaInfo, InfrastructureInfo, PrometheusInfo, ServiceInfo,
33+
};
3234
use crate::domain::environment::repository::EnvironmentRepository;
3335
use crate::domain::environment::state::AnyEnvironmentState;
36+
use crate::domain::grafana::GrafanaConfig;
37+
use crate::domain::mysql::MysqlServiceConfig;
38+
use crate::domain::prometheus::PrometheusConfig;
39+
use crate::domain::tracker::config::TrackerConfig;
3440
use crate::domain::EnvironmentName;
3541

3642
/// Default SSH port when not specified
@@ -121,7 +127,24 @@ impl ShowCommandHandler {
121127
let created_at = any_env.created_at();
122128
let state_name = any_env.state_name().to_string();
123129

124-
let mut info = EnvironmentInfo::new(name, state, provider, created_at, state_name);
130+
let tracker_config = any_env.tracker_config();
131+
let docker_images = DockerImagesInfo::new(
132+
TrackerConfig::docker_image().full_reference(),
133+
if tracker_config.uses_mysql() {
134+
Some(MysqlServiceConfig::docker_image().full_reference())
135+
} else {
136+
None
137+
},
138+
any_env
139+
.prometheus_config()
140+
.map(|_| PrometheusConfig::docker_image().full_reference()),
141+
any_env
142+
.grafana_config()
143+
.map(|_| GrafanaConfig::docker_image().full_reference()),
144+
);
145+
146+
let mut info =
147+
EnvironmentInfo::new(name, state, provider, created_at, docker_images, state_name);
125148

126149
// Add infrastructure info if instance IP is available
127150
if let Some(instance_ip) = any_env.instance_ip() {
@@ -144,7 +167,6 @@ impl ShowCommandHandler {
144167
if Self::should_show_services(any_env.state_name()) {
145168
// Always compute from tracker config to show proper service information
146169
// including TLS domains, localhost hints, and HTTPS status
147-
let tracker_config = any_env.tracker_config();
148170
let grafana_config = any_env.grafana_config();
149171
let services =
150172
ServiceInfo::from_tracker_config(tracker_config, instance_ip, grafana_config);
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
use serde::Serialize;
2+
3+
/// Docker image information for the deployment stack
4+
///
5+
/// Contains the Docker image references for all services in the deployment.
6+
/// Optional services (`MySQL`, Prometheus, Grafana) are `None` if not configured.
7+
#[derive(Debug, Clone, Serialize)]
8+
pub struct DockerImagesInfo {
9+
/// Tracker Docker image reference (e.g. `torrust/tracker:develop`)
10+
pub tracker: String,
11+
12+
/// `MySQL` Docker image reference (e.g. `mysql:8.4`), present when `MySQL` is configured
13+
pub mysql: Option<String>,
14+
15+
/// Prometheus Docker image reference (e.g. `prom/prometheus:v3.5.0`), present when configured
16+
pub prometheus: Option<String>,
17+
18+
/// Grafana Docker image reference (e.g. `grafana/grafana:12.3.1`), present when configured
19+
pub grafana: Option<String>,
20+
}
21+
22+
impl DockerImagesInfo {
23+
/// Create a new `DockerImagesInfo` with the tracker image and optional service images
24+
#[must_use]
25+
pub fn new(
26+
tracker: String,
27+
mysql: Option<String>,
28+
prometheus: Option<String>,
29+
grafana: Option<String>,
30+
) -> Self {
31+
Self {
32+
tracker,
33+
mysql,
34+
prometheus,
35+
grafana,
36+
}
37+
}
38+
}

0 commit comments

Comments
 (0)