11name : scanner-images
22
33# Build and publish the custom security scanner Docker images to
4- # ghcr.io/smart-mcp-proxy/scanner-*. Vendor images (trivy, semgrep, the
5- # AI scanner which lives in a separate repo) are NOT built here.
4+ # ghcr.io/smart-mcp-proxy/scanner-*. Vendor images (semgrep, the AI scanner
5+ # which lives in a separate repo) are NOT built here. trivy IS wrapped in our
6+ # own image (docker/scanners/trivy) to pre-cache the vuln DB (MCP-2150).
67#
78# Triggers:
89# - push to main that touches docker/scanners/** or this workflow file
10+ # - weekly schedule: rebuilds trivy to pick up a fresh vulnerability DB
911# - manual dispatch (for ad-hoc rebuilds)
1012# - pull requests: builds without pushing (to catch Dockerfile drift)
1113
1921 paths :
2022 - ' docker/scanners/**'
2123 - ' .github/workflows/scanner-images.yml'
24+ schedule :
25+ # Weekly Monday 03:00 UTC — keeps the trivy vuln DB reasonably current.
26+ - cron : ' 0 3 * * 1'
2227 workflow_dispatch :
2328 inputs :
2429 scanner :
@@ -33,40 +38,62 @@ permissions:
3338
3439jobs :
3540 build :
36- runs-on : ubuntu-latest
41+ # Most scanners use ubuntu-latest (amd64) with QEMU for multi-arch builds.
42+ # ramparts uses per-arch native runners to avoid QEMU timeout on the Rust
43+ # `cargo install` step: `matrix.runner` selects the runner per entry.
44+ runs-on : ${{ matrix.runner }}
3745 strategy :
3846 fail-fast : false
3947 matrix :
40- # `platforms` defaults to multi-arch. ramparts overrides to linux/amd64
41- # only: it is the lone Rust-from-source wrapper, and `cargo install
42- # ramparts` for an emulated linux/arm64 leg runs under QEMU (~5-10x
43- # slower). A cold build of both arches blows past the runner budget and
44- # the job gets cancelled mid-compile (MCP-2395). amd64-only finishes in
45- # time; the binary is consumed on the host arch by Docker isolation.
48+ # `arch` is set only for ramparts per-arch entries. Entries without
49+ # `arch` push a combined multi-arch :latest directly; per-arch entries
50+ # push :<sha>-<arch> / :latest-<arch> tags and the merge-ramparts job
51+ # assembles the final multi-arch :latest manifest.
4652 include :
4753 - id : snyk
54+ runner : ubuntu-latest
4855 image : ghcr.io/smart-mcp-proxy/scanner-snyk
4956 context : docker/scanners/snyk
5057 platforms : linux/amd64,linux/arm64
5158 - id : cisco
59+ runner : ubuntu-latest
5260 image : ghcr.io/smart-mcp-proxy/scanner-cisco
5361 context : docker/scanners/cisco
5462 platforms : linux/amd64,linux/arm64
55- - id : ramparts
56- image : ghcr.io/smart-mcp-proxy/scanner-ramparts
57- context : docker/scanners/ramparts
58- platforms : linux/amd64
63+ - id : trivy
64+ runner : ubuntu-latest
65+ image : ghcr.io/smart-mcp-proxy/scanner-trivy
66+ context : docker/scanners/trivy
67+ platforms : linux/amd64,linux/arm64
5968 - id : proximity
69+ runner : ubuntu-latest
6070 image : ghcr.io/smart-mcp-proxy/scanner-proximity
6171 context : docker/scanners/proximity
6272 platforms : linux/amd64,linux/arm64
73+ # ramparts: native per-arch builds (MCP-2150).
74+ # Using QEMU to emulate arm64 on an amd64 runner blows past the job
75+ # budget (~5-10x slower for `cargo install`; MCP-2395). Instead each
76+ # arch builds on its native runner and the merge-ramparts job combines
77+ # them into a multi-arch :latest manifest.
78+ - id : ramparts
79+ arch : amd64
80+ runner : ubuntu-latest
81+ image : ghcr.io/smart-mcp-proxy/scanner-ramparts
82+ context : docker/scanners/ramparts
83+ platforms : linux/amd64
84+ - id : ramparts
85+ arch : arm64
86+ runner : ubuntu-24.04-arm
87+ image : ghcr.io/smart-mcp-proxy/scanner-ramparts
88+ context : docker/scanners/ramparts
89+ platforms : linux/arm64
6390 steps :
6491 - name : Checkout
6592 uses : actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
6693
6794 - name : Skip if not selected
6895 if : ${{ github.event_name == 'workflow_dispatch' && inputs.scanner != '' && inputs.scanner != matrix.id }}
69- run : echo "Skipping ${{ matrix.id }} (workflow_dispatch selected ${{ inputs.scanner }})" && exit 0
96+ run : echo "Skipping ${{ matrix.id }}${{ matrix.arch && format('-{0}', matrix.arch) || '' }} (workflow_dispatch selected ${{ inputs.scanner }})" && exit 0
7097
7198 - name : Set up QEMU
7299 uses : docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
@@ -88,10 +115,22 @@ jobs:
88115 set -euo pipefail
89116 short_sha="${GITHUB_SHA:0:7}"
90117 echo "short_sha=$short_sha" >> "$GITHUB_OUTPUT"
91- if [ "${GITHUB_REF}" = "refs/heads/main" ]; then
92- echo "tags=${{ matrix.image }}:latest,${{ matrix.image }}:$short_sha" >> "$GITHUB_OUTPUT"
118+ arch="${{ matrix.arch }}"
119+ if [ -n "$arch" ]; then
120+ # Per-arch build (ramparts): push arch-suffixed tags only.
121+ # The merge-ramparts job assembles :latest and :<sha> from these.
122+ if [ "${GITHUB_REF}" = "refs/heads/main" ]; then
123+ echo "tags=${{ matrix.image }}:latest-${arch},${{ matrix.image }}:${short_sha}-${arch}" >> "$GITHUB_OUTPUT"
124+ else
125+ echo "tags=${{ matrix.image }}:pr-${{ github.event.pull_request.number || 'dev' }}-${arch}" >> "$GITHUB_OUTPUT"
126+ fi
93127 else
94- echo "tags=${{ matrix.image }}:pr-${{ github.event.pull_request.number || 'dev' }}" >> "$GITHUB_OUTPUT"
128+ # Multi-arch build: push :latest and :<sha> directly.
129+ if [ "${GITHUB_REF}" = "refs/heads/main" ]; then
130+ echo "tags=${{ matrix.image }}:latest,${{ matrix.image }}:$short_sha" >> "$GITHUB_OUTPUT"
131+ else
132+ echo "tags=${{ matrix.image }}:pr-${{ github.event.pull_request.number || 'dev' }}" >> "$GITHUB_OUTPUT"
133+ fi
95134 fi
96135
97136 - name : Build and push
@@ -101,8 +140,8 @@ jobs:
101140 platforms : ${{ matrix.platforms }}
102141 push : ${{ github.event_name != 'pull_request' }}
103142 tags : ${{ steps.tags.outputs.tags }}
104- cache-from : type=gha,scope=scanner-${{ matrix.id }}
105- cache-to : type=gha,scope=scanner-${{ matrix.id }},mode=max
143+ cache-from : type=gha,scope=scanner-${{ matrix.id }}${{ matrix.arch && format('-{0}', matrix.arch) || '' }}
144+ cache-to : type=gha,scope=scanner-${{ matrix.id }}${{ matrix.arch && format('-{0}', matrix.arch) || '' }} ,mode=max
106145 labels : |
107146 org.opencontainers.image.source=https://github.com/smart-mcp-proxy/mcpproxy-go
108147 org.opencontainers.image.revision=${{ github.sha }}
@@ -111,12 +150,12 @@ jobs:
111150 # multi-platform builds (linux/amd64,linux/arm64) cannot be loaded to the runner with
112151 # `load: true`, so there is no local image to scan on PRs. On push the image is
113152 # already in GHCR and Trivy pulls it directly.
114- # exit-code: '0' → report-only, never fails the build. Scanner base images routinely
115- # carry unfixable CVEs (upstream OS packages); blocking builds on those would create
116- # constant noise with no actionable remediation. Visibility via the Security tab and
117- # the workflow log is the goal .
153+ # Skipped for per-arch ramparts builds (matrix.arch set): those entries push
154+ # arch-suffixed tags that get merged by merge-ramparts; the merged :latest is
155+ # what should be scanned, not the intermediate arch-specific tags.
156+ # exit-code: '0' → report-only, never fails the build .
118157 - name : Scan image with Trivy
119- if : github.event_name != 'pull_request'
158+ if : github.event_name != 'pull_request' && !matrix.arch
120159 uses : aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
121160 with :
122161 image-ref : ${{ matrix.image }}:${{ steps.tags.outputs.short_sha }}
@@ -127,7 +166,7 @@ jobs:
127166 format : table
128167
129168 - name : Upload Trivy SARIF to Security tab
130- if : github.event_name != 'pull_request'
169+ if : github.event_name != 'pull_request' && !matrix.arch
131170 uses : aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
132171 with :
133172 image-ref : ${{ matrix.image }}:${{ steps.tags.outputs.short_sha }}
@@ -139,8 +178,40 @@ jobs:
139178 output : trivy-results-${{ matrix.id }}.sarif
140179
141180 - name : Upload SARIF
142- if : github.event_name != 'pull_request'
181+ if : github.event_name != 'pull_request' && !matrix.arch
143182 uses : github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
144183 with :
145184 sarif_file : trivy-results-${{ matrix.id }}.sarif
146185 category : trivy-${{ matrix.id }}
186+
187+ # Assemble the multi-arch ramparts manifest from the two per-arch images
188+ # pushed by the build job. Runs only on push to main and workflow_dispatch
189+ # (not pull_request, since per-arch images aren't pushed on PRs).
190+ merge-ramparts :
191+ needs : build
192+ if : github.event_name != 'pull_request'
193+ runs-on : ubuntu-latest
194+ steps :
195+ - name : Log in to GHCR
196+ uses : docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
197+ with :
198+ registry : ghcr.io
199+ username : ${{ github.actor }}
200+ password : ${{ secrets.GITHUB_TOKEN }}
201+
202+ - name : Set up Buildx
203+ uses : docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
204+
205+ - name : Create multi-arch :latest manifest
206+ run : |
207+ docker buildx imagetools create \
208+ -t ghcr.io/smart-mcp-proxy/scanner-ramparts:latest \
209+ ghcr.io/smart-mcp-proxy/scanner-ramparts:latest-amd64 \
210+ ghcr.io/smart-mcp-proxy/scanner-ramparts:latest-arm64
211+
212+ - name : Create multi-arch SHA-tagged manifest
213+ run : |
214+ docker buildx imagetools create \
215+ -t ghcr.io/smart-mcp-proxy/scanner-ramparts:${GITHUB_SHA:0:7} \
216+ ghcr.io/smart-mcp-proxy/scanner-ramparts:${GITHUB_SHA:0:7}-amd64 \
217+ ghcr.io/smart-mcp-proxy/scanner-ramparts:${GITHUB_SHA:0:7}-arm64
0 commit comments