|
| 1 | +name: Weekly Security Rebuild |
| 2 | + |
| 3 | +# Rebuild weekly to pick up Go toolchain and OS patches, republishing only when it clears a CVE (COSY-926). |
| 4 | + |
| 5 | +on: |
| 6 | + schedule: |
| 7 | + # ~22h ahead of the secops Wednesday 06:00 UTC scan, so anything cleared here is never filed. |
| 8 | + - cron: "0 8 * * TUE" |
| 9 | + workflow_dispatch: |
| 10 | + inputs: |
| 11 | + dry_run: |
| 12 | + description: "Scan and rebuild, but never publish" |
| 13 | + type: boolean |
| 14 | + default: false |
| 15 | + image: |
| 16 | + description: "Image to scan and republish. Override to test in a fork." |
| 17 | + type: string |
| 18 | + default: localstack/localstack-docker-desktop |
| 19 | + |
| 20 | +concurrency: |
| 21 | + group: security-rebuild |
| 22 | + cancel-in-progress: false |
| 23 | + |
| 24 | +jobs: |
| 25 | + security-rebuild: |
| 26 | + name: Rebuild and republish if CVEs clear |
| 27 | + runs-on: ubuntu-latest |
| 28 | + permissions: |
| 29 | + contents: write |
| 30 | + env: |
| 31 | + PUBLISHED_IMAGE: ${{ inputs.image || 'localstack/localstack-docker-desktop' }} |
| 32 | + PLATFORMS: linux/amd64,linux/arm64 |
| 33 | + GIT_AUTHOR_NAME: localstack[bot] |
| 34 | + GIT_AUTHOR_EMAIL: 88328844+localstack-bot@users.noreply.github.com |
| 35 | + GIT_COMMITTER_NAME: localstack[bot] |
| 36 | + GIT_COMMITTER_EMAIL: 88328844+localstack-bot@users.noreply.github.com |
| 37 | + # Mirror the Trivy DBs to dodge GHCR rate limits (trivy-action#389), as aws_flink.yml does. |
| 38 | + TRIVY_DB_REPOSITORY: "ghcr.io/aquasecurity/trivy-db,public.ecr.aws/aquasecurity/trivy-db" |
| 39 | + TRIVY_JAVA_DB_REPOSITORY: "ghcr.io/aquasecurity/trivy-java-db,public.ecr.aws/aquasecurity/trivy-java-db" |
| 40 | + |
| 41 | + steps: |
| 42 | + - name: Checkout |
| 43 | + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 |
| 44 | + |
| 45 | + - name: Read published version |
| 46 | + id: version |
| 47 | + run: | |
| 48 | + TAG=$(sed -n 's/^TAG?=\(.*\)$/\1/p' Makefile) |
| 49 | + echo "Currently published: ${PUBLISHED_IMAGE}:${TAG}" |
| 50 | + echo "tag=${TAG}" >> "$GITHUB_OUTPUT" |
| 51 | +
|
| 52 | + - name: Scan published image |
| 53 | + # --ignore-unfixed matches what the secops pipeline files, so an unfixable advisory can't loop here forever. |
| 54 | + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 |
| 55 | + with: |
| 56 | + image-ref: ${{ env.PUBLISHED_IMAGE }}:${{ steps.version.outputs.tag }} |
| 57 | + scanners: vuln |
| 58 | + severity: HIGH,CRITICAL |
| 59 | + ignore-unfixed: true |
| 60 | + format: json |
| 61 | + output: published.json |
| 62 | + |
| 63 | + - name: Determine fixable CVEs on the published image |
| 64 | + id: before |
| 65 | + run: | |
| 66 | + jq -r '[.Results[]?.Vulnerabilities[]?.VulnerabilityID] | unique | .[]' published.json | sort -u > published_cves.txt |
| 67 | + echo "Fixable HIGH/CRITICAL CVEs on the published image:"; cat published_cves.txt || true |
| 68 | + if [ -s published_cves.txt ]; then |
| 69 | + echo "proceed=true" >> "$GITHUB_OUTPUT" |
| 70 | + else |
| 71 | + echo "Published image is clean — nothing to rebuild." |
| 72 | + echo "proceed=false" >> "$GITHUB_OUTPUT" |
| 73 | + fi |
| 74 | +
|
| 75 | + - name: Set up QEMU |
| 76 | + if: steps.before.outputs.proceed == 'true' |
| 77 | + uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0 |
| 78 | + |
| 79 | + - name: Set up Buildx |
| 80 | + if: steps.before.outputs.proceed == 'true' |
| 81 | + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 |
| 82 | + with: |
| 83 | + platforms: linux/amd64,linux/arm64 |
| 84 | + |
| 85 | + - name: Build and smoke-test candidate (amd64, for scanning) |
| 86 | + if: steps.before.outputs.proceed == 'true' |
| 87 | + run: make smoke-test IMAGE=localstack-docker-desktop TAG=candidate |
| 88 | + |
| 89 | + - name: Scan candidate |
| 90 | + if: steps.before.outputs.proceed == 'true' |
| 91 | + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 |
| 92 | + with: |
| 93 | + image-ref: localstack-docker-desktop:candidate |
| 94 | + scanners: vuln |
| 95 | + severity: HIGH,CRITICAL |
| 96 | + ignore-unfixed: true |
| 97 | + format: json |
| 98 | + output: candidate.json |
| 99 | + |
| 100 | + - name: Compute cleared CVEs (published - candidate) |
| 101 | + id: delta |
| 102 | + if: steps.before.outputs.proceed == 'true' |
| 103 | + run: | |
| 104 | + jq -r '[.Results[]?.Vulnerabilities[]?.VulnerabilityID] | unique | .[]' candidate.json | sort -u > candidate_cves.txt |
| 105 | + comm -23 published_cves.txt candidate_cves.txt > cleared.txt |
| 106 | + comm -13 published_cves.txt candidate_cves.txt > introduced.txt |
| 107 | +
|
| 108 | + echo "Cleared by the rebuild:"; cat cleared.txt || true |
| 109 | + # Reported, not gated: we'd still rather ship a net improvement. |
| 110 | + if [ -s introduced.txt ]; then |
| 111 | + echo "::warning::Rebuild introduced new CVEs: $(paste -sd', ' introduced.txt)" |
| 112 | + fi |
| 113 | +
|
| 114 | + if [ -s cleared.txt ]; then |
| 115 | + echo "cleared=true" >> "$GITHUB_OUTPUT" |
| 116 | + else |
| 117 | + echo "cleared=false" >> "$GITHUB_OUTPUT" |
| 118 | + fi |
| 119 | +
|
| 120 | + - name: Report CVEs a rebuild cannot fix |
| 121 | + # Surface these instead of no-op'ing weekly until the SLA on the secops ticket burns. |
| 122 | + if: steps.before.outputs.proceed == 'true' && steps.delta.outputs.cleared == 'false' |
| 123 | + run: | |
| 124 | + echo "::warning::Rebuild cleared nothing; these need a dependency bump: $(paste -sd', ' published_cves.txt)" |
| 125 | + { |
| 126 | + echo "### Rebuild cleared no CVEs" |
| 127 | + echo |
| 128 | + echo "Still present after a clean rebuild — these need a \`vm/go.mod\` bump, not a rebuild:" |
| 129 | + echo |
| 130 | + sed 's/^/- /' published_cves.txt |
| 131 | + } >> "$GITHUB_STEP_SUMMARY" |
| 132 | +
|
| 133 | + - name: Bump version |
| 134 | + id: bump |
| 135 | + if: steps.delta.outputs.cleared == 'true' && !inputs.dry_run |
| 136 | + run: | |
| 137 | + NEW=$(./scripts/bump-version.sh cleared.txt) |
| 138 | + echo "Releasing ${NEW}" |
| 139 | + echo "version=${NEW}" >> "$GITHUB_OUTPUT" |
| 140 | +
|
| 141 | + - name: Login to Docker Hub |
| 142 | + if: steps.delta.outputs.cleared == 'true' && !inputs.dry_run |
| 143 | + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 |
| 144 | + with: |
| 145 | + username: ${{ secrets.DOCKERHUB_USERNAME }} |
| 146 | + password: ${{ secrets.DOCKERHUB_TOKEN }} |
| 147 | + |
| 148 | + - name: Publish (multi-arch) |
| 149 | + # Published here rather than via build-push-docker.yml: GITHUB_TOKEN pushes don't trigger workflows. |
| 150 | + if: steps.delta.outputs.cleared == 'true' && !inputs.dry_run |
| 151 | + env: |
| 152 | + NEW: ${{ steps.bump.outputs.version }} |
| 153 | + run: | |
| 154 | + echo "Publishing ${PUBLISHED_IMAGE}:${NEW}, clearing:"; cat cleared.txt |
| 155 | + docker buildx build --push --pull --no-cache \ |
| 156 | + --platform "${PLATFORMS}" \ |
| 157 | + --tag "${PUBLISHED_IMAGE}:${NEW}" . |
| 158 | +
|
| 159 | + # After the publish on purpose: a failed publish must not leave TAG pointing at an unpushed image. |
| 160 | + - name: Commit and tag |
| 161 | + if: steps.delta.outputs.cleared == 'true' && !inputs.dry_run |
| 162 | + run: | |
| 163 | + git add Makefile Dockerfile CHANGELOG.md |
| 164 | + git commit -m "Security rebuild ${{ steps.bump.outputs.version }}" |
| 165 | + git tag "v${{ steps.bump.outputs.version }}" |
| 166 | + git push origin HEAD:main "v${{ steps.bump.outputs.version }}" |
| 167 | +
|
| 168 | + - name: Summary |
| 169 | + if: always() |
| 170 | + run: | |
| 171 | + { |
| 172 | + echo "### Weekly security rebuild" |
| 173 | + echo |
| 174 | + echo "- Published image: \`${PUBLISHED_IMAGE}:${{ steps.version.outputs.tag }}\`" |
| 175 | + echo "- Fixable HIGH/CRITICAL on the published image: $(wc -l < published_cves.txt 2>/dev/null || echo 0)" |
| 176 | + echo "- Cleared by rebuild: $(wc -l < cleared.txt 2>/dev/null || echo 0)" |
| 177 | + echo "- Released: ${{ steps.bump.outputs.version || 'no (nothing cleared)' }}" |
| 178 | + } >> "$GITHUB_STEP_SUMMARY" |
0 commit comments