Skip to content

fix(background): reject unverified continued completion (#146) #258

fix(background): reject unverified continued completion (#146)

fix(background): reject unverified continued completion (#146) #258

Workflow file for this run

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"