feat: Verdict reproducibility (discussion #56): An identical re-scan … #89
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: ci | |
| on: | |
| push: | |
| branches: [main] | |
| tags: ["v*"] | |
| pull_request: | |
| jobs: | |
| # Fail the build if go.sum | |
| # integrity is off, or if vendor/ has drifted from go.mod/go.sum (e.g. the dep | |
| # was bumped but `go mod vendor` wasn't re-run and committed). | |
| vendor: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-go@v5 | |
| with: | |
| go-version-file: go.mod # tracks the toolchain in go.mod, no hardcoding | |
| - name: verify module checksums against go.sum | |
| run: go mod verify | |
| - name: vendor/ is in sync with go.mod / go.sum | |
| run: | | |
| go mod tidy | |
| go mod vendor | |
| if ! git diff --exit-code -- go.mod go.sum vendor/; then | |
| echo "::error::go.mod/go.sum/vendor are out of date. Run 'go mod tidy && go mod vendor' and commit the result." | |
| exit 1 | |
| fi | |
| # Runs on every push and PR. | |
| test: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 # full history + tags so `git describe` works | |
| - uses: actions/setup-go@v5 | |
| with: | |
| go-version: "1.22" | |
| - run: go vet ./... | |
| - run: go build ./... | |
| - run: go test ./... | |
| # Builds release artifacts as hardened static-PIE binaries (PIE + full RELRO, | |
| # via the external linker; netgo+osusergo keep them static and portable). No | |
| # UPX: it strips PIE/RELRO, trips antivirus, and hurts reproducibility for a | |
| # marginal size win on an on-demand CLI (#30). On a v* tag it also creates the | |
| # GitHub Release; otherwise artifacts are just uploaded for inspection. | |
| release: | |
| needs: test | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write # required to create the Release and upload assets | |
| strategy: | |
| matrix: | |
| goarch: [amd64, arm64] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 # tags needed for version stamping | |
| - uses: actions/setup-go@v5 | |
| with: | |
| go-version: "1.22" | |
| - name: derive version | |
| id: ver | |
| run: | | |
| VERSION="$(git describe --tags --always --dirty 2>/dev/null || echo dev)" | |
| COMMIT="$(git rev-parse --short=12 HEAD 2>/dev/null || true)" | |
| DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)" | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| echo "commit=$COMMIT" >> "$GITHUB_OUTPUT" | |
| echo "date=$DATE" >> "$GITHUB_OUTPUT" | |
| echo "Building $VERSION ($COMMIT)" | |
| - name: install C toolchain (external linker → PIE + full RELRO) | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y gcc binutils | |
| if [ "${{ matrix.goarch }}" = "arm64" ]; then | |
| sudo apt-get install -y gcc-aarch64-linux-gnu libc6-dev-arm64-cross | |
| fi | |
| - name: build hardened static binary (PIE + full RELRO, version-stamped) | |
| env: | |
| # CGO is enabled only to drive the external linker (needed for | |
| # DT_BIND_NOW / full RELRO). netgo+osusergo force Go's pure resolver | |
| # and user lookup, so the result stays fully static and portable — | |
| # no glibc NSS at runtime. -static-pie gives a position-independent | |
| # static ELF (ET_DYN) that satisfies namcap's PIE and FULL RELRO | |
| # checks (#30). | |
| CGO_ENABLED: "1" | |
| GOOS: linux | |
| GOARCH: ${{ matrix.goarch }} | |
| CC: ${{ matrix.goarch == 'arm64' && 'aarch64-linux-gnu-gcc' || 'gcc' }} | |
| PKG: github.com/manticore-projects/aurscan/internal/version | |
| run: | | |
| go build -trimpath -buildmode=pie -tags 'netgo osusergo' \ | |
| -ldflags="-s -w -linkmode=external \ | |
| -extldflags '-static-pie -Wl,-z,relro -Wl,-z,now' \ | |
| -X ${PKG}.Version=${{ steps.ver.outputs.version }} \ | |
| -X ${PKG}.Commit=${{ steps.ver.outputs.commit }} \ | |
| -X ${PKG}.Date=${{ steps.ver.outputs.date }}" \ | |
| -o aurscan-linux-${{ matrix.goarch }} ./cmd/aurscan | |
| - name: verify hardening (PIE + full RELRO + static) | |
| run: | | |
| bin=aurscan-linux-${{ matrix.goarch }} | |
| readelf -h "$bin" | grep -q 'DYN (Position-Independent' || { echo "::error::$bin is not PIE (ET_DYN)"; exit 1; } | |
| readelf -d "$bin" | grep -qi 'BIND_NOW' || { echo "::error::$bin lacks full RELRO (no BIND_NOW)"; exit 1; } | |
| file "$bin" | grep -q 'static-pie' || { echo "::error::$bin is not static-pie"; exit 1; } | |
| echo "$bin: PIE + full RELRO + static-pie — ok" | |
| - name: smoke test (amd64 only — the runner is amd64) | |
| if: matrix.goarch == 'amd64' | |
| run: ./aurscan-linux-amd64 --version | |
| - name: upload artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: aurscan-linux-${{ matrix.goarch }} | |
| path: aurscan-linux-${{ matrix.goarch }} | |
| - name: attach to release | |
| if: startsWith(github.ref, 'refs/tags/v') | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| files: aurscan-linux-${{ matrix.goarch }} | |
| # Derive the signing key from the imported secret material instead of | |
| # trusting GPG_KEY_ID to match (the previous "No secret key" failure), prints a | |
| # secret-free diagnostic, and fails clearly if GPG_PRIVATE_KEY carried no secret. | |
| sign: | |
| needs: release | |
| if: startsWith(github.ref, 'refs/tags/v') | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: download built binaries | |
| uses: actions/download-artifact@v4 | |
| with: | |
| path: dist | |
| merge-multiple: true | |
| - name: generate SHA256SUMS | |
| working-directory: dist | |
| run: | | |
| sha256sum aurscan-linux-amd64 aurscan-linux-arm64 > SHA256SUMS | |
| echo "---- SHA256SUMS ----"; cat SHA256SUMS | |
| - name: import signing key | |
| env: | |
| GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} | |
| run: | | |
| if [ -z "$GPG_PRIVATE_KEY" ]; then | |
| echo "::error::GPG_PRIVATE_KEY secret is not set"; exit 1 | |
| fi | |
| export GNUPGHOME="$(mktemp -d)" | |
| echo "GNUPGHOME=$GNUPGHOME" >> "$GITHUB_ENV" | |
| echo "allow-loopback-pinentry" > "$GNUPGHOME/gpg-agent.conf" | |
| gpgconf --kill gpg-agent || true | |
| printf '%s\n' "$GPG_PRIVATE_KEY" | gpg --batch --import | |
| # Fail loudly if the export contained no SECRET key (e.g. a public key | |
| # was exported by mistake) — this is the usual root of "No secret key". | |
| if [ "$(gpg --list-secret-keys --with-colons | grep -c '^sec')" -eq 0 ]; then | |
| echo "::error::GPG_PRIVATE_KEY imported no secret key. Re-export with" | |
| echo "::error:: gpg --armor --export-secret-keys <KEYID> (or --export-secret-subkeys <SUBKEYID>!)" | |
| exit 1 | |
| fi | |
| # Secret-free diagnostic: which key(s) and capabilities did we get? | |
| echo "---- imported secret keys ----" | |
| gpg --list-secret-keys --keyid-format=long | grep -E 'sec|ssb|uid' | |
| # Resolve the signing key. If GPG_KEY_ID is set it must match an | |
| # imported secret key; otherwise auto-derive the first secret key's | |
| # primary fingerprint (gpg then auto-selects its signing subkey). | |
| want='${{ secrets.GPG_KEY_ID }}' | |
| if [ -n "$want" ]; then | |
| if ! gpg --list-secret-keys --with-colons | grep -qiE ":(${want#0x}):"; then | |
| echo "::error::GPG_KEY_ID ($want) does not match any imported secret key (see list above)." | |
| echo "::error::Fix GPG_KEY_ID, or unset it to auto-select the imported key." | |
| exit 1 | |
| fi | |
| key="$want" | |
| else | |
| key="$(gpg --list-secret-keys --with-colons | awk -F: '$1=="fpr"{print $10; exit}')" | |
| fi | |
| echo "SIGN_KEY=$key" >> "$GITHUB_ENV" | |
| echo "Will sign with: $key" | |
| - name: sign SHA256SUMS (detached, armored) | |
| working-directory: dist | |
| env: | |
| GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} | |
| run: | | |
| gpg --batch --yes --pinentry-mode loopback \ | |
| ${GPG_PASSPHRASE:+--passphrase "$GPG_PASSPHRASE"} \ | |
| --local-user "$SIGN_KEY" \ | |
| --armor --detach-sign --output SHA256SUMS.asc SHA256SUMS | |
| gpg --verify SHA256SUMS.asc SHA256SUMS | |
| - name: attach checksums + signature to the release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| files: | | |
| dist/SHA256SUMS | |
| dist/SHA256SUMS.asc |