fix(background): reject unverified continued completion (#146) #258
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Security | |
| on: | |
| push: | |
| branches: [main] | |
| paths-ignore: | |
| - "**/*.md" | |
| - "docs/**" | |
| # No paths-ignore here: the govulncheck jobs are merge-blocking required | |
| # checks, and a required check whose workflow never starts on a docs-only PR | |
| # stays "Expected" forever, blocking the PR (the ci.yml:17-22 trap, #81). | |
| # The full Security run costs well under two minutes; the push trigger above | |
| # keeps its filter because required checks only gate the PR path. | |
| pull_request: | |
| branches: [main] | |
| schedule: | |
| # Monday 06:00 UTC — surface CVEs disclosed against the vendored Syncthing | |
| # fork or the alpine base even when the repo is quiet between releases. | |
| # (GitHub pauses scheduled workflows after ~60 days of repo inactivity.) | |
| - cron: "0 6 * * 1" | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| jobs: | |
| go-lint: | |
| name: Go Lint | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 | |
| with: | |
| go-version-file: notify/go.mod | |
| # gofmt is dependency-free, so it checks the hand-written bridge package | |
| # without materialising the patched Syncthing tree. | |
| - name: gofmt | |
| run: | | |
| unformatted="$(gofmt -l go/bridge notify)" | |
| if [ -n "$unformatted" ]; then | |
| echo "::error::These files need gofmt:" | |
| echo "$unformatted" | |
| exit 1 | |
| fi | |
| - name: go vet (notify) | |
| working-directory: notify | |
| run: go vet ./... | |
| # Optional next step: add golangci-lint here (golangci-lint-action@v9 needs | |
| # golangci-lint v2 + a .golangci.yml). Left out for now so every check in | |
| # this gate is verified-green. | |
| govulncheck-go: | |
| name: govulncheck (go bridge) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 | |
| with: | |
| go-version-file: go/go.mod | |
| cache-dependency-path: go/go.sum | |
| # Materialise the patched Syncthing/go-stun forks so govulncheck scans the | |
| # code actually compiled into the app — the only scanner that sees CVEs | |
| # inside the replace-shadowed fork. | |
| - name: Patch vendored dependencies | |
| working-directory: go | |
| run: make patch | |
| - name: govulncheck | |
| working-directory: go | |
| run: | | |
| # Pinned (same reasoning as the gomobile pin in ci.yml): a govulncheck | |
| # release must not change scan results without a repo change (#85). | |
| go install golang.org/x/vuln/cmd/govulncheck@v1.6.0 | |
| govulncheck -tags noassets ./bridge/... | |
| govulncheck-notify: | |
| name: govulncheck (notify) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 | |
| with: | |
| go-version-file: notify/go.mod | |
| - name: govulncheck | |
| working-directory: notify | |
| run: | | |
| # Pinned (same reasoning as the gomobile pin in ci.yml): a govulncheck | |
| # release must not change scan results without a repo change (#85). | |
| go install golang.org/x/vuln/cmd/govulncheck@v1.6.0 | |
| govulncheck ./... | |
| image-scan: | |
| name: Image Scan (published) | |
| # Schedule only: catches base-image / Go-binary CVEs disclosed against the | |
| # published image after the last build, without a commit. Build-time scanning | |
| # is owned by docker.yml. The scheduled scan resolves the public release's | |
| # recorded OCI digest and never follows a mutable convenience tag. | |
| if: github.event_name == 'schedule' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| permissions: | |
| contents: read | |
| packages: read | |
| steps: | |
| - name: Resolve the published helper digest | |
| id: release-image | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| TARGET_RELEASE_TAG: notify-v2.0.2 | |
| FALLBACK_RELEASE_TAG: notify-v2.0.0 | |
| run: | | |
| set -euo pipefail | |
| mkdir -p /tmp/vaultsync-notify-release | |
| target_json=/tmp/vaultsync-notify-release/target-release.json | |
| target_error=/tmp/vaultsync-notify-release/target-release.error | |
| if gh api \ | |
| "repos/${GITHUB_REPOSITORY}/releases/tags/${TARGET_RELEASE_TAG}" \ | |
| > "$target_json" 2> "$target_error"; then | |
| if jq -e --arg tag "$TARGET_RELEASE_TAG" \ | |
| '.tag_name == $tag and .draft == false and .prerelease == false' \ | |
| "$target_json" > /dev/null; then | |
| release_tag=$TARGET_RELEASE_TAG | |
| elif jq -e '.draft == true or .prerelease == true' \ | |
| "$target_json" > /dev/null; then | |
| release_tag=$FALLBACK_RELEASE_TAG | |
| else | |
| echo "Target helper release metadata is not canonical" >&2 | |
| exit 1 | |
| fi | |
| elif grep -Fq 'HTTP 404' "$target_error"; then | |
| release_tag=$FALLBACK_RELEASE_TAG | |
| else | |
| echo "Target helper release lookup failed" >&2 | |
| exit 1 | |
| fi | |
| selected_json=/tmp/vaultsync-notify-release/selected-release.json | |
| gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${release_tag}" \ | |
| > "$selected_json" | |
| jq -e --arg tag "$release_tag" \ | |
| '.tag_name == $tag and .draft == false and .prerelease == false' \ | |
| "$selected_json" > /dev/null | |
| echo "Scanning immutable helper release ${release_tag}" | |
| gh release download "$release_tag" --repo "$GITHUB_REPOSITORY" \ | |
| --pattern IMAGE-DIGESTS --dir /tmp/vaultsync-notify-release | |
| mapfile -t digests < <( | |
| sed -n 's/^index_digest=//p' /tmp/vaultsync-notify-release/IMAGE-DIGESTS | |
| ) | |
| if [ "${#digests[@]}" -ne 1 ]; then | |
| echo "IMAGE-DIGESTS must contain exactly one index_digest" >&2 | |
| exit 1 | |
| fi | |
| digest=${digests[0]} | |
| printf '%s\n' "$digest" | grep -Eq '^sha256:[0-9a-f]{64}$' | |
| printf 'digest=%s\n' "$digest" >> "$GITHUB_OUTPUT" | |
| - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Trivy scan of ghcr image | |
| uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 | |
| with: | |
| image-ref: ghcr.io/psimaker/vaultsync-notify@${{ steps.release-image.outputs.digest }} | |
| scanners: vuln,secret | |
| severity: HIGH,CRITICAL | |
| ignore-unfixed: true | |
| exit-code: "1" |