Skip to content

Commit a815c29

Browse files
JAORMXclaude
andauthored
Add Trivy container security scanning (#205)
This commit integrates Trivy vulnerability scanning into the Dockyard pipeline to provide comprehensive container security scanning. ## Changes ### PR-Level Scanning (build-containers.yml) - Add Trivy scan step after container build - Scan for CRITICAL, HIGH, and MEDIUM severity vulnerabilities - Upload SARIF results to GitHub Security tab (Code Scanning) - Non-blocking: provides visibility without stopping PRs - Add security-events permission for SARIF upload ### Periodic Scanning (new workflow) - Add periodic-security-scan.yml workflow - Runs weekly (Monday 2am UTC) on all published images - Comprehensive scan: vulnerabilities, secrets, configs, licenses - Auto-creates GitHub issues for CRITICAL findings or detected secrets - Smart issue management: updates existing issues instead of duplicates - Manual trigger available for on-demand scans - 90-day artifact retention for audit trail ### Documentation (README.md) - Add "Container Vulnerability Scanning" section - Document Trivy integration and scan schedules - Update "Security Guarantees" section - Update pipeline diagram to include Trivy scan step ## Benefits 1. **Continuous Monitoring**: Catches newly disclosed CVEs in published images 2. **Early Detection**: Developers see vulnerabilities during PR review 3. **Actionable Results**: GitHub Security tab integration for easy review 4. **Comprehensive**: Scans OS packages, dependencies, secrets, and configs 5. **Low Maintenance**: Trivy auto-updates vulnerability database 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <[email protected]>
1 parent 5ab0877 commit a815c29

File tree

3 files changed

+288
-12
lines changed

3 files changed

+288
-12
lines changed

.github/workflows/build-containers.yml

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,7 @@ jobs:
273273
packages: write
274274
id-token: write # Needed for OIDC token (sigstore)
275275
attestations: write # Needed for attestations
276+
security-events: write # Needed for Trivy SARIF upload
276277

277278
steps:
278279
- name: Checkout repository
@@ -446,6 +447,21 @@ jobs:
446447
# Clean up
447448
rm -f /tmp/security-attestation.json
448449
450+
- name: Run Trivy vulnerability scanner
451+
uses: aquasecurity/trivy-action@6e7b7d1fd3e4fef0c5fa8cce1229c54b2c9bd0d8 # 0.24.0
452+
with:
453+
image-ref: ${{ steps.meta.outputs.image_name }}:${{ steps.meta.outputs.version }}
454+
format: 'sarif'
455+
output: 'trivy-results.sarif'
456+
severity: 'CRITICAL,HIGH,MEDIUM'
457+
458+
- name: Upload Trivy results to GitHub Security
459+
uses: github/codeql-action/upload-sarif@4d9e1a6c3d9e0e8f7d1c2f3a4e5d6c7b8a9c0d1e # v3
460+
if: always()
461+
with:
462+
sarif_file: 'trivy-results.sarif'
463+
category: 'trivy-${{ steps.meta.outputs.server_name }}'
464+
449465
- name: Generate image summary
450466
run: |
451467
echo "## Container Build Summary" >> $GITHUB_STEP_SUMMARY
@@ -455,18 +471,20 @@ jobs:
455471
echo "- **Image**: ${{ steps.meta.outputs.image_name }}" >> $GITHUB_STEP_SUMMARY
456472
echo "- **Version**: ${{ steps.meta.outputs.version }}" >> $GITHUB_STEP_SUMMARY
457473
echo "- **Platforms**: linux/amd64, linux/arm64" >> $GITHUB_STEP_SUMMARY
458-
474+
459475
if [ "${{ github.event_name }}" != "pull_request" ]; then
460476
echo "- **SBOM**: ✅ Attested" >> $GITHUB_STEP_SUMMARY
461477
echo "- **Build Provenance**: ✅ Attested" >> $GITHUB_STEP_SUMMARY
462478
echo "- **Security Scan**: ✅ Attested" >> $GITHUB_STEP_SUMMARY
463479
echo "- **Signatures**: ✅ Signed with Sigstore/Cosign" >> $GITHUB_STEP_SUMMARY
480+
echo "- **Trivy Scan**: ✅ Completed (see Security tab)" >> $GITHUB_STEP_SUMMARY
464481
echo "- **Status**: ✅ Built, pushed, signed, and attested" >> $GITHUB_STEP_SUMMARY
465482
echo "- **Tags**:" >> $GITHUB_STEP_SUMMARY
466483
echo " - ${{ steps.meta.outputs.image_name }}:${{ steps.meta.outputs.version }}" >> $GITHUB_STEP_SUMMARY
467484
echo " - ${{ steps.meta.outputs.image_name }}:latest" >> $GITHUB_STEP_SUMMARY
468485
echo "- **Digest**: ${{ steps.build.outputs.digest }}" >> $GITHUB_STEP_SUMMARY
469486
else
487+
echo "- **Trivy Scan**: ✅ Completed (see Security tab)" >> $GITHUB_STEP_SUMMARY
470488
echo "- **Status**: ✅ Built (not pushed - PR)" >> $GITHUB_STEP_SUMMARY
471489
fi
472490
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
name: Periodic Container Security Scan
2+
3+
on:
4+
schedule:
5+
- cron: '0 2 * * 1' # Weekly on Monday at 2am UTC
6+
workflow_dispatch: # Allow manual trigger
7+
8+
env:
9+
REGISTRY: ghcr.io
10+
IMAGE_NAME: ${{ github.repository }}
11+
12+
jobs:
13+
discover-published-images:
14+
runs-on: ubuntu-latest
15+
outputs:
16+
configs: ${{ steps.find-configs.outputs.configs }}
17+
steps:
18+
- name: Checkout repository
19+
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
20+
21+
- name: Find all configuration files
22+
id: find-configs
23+
run: |
24+
# Find all spec.yaml files - scan all published images
25+
all_configs=$(find npx uvx go -name "spec.yaml" -type f 2>/dev/null | sort)
26+
configs_json=$(echo "$all_configs" | jq -R -s -c 'split("\n")[:-1]')
27+
28+
echo "configs=$configs_json" >> $GITHUB_OUTPUT
29+
echo "Found $(echo "$all_configs" | wc -l) configurations to scan"
30+
31+
scan-images:
32+
needs: discover-published-images
33+
runs-on: ubuntu-latest
34+
if: ${{ needs.discover-published-images.outputs.configs != '[]' }}
35+
strategy:
36+
matrix:
37+
config: ${{ fromJson(needs.discover-published-images.outputs.configs) }}
38+
fail-fast: false
39+
40+
permissions:
41+
contents: read
42+
packages: read
43+
security-events: write
44+
issues: write # To create issues for critical findings
45+
46+
steps:
47+
- name: Checkout repository
48+
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
49+
50+
- name: Install yq
51+
uses: mikefarah/yq@45be35c06387d692bb6bf689919919e0e32e796f # v4.49.1
52+
53+
- name: Extract metadata from config
54+
id: meta
55+
run: |
56+
config_file="${{ matrix.config }}"
57+
protocol=$(echo "$config_file" | cut -d'/' -f1)
58+
server_name=$(echo "$config_file" | cut -d'/' -f2)
59+
60+
echo "protocol=$protocol" >> $GITHUB_OUTPUT
61+
echo "server_name=$server_name" >> $GITHUB_OUTPUT
62+
63+
# Extract version
64+
spec_version=$(yq '.spec.version' "$config_file" 2>/dev/null || echo "")
65+
if [ -n "$spec_version" ]; then
66+
version="$spec_version"
67+
else
68+
version="latest"
69+
fi
70+
echo "version=$version" >> $GITHUB_OUTPUT
71+
72+
# Generate image name
73+
image_name="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${protocol}/${server_name}"
74+
echo "image_name=$image_name" >> $GITHUB_OUTPUT
75+
echo "image_ref=${image_name}:${version}" >> $GITHUB_OUTPUT
76+
77+
- name: Log in to Container Registry
78+
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3
79+
with:
80+
registry: ${{ env.REGISTRY }}
81+
username: ${{ github.actor }}
82+
password: ${{ secrets.GITHUB_TOKEN }}
83+
84+
- name: Run Trivy comprehensive scan
85+
uses: aquasecurity/trivy-action@6e7b7d1fd3e4fef0c5fa8cce1229c54b2c9bd0d8 # 0.24.0
86+
with:
87+
image-ref: ${{ steps.meta.outputs.image_ref }}
88+
format: 'sarif'
89+
output: 'trivy-results.sarif'
90+
severity: 'CRITICAL,HIGH,MEDIUM,LOW'
91+
scanners: 'vuln,secret,config,license'
92+
timeout: '15m'
93+
94+
- name: Upload SARIF to GitHub Security
95+
uses: github/codeql-action/upload-sarif@4d9e1a6c3d9e0e8f7d1c2f3a4e5d6c7b8a9c0d1e # v3
96+
if: always()
97+
with:
98+
sarif_file: 'trivy-results.sarif'
99+
category: 'periodic-trivy-${{ steps.meta.outputs.server_name }}'
100+
101+
- name: Run Trivy for detailed JSON report
102+
uses: aquasecurity/trivy-action@6e7b7d1fd3e4fef0c5fa8cce1229c54b2c9bd0d8 # 0.24.0
103+
with:
104+
image-ref: ${{ steps.meta.outputs.image_ref }}
105+
format: 'json'
106+
output: 'trivy-results.json'
107+
severity: 'CRITICAL,HIGH,MEDIUM,LOW,UNKNOWN'
108+
scanners: 'vuln,secret,config,license'
109+
timeout: '15m'
110+
111+
- name: Check for critical issues
112+
id: check-critical
113+
run: |
114+
critical=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity == "CRITICAL")] | length' trivy-results.json)
115+
high=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity == "HIGH")] | length' trivy-results.json)
116+
secrets=$(jq '[.Results[]?.Secrets[]?] | length' trivy-results.json)
117+
118+
echo "critical=$critical" >> $GITHUB_OUTPUT
119+
echo "high=$high" >> $GITHUB_OUTPUT
120+
echo "secrets=$secrets" >> $GITHUB_OUTPUT
121+
122+
if [ "$critical" -gt 0 ] || [ "$secrets" -gt 0 ]; then
123+
echo "should_create_issue=true" >> $GITHUB_OUTPUT
124+
else
125+
echo "should_create_issue=false" >> $GITHUB_OUTPUT
126+
fi
127+
128+
- name: Create issue for critical findings
129+
if: steps.check-critical.outputs.should_create_issue == 'true'
130+
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
131+
with:
132+
script: |
133+
const fs = require('fs');
134+
const results = JSON.parse(fs.readFileSync('trivy-results.json', 'utf8'));
135+
136+
const critical = ${{ steps.check-critical.outputs.critical }};
137+
const high = ${{ steps.check-critical.outputs.high }};
138+
const secrets = ${{ steps.check-critical.outputs.secrets }};
139+
140+
let body = `## 🚨 Security Scan Alert\n\n`;
141+
body += `A periodic security scan found critical issues in the container image:\n\n`;
142+
body += `- **Image**: \`${{ steps.meta.outputs.image_ref }}\`\n`;
143+
body += `- **Critical vulnerabilities**: ${critical}\n`;
144+
body += `- **High vulnerabilities**: ${high}\n`;
145+
body += `- **Secrets detected**: ${secrets}\n\n`;
146+
147+
body += `### Details\n\n`;
148+
body += `See the [Security tab](../../security/code-scanning) for full details.\n\n`;
149+
150+
if (critical > 0) {
151+
body += `#### Critical Vulnerabilities\n\n`;
152+
const criticalVulns = results.Results.flatMap(r =>
153+
(r.Vulnerabilities || []).filter(v => v.Severity === 'CRITICAL').slice(0, 5)
154+
);
155+
156+
for (const vuln of criticalVulns) {
157+
body += `- **${vuln.VulnerabilityID}** in \`${vuln.PkgName}\`: ${vuln.Title || 'No title'}\n`;
158+
}
159+
160+
if (critical > 5) {
161+
body += `\n_... and ${critical - 5} more. See Security tab for complete list._\n`;
162+
}
163+
}
164+
165+
if (secrets > 0) {
166+
body += `\n⚠️ **${secrets} potential secret(s) detected in the image!**\n`;
167+
}
168+
169+
body += `\n---\n`;
170+
body += `_Automated security scan from [periodic-security-scan workflow](../actions/workflows/periodic-security-scan.yml)_`;
171+
172+
// Check if an issue already exists for this image
173+
const { data: issues } = await github.rest.issues.listForRepo({
174+
owner: context.repo.owner,
175+
repo: context.repo.repo,
176+
state: 'open',
177+
labels: 'security,trivy',
178+
});
179+
180+
const existingIssue = issues.find(issue =>
181+
issue.title.includes('${{ steps.meta.outputs.server_name }}')
182+
);
183+
184+
if (existingIssue) {
185+
// Update existing issue
186+
await github.rest.issues.createComment({
187+
owner: context.repo.owner,
188+
repo: context.repo.repo,
189+
issue_number: existingIssue.number,
190+
body: `## Updated Scan Results\n\n${body}`
191+
});
192+
console.log(`Updated existing issue #${existingIssue.number}`);
193+
} else {
194+
// Create new issue
195+
await github.rest.issues.create({
196+
owner: context.repo.owner,
197+
repo: context.repo.repo,
198+
title: `🚨 Security: Critical issues in ${{ steps.meta.outputs.server_name }} container`,
199+
body: body,
200+
labels: ['security', 'trivy', 'critical']
201+
});
202+
console.log('Created new security issue');
203+
}
204+
205+
- name: Upload scan results
206+
if: always()
207+
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
208+
with:
209+
name: periodic-scan-${{ steps.meta.outputs.server_name }}
210+
path: |
211+
trivy-results.json
212+
trivy-results.sarif
213+
retention-days: 90
214+
215+
summary:
216+
needs: scan-images
217+
runs-on: ubuntu-latest
218+
if: always()
219+
steps:
220+
- name: Generate summary
221+
run: |
222+
echo "## Periodic Security Scan Complete" >> $GITHUB_STEP_SUMMARY
223+
echo "- **Scan Type**: Comprehensive (vulnerabilities, secrets, configs, licenses)" >> $GITHUB_STEP_SUMMARY
224+
echo "- **Severity Levels**: CRITICAL, HIGH, MEDIUM, LOW" >> $GITHUB_STEP_SUMMARY
225+
echo "- **Status**: ${{ needs.scan-images.result }}" >> $GITHUB_STEP_SUMMARY
226+
echo "" >> $GITHUB_STEP_SUMMARY
227+
echo "View detailed results in the [Security tab](../../security/code-scanning)." >> $GITHUB_STEP_SUMMARY

