# ============================================================================= # replay-verify.yml # Sprint: SPRINT_20251228_001_BE_replay_manifest_ci (T4) # Description: CI workflow template for SBOM hash drift detection # ============================================================================= # # This workflow verifies that SBOM generation and verdict computation are # deterministic by comparing replay manifest hashes across builds. # # Usage: # 1. Copy this template to your project's .gitea/workflows/ directory # 2. Adjust the image name and scan parameters as needed # 3. Optionally enable the SBOM attestation step # # Exit codes: # 0 - Verification passed, all hashes match # 1 - Drift detected, hashes differ # 2 - Verification error (missing inputs, invalid manifest) # # ============================================================================= name: SBOM Replay Verification on: push: branches: [main, develop] pull_request: branches: [main] workflow_dispatch: inputs: fail_on_drift: description: 'Fail build if hash drift detected' required: false default: 'true' type: boolean strict_mode: description: 'Enable strict verification mode' required: false default: 'false' type: boolean env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} STELLAOPS_VERSION: '1.0.0' jobs: build-and-scan: runs-on: ubuntu-latest permissions: contents: read packages: write id-token: write # For OIDC-based signing outputs: image_digest: ${{ steps.build.outputs.digest }} sbom_digest: ${{ steps.scan.outputs.sbom_digest }} verdict_digest: ${{ steps.scan.outputs.verdict_digest }} replay_manifest: ${{ steps.scan.outputs.replay_manifest }} steps: - name: Checkout repository uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Log in to container registry if: github.event_name != 'pull_request' uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata for Docker id: meta uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | type=sha,prefix= type=ref,event=branch type=ref,event=pr - name: Build and push image id: build uses: docker/build-push-action@v5 with: context: . push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max provenance: true sbom: false # We generate our own SBOM - name: Install StellaOps CLI run: | curl -sSfL https://stellaops.io/install.sh | sh -s -- -v ${{ env.STELLAOPS_VERSION }} echo "$HOME/.stellaops/bin" >> $GITHUB_PATH - name: Scan image and generate replay manifest id: scan env: IMAGE_REF: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }} run: | # Scan image with StellaOps stella scan \ --image "${IMAGE_REF}" \ --output-sbom sbom.json \ --output-findings findings.json \ --output-verdict verdict.json \ --format cyclonedx-1.6 # Export replay manifest for CI verification stella replay export \ --image "${IMAGE_REF}" \ --output replay.json \ --include-feeds \ --include-reachability \ --pretty # Extract digests for outputs SBOM_DIGEST=$(sha256sum sbom.json | cut -d' ' -f1) VERDICT_DIGEST=$(sha256sum verdict.json | cut -d' ' -f1) echo "sbom_digest=sha256:${SBOM_DIGEST}" >> $GITHUB_OUTPUT echo "verdict_digest=sha256:${VERDICT_DIGEST}" >> $GITHUB_OUTPUT echo "replay_manifest=replay.json" >> $GITHUB_OUTPUT # Display summary echo "### Scan Results" >> $GITHUB_STEP_SUMMARY echo "| Artifact | Digest |" >> $GITHUB_STEP_SUMMARY echo "|----------|--------|" >> $GITHUB_STEP_SUMMARY echo "| Image | \`${{ steps.build.outputs.digest }}\` |" >> $GITHUB_STEP_SUMMARY echo "| SBOM | \`sha256:${SBOM_DIGEST}\` |" >> $GITHUB_STEP_SUMMARY echo "| Verdict | \`sha256:${VERDICT_DIGEST}\` |" >> $GITHUB_STEP_SUMMARY - name: Upload scan artifacts uses: actions/upload-artifact@v4 with: name: scan-artifacts-${{ github.sha }} path: | sbom.json findings.json verdict.json replay.json retention-days: 30 verify-determinism: runs-on: ubuntu-latest needs: build-and-scan steps: - name: Download scan artifacts uses: actions/download-artifact@v4 with: name: scan-artifacts-${{ github.sha }} - name: Install StellaOps CLI run: | curl -sSfL https://stellaops.io/install.sh | sh -s -- -v ${{ env.STELLAOPS_VERSION }} echo "$HOME/.stellaops/bin" >> $GITHUB_PATH - name: Verify SBOM determinism id: verify env: FAIL_ON_DRIFT: ${{ inputs.fail_on_drift || 'true' }} STRICT_MODE: ${{ inputs.strict_mode || 'false' }} run: | # Build verification flags VERIFY_FLAGS="--manifest replay.json" if [ "${FAIL_ON_DRIFT}" = "true" ]; then VERIFY_FLAGS="${VERIFY_FLAGS} --fail-on-drift" fi if [ "${STRICT_MODE}" = "true" ]; then VERIFY_FLAGS="${VERIFY_FLAGS} --strict-mode" fi # Run verification stella replay export verify ${VERIFY_FLAGS} EXIT_CODE=$? # Report results if [ $EXIT_CODE -eq 0 ]; then echo "✅ Verification passed - all hashes match" >> $GITHUB_STEP_SUMMARY echo "status=success" >> $GITHUB_OUTPUT elif [ $EXIT_CODE -eq 1 ]; then echo "⚠️ Drift detected - hashes differ from expected" >> $GITHUB_STEP_SUMMARY echo "status=drift" >> $GITHUB_OUTPUT else echo "❌ Verification error" >> $GITHUB_STEP_SUMMARY echo "status=error" >> $GITHUB_OUTPUT fi exit $EXIT_CODE - name: Comment on PR (on drift) if: failure() && github.event_name == 'pull_request' uses: actions/github-script@v7 with: script: | github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: `## ⚠️ SBOM Determinism Check Failed Hash drift detected between scan runs. This may indicate non-deterministic build or scan behavior. **Expected digests:** - SBOM: \`${{ needs.build-and-scan.outputs.sbom_digest }}\` - Verdict: \`${{ needs.build-and-scan.outputs.verdict_digest }}\` **Possible causes:** - Non-deterministic build artifacts (timestamps, random values) - Changed dependencies between runs - Environment differences **Next steps:** 1. Review the replay manifest in the artifacts 2. Check build logs for non-deterministic elements 3. Consider using \`--strict-mode\` for detailed drift analysis` }) # Optional: Attest SBOM to OCI registry attest-sbom: runs-on: ubuntu-latest needs: [build-and-scan, verify-determinism] if: github.event_name != 'pull_request' && success() permissions: packages: write id-token: write steps: - name: Download scan artifacts uses: actions/download-artifact@v4 with: name: scan-artifacts-${{ github.sha }} - name: Install StellaOps CLI run: | curl -sSfL https://stellaops.io/install.sh | sh -s -- -v ${{ env.STELLAOPS_VERSION }} echo "$HOME/.stellaops/bin" >> $GITHUB_PATH - name: Log in to container registry uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Attach SBOM attestation env: IMAGE_REF: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ needs.build-and-scan.outputs.image_digest }} run: | # Sign and attach SBOM as in-toto attestation stella attest attach \ --image "${IMAGE_REF}" \ --sbom sbom.json \ --predicate-type https://cyclonedx.org/bom/v1.6 \ --sign keyless echo "### SBOM Attestation" >> $GITHUB_STEP_SUMMARY echo "SBOM attached to \`${IMAGE_REF}\`" >> $GITHUB_STEP_SUMMARY