name: Docker Vulnerability Scan
on:
schedule:
# Run Monday at midnight UTC
- cron: '0 0 * * 1'
workflow_dispatch:
inputs:
image-tag:
description: 'Image tag to scan'
required: true
default: main
type: string
permissions:
contents: read
env:
DOCKER_REGISTRY: "ghcr.io/comet-ml/opik"
jobs:
scan-docker-images:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
image:
- opik-backend
- opik-frontend
- opik-frontend-comet
- opik-python-backend
- opik-sandbox-executor-python
name: Scan ${{ matrix.image }}
steps:
- name: Pull Docker image
id: pull
run: |
IMAGE_URL="${{ env.DOCKER_REGISTRY }}/${{ matrix.image }}:${{ inputs.image-tag || 'main' }}"
echo "Pulling Docker image: ${IMAGE_URL}"
docker pull ${IMAGE_URL}
BASENAME=$(basename ${IMAGE_URL} | cut -d: -f1)
REPORT_NAME="trivy_${BASENAME}.txt"
echo "image-url=${IMAGE_URL}" >> "$GITHUB_OUTPUT"
echo "report-name=${REPORT_NAME}" >> "$GITHUB_OUTPUT"
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@v0.35.0
env:
TRIVY_DISABLE_VEX_NOTICE: true
with:
image-ref: '${{ steps.pull.outputs.image-url }}'
scan-type: 'image'
format: 'table'
ignore-unfixed: 'false'
output: '${{ steps.pull.outputs.report-name }}'
scanners: vuln
severity: 'CRITICAL,HIGH'
hide-progress: true
timeout: 10m0s
- name: Set outputs
id: set-outputs
run: |
REPORT_NAME="${{ steps.pull.outputs.report-name }}"
echo "report-file=${REPORT_NAME}" >> "$GITHUB_OUTPUT"
if [ -f "${REPORT_NAME}" ]; then
echo "Report generated: ${REPORT_NAME}"
ls -lh "${REPORT_NAME}"
else
echo "Warning: Report file not found at ${REPORT_NAME}"
fi
- name: Write to job summary
run: |
{
echo "## Trivy Vulnerability Scan: \`${{ steps.pull.outputs.image-url }}\`"
echo ""
echo "Click to expand report
"
echo ""
echo '```'
cat "${{ steps.pull.outputs.report-name }}" 2>/dev/null || echo "Report not found"
echo '```'
echo ""
echo " "
} >> "$GITHUB_STEP_SUMMARY"
- name: Upload scan results as artifact
uses: actions/upload-artifact@v7
with:
name: trivy-scan-${{ matrix.image }}
path: ${{ steps.pull.outputs.report-name }}
retention-days: 30
if-no-files-found: warn
- name: Checkout scripts
uses: actions/checkout@v6
with:
sparse-checkout: scripts/analyze_trivy_report.sh
sparse-checkout-cone-mode: false
path: repo
- name: Analyze vulnerabilities
id: analyze
run: |
./repo/scripts/analyze_trivy_report.sh \
"${{ steps.pull.outputs.report-name }}" \
"${{ steps.pull.outputs.image-url }}" \
--workflow-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
--artifact-name "trivy-scan-${{ matrix.image }}"
- name: Format base image Slack message
if: steps.analyze.outputs.has-base-image-vulns == 'true'
id: format-base
env:
MESSAGE: ${{ steps.analyze.outputs.base-image-message }}
MENTION: ${{ secrets.SLACK_MENTION_USERS }}
run: |
python3 << 'PYTHON_SCRIPT'
import re, os
input_message = os.environ.get('MESSAGE', '').strip()
mention = os.environ.get('MENTION', '').strip()
result = re.sub(r"'(.*?)'", r"`\1`", input_message)
if mention:
# Ensure each user ID is wrapped in <@...> for Slack mention syntax
parts = mention.split()
wrapped = ' '.join(
uid if uid.startswith('<@') else f'<@{uid}>'
for uid in parts
)
result = wrapped + '\n\n' + result
with open(os.environ.get('GITHUB_OUTPUT', '/dev/null'), 'a') as f:
f.write('message< for Slack mention syntax
parts = mention.split()
wrapped = ' '.join(
uid if uid.startswith('<@') else f'<@{uid}>'
for uid in parts
)
result = wrapped + '\n\n' + result
with open(os.environ.get('GITHUB_OUTPUT', '/dev/null'), 'a') as f:
f.write('message<
${{ steps.format-base.outputs.message }}
- name: Notify Slack - application vulnerabilities
if: steps.analyze.outputs.has-app-vulns == 'true'
uses: rtCamp/action-slack-notify@v2
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_VULNERABILITY_WEBHOOK_URL }}
SLACK_COLOR: danger
SLACK_USERNAME: 'GitHub Actions'
SLACK_ICON: 'https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png'
SLACK_LINK_NAMES: true
MSG_MINIMAL: false
ENABLE_ESCAPES: true
SLACK_TITLE: ${{ steps.analyze.outputs.app-title }}
SLACK_MESSAGE: |
*Run:* <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}/attempts/${{ github.run_attempt }}>
${{ steps.format-app.outputs.message }}