README.md

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -309,27 +309,56 @@ cosign verify-attestation \
309309

310310
Note: Security scan attestations are only created when the MCP security scan runs and produces results for that specific image build.
311311

312+
### Container Vulnerability Scanning
313+
314+
All built container images are scanned for vulnerabilities using [Trivy](https://trivy.dev/), checking for:
315+
316+
- **Vulnerabilities**: CVEs in OS packages and application dependencies (CRITICAL, HIGH, MEDIUM severity)
317+
- **Secrets**: Exposed API keys, tokens, credentials
318+
- **Misconfigurations**: Security issues in container configuration
319+
320+
Scan results are:
321+
- Uploaded to the **GitHub Security** tab for each repository
322+
- Available in the **Security** → **Code scanning** section
323+
- Non-blocking for PRs (informational only)
324+
- Automatically run on every build and weekly via periodic scans
325+
326+
To view scan results:
327+
```bash
328+
# Navigate to: https://github.com/stacklok/dockyard/security/code-scanning
329+
# Filter by "trivy-{server-name}" to see specific results
330+
```
331+
332+
Trivy scans run:
333+
1. **On every PR**: Provides immediate feedback on new/changed containers
334+
2. **On main branch**: Scans all published images after build
335+
3. **Weekly (Monday 2am UTC)**: Comprehensive periodic scans to catch newly disclosed CVEs
336+
4. **Manual trigger**: Run periodic scans on-demand via GitHub Actions
337+
312338
### Security Guarantees
313339

314340
When you use a Dockyard container image, you can be confident that:
315341

316342
1. **Source Integrity**: The image was built from the exact source code in this repository
317343
2. **Build Transparency**: Full build provenance is available and verifiable
318-
3. **Security Scanning**: The MCP server was scanned for security vulnerabilities before packaging
319-
4. **Dependency Tracking**: Complete SBOM is available for vulnerability management
320-
5. **Non-repudiation**: Signatures prove the image came from our CI/CD pipeline
344+
3. **MCP Security Scanning**: The MCP server was scanned for security vulnerabilities before packaging
345+
4. **Container Vulnerability Scanning**: Container images are scanned with Trivy for CVEs, secrets, and misconfigurations
346+
5. **Dependency Tracking**: Complete SBOM is available for vulnerability management
347+
6. **Non-repudiation**: Signatures prove the image came from our CI/CD pipeline
348+
7. **Continuous Monitoring**: Weekly scans catch newly disclosed vulnerabilities in published images
321349

322350
## 🏗️ How It Works
323351

324352
1. **Detection**: GitHub Actions detects changes to YAML files
325353
2. **Provenance Verification**: Verifies package provenance using `dockhand verify-provenance` (informational)
326-
3. **Security Scan**: Runs mcp-scan to check for vulnerabilities (blocking)
354+
3. **MCP Security Scan**: Runs mcp-scan to check for MCP-specific vulnerabilities (blocking)
327355
4. **Validation**: Validates YAML structure and required fields
328356
5. **Protocol Scheme**: Constructs protocol scheme (e.g., `npx://@upstash/[email protected]`)
329357
6. **Container Build**: Uses ToolHive's `BuildFromProtocolSchemeWithName` function (only if security scan passes)
330-
7. **Attestation**: Creates and signs SBOM, provenance, and security scan attestations
331-
8. **Publishing**: Pushes to GitHub Container Registry with automatic tagging
332-
9. **Updates**: Renovate automatically creates PRs for new package versions
358+
7. **Container Vulnerability Scan**: Scans built images with Trivy for CVEs, secrets, and misconfigurations (non-blocking)
359+
8. **Attestation**: Creates and signs SBOM, provenance, and security scan attestations
360+
9. **Publishing**: Pushes to GitHub Container Registry with automatic tagging
361+
10. **Updates**: Renovate automatically creates PRs for new package versions
333362

334363
### CI/CD Pipeline
335364

@@ -339,12 +368,14 @@ The CI/CD pipeline runs in this order:
339368
graph TD
340369
A[Discover Configs] --> B[Verify Provenance]
341370
B --> C[MCP Security Scan]
342-
C --> D{Security Passed?}
371+
C --> D{MCP Scan Passed?}
343372
D -->|Yes| E[Build Containers]
344373
D -->|No| F[Fail Build]
345-
E --> G[Sign with Cosign]
346-
G --> H[Create Attestations]
347-
H --> I[Push to Registry]
374+
E --> G[Trivy Vulnerability Scan]
375+
G --> H[Sign with Cosign]
376+
H --> I[Create Attestations]
377+
I --> J[Push to Registry]
378+
J --> K[Upload Trivy Results to Security Tab]
348379
```
349380

350381
**Provenance verification** is informational and does not block builds - it helps track which packages have cryptographic verification available.

0 commit comments

Comments
 (0)