Skip to content

Daily Security Rebuild #4

Daily Security Rebuild

Daily Security Rebuild #4

name: Weekly Security Rebuild
# Rebuild weekly to pick up Go toolchain and OS patches, republishing only when it clears a CVE (COSY-926).
on:
schedule:
# ~22h ahead of the secops Wednesday 06:00 UTC scan, so anything cleared here is never filed.
- cron: "0 8 * * TUE"
workflow_dispatch:
inputs:
dry_run:
description: "Scan and rebuild, but never publish"
type: boolean
default: false
image:
description: "Image to scan and republish. Override to test in a fork."
type: string
default: localstack/localstack-docker-desktop
concurrency:
group: security-rebuild
cancel-in-progress: false
jobs:
security-rebuild:
name: Rebuild and republish if CVEs clear
runs-on: ubuntu-latest
permissions:
contents: write
env:
PUBLISHED_IMAGE: ${{ inputs.image || 'localstack/localstack-docker-desktop' }}
PLATFORMS: linux/amd64,linux/arm64
GIT_AUTHOR_NAME: localstack[bot]
GIT_AUTHOR_EMAIL: 88328844+localstack-bot@users.noreply.github.com
GIT_COMMITTER_NAME: localstack[bot]
GIT_COMMITTER_EMAIL: 88328844+localstack-bot@users.noreply.github.com
# Mirror the Trivy DBs to dodge GHCR rate limits (trivy-action#389), as aws_flink.yml does.
TRIVY_DB_REPOSITORY: "ghcr.io/aquasecurity/trivy-db,public.ecr.aws/aquasecurity/trivy-db"
TRIVY_JAVA_DB_REPOSITORY: "ghcr.io/aquasecurity/trivy-java-db,public.ecr.aws/aquasecurity/trivy-java-db"
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Read published version
id: version
run: |
TAG=$(sed -n 's/^TAG?=\(.*\)$/\1/p' Makefile)
echo "Currently published: ${PUBLISHED_IMAGE}:${TAG}"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
- name: Scan published image
# --ignore-unfixed matches what the secops pipeline files, so an unfixable advisory can't loop here forever.
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
image-ref: ${{ env.PUBLISHED_IMAGE }}:${{ steps.version.outputs.tag }}
scanners: vuln
severity: HIGH,CRITICAL
ignore-unfixed: true
format: json
output: published.json
- name: Determine fixable CVEs on the published image
id: before
run: |
jq -r '[.Results[]?.Vulnerabilities[]?.VulnerabilityID] | unique | .[]' published.json | sort -u > published_cves.txt
echo "Fixable HIGH/CRITICAL CVEs on the published image:"; cat published_cves.txt || true
if [ -s published_cves.txt ]; then
echo "proceed=true" >> "$GITHUB_OUTPUT"
else
echo "Published image is clean — nothing to rebuild."
echo "proceed=false" >> "$GITHUB_OUTPUT"
fi
- name: Set up QEMU
if: steps.before.outputs.proceed == 'true'
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
- name: Set up Buildx
if: steps.before.outputs.proceed == 'true'
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
with:
platforms: linux/amd64,linux/arm64
- name: Build and smoke-test candidate (amd64, for scanning)
if: steps.before.outputs.proceed == 'true'
run: make smoke-test IMAGE=localstack-docker-desktop TAG=candidate
- name: Scan candidate
if: steps.before.outputs.proceed == 'true'
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
image-ref: localstack-docker-desktop:candidate
scanners: vuln
severity: HIGH,CRITICAL
ignore-unfixed: true
format: json
output: candidate.json
- name: Compute cleared CVEs (published - candidate)
id: delta
if: steps.before.outputs.proceed == 'true'
run: |
jq -r '[.Results[]?.Vulnerabilities[]?.VulnerabilityID] | unique | .[]' candidate.json | sort -u > candidate_cves.txt
comm -23 published_cves.txt candidate_cves.txt > cleared.txt
comm -13 published_cves.txt candidate_cves.txt > introduced.txt
echo "Cleared by the rebuild:"; cat cleared.txt || true
# Reported, not gated: we'd still rather ship a net improvement.
if [ -s introduced.txt ]; then
echo "::warning::Rebuild introduced new CVEs: $(paste -sd', ' introduced.txt)"
fi
if [ -s cleared.txt ]; then
echo "cleared=true" >> "$GITHUB_OUTPUT"
else
echo "cleared=false" >> "$GITHUB_OUTPUT"
fi
- name: Report CVEs a rebuild cannot fix
# Surface these instead of no-op'ing weekly until the SLA on the secops ticket burns.
if: steps.before.outputs.proceed == 'true' && steps.delta.outputs.cleared == 'false'
run: |
echo "::warning::Rebuild cleared nothing; these need a dependency bump: $(paste -sd', ' published_cves.txt)"
{
echo "### Rebuild cleared no CVEs"
echo
echo "Still present after a clean rebuild — these need a \`vm/go.mod\` bump, not a rebuild:"
echo
sed 's/^/- /' published_cves.txt
} >> "$GITHUB_STEP_SUMMARY"
- name: Bump version
id: bump
if: steps.delta.outputs.cleared == 'true' && !inputs.dry_run
run: |
NEW=$(./scripts/bump-version.sh cleared.txt)
echo "Releasing ${NEW}"
echo "version=${NEW}" >> "$GITHUB_OUTPUT"
- name: Login to Docker Hub
if: steps.delta.outputs.cleared == 'true' && !inputs.dry_run
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Publish (multi-arch)
# Published here rather than via build-push-docker.yml: GITHUB_TOKEN pushes don't trigger workflows.
# v${NEW} is what the secops scan resolves; latest is what docker pull and the marketplace read.
if: steps.delta.outputs.cleared == 'true' && !inputs.dry_run
env:
NEW: ${{ steps.bump.outputs.version }}
run: |
echo "Publishing ${PUBLISHED_IMAGE}:${NEW} (also v${NEW}, latest), clearing:"; cat cleared.txt
docker buildx build --push --pull --no-cache \
--platform "${PLATFORMS}" \
--tag "${PUBLISHED_IMAGE}:${NEW}" \
--tag "${PUBLISHED_IMAGE}:v${NEW}" \
--tag "${PUBLISHED_IMAGE}:latest" .
# After the publish on purpose: a failed publish must not leave TAG pointing at an unpushed image.
- name: Commit, tag and release
if: steps.delta.outputs.cleared == 'true' && !inputs.dry_run
env:
NEW: ${{ steps.bump.outputs.version }}
GH_TOKEN: ${{ github.token }}
run: |
git add Makefile Dockerfile CHANGELOG.md
git commit -m "Security rebuild ${NEW}"
git tag "v${NEW}"
git push origin HEAD:main "v${NEW}"
# The secops scan reads our GitHub releases, not our tags; a bare tag leaves it on the old version.
gh release create "v${NEW}" --title "${NEW}" \
--notes "Weekly security rebuild (COSY-926). Cleared: $(paste -sd', ' cleared.txt)."
- name: Summary
if: always()
run: |
{
echo "### Weekly security rebuild"
echo
echo "- Published image: \`${PUBLISHED_IMAGE}:${{ steps.version.outputs.tag }}\`"
echo "- Fixable HIGH/CRITICAL on the published image: $(wc -l < published_cves.txt 2>/dev/null || echo 0)"
echo "- Cleared by rebuild: $(wc -l < cleared.txt 2>/dev/null || echo 0)"
echo "- Released: ${{ steps.bump.outputs.version || 'no (nothing cleared)' }}"
} >> "$GITHUB_STEP_SUMMARY"